@hachej/boring-ui-kit 0.4.3 → 0.4.5

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 (57) hide show
  1. package/README.md +56 -0
  2. package/dist/chat-outbox.d.ts +18 -0
  3. package/dist/chat-outbox.js +12 -0
  4. package/dist/mark-store.d.ts +38 -0
  5. package/dist/mark-store.js +84 -0
  6. package/dist/markdown-editor.d.ts +6 -0
  7. package/dist/markdown-editor.js +17 -2
  8. package/dist/markdown-highlight-layer.d.ts +26 -0
  9. package/dist/markdown-highlight-layer.js +107 -0
  10. package/dist/markdown-quote.d.ts +23 -0
  11. package/dist/markdown-quote.js +39 -0
  12. package/dist/native-chat.d.ts +21 -0
  13. package/dist/native-chat.js +111 -7
  14. package/dist/remote-chat.js +16 -14
  15. package/dist/shared-chat/error.d.ts +10 -0
  16. package/dist/shared-chat/error.js +13 -0
  17. package/dist/shared-chat/fake.d.ts +39 -0
  18. package/dist/shared-chat/fake.js +90 -0
  19. package/dist/shared-chat/index.d.ts +51 -0
  20. package/dist/shared-chat/index.js +255 -0
  21. package/dist/shared-chat/socket.d.ts +20 -0
  22. package/dist/shared-chat/socket.js +114 -0
  23. package/dist/shared-chat/source.d.ts +117 -0
  24. package/dist/shared-chat/source.js +6 -0
  25. package/dist/voice-capture.d.ts +43 -0
  26. package/dist/voice-capture.js +186 -0
  27. package/package.json +19 -3
  28. package/r/agent-target.json +36 -0
  29. package/r/app-shell.json +26 -11
  30. package/r/board-viewer.json +5 -5
  31. package/r/button.json +1 -1
  32. package/r/diagram-viewer.json +1 -1
  33. package/r/dock.json +1 -1
  34. package/r/documents.json +10 -10
  35. package/r/feedback-preview.json +1 -1
  36. package/r/feedback.json +5 -5
  37. package/r/file-tree.json +5 -5
  38. package/r/highlights.json +29 -0
  39. package/r/html-viewer.json +3 -3
  40. package/r/maquette-viewer.json +2 -2
  41. package/r/markdown-editor.json +3 -3
  42. package/r/mockup-viewer.json +2 -2
  43. package/r/pi-ambient.json +7 -7
  44. package/r/pi-app.json +15 -14
  45. package/r/pi-chat.json +50 -20
  46. package/r/pi-workspace.json +4 -4
  47. package/r/provider-setup.json +2 -2
  48. package/r/record-viewer.json +40 -0
  49. package/r/registry.json +380 -86
  50. package/r/requests-board.json +41 -0
  51. package/r/shared-chat.json +79 -0
  52. package/r/task-list-viewer.json +2 -2
  53. package/r/theme.json +15 -0
  54. package/r/utils.json +6 -0
  55. package/r/version-history.json +3 -3
  56. package/r/viewers.json +15 -9
  57. package/r/windows.json +28 -0
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "highlights",
4
+ "title": "Anchored highlights",
5
+ "description": "Exact Markdown highlights and comments in a conditional workspace sidecar.",
6
+ "dependencies": [
7
+ "@hachej/boring-ui-kit@^0.4.5",
8
+ "@hachej/boring-files@^0.4.5",
9
+ "react@^19.0.0",
10
+ "react-dom@^19.0.0"
11
+ ],
12
+ "registryDependencies": [
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/markdown-editor.json",
14
+ "button",
15
+ "textarea",
16
+ "label",
17
+ "item",
18
+ "dropdown-menu"
19
+ ],
20
+ "files": [
21
+ {
22
+ "path": "registry/highlights/highlights.tsx",
23
+ "content": "'use client';\n\nimport { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ResourceLocator } from '@hachej/boring-files';\nimport { randomUUID } from '@hachej/boring-files/platform';\nimport type { MarkdownController } from '@hachej/boring-ui-kit/markdown';\nimport { captureMarkdownQuote, markdownQuoteResolution, type MarkdownQuoteAnchor, type MarkdownQuoteRange } from '@hachej/boring-ui-kit/markdown-quote';\nimport { createMarkStore, type MarkStore, type MarkResource, type MarkColour } from '@hachej/boring-ui-kit/mark-store';\nimport { markdownMarkerBackground, type MarkdownQuoteCapture } from '@hachej/boring-ui-kit/markdown-editor';\nimport { Button } from '@/components/ui/button';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Label } from '@/components/ui/label';\nimport { Item, ItemContent, ItemDescription, ItemGroup } from '@/components/ui/item';\nimport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem } from '@/components/ui/dropdown-menu';\nimport { MarkdownEditor } from '../markdown-editor/markdown-editor';\n\nexport interface HighlightAction { readonly id: string; readonly label: string; readonly run: (id: string) => void }\nexport interface Highlight { readonly id: string; readonly anchor: MarkdownQuoteAnchor; readonly colour: MarkColour; readonly note?: string; readonly actions?: readonly HighlightAction[] }\nexport interface HighlightsProps {\n readonly controller: MarkdownController;\n readonly resource?: MarkResource;\n readonly locator?: ResourceLocator;\n readonly store?: MarkStore;\n readonly marks?: readonly Highlight[];\n readonly locale?: 'en' | 'fr';\n}\nconst labels = {\n en: { title: 'Highlights', add: 'Highlight selection', note: 'Comment', save: 'Save comment', remove: 'Remove highlight', actions: 'Highlight actions', tray: (count: number) => `${count} ${count === 1 ? 'highlight could' : 'highlights could'} not be placed`, missing: 'This passage is no longer in the document', ambiguous: 'This passage appears several times — pick the right one', candidate: 'Choose passage', attach: 'Attach to selection', hint: 'Select a passage in the document first.', refused: \"This selection can't be highlighted here. Try selecting it in Markdown source.\", conflict: 'Highlights changed elsewhere. Reload before editing.', unknown: \"We couldn't confirm the save. Check before making another change.\", check: 'Check save outcome', reload: 'Reload highlights', failed: \"Your highlights couldn't be saved.\", unavailable: \"Highlights aren't available.\", source: 'View marked text', confirmed: 'Your choice is kept for this version.', commented: 'Has a comment' },\n fr: { title: 'Surlignages', add: 'Surligner la sélection', note: 'Commentaire', save: 'Enregistrer le commentaire', remove: 'Supprimer le surlignage', actions: 'Actions du surlignage', tray: (count: number) => count === 1 ? \"1 surlignage n'a pas pu être placé\" : `${count} surlignages n'ont pas pu être placés`, missing: \"Ce passage n'est plus dans le document.\", ambiguous: 'Ce passage apparaît plusieurs fois — choisissez le bon.', candidate: 'Choisir ce passage', attach: 'Rattacher à la sélection', hint: \"Sélectionnez d'abord un passage dans le document.\", refused: 'Ce passage ne peut pas être surligné ici. Essayez de le sélectionner dans la source Markdown.', conflict: 'Les surlignages ont changé ailleurs. Rechargez avant de modifier.', unknown: \"L'enregistrement n'a pas été confirmé. Vérifiez avant de continuer.\", check: 'Vérifier le résultat', reload: 'Recharger les surlignages', failed: \"Les surlignages n'ont pas pu être enregistrés.\", unavailable: 'Les surlignages ne sont pas disponibles.', source: 'Voir le texte surligné', confirmed: 'Votre choix est conservé pour cette version.', commented: 'Avec un commentaire' },\n};\nconst empty = { marks: [], save: { kind: 'idle' }, dirty: false, lifecycle: 'active' } as const;\nconst idleSubscribe = () => () => {};\ntype Choice = { readonly source: string; readonly range: MarkdownQuoteRange };\n\n/** Borrows supplied controllers/stores. Only the default sidecar store is owned here. */\nexport function Highlights({ controller, resource, locator, store: supplied, marks: suppliedMarks, locale = 'en' }: HighlightsProps) {\n const l = labels[locale];\n const [owned, setOwned] = useState<{ store: MarkStore; client: MarkResource['client']; key: string } | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [active, setActive] = useState<string | null>(null);\n const [note, setNote] = useState('');\n const [capture, setCapture] = useState<{ controller: MarkdownController; text: string; value: MarkdownQuoteCapture } | null>(null);\n const [choices, setChoices] = useState<Readonly<Record<string, Choice>>>({});\n const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);\n const path = locator?.resource.path, providerId = locator?.resource.providerId, view = JSON.stringify(locator?.view);\n const client = resource?.client, scope = resource?.identity.scopeId, principal = resource?.identity.principalId, initiator = resource?.identity.initiatorId;\n const ownerKey = JSON.stringify([scope, principal, initiator, path, providerId, view]);\n useEffect(() => {\n if (supplied || !resource || !locator) { setOwned(null); return; }\n let live = true, acquired: MarkStore | undefined;\n setOwned(null); setError(null);\n void createMarkStore(resource, locator).then(store => { acquired = store; if (live) setOwned({ store, client: resource.client, key: ownerKey }); else store.dispose(); }, () => { if (live) setError(l.unavailable); });\n return () => { live = false; acquired?.dispose(); };\n }, [supplied, client, scope, principal, initiator, path, providerId, view]);\n const store = supplied ?? (owned?.client === client && owned?.key === ownerKey ? owned.store : null);\n const binding = useRef({ controller, store }); binding.current = { controller, store };\n const live = useRef(false);\n useEffect(() => { live.current = true; return () => { live.current = false; }; }, []);\n const snapshot = useSyncExternalStore(store?.subscribe ?? idleSubscribe, store?.getSnapshot ?? (() => empty), store?.getSnapshot ?? (() => empty));\n const marks: readonly Highlight[] = suppliedMarks ?? snapshot.marks;\n const result = snapshot.save.kind === 'settled' ? snapshot.save.result : null;\n const busy = snapshot.lifecycle !== 'active' || snapshot.save.kind === 'pending' || !!result && result.kind !== 'saved';\n const currentBinding = () => live.current && binding.current.controller === controller && binding.current.store === store && controller.getSnapshot().lifecycle === 'active';\n const currentText = () => currentBinding() && controller.getSnapshot().text === state.text;\n const selection = capture?.controller === controller && capture.text === state.text && capture.value.kind === 'captured' ? capture.value.anchor : null;\n const selectQuote = useCallback((value: MarkdownQuoteCapture) => { setCapture({ controller, text: controller.getSnapshot().text, value }); }, [controller]);\n const placed = useMemo(() => marks.map(mark => ({ mark, placement: markdownQuoteResolution.resolve(mark.anchor, state.text, String(state.bufferVersion)) })), [marks, state.text, state.bufferVersion]);\n const decorations = useMemo(() => marks.map(mark => ({ ...mark, active: mark.id === active, commented: !!mark.note?.trim(), commentLabel: l.commented, ...(choices[mark.id]?.source === state.text ? { confirmed: choices[mark.id] } : {}) })), [marks, active, l.commented, choices, state.text]);\n const current = marks.find(m => m.id === active);\n const activate = useCallback((id: string) => { setActive(id); setNote(store?.getSnapshot().marks.find(m => m.id === id)?.note ?? ''); }, [store]);\n useEffect(() => { setActive(null); setCapture(null); setChoices({}); setError(null); }, [controller, store]);\n const run = async (operation: () => Promise<unknown>) => {\n if (!currentBinding()) return;\n setError(null);\n try {\n const value = await operation();\n if (currentBinding() && value && typeof value === 'object' && 'kind' in value && !['saved', 'available', 'missing'].includes(String(value.kind))) setError(value.kind === 'conflict' ? l.conflict : value.kind === 'unknown' ? l.unknown : l.failed);\n } catch { if (currentBinding()) setError(l.failed); }\n };\n const add = () => {\n if (!currentText() || !selection || !store || busy) return;\n void run(() => store.add({ id: randomUUID(), anchor: selection, colour: 'accent', note: '', author: resource?.identity.principalId ?? state.identity.principalId, time: new Date().toISOString() }));\n };\n const choose = (mark: Highlight, range: MarkdownQuoteRange) => {\n if (!currentText() || busy) return;\n const fresh = markdownQuoteResolution.resolve(mark.anchor, controller.getSnapshot().text, String(state.bufferVersion));\n if (fresh.kind !== 'ambiguous' || !fresh.candidates.some(r => r.start === range.start && r.end === range.end)) return;\n setChoices(previous => ({ ...previous, [mark.id]: { source: state.text, range } })); activate(mark.id);\n const captured = captureMarkdownQuote(state.text, range);\n if (store && captured.kind === 'captured') void run(() => store.edit(mark.id, { anchor: captured.anchor }));\n };\n const lost = placed.filter(p => (p.placement.kind === 'missing' || p.placement.kind === 'ambiguous') && choices[p.mark.id]?.source !== state.text);\n const noteId = useId();\n const hintId = `${noteId}-selection-hint`;\n const hint = capture?.controller === controller && capture.text === state.text && capture.value.kind === 'refused' && capture.value.reason.includes('no exact source projection') ? l.refused : l.hint;\n return <section data-boring=\"highlights\" className=\"flex min-w-0 flex-col gap-4\" lang={locale}>\n <header className=\"flex flex-col gap-2\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\"><h2>{l.title}</h2>\n {store && <Button variant=\"secondary\" size=\"sm\" disabled={busy || !selection} aria-describedby={!selection ? hintId : undefined} onMouseDown={event => event.preventDefault()} onClick={add}>{l.add}</Button>}\n </div>\n {store && !selection && <ItemDescription id={hintId}>{hint}</ItemDescription>}\n </header>\n {error && <p role=\"alert\">{error}</p>}\n {result && result.kind !== 'saved' && <div role=\"alert\"><p>{result.kind === 'unknown' ? l.unknown : result.kind === 'conflict' ? l.conflict : l.failed}</p>\n {result.kind === 'unknown' ? <Button variant=\"outline\" size=\"sm\" onClick={() => void run(() => store!.reconcile())}>{l.check}</Button> : <Button variant=\"outline\" size=\"sm\" onClick={() => void run(async () => { await store!.reload(); if (currentBinding()) { setActive(null); setChoices({}); } })}>{l.reload}</Button>}\n </div>}\n <MarkdownEditor controller={controller} highlights={decorations} onQuoteSelection={selectQuote} onHighlightActivate={activate} autosave={false} />\n <details><summary>{l.source}</summary><AnnotatedSource source={state.text} placements={placed} choices={choices} active={active} commentLabel={l.commented} activate={activate} /></details>\n {current && <div className=\"flex flex-col items-start gap-2\" data-testid=\"highlight-context\">\n {(store || current.actions?.length) && <DropdownMenu><DropdownMenuTrigger render={<Button variant=\"outline\" size=\"sm\" />}>{l.actions}</DropdownMenuTrigger>\n <DropdownMenuContent><DropdownMenuGroup>\n {current.actions?.map(action => <DropdownMenuItem key={action.id} onClick={() => { if (currentText()) action.run(current.id); }}>{action.label}</DropdownMenuItem>)}\n {store && <DropdownMenuItem disabled={busy} onClick={() => { if (currentText()) void run(() => store.remove(current.id)); }}>{l.remove}</DropdownMenuItem>}\n </DropdownMenuGroup></DropdownMenuContent>\n </DropdownMenu>}\n {choices[current.id]?.source === state.text && <p role=\"status\">{l.confirmed}</p>}\n {store && <><Label htmlFor={noteId}>{l.note}</Label><Textarea id={noteId} value={note} disabled={busy} onChange={event => setNote(event.target.value)} />\n <Button variant=\"secondary\" size=\"sm\" disabled={busy} onClick={() => { if (currentText()) void run(() => store.edit(current.id, { note })); }}>{l.save}</Button></>}\n </div>}\n {lost.length > 0 && <aside aria-label={l.tray(lost.length)} className=\"flex flex-col gap-3\"><h3>{l.tray(lost.length)}</h3>\n <ItemGroup>{lost.map(({ mark, placement }, lostIndex) => <Item key={mark.id} role=\"listitem\" variant=\"outline\" size=\"sm\" data-testid=\"lost-mark\">\n <ItemContent className=\"min-w-0 gap-3\">\n <div className=\"flex flex-col gap-1\"><p><q>{mark.anchor.exact}</q></p><ItemDescription>{placement.kind === 'missing' ? l.missing : l.ambiguous}</ItemDescription></div>\n {placement.kind === 'ambiguous' && <div className=\"grid gap-3 sm:grid-cols-2\">{placement.candidates.map((range, index) => <div key={range.start} className=\"flex min-w-0 flex-col items-start gap-2\" data-testid=\"highlight-candidate\">\n <CandidateContext source={state.text} range={range} />\n <Button variant=\"outline\" size=\"sm\" disabled={busy} onClick={() => choose(mark, range)}>{l.candidate} {index + 1}</Button>\n </div>)}</div>}\n {store && <div className=\"flex flex-col items-start gap-2\">\n <Button variant=\"outline\" size=\"sm\" disabled={!selection} aria-describedby={!selection ? `${hintId}-${lostIndex}` : undefined} onClick={() => { if (currentText() && selection) void run(() => store.edit(mark.id, { anchor: selection })); }}>{l.attach}</Button>\n {!selection && <ItemDescription id={`${hintId}-${lostIndex}`}>{hint}</ItemDescription>}\n </div>}\n </ItemContent>\n </Item>)}</ItemGroup>\n </aside>}\n </section>;\n}\n\nfunction CandidateContext({ source, range }: { readonly source: string; readonly range: MarkdownQuoteRange }) {\n const before = source.slice(0, range.start), lineStart = before.lastIndexOf('\\n') + 1;\n const previousWords = [...before.slice(lineStart).matchAll(/\\S+/gu)];\n const from = lineStart + (previousWords.slice(-5)[0]?.index ?? before.length - lineStart);\n const lineEnd = source.indexOf('\\n', range.end);\n const after = source.slice(range.end, lineEnd === -1 ? source.length : lineEnd);\n const nextWords = [...after.matchAll(/\\S+/gu)].slice(0, 5), last = nextWords.at(-1);\n const to = range.end + (last ? last.index + last[0].length : 0);\n return <p className=\"break-words\" data-testid=\"candidate-context\">\n {source.slice(0, from).trim() ? '… ' : ''}{source.slice(from, range.start).replace(/\\s+/gu, ' ')}\n <mark style={{ backgroundColor: markdownMarkerBackground('primary'), color: 'var(--foreground)' }}>{source.slice(range.start, range.end)}</mark>\n {source.slice(range.end, to).replace(/\\s+/gu, ' ')}{source.slice(to).trim() ? ' …' : ''}\n </p>;\n}\n\nfunction CommentIndicator({ id, label, activate }: { readonly id: string; readonly label: string; readonly activate: (id: string) => void }) {\n return <span data-mark-comment={id} role=\"img\" aria-label={label} className=\"inline-flex align-super\" style={{ color: 'var(--muted-foreground)', marginInlineStart: '0.15em' }} onClick={() => activate(id)}>\n <svg viewBox=\"0 0 16 16\" className=\"size-3\" aria-hidden=\"true\"><path d=\"M3 2.5h10v8H7l-4 3v-11Z\" fill=\"none\" stroke=\"currentColor\" /></svg>\n </span>;\n}\n\nfunction AnnotatedSource({ source, placements, choices, active, commentLabel, activate }: { readonly source: string; readonly placements: readonly { mark: Highlight; placement: ReturnType<typeof markdownQuoteResolution.resolve> }[]; readonly choices: Readonly<Record<string, Choice>>; readonly active: string | null; readonly commentLabel: string; readonly activate: (id: string) => void }) {\n const ranges = placements.flatMap(({ mark, placement }) => {\n if (!['accent', 'primary', 'muted'].includes(mark.colour)) return [];\n const choice = choices[mark.id];\n const range = choice?.source === source && placement.kind === 'ambiguous' && placement.candidates.some(r => r.start === choice.range.start && r.end === choice.range.end) ? choice.range : placement.kind === 'exact' || placement.kind === 'moved' ? placement.range : null;\n return range ? [{ mark, range }] : [];\n });\n const cuts = [...new Set([0, source.length, ...ranges.flatMap(r => [r.range.start, r.range.end])])].sort((a, b) => a - b);\n return <pre className=\"whitespace-pre-wrap break-words\">{cuts.slice(0, -1).map((start, index) => {\n const end = cuts[index + 1]!, covering = ranges.filter(r => r.range.start <= start && r.range.end >= end);\n return covering.length ? <Fragment key={start}><mark role=\"button\" tabIndex={0} aria-label={covering.map(r => r.mark.anchor.fallback).join('; ')} style={{ backgroundColor: markdownMarkerBackground(covering[0]!.mark.colour), color: 'var(--foreground)', ...(covering.some(r => r.mark.id === active) ? { outline: 'var(--border-width, 1px) solid var(--ring)' } : {}) }}\n onClick={() => activate(covering[0]!.mark.id)} onKeyDown={event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); activate(covering[0]!.mark.id); } }}>{source.slice(start, end)}</mark>\n {covering.filter(r => r.range.end === end && r.mark.note?.trim()).map(r => <CommentIndicator key={r.mark.id} id={r.mark.id} label={commentLabel} activate={activate} />)}\n </Fragment> : source.slice(start, end);\n })}</pre>;\n}\n",
24
+ "type": "registry:component",
25
+ "target": "components/highlights/highlights.tsx"
26
+ }
27
+ ],
28
+ "type": "registry:block"
29
+ }
@@ -4,13 +4,13 @@
4
4
  "title": "HTML source and passive preview",
5
5
  "description": "Exact HTML source editing with a reconstructed passive preview and borrowed controller. Browser security qualification remains separate.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.3",
8
- "@hachej/boring-files@^0.4.3",
7
+ "@hachej/boring-ui-kit@^0.4.5",
8
+ "@hachej/boring-files@^0.4.5",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/button.json"
14
14
  ],
15
15
  "files": [
16
16
  {
@@ -8,8 +8,8 @@
8
8
  "lucide-react@^1.51.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/theme.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/diagram-viewer.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/theme.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/diagram-viewer.json",
13
13
  "alert",
14
14
  "badge",
15
15
  "bubble",
@@ -4,8 +4,8 @@
4
4
  "title": "Markdown editor",
5
5
  "description": "A host-owned styled wrapper over the concrete borrowed Markdown controller.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.3",
8
- "@hachej/boring-files@^0.4.3",
7
+ "@hachej/boring-ui-kit@^0.4.5",
8
+ "@hachej/boring-files@^0.4.5",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0",
11
11
  "marked@^17.0.6",
@@ -23,7 +23,7 @@
23
23
  "@tiptap/extension-floating-menu@^3.31.4"
24
24
  ],
25
25
  "registryDependencies": [
26
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json"
26
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/button.json"
27
27
  ],
28
28
  "files": [
29
29
  {
@@ -10,7 +10,7 @@
10
10
  "zod@^4.6.5"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/viewers.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/viewers.json",
14
14
  "badge",
15
15
  "toggle",
16
16
  "tooltip"
@@ -36,7 +36,7 @@
36
36
  },
37
37
  {
38
38
  "path": "registry/mockup-viewer/mockup-tailwind.ts",
39
- "content": "// Generated by build-tailwind.mjs from tailwindcss@4.3.3 and registry.json.\n/* MIT License\n\nCopyright (c) Tailwind Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n */\nexport const sources = {\n \"theme\": \"@theme default {\\n --font-sans:\\n -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,\\n sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\\n --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;\\n --font-mono:\\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',\\n monospace;\\n\\n --color-red-50: oklch(97.1% 0.013 17.38);\\n --color-red-100: oklch(93.6% 0.032 17.717);\\n --color-red-200: oklch(88.5% 0.062 18.334);\\n --color-red-300: oklch(80.8% 0.114 19.571);\\n --color-red-400: oklch(70.4% 0.191 22.216);\\n --color-red-500: oklch(63.7% 0.237 25.331);\\n --color-red-600: oklch(57.7% 0.245 27.325);\\n --color-red-700: oklch(50.5% 0.213 27.518);\\n --color-red-800: oklch(44.4% 0.177 26.899);\\n --color-red-900: oklch(39.6% 0.141 25.723);\\n --color-red-950: oklch(25.8% 0.092 26.042);\\n\\n --color-orange-50: oklch(98% 0.016 73.684);\\n --color-orange-100: oklch(95.4% 0.038 75.164);\\n --color-orange-200: oklch(90.1% 0.076 70.697);\\n --color-orange-300: oklch(83.7% 0.128 66.29);\\n --color-orange-400: oklch(75% 0.183 55.934);\\n --color-orange-500: oklch(70.5% 0.213 47.604);\\n --color-orange-600: oklch(64.6% 0.222 41.116);\\n --color-orange-700: oklch(55.3% 0.195 38.402);\\n --color-orange-800: oklch(47% 0.157 37.304);\\n --color-orange-900: oklch(40.8% 0.123 38.172);\\n --color-orange-950: oklch(26.6% 0.079 36.259);\\n\\n --color-amber-50: oklch(98.7% 0.022 95.277);\\n --color-amber-100: oklch(96.2% 0.059 95.617);\\n --color-amber-200: oklch(92.4% 0.12 95.746);\\n --color-amber-300: oklch(87.9% 0.169 91.605);\\n --color-amber-400: oklch(82.8% 0.189 84.429);\\n --color-amber-500: oklch(76.9% 0.188 70.08);\\n --color-amber-600: oklch(66.6% 0.179 58.318);\\n --color-amber-700: oklch(55.5% 0.163 48.998);\\n --color-amber-800: oklch(47.3% 0.137 46.201);\\n --color-amber-900: oklch(41.4% 0.112 45.904);\\n --color-amber-950: oklch(27.9% 0.077 45.635);\\n\\n --color-yellow-50: oklch(98.7% 0.026 102.212);\\n --color-yellow-100: oklch(97.3% 0.071 103.193);\\n --color-yellow-200: oklch(94.5% 0.129 101.54);\\n --color-yellow-300: oklch(90.5% 0.182 98.111);\\n --color-yellow-400: oklch(85.2% 0.199 91.936);\\n --color-yellow-500: oklch(79.5% 0.184 86.047);\\n --color-yellow-600: oklch(68.1% 0.162 75.834);\\n --color-yellow-700: oklch(55.4% 0.135 66.442);\\n --color-yellow-800: oklch(47.6% 0.114 61.907);\\n --color-yellow-900: oklch(42.1% 0.095 57.708);\\n --color-yellow-950: oklch(28.6% 0.066 53.813);\\n\\n --color-lime-50: oklch(98.6% 0.031 120.757);\\n --color-lime-100: oklch(96.7% 0.067 122.328);\\n --color-lime-200: oklch(93.8% 0.127 124.321);\\n --color-lime-300: oklch(89.7% 0.196 126.665);\\n --color-lime-400: oklch(84.1% 0.238 128.85);\\n --color-lime-500: oklch(76.8% 0.233 130.85);\\n --color-lime-600: oklch(64.8% 0.2 131.684);\\n --color-lime-700: oklch(53.2% 0.157 131.589);\\n --color-lime-800: oklch(45.3% 0.124 130.933);\\n --color-lime-900: oklch(40.5% 0.101 131.063);\\n --color-lime-950: oklch(27.4% 0.072 132.109);\\n\\n --color-green-50: oklch(98.2% 0.018 155.826);\\n --color-green-100: oklch(96.2% 0.044 156.743);\\n --color-green-200: oklch(92.5% 0.084 155.995);\\n --color-green-300: oklch(87.1% 0.15 154.449);\\n --color-green-400: oklch(79.2% 0.209 151.711);\\n --color-green-500: oklch(72.3% 0.219 149.579);\\n --color-green-600: oklch(62.7% 0.194 149.214);\\n --color-green-700: oklch(52.7% 0.154 150.069);\\n --color-green-800: oklch(44.8% 0.119 151.328);\\n --color-green-900: oklch(39.3% 0.095 152.535);\\n --color-green-950: oklch(26.6% 0.065 152.934);\\n\\n --color-emerald-50: oklch(97.9% 0.021 166.113);\\n --color-emerald-100: oklch(95% 0.052 163.051);\\n --color-emerald-200: oklch(90.5% 0.093 164.15);\\n --color-emerald-300: oklch(84.5% 0.143 164.978);\\n --color-emerald-400: oklch(76.5% 0.177 163.223);\\n --color-emerald-500: oklch(69.6% 0.17 162.48);\\n --color-emerald-600: oklch(59.6% 0.145 163.225);\\n --color-emerald-700: oklch(50.8% 0.118 165.612);\\n --color-emerald-800: oklch(43.2% 0.095 166.913);\\n --color-emerald-900: oklch(37.8% 0.077 168.94);\\n --color-emerald-950: oklch(26.2% 0.051 172.552);\\n\\n --color-teal-50: oklch(98.4% 0.014 180.72);\\n --color-teal-100: oklch(95.3% 0.051 180.801);\\n --color-teal-200: oklch(91% 0.096 180.426);\\n --color-teal-300: oklch(85.5% 0.138 181.071);\\n --color-teal-400: oklch(77.7% 0.152 181.912);\\n --color-teal-500: oklch(70.4% 0.14 182.503);\\n --color-teal-600: oklch(60% 0.118 184.704);\\n --color-teal-700: oklch(51.1% 0.096 186.391);\\n --color-teal-800: oklch(43.7% 0.078 188.216);\\n --color-teal-900: oklch(38.6% 0.063 188.416);\\n --color-teal-950: oklch(27.7% 0.046 192.524);\\n\\n --color-cyan-50: oklch(98.4% 0.019 200.873);\\n --color-cyan-100: oklch(95.6% 0.045 203.388);\\n --color-cyan-200: oklch(91.7% 0.08 205.041);\\n --color-cyan-300: oklch(86.5% 0.127 207.078);\\n --color-cyan-400: oklch(78.9% 0.154 211.53);\\n --color-cyan-500: oklch(71.5% 0.143 215.221);\\n --color-cyan-600: oklch(60.9% 0.126 221.723);\\n --color-cyan-700: oklch(52% 0.105 223.128);\\n --color-cyan-800: oklch(45% 0.085 224.283);\\n --color-cyan-900: oklch(39.8% 0.07 227.392);\\n --color-cyan-950: oklch(30.2% 0.056 229.695);\\n\\n --color-sky-50: oklch(97.7% 0.013 236.62);\\n --color-sky-100: oklch(95.1% 0.026 236.824);\\n --color-sky-200: oklch(90.1% 0.058 230.902);\\n --color-sky-300: oklch(82.8% 0.111 230.318);\\n --color-sky-400: oklch(74.6% 0.16 232.661);\\n --color-sky-500: oklch(68.5% 0.169 237.323);\\n --color-sky-600: oklch(58.8% 0.158 241.966);\\n --color-sky-700: oklch(50% 0.134 242.749);\\n --color-sky-800: oklch(44.3% 0.11 240.79);\\n --color-sky-900: oklch(39.1% 0.09 240.876);\\n --color-sky-950: oklch(29.3% 0.066 243.157);\\n\\n --color-blue-50: oklch(97% 0.014 254.604);\\n --color-blue-100: oklch(93.2% 0.032 255.585);\\n --color-blue-200: oklch(88.2% 0.059 254.128);\\n --color-blue-300: oklch(80.9% 0.105 251.813);\\n --color-blue-400: oklch(70.7% 0.165 254.624);\\n --color-blue-500: oklch(62.3% 0.214 259.815);\\n --color-blue-600: oklch(54.6% 0.245 262.881);\\n --color-blue-700: oklch(48.8% 0.243 264.376);\\n --color-blue-800: oklch(42.4% 0.199 265.638);\\n --color-blue-900: oklch(37.9% 0.146 265.522);\\n --color-blue-950: oklch(28.2% 0.091 267.935);\\n\\n --color-indigo-50: oklch(96.2% 0.018 272.314);\\n --color-indigo-100: oklch(93% 0.034 272.788);\\n --color-indigo-200: oklch(87% 0.065 274.039);\\n --color-indigo-300: oklch(78.5% 0.115 274.713);\\n --color-indigo-400: oklch(67.3% 0.182 276.935);\\n --color-indigo-500: oklch(58.5% 0.233 277.117);\\n --color-indigo-600: oklch(51.1% 0.262 276.966);\\n --color-indigo-700: oklch(45.7% 0.24 277.023);\\n --color-indigo-800: oklch(39.8% 0.195 277.366);\\n --color-indigo-900: oklch(35.9% 0.144 278.697);\\n --color-indigo-950: oklch(25.7% 0.09 281.288);\\n\\n --color-violet-50: oklch(96.9% 0.016 293.756);\\n --color-violet-100: oklch(94.3% 0.029 294.588);\\n --color-violet-200: oklch(89.4% 0.057 293.283);\\n --color-violet-300: oklch(81.1% 0.111 293.571);\\n --color-violet-400: oklch(70.2% 0.183 293.541);\\n --color-violet-500: oklch(60.6% 0.25 292.717);\\n --color-violet-600: oklch(54.1% 0.281 293.009);\\n --color-violet-700: oklch(49.1% 0.27 292.581);\\n --color-violet-800: oklch(43.2% 0.232 292.759);\\n --color-violet-900: oklch(38% 0.189 293.745);\\n --color-violet-950: oklch(28.3% 0.141 291.089);\\n\\n --color-purple-50: oklch(97.7% 0.014 308.299);\\n --color-purple-100: oklch(94.6% 0.033 307.174);\\n --color-purple-200: oklch(90.2% 0.063 306.703);\\n --color-purple-300: oklch(82.7% 0.119 306.383);\\n --color-purple-400: oklch(71.4% 0.203 305.504);\\n --color-purple-500: oklch(62.7% 0.265 303.9);\\n --color-purple-600: oklch(55.8% 0.288 302.321);\\n --color-purple-700: oklch(49.6% 0.265 301.924);\\n --color-purple-800: oklch(43.8% 0.218 303.724);\\n --color-purple-900: oklch(38.1% 0.176 304.987);\\n --color-purple-950: oklch(29.1% 0.149 302.717);\\n\\n --color-fuchsia-50: oklch(97.7% 0.017 320.058);\\n --color-fuchsia-100: oklch(95.2% 0.037 318.852);\\n --color-fuchsia-200: oklch(90.3% 0.076 319.62);\\n --color-fuchsia-300: oklch(83.3% 0.145 321.434);\\n --color-fuchsia-400: oklch(74% 0.238 322.16);\\n --color-fuchsia-500: oklch(66.7% 0.295 322.15);\\n --color-fuchsia-600: oklch(59.1% 0.293 322.896);\\n --color-fuchsia-700: oklch(51.8% 0.253 323.949);\\n --color-fuchsia-800: oklch(45.2% 0.211 324.591);\\n --color-fuchsia-900: oklch(40.1% 0.17 325.612);\\n --color-fuchsia-950: oklch(29.3% 0.136 325.661);\\n\\n --color-pink-50: oklch(97.1% 0.014 343.198);\\n --color-pink-100: oklch(94.8% 0.028 342.258);\\n --color-pink-200: oklch(89.9% 0.061 343.231);\\n --color-pink-300: oklch(82.3% 0.12 346.018);\\n --color-pink-400: oklch(71.8% 0.202 349.761);\\n --color-pink-500: oklch(65.6% 0.241 354.308);\\n --color-pink-600: oklch(59.2% 0.249 0.584);\\n --color-pink-700: oklch(52.5% 0.223 3.958);\\n --color-pink-800: oklch(45.9% 0.187 3.815);\\n --color-pink-900: oklch(40.8% 0.153 2.432);\\n --color-pink-950: oklch(28.4% 0.109 3.907);\\n\\n --color-rose-50: oklch(96.9% 0.015 12.422);\\n --color-rose-100: oklch(94.1% 0.03 12.58);\\n --color-rose-200: oklch(89.2% 0.058 10.001);\\n --color-rose-300: oklch(81% 0.117 11.638);\\n --color-rose-400: oklch(71.2% 0.194 13.428);\\n --color-rose-500: oklch(64.5% 0.246 16.439);\\n --color-rose-600: oklch(58.6% 0.253 17.585);\\n --color-rose-700: oklch(51.4% 0.222 16.935);\\n --color-rose-800: oklch(45.5% 0.188 13.697);\\n --color-rose-900: oklch(41% 0.159 10.272);\\n --color-rose-950: oklch(27.1% 0.105 12.094);\\n\\n --color-slate-50: oklch(98.4% 0.003 247.858);\\n --color-slate-100: oklch(96.8% 0.007 247.896);\\n --color-slate-200: oklch(92.9% 0.013 255.508);\\n --color-slate-300: oklch(86.9% 0.022 252.894);\\n --color-slate-400: oklch(70.4% 0.04 256.788);\\n --color-slate-500: oklch(55.4% 0.046 257.417);\\n --color-slate-600: oklch(44.6% 0.043 257.281);\\n --color-slate-700: oklch(37.2% 0.044 257.287);\\n --color-slate-800: oklch(27.9% 0.041 260.031);\\n --color-slate-900: oklch(20.8% 0.042 265.755);\\n --color-slate-950: oklch(12.9% 0.042 264.695);\\n\\n --color-gray-50: oklch(98.5% 0.002 247.839);\\n --color-gray-100: oklch(96.7% 0.003 264.542);\\n --color-gray-200: oklch(92.8% 0.006 264.531);\\n --color-gray-300: oklch(87.2% 0.01 258.338);\\n --color-gray-400: oklch(70.7% 0.022 261.325);\\n --color-gray-500: oklch(55.1% 0.027 264.364);\\n --color-gray-600: oklch(44.6% 0.03 256.802);\\n --color-gray-700: oklch(37.3% 0.034 259.733);\\n --color-gray-800: oklch(27.8% 0.033 256.848);\\n --color-gray-900: oklch(21% 0.034 264.665);\\n --color-gray-950: oklch(13% 0.028 261.692);\\n\\n --color-zinc-50: oklch(98.5% 0 none);\\n --color-zinc-100: oklch(96.7% 0.001 286.375);\\n --color-zinc-200: oklch(92% 0.004 286.32);\\n --color-zinc-300: oklch(87.1% 0.006 286.286);\\n --color-zinc-400: oklch(70.5% 0.015 286.067);\\n --color-zinc-500: oklch(55.2% 0.016 285.938);\\n --color-zinc-600: oklch(44.2% 0.017 285.786);\\n --color-zinc-700: oklch(37% 0.013 285.805);\\n --color-zinc-800: oklch(27.4% 0.006 286.033);\\n --color-zinc-900: oklch(21% 0.006 285.885);\\n --color-zinc-950: oklch(14.1% 0.005 285.823);\\n\\n --color-neutral-50: oklch(98.5% 0 none);\\n --color-neutral-100: oklch(97% 0 none);\\n --color-neutral-200: oklch(92.2% 0 none);\\n --color-neutral-300: oklch(87% 0 none);\\n --color-neutral-400: oklch(70.8% 0 none);\\n --color-neutral-500: oklch(55.6% 0 none);\\n --color-neutral-600: oklch(43.9% 0 none);\\n --color-neutral-700: oklch(37.1% 0 none);\\n --color-neutral-800: oklch(26.9% 0 none);\\n --color-neutral-900: oklch(20.5% 0 none);\\n --color-neutral-950: oklch(14.5% 0 none);\\n\\n --color-stone-50: oklch(98.5% 0.001 106.423);\\n --color-stone-100: oklch(97% 0.001 106.424);\\n --color-stone-200: oklch(92.3% 0.003 48.717);\\n --color-stone-300: oklch(86.9% 0.005 56.366);\\n --color-stone-400: oklch(70.9% 0.01 56.259);\\n --color-stone-500: oklch(55.3% 0.013 58.071);\\n --color-stone-600: oklch(44.4% 0.011 73.639);\\n --color-stone-700: oklch(37.4% 0.01 67.558);\\n --color-stone-800: oklch(26.8% 0.007 34.298);\\n --color-stone-900: oklch(21.6% 0.006 56.043);\\n --color-stone-950: oklch(14.7% 0.004 49.25);\\n\\n --color-mauve-50: oklch(98.5% 0 none);\\n --color-mauve-100: oklch(96% 0.003 325.6);\\n --color-mauve-200: oklch(92.2% 0.005 325.62);\\n --color-mauve-300: oklch(86.5% 0.012 325.68);\\n --color-mauve-400: oklch(71.1% 0.019 323.02);\\n --color-mauve-500: oklch(54.2% 0.034 322.5);\\n --color-mauve-600: oklch(43.5% 0.029 321.78);\\n --color-mauve-700: oklch(36.4% 0.029 323.89);\\n --color-mauve-800: oklch(26.3% 0.024 320.12);\\n --color-mauve-900: oklch(21.2% 0.019 322.12);\\n --color-mauve-950: oklch(14.5% 0.008 326);\\n\\n --color-olive-50: oklch(98.8% 0.003 106.5);\\n --color-olive-100: oklch(96.6% 0.005 106.5);\\n --color-olive-200: oklch(93% 0.007 106.5);\\n --color-olive-300: oklch(88% 0.011 106.6);\\n --color-olive-400: oklch(73.7% 0.021 106.9);\\n --color-olive-500: oklch(58% 0.031 107.3);\\n --color-olive-600: oklch(46.6% 0.025 107.3);\\n --color-olive-700: oklch(39.4% 0.023 107.4);\\n --color-olive-800: oklch(28.6% 0.016 107.4);\\n --color-olive-900: oklch(22.8% 0.013 107.4);\\n --color-olive-950: oklch(15.3% 0.006 107.1);\\n\\n --color-mist-50: oklch(98.7% 0.002 197.1);\\n --color-mist-100: oklch(96.3% 0.002 197.1);\\n --color-mist-200: oklch(92.5% 0.005 214.3);\\n --color-mist-300: oklch(87.2% 0.007 219.6);\\n --color-mist-400: oklch(72.3% 0.014 214.4);\\n --color-mist-500: oklch(56% 0.021 213.5);\\n --color-mist-600: oklch(45% 0.017 213.2);\\n --color-mist-700: oklch(37.8% 0.015 216);\\n --color-mist-800: oklch(27.5% 0.011 216.9);\\n --color-mist-900: oklch(21.8% 0.008 223.9);\\n --color-mist-950: oklch(14.8% 0.004 228.8);\\n\\n --color-taupe-50: oklch(98.6% 0.002 67.8);\\n --color-taupe-100: oklch(96% 0.002 17.2);\\n --color-taupe-200: oklch(92.2% 0.005 34.3);\\n --color-taupe-300: oklch(86.8% 0.007 39.5);\\n --color-taupe-400: oklch(71.4% 0.014 41.2);\\n --color-taupe-500: oklch(54.7% 0.021 43.1);\\n --color-taupe-600: oklch(43.8% 0.017 39.3);\\n --color-taupe-700: oklch(36.7% 0.016 35.7);\\n --color-taupe-800: oklch(26.8% 0.011 36.5);\\n --color-taupe-900: oklch(21.4% 0.009 43.1);\\n --color-taupe-950: oklch(14.7% 0.004 49.3);\\n\\n --color-black: #000;\\n --color-white: #fff;\\n\\n --spacing: 0.25rem;\\n\\n --breakpoint-sm: 40rem;\\n --breakpoint-md: 48rem;\\n --breakpoint-lg: 64rem;\\n --breakpoint-xl: 80rem;\\n --breakpoint-2xl: 96rem;\\n\\n --container-3xs: 16rem;\\n --container-2xs: 18rem;\\n --container-xs: 20rem;\\n --container-sm: 24rem;\\n --container-md: 28rem;\\n --container-lg: 32rem;\\n --container-xl: 36rem;\\n --container-2xl: 42rem;\\n --container-3xl: 48rem;\\n --container-4xl: 56rem;\\n --container-5xl: 64rem;\\n --container-6xl: 72rem;\\n --container-7xl: 80rem;\\n\\n --text-xs: 0.75rem;\\n --text-xs--line-height: calc(1 / 0.75);\\n --text-sm: 0.875rem;\\n --text-sm--line-height: calc(1.25 / 0.875);\\n --text-base: 1rem;\\n --text-base--line-height: calc(1.5 / 1);\\n --text-lg: 1.125rem;\\n --text-lg--line-height: calc(1.75 / 1.125);\\n --text-xl: 1.25rem;\\n --text-xl--line-height: calc(1.75 / 1.25);\\n --text-2xl: 1.5rem;\\n --text-2xl--line-height: calc(2 / 1.5);\\n --text-3xl: 1.875rem;\\n --text-3xl--line-height: calc(2.25 / 1.875);\\n --text-4xl: 2.25rem;\\n --text-4xl--line-height: calc(2.5 / 2.25);\\n --text-5xl: 3rem;\\n --text-5xl--line-height: 1;\\n --text-6xl: 3.75rem;\\n --text-6xl--line-height: 1;\\n --text-7xl: 4.5rem;\\n --text-7xl--line-height: 1;\\n --text-8xl: 6rem;\\n --text-8xl--line-height: 1;\\n --text-9xl: 8rem;\\n --text-9xl--line-height: 1;\\n\\n --font-weight-thin: 100;\\n --font-weight-extralight: 200;\\n --font-weight-light: 300;\\n --font-weight-normal: 400;\\n --font-weight-medium: 500;\\n --font-weight-semibold: 600;\\n --font-weight-bold: 700;\\n --font-weight-extrabold: 800;\\n --font-weight-black: 900;\\n\\n --tracking-tighter: -0.05em;\\n --tracking-tight: -0.025em;\\n --tracking-normal: 0em;\\n --tracking-wide: 0.025em;\\n --tracking-wider: 0.05em;\\n --tracking-widest: 0.1em;\\n\\n --leading-tight: 1.25;\\n --leading-snug: 1.375;\\n --leading-normal: 1.5;\\n --leading-relaxed: 1.625;\\n --leading-loose: 2;\\n\\n --radius-xs: 0.125rem;\\n --radius-sm: 0.25rem;\\n --radius-md: 0.375rem;\\n --radius-lg: 0.5rem;\\n --radius-xl: 0.75rem;\\n --radius-2xl: 1rem;\\n --radius-3xl: 1.5rem;\\n --radius-4xl: 2rem;\\n\\n --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);\\n --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);\\n --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\\n --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\\n --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);\\n --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);\\n\\n --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);\\n\\n --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);\\n --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);\\n --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);\\n --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);\\n --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);\\n --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);\\n\\n --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);\\n --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);\\n --text-shadow-sm:\\n 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);\\n --text-shadow-md:\\n 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);\\n --text-shadow-lg:\\n 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);\\n\\n --ease-in: cubic-bezier(0.4, 0, 1, 1);\\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\\n\\n --animate-spin: spin 1s linear infinite;\\n --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;\\n --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\\n --animate-bounce: bounce 1s infinite;\\n\\n @keyframes spin {\\n to {\\n transform: rotate(360deg);\\n }\\n }\\n\\n @keyframes ping {\\n 75%,\\n 100% {\\n transform: scale(2);\\n opacity: 0;\\n }\\n }\\n\\n @keyframes pulse {\\n 50% {\\n opacity: 0.5;\\n }\\n }\\n\\n @keyframes bounce {\\n 0%,\\n 100% {\\n transform: translateY(-25%);\\n animation-timing-function: cubic-bezier(0.8, 0, 1, 1);\\n }\\n\\n 50% {\\n transform: none;\\n animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\\n }\\n }\\n\\n --blur-xs: 4px;\\n --blur-sm: 8px;\\n --blur-md: 12px;\\n --blur-lg: 16px;\\n --blur-xl: 24px;\\n --blur-2xl: 40px;\\n --blur-3xl: 64px;\\n\\n --perspective-dramatic: 100px;\\n --perspective-near: 300px;\\n --perspective-normal: 500px;\\n --perspective-midrange: 800px;\\n --perspective-distant: 1200px;\\n\\n --aspect-video: 16 / 9;\\n\\n --default-transition-duration: 150ms;\\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n --default-font-family: --theme(--font-sans, initial);\\n --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);\\n --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);\\n --default-mono-font-family: --theme(--font-mono, initial);\\n --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);\\n --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);\\n}\\n\\n/* Deprecated */\\n@theme default inline reference {\\n --blur: 8px;\\n --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);\\n --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06);\\n --radius: 0.25rem;\\n --max-width-prose: 65ch;\\n}\\n\",\n \"preflight\": \"/*\\n 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\\n 2. Remove default margins and padding\\n 3. Reset all borders.\\n*/\\n\\n*,\\n::after,\\n::before,\\n::backdrop,\\n::file-selector-button {\\n box-sizing: border-box; /* 1 */\\n margin: 0; /* 2 */\\n padding: 0; /* 2 */\\n border: 0 solid; /* 3 */\\n}\\n\\n/*\\n 1. Use a consistent sensible line-height in all browsers.\\n 2. Prevent adjustments of font size after orientation changes in iOS.\\n 3. Use a more readable tab size.\\n 4. Use the user's configured `sans` font-family by default.\\n 5. Use the user's configured `sans` font-feature-settings by default.\\n 6. Use the user's configured `sans` font-variation-settings by default.\\n 7. Disable tap highlights on iOS.\\n*/\\n\\nhtml,\\n:host {\\n line-height: 1.5; /* 1 */\\n -webkit-text-size-adjust: 100%; /* 2 */\\n tab-size: 4; /* 3 */\\n font-family: --theme(\\n --default-font-family,\\n -apple-system,\\n BlinkMacSystemFont,\\n 'Segoe UI',\\n Roboto,\\n 'Helvetica Neue',\\n 'Noto Sans',\\n Arial,\\n sans-serif,\\n 'Apple Color Emoji',\\n 'Segoe UI Emoji',\\n 'Segoe UI Symbol',\\n 'Noto Color Emoji'\\n ); /* 4 */\\n font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */\\n font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */\\n -webkit-tap-highlight-color: transparent; /* 7 */\\n}\\n\\n/*\\n 1. Add the correct height in Firefox.\\n 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\\n 3. Reset the default border style to a 1px solid border.\\n*/\\n\\nhr {\\n height: 0; /* 1 */\\n color: inherit; /* 2 */\\n border-top-width: 1px; /* 3 */\\n}\\n\\n/*\\n Add the correct text decoration in Chrome, Edge, and Safari.\\n*/\\n\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\n\\n/*\\n Remove the default font size and weight for headings.\\n*/\\n\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\n\\n/*\\n Reset links to optimize for opt-in styling instead of opt-out.\\n*/\\n\\na {\\n color: inherit;\\n -webkit-text-decoration: inherit;\\n text-decoration: inherit;\\n}\\n\\n/*\\n Add the correct font weight in Edge and Safari.\\n*/\\n\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\n\\n/*\\n 1. Use the user's configured `mono` font-family by default.\\n 2. Use the user's configured `mono` font-feature-settings by default.\\n 3. Use the user's configured `mono` font-variation-settings by default.\\n 4. Correct the odd `em` font sizing in all browsers.\\n*/\\n\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family: --theme(\\n --default-mono-font-family,\\n ui-monospace,\\n SFMono-Regular,\\n Menlo,\\n Monaco,\\n Consolas,\\n 'Liberation Mono',\\n 'Courier New',\\n monospace\\n ); /* 1 */\\n font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */\\n font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */\\n font-size: 1em; /* 4 */\\n}\\n\\n/*\\n Add the correct font size in all browsers.\\n*/\\n\\nsmall {\\n font-size: 80%;\\n}\\n\\n/*\\n Prevent `sub` and `sup` elements from affecting the line height in all browsers.\\n*/\\n\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\n\\nsub {\\n bottom: -0.25em;\\n}\\n\\nsup {\\n top: -0.5em;\\n}\\n\\n/*\\n 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\\n 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\\n 3. Remove gaps between table borders by default.\\n*/\\n\\ntable {\\n text-indent: 0; /* 1 */\\n border-color: inherit; /* 2 */\\n border-collapse: collapse; /* 3 */\\n}\\n\\n/*\\n Use the modern Firefox focus style for all focusable elements.\\n*/\\n\\n:-moz-focusring:where(:not(iframe)) {\\n outline: auto;\\n}\\n\\n/*\\n Add the correct vertical alignment in Chrome and Firefox.\\n*/\\n\\nprogress {\\n vertical-align: baseline;\\n}\\n\\n/*\\n Add the correct display in Chrome and Safari.\\n*/\\n\\nsummary {\\n display: list-item;\\n}\\n\\n/*\\n Make lists unstyled by default.\\n*/\\n\\nol,\\nul,\\nmenu {\\n list-style: none;\\n}\\n\\n/*\\n 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\\n 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\\n This can trigger a poorly considered lint error in some tools but is included by design.\\n*/\\n\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block; /* 1 */\\n vertical-align: middle; /* 2 */\\n}\\n\\n/*\\n Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\\n*/\\n\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n\\n/*\\n 1. Inherit font styles in all browsers.\\n 2. Remove border radius in all browsers.\\n 3. Remove background color in all browsers.\\n 4. Ensure consistent opacity for disabled states in all browsers.\\n*/\\n\\nbutton,\\ninput,\\nselect,\\noptgroup,\\ntextarea,\\n::file-selector-button {\\n font: inherit; /* 1 */\\n font-feature-settings: inherit; /* 1 */\\n font-variation-settings: inherit; /* 1 */\\n letter-spacing: inherit; /* 1 */\\n color: inherit; /* 1 */\\n border-radius: 0; /* 2 */\\n background-color: transparent; /* 3 */\\n opacity: 1; /* 4 */\\n}\\n\\n/*\\n Restore default font weight.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup {\\n font-weight: bolder;\\n}\\n\\n/*\\n Restore indentation.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup option {\\n padding-inline-start: 20px;\\n}\\n\\n/*\\n Restore space after button.\\n*/\\n\\n::file-selector-button {\\n margin-inline-end: 4px;\\n}\\n\\n/*\\n Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\\n*/\\n\\n::placeholder {\\n opacity: 1;\\n}\\n\\n/*\\n Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not\\n crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194)\\n*/\\n\\n@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or\\n (contain-intrinsic-size: 1px) /* Safari 17+ */ {\\n ::placeholder {\\n color: color-mix(in oklab, currentcolor 50%, transparent);\\n }\\n}\\n\\n/*\\n Prevent resizing textareas horizontally by default.\\n*/\\n\\ntextarea {\\n resize: vertical;\\n}\\n\\n/*\\n Remove the inner padding in Chrome and Safari on macOS.\\n*/\\n\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n\\n/*\\n 1. Ensure date/time inputs have the same height when empty in iOS Safari.\\n 2. Ensure text alignment can be changed on date/time inputs in iOS Safari.\\n*/\\n\\n::-webkit-date-and-time-value {\\n min-height: 1lh; /* 1 */\\n text-align: inherit; /* 2 */\\n}\\n\\n/*\\n Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.\\n*/\\n\\n::-webkit-datetime-edit {\\n display: inline-flex;\\n}\\n\\n/*\\n Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.\\n*/\\n\\n::-webkit-datetime-edit-fields-wrapper {\\n padding: 0;\\n}\\n\\n::-webkit-datetime-edit,\\n::-webkit-datetime-edit-year-field,\\n::-webkit-datetime-edit-month-field,\\n::-webkit-datetime-edit-day-field,\\n::-webkit-datetime-edit-hour-field,\\n::-webkit-datetime-edit-minute-field,\\n::-webkit-datetime-edit-second-field,\\n::-webkit-datetime-edit-millisecond-field,\\n::-webkit-datetime-edit-meridiem-field {\\n padding-block: 0;\\n}\\n\\n/*\\n Center dropdown marker shown on inputs with paired `<datalist>`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499)\\n*/\\n\\n::-webkit-calendar-picker-indicator {\\n line-height: 1;\\n}\\n\\n/*\\n Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\\n*/\\n\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\n\\n/*\\n Correct the inability to style the border radius in iOS Safari.\\n*/\\n\\nbutton,\\ninput:where([type='button'], [type='reset'], [type='submit']),\\n::file-selector-button {\\n appearance: button;\\n}\\n\\n/*\\n Correct the cursor style of increment and decrement buttons in Safari.\\n*/\\n\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n\\n/*\\n Make elements with the HTML hidden attribute stay hidden by default.\\n*/\\n\\n[hidden]:where(:not([hidden='until-found'])) {\\n display: none !important;\\n}\\n\",\n \"utilities\": \"@tailwind utilities;\\n\",\n \"bindings\": \"@theme inline { --color-background: var(--background);\\n--color-foreground: var(--foreground);\\n--color-card: var(--card);\\n--color-card-foreground: var(--card-foreground);\\n--color-popover: var(--popover);\\n--color-popover-foreground: var(--popover-foreground);\\n--color-primary: var(--primary);\\n--color-primary-foreground: var(--primary-foreground);\\n--color-secondary: var(--secondary);\\n--color-secondary-foreground: var(--secondary-foreground);\\n--color-muted: var(--muted);\\n--color-muted-foreground: var(--muted-foreground);\\n--color-accent: var(--accent);\\n--color-accent-foreground: var(--accent-foreground);\\n--color-destructive: var(--destructive);\\n--color-border: var(--border);\\n--color-input: var(--input);\\n--color-ring: var(--ring);\\n--radius-sm: calc(var(--radius) - 4px);\\n--radius-md: calc(var(--radius) - 2px);\\n--radius-lg: var(--radius);\\n--radius-xl: calc(var(--radius) + 4px);\\n--radius-2xl: calc(var(--radius) * 1.8);\\n--radius-3xl: calc(var(--radius) * 2.2);\\n--default-border-width: var(--border-width, 1px);\\n--shadow-xs: var(--shadow-xs);\\n--shadow-sm: var(--shadow-sm);\\n--shadow-md: var(--shadow-md);\\n--shadow-lg: var(--shadow-lg);\\n--shadow-xl: var(--shadow-xl);\\n--shadow-2xl: var(--shadow-2xl); }\"\n};\n",
39
+ "content": "// Generated by build-tailwind.mjs from tailwindcss@4.3.3 and registry.json.\n/* MIT License\n\nCopyright (c) Tailwind Labs, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n */\nexport const sources = {\n \"theme\": \"@theme default {\\n --font-sans:\\n -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', Arial,\\n sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';\\n --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;\\n --font-mono:\\n ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',\\n monospace;\\n\\n --color-red-50: oklch(97.1% 0.013 17.38);\\n --color-red-100: oklch(93.6% 0.032 17.717);\\n --color-red-200: oklch(88.5% 0.062 18.334);\\n --color-red-300: oklch(80.8% 0.114 19.571);\\n --color-red-400: oklch(70.4% 0.191 22.216);\\n --color-red-500: oklch(63.7% 0.237 25.331);\\n --color-red-600: oklch(57.7% 0.245 27.325);\\n --color-red-700: oklch(50.5% 0.213 27.518);\\n --color-red-800: oklch(44.4% 0.177 26.899);\\n --color-red-900: oklch(39.6% 0.141 25.723);\\n --color-red-950: oklch(25.8% 0.092 26.042);\\n\\n --color-orange-50: oklch(98% 0.016 73.684);\\n --color-orange-100: oklch(95.4% 0.038 75.164);\\n --color-orange-200: oklch(90.1% 0.076 70.697);\\n --color-orange-300: oklch(83.7% 0.128 66.29);\\n --color-orange-400: oklch(75% 0.183 55.934);\\n --color-orange-500: oklch(70.5% 0.213 47.604);\\n --color-orange-600: oklch(64.6% 0.222 41.116);\\n --color-orange-700: oklch(55.3% 0.195 38.402);\\n --color-orange-800: oklch(47% 0.157 37.304);\\n --color-orange-900: oklch(40.8% 0.123 38.172);\\n --color-orange-950: oklch(26.6% 0.079 36.259);\\n\\n --color-amber-50: oklch(98.7% 0.022 95.277);\\n --color-amber-100: oklch(96.2% 0.059 95.617);\\n --color-amber-200: oklch(92.4% 0.12 95.746);\\n --color-amber-300: oklch(87.9% 0.169 91.605);\\n --color-amber-400: oklch(82.8% 0.189 84.429);\\n --color-amber-500: oklch(76.9% 0.188 70.08);\\n --color-amber-600: oklch(66.6% 0.179 58.318);\\n --color-amber-700: oklch(55.5% 0.163 48.998);\\n --color-amber-800: oklch(47.3% 0.137 46.201);\\n --color-amber-900: oklch(41.4% 0.112 45.904);\\n --color-amber-950: oklch(27.9% 0.077 45.635);\\n\\n --color-yellow-50: oklch(98.7% 0.026 102.212);\\n --color-yellow-100: oklch(97.3% 0.071 103.193);\\n --color-yellow-200: oklch(94.5% 0.129 101.54);\\n --color-yellow-300: oklch(90.5% 0.182 98.111);\\n --color-yellow-400: oklch(85.2% 0.199 91.936);\\n --color-yellow-500: oklch(79.5% 0.184 86.047);\\n --color-yellow-600: oklch(68.1% 0.162 75.834);\\n --color-yellow-700: oklch(55.4% 0.135 66.442);\\n --color-yellow-800: oklch(47.6% 0.114 61.907);\\n --color-yellow-900: oklch(42.1% 0.095 57.708);\\n --color-yellow-950: oklch(28.6% 0.066 53.813);\\n\\n --color-lime-50: oklch(98.6% 0.031 120.757);\\n --color-lime-100: oklch(96.7% 0.067 122.328);\\n --color-lime-200: oklch(93.8% 0.127 124.321);\\n --color-lime-300: oklch(89.7% 0.196 126.665);\\n --color-lime-400: oklch(84.1% 0.238 128.85);\\n --color-lime-500: oklch(76.8% 0.233 130.85);\\n --color-lime-600: oklch(64.8% 0.2 131.684);\\n --color-lime-700: oklch(53.2% 0.157 131.589);\\n --color-lime-800: oklch(45.3% 0.124 130.933);\\n --color-lime-900: oklch(40.5% 0.101 131.063);\\n --color-lime-950: oklch(27.4% 0.072 132.109);\\n\\n --color-green-50: oklch(98.2% 0.018 155.826);\\n --color-green-100: oklch(96.2% 0.044 156.743);\\n --color-green-200: oklch(92.5% 0.084 155.995);\\n --color-green-300: oklch(87.1% 0.15 154.449);\\n --color-green-400: oklch(79.2% 0.209 151.711);\\n --color-green-500: oklch(72.3% 0.219 149.579);\\n --color-green-600: oklch(62.7% 0.194 149.214);\\n --color-green-700: oklch(52.7% 0.154 150.069);\\n --color-green-800: oklch(44.8% 0.119 151.328);\\n --color-green-900: oklch(39.3% 0.095 152.535);\\n --color-green-950: oklch(26.6% 0.065 152.934);\\n\\n --color-emerald-50: oklch(97.9% 0.021 166.113);\\n --color-emerald-100: oklch(95% 0.052 163.051);\\n --color-emerald-200: oklch(90.5% 0.093 164.15);\\n --color-emerald-300: oklch(84.5% 0.143 164.978);\\n --color-emerald-400: oklch(76.5% 0.177 163.223);\\n --color-emerald-500: oklch(69.6% 0.17 162.48);\\n --color-emerald-600: oklch(59.6% 0.145 163.225);\\n --color-emerald-700: oklch(50.8% 0.118 165.612);\\n --color-emerald-800: oklch(43.2% 0.095 166.913);\\n --color-emerald-900: oklch(37.8% 0.077 168.94);\\n --color-emerald-950: oklch(26.2% 0.051 172.552);\\n\\n --color-teal-50: oklch(98.4% 0.014 180.72);\\n --color-teal-100: oklch(95.3% 0.051 180.801);\\n --color-teal-200: oklch(91% 0.096 180.426);\\n --color-teal-300: oklch(85.5% 0.138 181.071);\\n --color-teal-400: oklch(77.7% 0.152 181.912);\\n --color-teal-500: oklch(70.4% 0.14 182.503);\\n --color-teal-600: oklch(60% 0.118 184.704);\\n --color-teal-700: oklch(51.1% 0.096 186.391);\\n --color-teal-800: oklch(43.7% 0.078 188.216);\\n --color-teal-900: oklch(38.6% 0.063 188.416);\\n --color-teal-950: oklch(27.7% 0.046 192.524);\\n\\n --color-cyan-50: oklch(98.4% 0.019 200.873);\\n --color-cyan-100: oklch(95.6% 0.045 203.388);\\n --color-cyan-200: oklch(91.7% 0.08 205.041);\\n --color-cyan-300: oklch(86.5% 0.127 207.078);\\n --color-cyan-400: oklch(78.9% 0.154 211.53);\\n --color-cyan-500: oklch(71.5% 0.143 215.221);\\n --color-cyan-600: oklch(60.9% 0.126 221.723);\\n --color-cyan-700: oklch(52% 0.105 223.128);\\n --color-cyan-800: oklch(45% 0.085 224.283);\\n --color-cyan-900: oklch(39.8% 0.07 227.392);\\n --color-cyan-950: oklch(30.2% 0.056 229.695);\\n\\n --color-sky-50: oklch(97.7% 0.013 236.62);\\n --color-sky-100: oklch(95.1% 0.026 236.824);\\n --color-sky-200: oklch(90.1% 0.058 230.902);\\n --color-sky-300: oklch(82.8% 0.111 230.318);\\n --color-sky-400: oklch(74.6% 0.16 232.661);\\n --color-sky-500: oklch(68.5% 0.169 237.323);\\n --color-sky-600: oklch(58.8% 0.158 241.966);\\n --color-sky-700: oklch(50% 0.134 242.749);\\n --color-sky-800: oklch(44.3% 0.11 240.79);\\n --color-sky-900: oklch(39.1% 0.09 240.876);\\n --color-sky-950: oklch(29.3% 0.066 243.157);\\n\\n --color-blue-50: oklch(97% 0.014 254.604);\\n --color-blue-100: oklch(93.2% 0.032 255.585);\\n --color-blue-200: oklch(88.2% 0.059 254.128);\\n --color-blue-300: oklch(80.9% 0.105 251.813);\\n --color-blue-400: oklch(70.7% 0.165 254.624);\\n --color-blue-500: oklch(62.3% 0.214 259.815);\\n --color-blue-600: oklch(54.6% 0.245 262.881);\\n --color-blue-700: oklch(48.8% 0.243 264.376);\\n --color-blue-800: oklch(42.4% 0.199 265.638);\\n --color-blue-900: oklch(37.9% 0.146 265.522);\\n --color-blue-950: oklch(28.2% 0.091 267.935);\\n\\n --color-indigo-50: oklch(96.2% 0.018 272.314);\\n --color-indigo-100: oklch(93% 0.034 272.788);\\n --color-indigo-200: oklch(87% 0.065 274.039);\\n --color-indigo-300: oklch(78.5% 0.115 274.713);\\n --color-indigo-400: oklch(67.3% 0.182 276.935);\\n --color-indigo-500: oklch(58.5% 0.233 277.117);\\n --color-indigo-600: oklch(51.1% 0.262 276.966);\\n --color-indigo-700: oklch(45.7% 0.24 277.023);\\n --color-indigo-800: oklch(39.8% 0.195 277.366);\\n --color-indigo-900: oklch(35.9% 0.144 278.697);\\n --color-indigo-950: oklch(25.7% 0.09 281.288);\\n\\n --color-violet-50: oklch(96.9% 0.016 293.756);\\n --color-violet-100: oklch(94.3% 0.029 294.588);\\n --color-violet-200: oklch(89.4% 0.057 293.283);\\n --color-violet-300: oklch(81.1% 0.111 293.571);\\n --color-violet-400: oklch(70.2% 0.183 293.541);\\n --color-violet-500: oklch(60.6% 0.25 292.717);\\n --color-violet-600: oklch(54.1% 0.281 293.009);\\n --color-violet-700: oklch(49.1% 0.27 292.581);\\n --color-violet-800: oklch(43.2% 0.232 292.759);\\n --color-violet-900: oklch(38% 0.189 293.745);\\n --color-violet-950: oklch(28.3% 0.141 291.089);\\n\\n --color-purple-50: oklch(97.7% 0.014 308.299);\\n --color-purple-100: oklch(94.6% 0.033 307.174);\\n --color-purple-200: oklch(90.2% 0.063 306.703);\\n --color-purple-300: oklch(82.7% 0.119 306.383);\\n --color-purple-400: oklch(71.4% 0.203 305.504);\\n --color-purple-500: oklch(62.7% 0.265 303.9);\\n --color-purple-600: oklch(55.8% 0.288 302.321);\\n --color-purple-700: oklch(49.6% 0.265 301.924);\\n --color-purple-800: oklch(43.8% 0.218 303.724);\\n --color-purple-900: oklch(38.1% 0.176 304.987);\\n --color-purple-950: oklch(29.1% 0.149 302.717);\\n\\n --color-fuchsia-50: oklch(97.7% 0.017 320.058);\\n --color-fuchsia-100: oklch(95.2% 0.037 318.852);\\n --color-fuchsia-200: oklch(90.3% 0.076 319.62);\\n --color-fuchsia-300: oklch(83.3% 0.145 321.434);\\n --color-fuchsia-400: oklch(74% 0.238 322.16);\\n --color-fuchsia-500: oklch(66.7% 0.295 322.15);\\n --color-fuchsia-600: oklch(59.1% 0.293 322.896);\\n --color-fuchsia-700: oklch(51.8% 0.253 323.949);\\n --color-fuchsia-800: oklch(45.2% 0.211 324.591);\\n --color-fuchsia-900: oklch(40.1% 0.17 325.612);\\n --color-fuchsia-950: oklch(29.3% 0.136 325.661);\\n\\n --color-pink-50: oklch(97.1% 0.014 343.198);\\n --color-pink-100: oklch(94.8% 0.028 342.258);\\n --color-pink-200: oklch(89.9% 0.061 343.231);\\n --color-pink-300: oklch(82.3% 0.12 346.018);\\n --color-pink-400: oklch(71.8% 0.202 349.761);\\n --color-pink-500: oklch(65.6% 0.241 354.308);\\n --color-pink-600: oklch(59.2% 0.249 0.584);\\n --color-pink-700: oklch(52.5% 0.223 3.958);\\n --color-pink-800: oklch(45.9% 0.187 3.815);\\n --color-pink-900: oklch(40.8% 0.153 2.432);\\n --color-pink-950: oklch(28.4% 0.109 3.907);\\n\\n --color-rose-50: oklch(96.9% 0.015 12.422);\\n --color-rose-100: oklch(94.1% 0.03 12.58);\\n --color-rose-200: oklch(89.2% 0.058 10.001);\\n --color-rose-300: oklch(81% 0.117 11.638);\\n --color-rose-400: oklch(71.2% 0.194 13.428);\\n --color-rose-500: oklch(64.5% 0.246 16.439);\\n --color-rose-600: oklch(58.6% 0.253 17.585);\\n --color-rose-700: oklch(51.4% 0.222 16.935);\\n --color-rose-800: oklch(45.5% 0.188 13.697);\\n --color-rose-900: oklch(41% 0.159 10.272);\\n --color-rose-950: oklch(27.1% 0.105 12.094);\\n\\n --color-slate-50: oklch(98.4% 0.003 247.858);\\n --color-slate-100: oklch(96.8% 0.007 247.896);\\n --color-slate-200: oklch(92.9% 0.013 255.508);\\n --color-slate-300: oklch(86.9% 0.022 252.894);\\n --color-slate-400: oklch(70.4% 0.04 256.788);\\n --color-slate-500: oklch(55.4% 0.046 257.417);\\n --color-slate-600: oklch(44.6% 0.043 257.281);\\n --color-slate-700: oklch(37.2% 0.044 257.287);\\n --color-slate-800: oklch(27.9% 0.041 260.031);\\n --color-slate-900: oklch(20.8% 0.042 265.755);\\n --color-slate-950: oklch(12.9% 0.042 264.695);\\n\\n --color-gray-50: oklch(98.5% 0.002 247.839);\\n --color-gray-100: oklch(96.7% 0.003 264.542);\\n --color-gray-200: oklch(92.8% 0.006 264.531);\\n --color-gray-300: oklch(87.2% 0.01 258.338);\\n --color-gray-400: oklch(70.7% 0.022 261.325);\\n --color-gray-500: oklch(55.1% 0.027 264.364);\\n --color-gray-600: oklch(44.6% 0.03 256.802);\\n --color-gray-700: oklch(37.3% 0.034 259.733);\\n --color-gray-800: oklch(27.8% 0.033 256.848);\\n --color-gray-900: oklch(21% 0.034 264.665);\\n --color-gray-950: oklch(13% 0.028 261.692);\\n\\n --color-zinc-50: oklch(98.5% 0 none);\\n --color-zinc-100: oklch(96.7% 0.001 286.375);\\n --color-zinc-200: oklch(92% 0.004 286.32);\\n --color-zinc-300: oklch(87.1% 0.006 286.286);\\n --color-zinc-400: oklch(70.5% 0.015 286.067);\\n --color-zinc-500: oklch(55.2% 0.016 285.938);\\n --color-zinc-600: oklch(44.2% 0.017 285.786);\\n --color-zinc-700: oklch(37% 0.013 285.805);\\n --color-zinc-800: oklch(27.4% 0.006 286.033);\\n --color-zinc-900: oklch(21% 0.006 285.885);\\n --color-zinc-950: oklch(14.1% 0.005 285.823);\\n\\n --color-neutral-50: oklch(98.5% 0 none);\\n --color-neutral-100: oklch(97% 0 none);\\n --color-neutral-200: oklch(92.2% 0 none);\\n --color-neutral-300: oklch(87% 0 none);\\n --color-neutral-400: oklch(70.8% 0 none);\\n --color-neutral-500: oklch(55.6% 0 none);\\n --color-neutral-600: oklch(43.9% 0 none);\\n --color-neutral-700: oklch(37.1% 0 none);\\n --color-neutral-800: oklch(26.9% 0 none);\\n --color-neutral-900: oklch(20.5% 0 none);\\n --color-neutral-950: oklch(14.5% 0 none);\\n\\n --color-stone-50: oklch(98.5% 0.001 106.423);\\n --color-stone-100: oklch(97% 0.001 106.424);\\n --color-stone-200: oklch(92.3% 0.003 48.717);\\n --color-stone-300: oklch(86.9% 0.005 56.366);\\n --color-stone-400: oklch(70.9% 0.01 56.259);\\n --color-stone-500: oklch(55.3% 0.013 58.071);\\n --color-stone-600: oklch(44.4% 0.011 73.639);\\n --color-stone-700: oklch(37.4% 0.01 67.558);\\n --color-stone-800: oklch(26.8% 0.007 34.298);\\n --color-stone-900: oklch(21.6% 0.006 56.043);\\n --color-stone-950: oklch(14.7% 0.004 49.25);\\n\\n --color-mauve-50: oklch(98.5% 0 none);\\n --color-mauve-100: oklch(96% 0.003 325.6);\\n --color-mauve-200: oklch(92.2% 0.005 325.62);\\n --color-mauve-300: oklch(86.5% 0.012 325.68);\\n --color-mauve-400: oklch(71.1% 0.019 323.02);\\n --color-mauve-500: oklch(54.2% 0.034 322.5);\\n --color-mauve-600: oklch(43.5% 0.029 321.78);\\n --color-mauve-700: oklch(36.4% 0.029 323.89);\\n --color-mauve-800: oklch(26.3% 0.024 320.12);\\n --color-mauve-900: oklch(21.2% 0.019 322.12);\\n --color-mauve-950: oklch(14.5% 0.008 326);\\n\\n --color-olive-50: oklch(98.8% 0.003 106.5);\\n --color-olive-100: oklch(96.6% 0.005 106.5);\\n --color-olive-200: oklch(93% 0.007 106.5);\\n --color-olive-300: oklch(88% 0.011 106.6);\\n --color-olive-400: oklch(73.7% 0.021 106.9);\\n --color-olive-500: oklch(58% 0.031 107.3);\\n --color-olive-600: oklch(46.6% 0.025 107.3);\\n --color-olive-700: oklch(39.4% 0.023 107.4);\\n --color-olive-800: oklch(28.6% 0.016 107.4);\\n --color-olive-900: oklch(22.8% 0.013 107.4);\\n --color-olive-950: oklch(15.3% 0.006 107.1);\\n\\n --color-mist-50: oklch(98.7% 0.002 197.1);\\n --color-mist-100: oklch(96.3% 0.002 197.1);\\n --color-mist-200: oklch(92.5% 0.005 214.3);\\n --color-mist-300: oklch(87.2% 0.007 219.6);\\n --color-mist-400: oklch(72.3% 0.014 214.4);\\n --color-mist-500: oklch(56% 0.021 213.5);\\n --color-mist-600: oklch(45% 0.017 213.2);\\n --color-mist-700: oklch(37.8% 0.015 216);\\n --color-mist-800: oklch(27.5% 0.011 216.9);\\n --color-mist-900: oklch(21.8% 0.008 223.9);\\n --color-mist-950: oklch(14.8% 0.004 228.8);\\n\\n --color-taupe-50: oklch(98.6% 0.002 67.8);\\n --color-taupe-100: oklch(96% 0.002 17.2);\\n --color-taupe-200: oklch(92.2% 0.005 34.3);\\n --color-taupe-300: oklch(86.8% 0.007 39.5);\\n --color-taupe-400: oklch(71.4% 0.014 41.2);\\n --color-taupe-500: oklch(54.7% 0.021 43.1);\\n --color-taupe-600: oklch(43.8% 0.017 39.3);\\n --color-taupe-700: oklch(36.7% 0.016 35.7);\\n --color-taupe-800: oklch(26.8% 0.011 36.5);\\n --color-taupe-900: oklch(21.4% 0.009 43.1);\\n --color-taupe-950: oklch(14.7% 0.004 49.3);\\n\\n --color-black: #000;\\n --color-white: #fff;\\n\\n --spacing: 0.25rem;\\n\\n --breakpoint-sm: 40rem;\\n --breakpoint-md: 48rem;\\n --breakpoint-lg: 64rem;\\n --breakpoint-xl: 80rem;\\n --breakpoint-2xl: 96rem;\\n\\n --container-3xs: 16rem;\\n --container-2xs: 18rem;\\n --container-xs: 20rem;\\n --container-sm: 24rem;\\n --container-md: 28rem;\\n --container-lg: 32rem;\\n --container-xl: 36rem;\\n --container-2xl: 42rem;\\n --container-3xl: 48rem;\\n --container-4xl: 56rem;\\n --container-5xl: 64rem;\\n --container-6xl: 72rem;\\n --container-7xl: 80rem;\\n\\n --text-xs: 0.75rem;\\n --text-xs--line-height: calc(1 / 0.75);\\n --text-sm: 0.875rem;\\n --text-sm--line-height: calc(1.25 / 0.875);\\n --text-base: 1rem;\\n --text-base--line-height: calc(1.5 / 1);\\n --text-lg: 1.125rem;\\n --text-lg--line-height: calc(1.75 / 1.125);\\n --text-xl: 1.25rem;\\n --text-xl--line-height: calc(1.75 / 1.25);\\n --text-2xl: 1.5rem;\\n --text-2xl--line-height: calc(2 / 1.5);\\n --text-3xl: 1.875rem;\\n --text-3xl--line-height: calc(2.25 / 1.875);\\n --text-4xl: 2.25rem;\\n --text-4xl--line-height: calc(2.5 / 2.25);\\n --text-5xl: 3rem;\\n --text-5xl--line-height: 1;\\n --text-6xl: 3.75rem;\\n --text-6xl--line-height: 1;\\n --text-7xl: 4.5rem;\\n --text-7xl--line-height: 1;\\n --text-8xl: 6rem;\\n --text-8xl--line-height: 1;\\n --text-9xl: 8rem;\\n --text-9xl--line-height: 1;\\n\\n --font-weight-thin: 100;\\n --font-weight-extralight: 200;\\n --font-weight-light: 300;\\n --font-weight-normal: 400;\\n --font-weight-medium: 500;\\n --font-weight-semibold: 600;\\n --font-weight-bold: 700;\\n --font-weight-extrabold: 800;\\n --font-weight-black: 900;\\n\\n --tracking-tighter: -0.05em;\\n --tracking-tight: -0.025em;\\n --tracking-normal: 0em;\\n --tracking-wide: 0.025em;\\n --tracking-wider: 0.05em;\\n --tracking-widest: 0.1em;\\n\\n --leading-tight: 1.25;\\n --leading-snug: 1.375;\\n --leading-normal: 1.5;\\n --leading-relaxed: 1.625;\\n --leading-loose: 2;\\n\\n --radius-xs: 0.125rem;\\n --radius-sm: 0.25rem;\\n --radius-md: 0.375rem;\\n --radius-lg: 0.5rem;\\n --radius-xl: 0.75rem;\\n --radius-2xl: 1rem;\\n --radius-3xl: 1.5rem;\\n --radius-4xl: 2rem;\\n\\n --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);\\n --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);\\n --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);\\n --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\\n --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);\\n --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);\\n\\n --inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);\\n --inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);\\n\\n --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);\\n --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);\\n --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);\\n --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);\\n --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);\\n --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);\\n\\n --text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);\\n --text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);\\n --text-shadow-sm:\\n 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);\\n --text-shadow-md:\\n 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);\\n --text-shadow-lg:\\n 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);\\n\\n --ease-in: cubic-bezier(0.4, 0, 1, 1);\\n --ease-out: cubic-bezier(0, 0, 0.2, 1);\\n --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\\n\\n --animate-spin: spin 1s linear infinite;\\n --animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;\\n --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\\n --animate-bounce: bounce 1s infinite;\\n\\n @keyframes spin {\\n to {\\n transform: rotate(360deg);\\n }\\n }\\n\\n @keyframes ping {\\n 75%,\\n 100% {\\n transform: scale(2);\\n opacity: 0;\\n }\\n }\\n\\n @keyframes pulse {\\n 50% {\\n opacity: 0.5;\\n }\\n }\\n\\n @keyframes bounce {\\n 0%,\\n 100% {\\n transform: translateY(-25%);\\n animation-timing-function: cubic-bezier(0.8, 0, 1, 1);\\n }\\n\\n 50% {\\n transform: none;\\n animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\\n }\\n }\\n\\n --blur-xs: 4px;\\n --blur-sm: 8px;\\n --blur-md: 12px;\\n --blur-lg: 16px;\\n --blur-xl: 24px;\\n --blur-2xl: 40px;\\n --blur-3xl: 64px;\\n\\n --perspective-dramatic: 100px;\\n --perspective-near: 300px;\\n --perspective-normal: 500px;\\n --perspective-midrange: 800px;\\n --perspective-distant: 1200px;\\n\\n --aspect-video: 16 / 9;\\n\\n --default-transition-duration: 150ms;\\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\\n --default-font-family: --theme(--font-sans, initial);\\n --default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);\\n --default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);\\n --default-mono-font-family: --theme(--font-mono, initial);\\n --default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);\\n --default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);\\n}\\n\\n/* Deprecated */\\n@theme default inline reference {\\n --blur: 8px;\\n --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);\\n --shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);\\n --drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06);\\n --radius: 0.25rem;\\n --max-width-prose: 65ch;\\n}\\n\",\n \"preflight\": \"/*\\n 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\\n 2. Remove default margins and padding\\n 3. Reset all borders.\\n*/\\n\\n*,\\n::after,\\n::before,\\n::backdrop,\\n::file-selector-button {\\n box-sizing: border-box; /* 1 */\\n margin: 0; /* 2 */\\n padding: 0; /* 2 */\\n border: 0 solid; /* 3 */\\n}\\n\\n/*\\n 1. Use a consistent sensible line-height in all browsers.\\n 2. Prevent adjustments of font size after orientation changes in iOS.\\n 3. Use a more readable tab size.\\n 4. Use the user's configured `sans` font-family by default.\\n 5. Use the user's configured `sans` font-feature-settings by default.\\n 6. Use the user's configured `sans` font-variation-settings by default.\\n 7. Disable tap highlights on iOS.\\n*/\\n\\nhtml,\\n:host {\\n line-height: 1.5; /* 1 */\\n -webkit-text-size-adjust: 100%; /* 2 */\\n tab-size: 4; /* 3 */\\n font-family: --theme(\\n --default-font-family,\\n -apple-system,\\n BlinkMacSystemFont,\\n 'Segoe UI',\\n Roboto,\\n 'Helvetica Neue',\\n 'Noto Sans',\\n Arial,\\n sans-serif,\\n 'Apple Color Emoji',\\n 'Segoe UI Emoji',\\n 'Segoe UI Symbol',\\n 'Noto Color Emoji'\\n ); /* 4 */\\n font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */\\n font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */\\n -webkit-tap-highlight-color: transparent; /* 7 */\\n}\\n\\n/*\\n 1. Add the correct height in Firefox.\\n 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\\n 3. Reset the default border style to a 1px solid border.\\n*/\\n\\nhr {\\n height: 0; /* 1 */\\n color: inherit; /* 2 */\\n border-top-width: 1px; /* 3 */\\n}\\n\\n/*\\n Add the correct text decoration in Chrome, Edge, and Safari.\\n*/\\n\\nabbr:where([title]) {\\n -webkit-text-decoration: underline dotted;\\n text-decoration: underline dotted;\\n}\\n\\n/*\\n Remove the default font size and weight for headings.\\n*/\\n\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\n font-size: inherit;\\n font-weight: inherit;\\n}\\n\\n/*\\n Reset links to optimize for opt-in styling instead of opt-out.\\n*/\\n\\na {\\n color: inherit;\\n -webkit-text-decoration: inherit;\\n text-decoration: inherit;\\n}\\n\\n/*\\n Add the correct font weight in Edge and Safari.\\n*/\\n\\nb,\\nstrong {\\n font-weight: bolder;\\n}\\n\\n/*\\n 1. Use the user's configured `mono` font-family by default.\\n 2. Use the user's configured `mono` font-feature-settings by default.\\n 3. Use the user's configured `mono` font-variation-settings by default.\\n 4. Correct the odd `em` font sizing in all browsers.\\n*/\\n\\ncode,\\nkbd,\\nsamp,\\npre {\\n font-family: --theme(\\n --default-mono-font-family,\\n ui-monospace,\\n SFMono-Regular,\\n Menlo,\\n Monaco,\\n Consolas,\\n 'Liberation Mono',\\n 'Courier New',\\n monospace\\n ); /* 1 */\\n font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */\\n font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */\\n font-size: 1em; /* 4 */\\n}\\n\\n/*\\n Add the correct font size in all browsers.\\n*/\\n\\nsmall {\\n font-size: 80%;\\n}\\n\\n/*\\n Prevent `sub` and `sup` elements from affecting the line height in all browsers.\\n*/\\n\\nsub,\\nsup {\\n font-size: 75%;\\n line-height: 0;\\n position: relative;\\n vertical-align: baseline;\\n}\\n\\nsub {\\n bottom: -0.25em;\\n}\\n\\nsup {\\n top: -0.5em;\\n}\\n\\n/*\\n 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\\n 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\\n 3. Remove gaps between table borders by default.\\n*/\\n\\ntable {\\n text-indent: 0; /* 1 */\\n border-color: inherit; /* 2 */\\n border-collapse: collapse; /* 3 */\\n}\\n\\n/*\\n Use the modern Firefox focus style for all focusable elements.\\n*/\\n\\n:-moz-focusring:where(:not(iframe)) {\\n outline: auto;\\n}\\n\\n/*\\n Add the correct vertical alignment in Chrome and Firefox.\\n*/\\n\\nprogress {\\n vertical-align: baseline;\\n}\\n\\n/*\\n Add the correct display in Chrome and Safari.\\n*/\\n\\nsummary {\\n display: list-item;\\n}\\n\\n/*\\n Make lists unstyled by default.\\n*/\\n\\nol,\\nul,\\nmenu {\\n list-style: none;\\n}\\n\\n/*\\n 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\\n 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\\n This can trigger a poorly considered lint error in some tools but is included by design.\\n*/\\n\\nimg,\\nsvg,\\nvideo,\\ncanvas,\\naudio,\\niframe,\\nembed,\\nobject {\\n display: block; /* 1 */\\n vertical-align: middle; /* 2 */\\n}\\n\\n/*\\n Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\\n*/\\n\\nimg,\\nvideo {\\n max-width: 100%;\\n height: auto;\\n}\\n\\n/*\\n 1. Inherit font styles in all browsers.\\n 2. Remove border radius in all browsers.\\n 3. Remove background color in all browsers.\\n 4. Ensure consistent opacity for disabled states in all browsers.\\n*/\\n\\nbutton,\\ninput,\\nselect,\\noptgroup,\\ntextarea,\\n::file-selector-button {\\n font: inherit; /* 1 */\\n font-feature-settings: inherit; /* 1 */\\n font-variation-settings: inherit; /* 1 */\\n letter-spacing: inherit; /* 1 */\\n color: inherit; /* 1 */\\n border-radius: 0; /* 2 */\\n background-color: transparent; /* 3 */\\n opacity: 1; /* 4 */\\n}\\n\\n/*\\n Restore default font weight.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup {\\n font-weight: bolder;\\n}\\n\\n/*\\n Restore indentation.\\n*/\\n\\n:where(select:is([multiple], [size])) optgroup option {\\n padding-inline-start: 20px;\\n}\\n\\n/*\\n Restore space after button.\\n*/\\n\\n::file-selector-button {\\n margin-inline-end: 4px;\\n}\\n\\n/*\\n Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\\n*/\\n\\n::placeholder {\\n opacity: 1;\\n}\\n\\n/*\\n Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not\\n crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194)\\n*/\\n\\n@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or\\n (contain-intrinsic-size: 1px) /* Safari 17+ */ {\\n ::placeholder {\\n color: color-mix(in oklab, currentcolor 50%, transparent);\\n }\\n}\\n\\n/*\\n Prevent resizing textareas horizontally by default.\\n*/\\n\\ntextarea {\\n resize: vertical;\\n}\\n\\n/*\\n Remove the inner padding in Chrome and Safari on macOS.\\n*/\\n\\n::-webkit-search-decoration {\\n -webkit-appearance: none;\\n}\\n\\n/*\\n 1. Ensure date/time inputs have the same height when empty in iOS Safari.\\n 2. Ensure text alignment can be changed on date/time inputs in iOS Safari.\\n*/\\n\\n::-webkit-date-and-time-value {\\n min-height: 1lh; /* 1 */\\n text-align: inherit; /* 2 */\\n}\\n\\n/*\\n Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.\\n*/\\n\\n::-webkit-datetime-edit {\\n display: inline-flex;\\n}\\n\\n/*\\n Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.\\n*/\\n\\n::-webkit-datetime-edit-fields-wrapper {\\n padding: 0;\\n}\\n\\n::-webkit-datetime-edit,\\n::-webkit-datetime-edit-year-field,\\n::-webkit-datetime-edit-month-field,\\n::-webkit-datetime-edit-day-field,\\n::-webkit-datetime-edit-hour-field,\\n::-webkit-datetime-edit-minute-field,\\n::-webkit-datetime-edit-second-field,\\n::-webkit-datetime-edit-millisecond-field,\\n::-webkit-datetime-edit-meridiem-field {\\n padding-block: 0;\\n}\\n\\n/*\\n Center dropdown marker shown on inputs with paired `<datalist>`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499)\\n*/\\n\\n::-webkit-calendar-picker-indicator {\\n line-height: 1;\\n}\\n\\n/*\\n Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\\n*/\\n\\n:-moz-ui-invalid {\\n box-shadow: none;\\n}\\n\\n/*\\n Correct the inability to style the border radius in iOS Safari.\\n*/\\n\\nbutton,\\ninput:where([type='button'], [type='reset'], [type='submit']),\\n::file-selector-button {\\n appearance: button;\\n}\\n\\n/*\\n Correct the cursor style of increment and decrement buttons in Safari.\\n*/\\n\\n::-webkit-inner-spin-button,\\n::-webkit-outer-spin-button {\\n height: auto;\\n}\\n\\n/*\\n Make elements with the HTML hidden attribute stay hidden by default.\\n*/\\n\\n[hidden]:where(:not([hidden='until-found'])) {\\n display: none !important;\\n}\\n\",\n \"utilities\": \"@tailwind utilities;\\n\",\n \"bindings\": \"@theme inline { --color-background: var(--background);\\n--color-foreground: var(--foreground);\\n--color-card: var(--card);\\n--color-card-foreground: var(--card-foreground);\\n--color-popover: var(--popover);\\n--color-popover-foreground: var(--popover-foreground);\\n--color-primary: var(--primary);\\n--color-primary-foreground: var(--primary-foreground);\\n--color-secondary: var(--secondary);\\n--color-secondary-foreground: var(--secondary-foreground);\\n--color-muted: var(--muted);\\n--color-muted-foreground: var(--muted-foreground);\\n--color-accent: var(--accent);\\n--color-accent-foreground: var(--accent-foreground);\\n--color-destructive: var(--destructive);\\n--color-border: var(--border);\\n--color-input: var(--input);\\n--color-ring: var(--ring);\\n--color-chart-1: var(--chart-1);\\n--color-chart-2: var(--chart-2);\\n--color-chart-3: var(--chart-3);\\n--color-chart-4: var(--chart-4);\\n--color-chart-5: var(--chart-5);\\n--radius-sm: calc(var(--radius) - 4px);\\n--radius-md: calc(var(--radius) - 2px);\\n--radius-lg: var(--radius);\\n--radius-xl: calc(var(--radius) + 4px);\\n--radius-2xl: calc(var(--radius) * 1.8);\\n--radius-3xl: calc(var(--radius) * 2.2);\\n--default-border-width: var(--border-width, 1px);\\n--shadow-xs: var(--shadow-xs);\\n--shadow-sm: var(--shadow-sm);\\n--shadow-md: var(--shadow-md);\\n--shadow-lg: var(--shadow-lg);\\n--shadow-xl: var(--shadow-xl);\\n--shadow-2xl: var(--shadow-2xl); }\"\n};\n",
40
40
  "type": "registry:lib",
41
41
  "target": "components/mockup-viewer/mockup-tailwind.ts"
42
42
  }
package/r/pi-ambient.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "title": "Pi ambient agent",
5
5
  "description": "AmbientChat, a compact floating agent bar that grows into a chat window over an existing page, and AgentNotifications, toasts for finished, failed and waiting tasks (with the one guarded browser Notification helper). A block over the pi-chat item; its in-window artifact panel uses the pi-workspace layout.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.3",
7
+ "@hachej/boring-ui-kit@^0.4.5",
8
8
  "react@^19.0.0",
9
9
  "react-dom@^19.0.0",
10
10
  "@earendil-works/pi-durable@~1.2.0",
@@ -12,11 +12,11 @@
12
12
  "lucide-react@^1.51.0"
13
13
  ],
14
14
  "registryDependencies": [
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/theme.json",
16
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/pi-chat.json",
17
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/pi-workspace.json",
18
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/utils.json",
19
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.3/r/button.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/theme.json",
16
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/pi-chat.json",
17
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/pi-workspace.json",
18
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/utils.json",
19
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.5/r/button.json",
20
20
  "message-scroller",
21
21
  "button",
22
22
  "toggle",
@@ -28,7 +28,7 @@
28
28
  "files": [
29
29
  {
30
30
  "path": "registry/pi-ambient/ambient.tsx",
31
- "content": "'use client';\n\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { ImageContent, ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport type { NativeChatController } from '@hachej/boring-ui-kit/native-chat';\nimport { BellIcon, ChevronDownIcon, CircleAlertIcon, CircleCheckIcon, ExternalLinkIcon, GripVerticalIcon, Loader2Icon, MessageCircleQuestionIcon, MinusIcon, PanelLeftIcon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { MessageScroller, MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem, MessageScrollerButton } from '@/components/ui/message-scroller';\nimport { Toggle } from '@/components/ui/toggle';\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar';\nimport { Badge } from '@/components/ui/badge';\nimport { Card } from '@/components/ui/card';\nimport { Separator } from '@/components/ui/separator';\nimport { BlockActions } from '../button/actions';\nimport type { BlockAction, BlockIcon } from '../button/actions';\nimport { useChatPlacement } from '../pi-chat/use-remote-chat';\nimport { CopyButton } from '../pi-chat/code-block';\nimport { Composer } from '../pi-chat/composer';\nimport type { ComposerFeedback, ComposerProps } from '../pi-chat/composer';\nimport { ConversationFiles } from '../pi-chat/conversation-files';\nimport type { AttachmentsConfig, ConversationsConfig, EffortConfig, MentionsConfig, ModelConfig, SlashConfig } from '../pi-chat/config';\nimport { notifyPermission, requestNotifyPermission } from './browser-notify';\nimport type { NotifyPermission } from './browser-notify';\nimport { ConversationHistory, TranscriptHistory } from '../pi-chat/history';\nimport type { CommandMentions } from '../pi-chat/markdown';\nimport { RowView } from '../pi-chat/message';\nimport type { RowContext } from '../pi-chat/message';\nimport { ChatNotices, Notice } from '../pi-chat/notice';\nimport { AgentNotifications, KindIcon, createNotificationStore, defaultNotificationLabels, finishNotice, pendingQuestion, useNotifications } from './notifications';\nimport type { AgentNotification, NotificationLabels, NotificationStore } from './notifications';\nimport { ChatTextProvider, defaultChatIcons, defaultChatLabels, useMergedText } from '../pi-chat/labels';\nimport type { ChatIcons, ChatLabels } from '../pi-chat/labels';\nimport { MessageQueue } from '../pi-chat/queue';\nimport { artifactKey, collectArtifacts } from '../pi-chat/artifact';\nimport type { ArtifactDescriptor, ArtifactsConfig } from '../pi-chat/artifact';\nimport type { ChatCard, Mode } from '../pi-chat/rows';\nimport { useChatSession, useTranscript } from '../pi-chat/session';\nimport type { ChatFeatureProps, ChatSession } from '../pi-chat/session';\nimport { ArtifactWorkspace } from '../pi-workspace/workspace';\nimport type { WorkspacePanelApi } from '../pi-workspace/workspace';\nimport { cn } from '../utils/utils';\n\nexport { AgentNotifications, createNotificationStore, defaultNotificationLabels, summaryOf, useNotifications, watchConversation } from './notifications';\nexport type { AgentNotification, AgentNotificationsProps, NewNotification, NotificationKind, NotificationLabels, NotificationStore } from './notifications';\n\nconst elapsedText = (seconds: number) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`;\n\n/** The floating window's own words, over the chat's (`defaultChatLabels`) and the toasts' (`defaultNotificationLabels`). */\nexport const defaultAmbientLabels = {\n ...defaultChatLabels,\n ...defaultNotificationLabels,\n /** Shown while the conversation has no title of its own (the active item of `conversations` provides one). */\n title: 'Agent',\n /** The input's placeholder while idle. */\n placeholder: 'Do anything',\n /** The input's placeholder while the agent works. */\n working: (seconds: number) => `Working for ${elapsedText(seconds)}`,\n region: (title: string) => `Agent: ${title}`,\n openAgent: (title: string, unread: boolean) => `Open agent: ${title}${unread ? ' (new activity)' : ''}`,\n openChat: (title: string) => `Open chat: ${title}`,\n minimize: 'Minimise',\n switchConversation: 'Switch conversation',\n statusWorking: 'Working',\n enableNotifications: 'Notify me when the page is in the background',\n dock: 'Dock chat',\n dockHint: 'Dock chat beside the panel',\n move: 'Move the agent window (arrow keys, Home resets)',\n moveHint: 'Drag to move',\n goodResponse: 'Good response',\n badResponse: 'Bad response',\n openFull: 'Open in full chat',\n empty: 'Ask the agent to do something.',\n loadFailedShort: 'Could not load the conversation.',\n disconnectedShort: 'Disconnected.',\n artifactPanel: 'Artifact panel',\n};\nexport type AmbientLabels = ChatLabels & NotificationLabels & typeof defaultAmbientLabels;\n\n/** The floating window's icons, over the chat's. */\nexport const defaultAmbientIcons = {\n ...defaultChatIcons,\n minimize: MinusIcon as BlockIcon,\n dock: PanelLeftIcon as BlockIcon,\n grip: GripVerticalIcon as BlockIcon,\n switchConversation: ChevronDownIcon as BlockIcon,\n notify: BellIcon as BlockIcon,\n openFull: ExternalLinkIcon as BlockIcon,\n goodResponse: ThumbsUpIcon as BlockIcon,\n badResponse: ThumbsDownIcon as BlockIcon,\n};\nexport type AmbientIcons = ChatIcons & typeof defaultAmbientIcons;\nconst AMBIENT_TEXT = { labels: defaultAmbientLabels as AmbientLabels, icons: defaultAmbientIcons as AmbientIcons };\n\n/** `bar` is the compact bar, `expanded` the same window grown into a chat, `minimized` a small pill. */\nexport type AmbientState = 'bar' | 'expanded' | 'minimized';\n\n/** What the artifact panel inside the window gets besides the full-screen and close controls: the versions of the open artifact and how to switch. */\nexport interface AmbientArtifactPanelApi extends WorkspacePanelApi {\n /** Every known version of the open artifact in this conversation, newest first. */\n readonly versions: readonly ArtifactDescriptor[];\n /** True while the panel follows the newest version; false when one older version is pinned. */\n readonly follow: boolean;\n /** `'latest'` follows the newest version; a descriptor from `versions` pins that one. */\n readonly select: (version: ArtifactDescriptor | 'latest') => void;\n}\n\nexport interface AmbientChatProps extends ChatFeatureProps {\n readonly controller: NativeChatController;\n /** Embed the expanded conversation in a host workspace. Switching this flag changes layout without remounting the transcript or composer. The host supplies the header. */\n readonly embedded?: boolean;\n /**\n * Every word the window shows, over `defaultAmbientLabels` (the chat's labels, the toasts' and its own): `title` while the conversation has\n * none of its own, `placeholder` while idle, `working(seconds)` while the agent works.\n */\n readonly labels?: Partial<AmbientLabels> | undefined;\n /** The window's icons (minimise, dock, grip, …) and the chat's, over `defaultAmbientIcons`. */\n readonly icons?: Partial<AmbientIcons> | undefined;\n /** The agent's avatar before the title in the bar. */\n readonly avatar?: ReactNode;\n /** Host actions in the bar, before Dock: `header` ones as buttons, `menu` ones in a \"…\" menu. Test ids `ambient-action-<id>`, the menu `ambient-action-more`. */\n readonly headerActions?: readonly BlockAction[] | undefined;\n /** Host-owned header controls, such as icon-anchored popovers, before the actions and Dock button. */\n readonly headerTools?: ReactNode;\n /** `contrast` is a dark window on any page: the root carries `.dark`, so it shows the host's own dark tokens; `surface` follows the host's theme. */\n readonly variant?: 'contrast' | 'surface';\n /** Controlled window state; otherwise `defaultState` starts it and the window keeps it. */\n readonly state?: AmbientState;\n readonly defaultState?: AmbientState;\n readonly onStateChange?: (state: AmbientState) => void;\n /** Where the bar was dragged is remembered under this key. */\n readonly storageKey?: string;\n /** `session` (default) remembers the position for this browser tab's session; `local` keeps it in `localStorage` across sessions. */\n readonly storage?: 'session' | 'local';\n /** Host controls in the composer row, between the effort picker and Send (for example a voice button the host guards itself). Absent: nothing is shown. */\n readonly tools?: ReactNode;\n /** Host content under the composer (for example a collapsed list of the person's requests). Absent: nothing is shown. */\n readonly belowComposer?: ReactNode;\n /** Two-row reference composer by default; older layouts require an explicit choice. */\n readonly composerLayout?: ComposerProps['layout'];\n /** Hand the conversation to a full `PiChat` or `ArtifactWorkspace`. Adds an \"Open in full chat\" action to replies. */\n readonly onOpenFull?: (conversationId: string) => void;\n /** Adds thumbs up and down to replies. `value` is `null` when the person takes the rating back. */\n readonly onFeedback?: (reply: { readonly key: string; readonly text: string }, value: 'up' | 'down' | null) => void;\n /** Toasts for finished, failed and waiting tasks. Pass a store to feed it for background conversations too; `false` turns them off. */\n readonly notifications?: NotificationStore | false;\n readonly autoDismissMs?: number;\n readonly maxToasts?: number;\n /** System notifications while the page is hidden. A bell in the header asks for permission when the person chooses to; nothing is asked on load. */\n readonly systemNotifications?: boolean;\n /**\n * The viewer shown inside the window when an artifact card is opened: the window widens into chat on the left and this panel on the right\n * (`ArtifactWorkspace`: draggable divider, full screen, a full-screen sheet on a phone). Build it from the viewer registry (`ViewerFrame`, ...),\n * wrapped in `ViewerWindowProvider` with `api.fullscreen` / `api.onFullscreenChange` so its bar shows the full screen button.\n */\n readonly artifactPanel?: (artifact: ArtifactDescriptor, api: AmbientArtifactPanelApi) => ReactNode;\n /** `window` (default when `artifactPanel` is given) opens artifacts inside the window; `host` calls `artifacts.open` instead, for hosts that show them in their own viewer. */\n readonly artifactTarget?: 'window' | 'host';\n /** The title's ⌄ opens this list to switch conversations or tasks. */\n readonly conversations?: ConversationsConfig;\n /** Adds a \"Dock chat\" action to the header, for a host that floats this window out of a docked chat (`ArtifactWorkspace` with `floatBelow`). It shares the controller with the docked `PiChat`, so nothing is lost. */\n readonly onDock?: () => void;\n /** The optional annotate toggle and notes chip in the composer (the registry `feedback` item's `useComposerFeedback`). Omit it and nothing changes. */\n readonly feedback?: ComposerFeedback;\n readonly className?: string;\n}\n\ninterface Position { readonly right: number; readonly bottom: number }\nconst MARGIN = 8, DEFAULT_POSITION: Position = { right: 16, bottom: 16 };\n\nfunction storageFor(persist: 'session' | 'local'): Storage | undefined { try { return persist === 'local' ? globalThis.localStorage : globalThis.sessionStorage; } catch { return undefined; } }\nfunction readPosition(key: string, persist: 'session' | 'local'): Position | undefined {\n try {\n const value: unknown = JSON.parse(storageFor(persist)?.getItem(key) ?? 'null');\n const { right, bottom } = (value ?? {}) as { right?: unknown; bottom?: unknown };\n return typeof right === 'number' && typeof bottom === 'number' && Number.isFinite(right) && Number.isFinite(bottom) ? { right, bottom } : undefined;\n } catch { return undefined; }\n}\nfunction writePosition(key: string, persist: 'session' | 'local', position: Position) {\n try { storageFor(persist)?.setItem(key, JSON.stringify(position)); } catch { /* the position is a convenience */ }\n}\nconst clamp = (position: Position, size: { w: number; h: number }, viewport: { w: number; h: number }): Position => ({\n right: Math.round(Math.min(Math.max(MARGIN, position.right), Math.max(MARGIN, viewport.w - size.w - MARGIN))),\n bottom: Math.round(Math.min(Math.max(MARGIN, position.bottom), Math.max(MARGIN, viewport.h - size.h - MARGIN))),\n});\n\n// The layout viewport that fixed positioning uses (without scrollbars), not the window's outer size.\nconst viewportSize = () => { const root = globalThis.document?.documentElement; return { w: root?.clientWidth || globalThis.innerWidth || 1024, h: root?.clientHeight || globalThis.innerHeight || 768 }; };\nfunction usePhone(): boolean {\n const query = '(max-width: 639px)';\n const [phone, setPhone] = useState(() => globalThis.matchMedia?.(query).matches === true);\n useEffect(() => {\n const list = globalThis.matchMedia?.(query);\n if (!list) return;\n const update = () => setPhone(list.matches);\n update();\n list.addEventListener('change', update);\n return () => list.removeEventListener('change', update);\n }, []);\n return phone;\n}\n\n/** Whole seconds since `active` became true; 0 while inactive. */\nfunction useElapsed(active: boolean): number {\n const [seconds, setSeconds] = useState(0);\n useEffect(() => {\n if (!active) { setSeconds(0); return; }\n const started = Date.now();\n setSeconds(0);\n const timer = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1000)), 1000);\n return () => clearInterval(timer);\n }, [active]);\n return seconds;\n}\n\n/** Pushes a toast when a run finishes or fails and when a question starts waiting; takes it back when the question is answered or a new run starts. */\nfunction useRunFeed({ enabled, store, conversationId, title, session, suppress, onOutcome, waiting }: {\n readonly enabled: boolean; readonly store: NotificationStore; readonly conversationId: string; readonly title: string; readonly session: ChatSession; readonly waiting: string;\n readonly suppress: () => boolean; readonly onOutcome: (outcome: 'done' | 'error' | null) => void;\n}) {\n const { working, waitingForAnswer, rows, connected } = session;\n const before = useRef({ working, waiting: waitingForAnswer });\n const latest = useRef({ rows, title, suppress, onOutcome, connected, waiting });\n latest.current = { rows, title, suppress, onOutcome, connected, waiting };\n useEffect(() => {\n const was = before.current;\n before.current = { working, waiting: waitingForAnswer };\n const now = latest.current;\n if (working && !was.working) { now.onOutcome(null); store.dismiss(`${conversationId}:done`); }\n if (!enabled) return;\n if (waitingForAnswer && !was.waiting && !now.suppress()) store.push({ id: `${conversationId}:input`, kind: 'input', title: now.title, summary: pendingQuestion(now.rows) ?? now.waiting, conversationId });\n if (!waitingForAnswer && was.waiting) store.dismiss(`${conversationId}:input`);\n if (was.working && !working && now.connected) {\n const finished = finishNotice(now.rows);\n if (!finished) return;\n now.onOutcome(finished.kind);\n if (!now.suppress()) store.push({ id: `${conversationId}:done`, kind: finished.kind, title: now.title, summary: finished.summary, conversationId });\n }\n }, [working, waitingForAnswer, enabled, store, conversationId]);\n}\n\n/** A compact floating agent bar over an existing page; it grows into a chat window in place. Borrows a concrete native controller like `PiChat`. */\nexport function AmbientChat(props: AmbientChatProps) {\n const active = useRef(props.controller); active.current = props.controller;\n const [mount, setMount] = useState({ controller: props.controller, sequence: 0 });\n if (mount.controller !== props.controller) setMount({ controller: props.controller, sequence: mount.sequence + 1 });\n // The window state and the toasts belong to the bar, not to one conversation: they survive switching conversations.\n const [local, setLocal] = useState<AmbientState>(props.defaultState ?? 'bar');\n if (props.embedded && local !== 'expanded') setLocal('expanded');\n const internalStore = useMemo(createNotificationStore, []);\n const text = useMergedText<AmbientLabels, AmbientIcons>(props.labels, props.icons, AMBIENT_TEXT);\n return <ChatTextProvider value={text}><MessageScrollerProvider key={mount.sequence} autoScroll><AmbientSession {...props} labels={text.labels} icons={text.icons} activeController={active} windowState={props.embedded ? 'expanded' : props.state ?? local} store={props.notifications || internalStore}\n onWindowState={next => { if (props.state === undefined) setLocal(next); props.onStateChange?.(next); }} /></MessageScrollerProvider></ChatTextProvider>;\n}\n\ntype SessionProps = Omit<AmbientChatProps, 'labels' | 'icons'> & { readonly labels: AmbientLabels; readonly icons: AmbientIcons; readonly activeController: { readonly current: NativeChatController }; readonly windowState: AmbientState; readonly store: NotificationStore; readonly onWindowState: (state: AmbientState) => void };\n\nfunction AmbientSession({ controller, embedded = false, labels, icons, avatar, headerActions, headerTools, messageActions, variant = 'contrast', mode = 'expert', actions,\n windowState, onWindowState, store, storageKey = 'boring.ambient.position', storage = 'session', tools, belowComposer, composerChips, composerLayout = 'card', onOpenFull, onFeedback, notifications, autoDismissMs, maxToasts, systemNotifications = false,\n renderEntry, renderTool, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, fileAccept = 'image/*', slash, mentions, attachments, onOpenFile, fileChanges, pasteToFile, model, effort, replyFooter, artifacts: hostArtifacts, artifactPanel, artifactTarget, conversations, onDock, feedback: composerFeedback, className,\n activeController }: SessionProps) {\n const { title } = labels;\n useChatPlacement(controller, embedded ? 'docked' : 'floating');\n const stateRef = useRef(windowState); stateRef.current = windowState;\n // The artifact open inside the window, and whether it follows the newest version. It belongs to the expanded window: leaving that state closes it.\n const [opened, setOpened] = useState<{ readonly descriptor: ArtifactDescriptor; readonly follow: boolean } | null>(null);\n const [fullscreen, setFullscreen] = useState(false);\n const setWindowState = (next: AmbientState) => {\n if (next !== 'expanded') { setOpened(null); setFullscreen(false); }\n if (next !== stateRef.current) onWindowState(next);\n };\n const expanded = windowState === 'expanded', minimized = windowState === 'minimized';\n const phone = usePhone();\n const root = useRef<HTMLDivElement>(null), titleButton = useRef<HTMLButtonElement>(null), pill = useRef<HTMLButtonElement>(null);\n const [picking, setPicking] = useState(false);\n const [feedback, setFeedback] = useState<Readonly<Record<string, 'up' | 'down'>>>({});\n const [outcome, setOutcome] = useState<'done' | 'error' | null>(null);\n const [permission, setPermission] = useState<NotifyPermission>('unsupported');\n useEffect(() => { setPermission(notifyPermission()); }, []);\n useEffect(() => { if (!outcome) return; const timer = setTimeout(() => setOutcome(null), 8000); return () => clearTimeout(timer); }, [outcome]);\n\n // Replies get copy, optional feedback and the time; the host may hand the conversation to its full chat.\n const conversationRef = useRef('');\n const replyActions = useMemo<NonNullable<RowContext['replyActions']>>(() => (reply, host) => <div data-testid=\"reply-actions\" className=\"mt-2 -ml-2 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 max-sm:opacity-100 pointer-coarse:opacity-100 motion-reduce:transition-none\">\n <CopyButton text={reply.text} label={labels.copyResponse} iconOnly {...(onCopy ? { onCopy } : {})} />\n {onFeedback && <>\n {(['up', 'down'] as const).map(value => <Toggle key={value} data-testid={`feedback-${value}`} pressed={feedback[reply.key] === value} aria-label={value === 'up' ? labels.goodResponse : labels.badResponse}\n title={value === 'up' ? labels.goodResponse : labels.badResponse}\n onPressedChange={pressed => { const next = pressed ? value : null; setFeedback(current => { const { [reply.key]: _removed, ...rest } = current; return next ? { ...rest, [reply.key]: next } : rest; }); onFeedback({ key: reply.key, text: reply.text }, next); }}>\n {value === 'up' ? <icons.goodResponse data-icon=\"inline-start\" aria-hidden=\"true\" /> : <icons.badResponse data-icon=\"inline-start\" aria-hidden=\"true\" />}</Toggle>)}\n </>}\n {onOpenFull && <Button variant=\"ghost\" size=\"icon\" data-testid=\"open-full\" aria-label={labels.openFull} title={labels.openFull} onClick={() => onOpenFull(conversationRef.current)}><icons.openFull data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {host}\n {reply.timestamp !== undefined && <time dateTime={new Date(reply.timestamp).toISOString()} data-testid=\"reply-time\" className=\"ml-1.5 text-xs tabular-nums text-muted-foreground\">{new Date(reply.timestamp).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}</time>}\n </div>, [onCopy, onFeedback, onOpenFull, feedback, labels, icons]);\n const scrollToBottom = useRef<() => void>(() => {});\n const rowExtras = useMemo(() => ({ replyActions }), [replyActions]);\n const inWindow = Boolean(artifactPanel) && artifactTarget !== 'host';\n const detect = hostArtifacts?.detect;\n const versionsRef = useRef<readonly ArtifactDescriptor[]>([]);\n const artifacts = useMemo<ArtifactsConfig | undefined>(() => !inWindow ? hostArtifacts : {\n open: descriptor => { const newest = versionsRef.current.filter(version => artifactKey(version) === artifactKey(descriptor)).reduce((top, version) => Math.max(top, version.ordinal ?? 0), 0); setOpened({ descriptor, follow: (descriptor.ordinal ?? 0) >= newest }); },\n isOpen: descriptor => opened !== null && artifactKey(opened.descriptor) === artifactKey(descriptor) && (opened.follow ? (descriptor.ordinal ?? 0) >= (versionsRef.current.find(version => artifactKey(version) === artifactKey(descriptor))?.ordinal ?? 0) : descriptor.revision === opened.descriptor.revision),\n ...(detect ? { detect } : {}),\n ...(hostArtifacts?.kinds ? { kinds: hostArtifacts.kinds } : {}),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [inWindow, hostArtifacts, opened, detect]);\n const session = useChatSession({ controller, activeController, mode, actions, renderEntry, renderTool, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, fileAccept,\n slash, mentions, attachments, model, effort, artifacts, messageActions, replyFooter, afterSend: () => scrollToBottom.current(), rowExtras, feedback: composerFeedback, onOpenFile, fileChanges, pasteToFile, composerChips });\n const { state, derived, rows, queued, queueActions, working, waitingForAnswer, connected, error, act, textarea, rowContext, composer, files, loading } = session;\n const empty = session.empty;\n const transcript = useTranscript(rows, 60, 40, { state: state.history, connected: !state.disposed && connected, load: controller.loadEarlier });\n const versions = useMemo(() => inWindow ? collectArtifacts(state.view, detect).sort((a, b) => (b.ordinal ?? 0) - (a.ordinal ?? 0)) : [], [inWindow, state.view, detect]);\n versionsRef.current = versions;\n const known = opened ? versions.filter(version => artifactKey(version) === artifactKey(opened.descriptor)) : [];\n const shownArtifact = opened ? (opened.follow ? known[0] ?? opened.descriptor : opened.descriptor) : undefined;\n const { stick, hidden } = transcript;\n scrollToBottom.current = () => { transcript.reset(); void stick.scrollToBottom('instant'); };\n const { barStart, barNote } = session.pickers();\n\n const conversationId = String(conversations?.activeId ?? state.conversationId);\n conversationRef.current = conversationId;\n const activeItem = conversations?.items.find(item => item.id === conversations.activeId);\n const shownTitle = activeItem?.title?.trim() || title;\n const toasts = useNotifications(store);\n useRunFeed({ enabled: notifications !== false, store, conversationId, title: shownTitle, session, onOutcome: setOutcome, waiting: labels.waitingForAnswer,\n suppress: () => stateRef.current === 'expanded' && globalThis.document?.visibilityState !== 'hidden' });\n const unread = !expanded && toasts.length > 0;\n const seconds = useElapsed(working);\n\n // Position: offsets from the bottom right corner, remembered for the session, clamped to the viewport at render so a smaller window restores them.\n const [position, setPosition] = useState<Position>(() => readPosition(storageKey, storage) ?? DEFAULT_POSITION);\n const [viewport, setViewport] = useState(viewportSize);\n const [size, setSize] = useState({ w: 0, h: 0 });\n useEffect(() => {\n const onResize = () => setViewport(viewportSize());\n globalThis.addEventListener('resize', onResize);\n return () => globalThis.removeEventListener('resize', onResize);\n }, []);\n useEffect(() => {\n const element = root.current;\n if (!element || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => setSize({ w: element.offsetWidth, h: element.offsetHeight }));\n observer.observe(element);\n setSize({ w: element.offsetWidth, h: element.offsetHeight });\n return () => observer.disconnect();\n }, [minimized, expanded]);\n // The size and the viewport also follow every render, so the clamp below never works from a stale measurement (the title or status icon can change the pill's width; a scrollbar can appear without a resize event).\n useLayoutEffect(() => {\n const element = root.current;\n const fresh = viewportSize();\n setViewport(current => current.w === fresh.w && current.h === fresh.h ? current : fresh);\n if (element) setSize(current => current.w === element.offsetWidth && current.h === element.offsetHeight ? current : { w: element.offsetWidth, h: element.offsetHeight });\n });\n const placed = clamp(position, size, viewport);\n // Moves and the release are followed on the window, so a drag keeps working when the pointer leaves the handle (and when pointer capture is unavailable).\n // `threshold` is how far the pointer must travel before it is a drag: a click on the pill (threshold > 0) still restores it.\n const dragged = useRef(false);\n const beginDrag = (event: PointerEvent<HTMLElement>, threshold: number) => {\n if (phone || event.button !== 0) return;\n dragged.current = false;\n const origin = { pointer: event.pointerId, x: event.clientX, y: event.clientY, start: placed };\n let last = origin.start, moving = threshold === 0;\n const move = (next: globalThis.PointerEvent) => {\n if (next.pointerId !== origin.pointer) return;\n if (!moving) { if (Math.hypot(next.clientX - origin.x, next.clientY - origin.y) < threshold) return; moving = true; dragged.current = true; }\n last = clamp({ right: origin.start.right - (next.clientX - origin.x), bottom: origin.start.bottom - (next.clientY - origin.y) }, size, viewport);\n setPosition(last);\n };\n const end = (next: globalThis.PointerEvent) => {\n if (next.pointerId !== origin.pointer) return;\n globalThis.removeEventListener('pointermove', move); globalThis.removeEventListener('pointerup', end); globalThis.removeEventListener('pointercancel', end);\n if (moving) writePosition(storageKey, storage, last);\n };\n globalThis.addEventListener('pointermove', move); globalThis.addEventListener('pointerup', end); globalThis.addEventListener('pointercancel', end);\n };\n const onGripDown = (event: PointerEvent<HTMLButtonElement>) => { event.preventDefault(); beginDrag(event, 0); };\n // Arrow keys move the window (Shift for big steps); Home puts it back in the default corner.\n const onGripKey = (event: KeyboardEvent<HTMLElement>) => {\n if (phone) return;\n const step = event.shiftKey ? 64 : 16;\n const delta = event.key === 'ArrowLeft' ? { right: step, bottom: 0 } : event.key === 'ArrowRight' ? { right: -step, bottom: 0 } : event.key === 'ArrowUp' ? { right: 0, bottom: step } : event.key === 'ArrowDown' ? { right: 0, bottom: -step } : undefined;\n if (!delta && event.key !== 'Home') return;\n event.preventDefault();\n const next = delta ? clamp({ right: placed.right + delta.right, bottom: placed.bottom + delta.bottom }, size, viewport) : clamp(DEFAULT_POSITION, size, viewport);\n setPosition(next); writePosition(storageKey, storage, next);\n };\n const grip = (className?: string) => <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-grip\" aria-label={labels.move} title={labels.moveHint} onPointerDown={onGripDown} onKeyDown={onGripKey}\n className={cn('touch-none', className)}><icons.grip data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>;\n\n const expand = () => { setOutcome(null); store.dismissConversation(conversationId); setWindowState('expanded'); };\n useEffect(() => {\n if (expanded && !globalThis.matchMedia?.('(pointer: coarse)').matches) textarea.current?.focus();\n }, [expanded, textarea]);\n // The title is disabled while the window is open, so focus goes back to it once the bar is showing again.\n const refocusTitle = useRef(false);\n useEffect(() => { if (!expanded && refocusTitle.current) { refocusTitle.current = false; titleButton.current?.focus(); } }, [expanded]);\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (embedded || event.key !== 'Escape' || event.defaultPrevented || !expanded || picking) return;\n event.preventDefault();\n // Escape steps back: full screen, then the artifact, then the window.\n if (fullscreen) return setFullscreen(false);\n if (opened) return setOpened(null);\n refocusTitle.current = true;\n setWindowState('bar');\n };\n const onActivate = useCallback((item: AgentNotification) => {\n if (item.conversationId !== undefined && item.conversationId !== conversationId) conversations?.onSelect(item.conversationId);\n expand();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [conversationId, conversations]);\n\n const connectionKind = state.connection.kind;\n const status = waitingForAnswer ? 'input' : working ? 'working' : outcome;\n const label = waitingForAnswer ? labels.waitingForAnswer : labels.working(seconds);\n const panelOpen = expanded && shownArtifact !== undefined && artifactPanel !== undefined;\n // The floating window is drawn by the host's Card behind it (a sibling layer, so switching placement never remounts the chat);\n // embedded and full-screen phone windows are plain page regions.\n const card = !embedded && !(phone && expanded);\n const surface = embedded ? 'relative flex min-h-0 flex-1 flex-col overflow-clip bg-transparent text-foreground' : cn('relative flex flex-col', card ? 'isolate' : 'h-dvh bg-background pt-[env(safe-area-inset-top)] text-foreground',\n !phone && expanded && (panelOpen ? 'h-full overflow-hidden' : 'max-h-[70dvh]'));\n // With an artifact open the window widens (most of the viewport, at most 72rem) and takes a fixed height so the panel fills it.\n const rootClass = embedded ? cn('pi-chat relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden', className) : cn('pi-chat pi-ambient fixed z-50', variant === 'contrast' && 'dark scheme-dark', phone ? (expanded ? 'inset-0' : 'inset-x-2 bottom-[max(0.5rem,env(safe-area-inset-bottom))]')\n : cn('transition-[width] motion-reduce:transition-none', minimized ? 'w-auto' : panelOpen ? 'h-[min(46rem,calc(100dvh-1rem))] w-[min(72rem,calc(100vw-1rem))]' : expanded ? 'w-[min(35rem,calc(100vw-1rem))]' : 'w-[min(26rem,calc(100vw-1rem))]'), className);\n const rootStyle = phone || embedded ? undefined : { right: placed.right, bottom: placed.bottom };\n\n if (minimized) return <div ref={root} data-boring=\"ambient-chat\" data-state=\"minimized\" data-variant={variant} className={cn(rootClass, phone && 'flex justify-end')} style={rootStyle} onKeyDown={onKeyDown}>\n {notifications !== false && <AgentNotifications store={store} variant={variant} placement=\"inline\" systemNotifications={systemNotifications} onActivate={onActivate} labels={labels}\n {...(autoDismissMs !== undefined ? { autoDismissMs } : {})} {...(maxToasts !== undefined ? { max: maxToasts } : {})} className=\"absolute right-0 bottom-full mb-3 w-[min(26rem,calc(100vw-1rem))]\" />}\n <div data-testid=\"ambient-pill-box\" onPointerDown={event => beginDrag(event, 4)} onKeyDown={onGripKey} onClickCapture={event => { if (dragged.current) { dragged.current = false; event.stopPropagation(); event.preventDefault(); } }}\n className={cn('relative flex max-w-[min(20rem,calc(100vw-1rem))] items-center gap-0.5', !phone && 'cursor-grab touch-none select-none active:cursor-grabbing')}>\n {!phone && grip('ml-1.5')}\n <Button ref={pill} variant=\"outline\" size=\"lg\" data-testid=\"ambient-pill\" onClick={() => setWindowState('bar')} aria-label={labels.openAgent(shownTitle, unread)}\n className=\"relative min-w-0 gap-2.5\">\n {status === 'working' ? <Loader2Icon data-icon=\"inline-start\" className=\"animate-spin motion-reduce:animate-none\" aria-hidden=\"true\" />\n : status === 'input' ? <MessageCircleQuestionIcon data-icon=\"inline-start\" aria-hidden=\"true\" />\n : status === 'done' ? <CircleCheckIcon data-icon=\"inline-start\" aria-hidden=\"true\" />\n : status === 'error' ? <CircleAlertIcon data-icon=\"inline-start\" aria-hidden=\"true\" /> : null}\n <span className=\"min-w-0 truncate\">{shownTitle}</span>\n {unread && <Badge data-testid=\"ambient-unread\" aria-hidden=\"true\" className=\"absolute -top-1.5 -right-1.5\" />}\n </Button>\n </div>\n </div>;\n\n const visibleRows = transcript.visible(rows);\n const conversation = <section className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n <header hidden={embedded} data-testid=\"ambient-header\" className={cn(embedded && '!hidden','flex h-12 shrink-0 items-center gap-0.5 px-2 max-sm:h-14 max-sm:px-3')}>\n <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-minimize\" aria-label={labels.minimize} title={labels.minimize} onClick={() => { setPicking(false); setWindowState(expanded ? 'bar' : 'minimized'); }}><icons.minimize data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>\n <span data-testid=\"ambient-status\" data-status={status ?? 'idle'} role=\"img\" aria-label={status === 'working' ? labels.statusWorking : status === 'input' ? labels.needsInput : status === 'done' ? labels.finished : status === 'error' ? labels.failedRun : undefined}\n aria-hidden={status ? undefined : 'true'} className={cn('flex shrink-0 items-center justify-center', status ? 'mx-1.5 w-5' : 'w-1.5')}>\n {status === 'working' ? <Loader2Icon className=\"size-4 animate-spin text-muted-foreground motion-reduce:animate-none\" aria-hidden=\"true\" /> : status ? <KindIcon kind={status === 'input' ? 'input' : status} className=\"size-[1.125rem]\" /> : null}\n </span>\n {avatar && <Avatar size=\"sm\" data-testid=\"ambient-avatar\" className=\"overflow-hidden\"><AvatarFallback className=\"*:max-h-full *:max-w-full\">{avatar}</AvatarFallback></Avatar>}\n <Button ref={titleButton} variant=\"ghost\" data-testid=\"ambient-title\" onClick={expand} disabled={expanded} aria-label={expanded ? shownTitle : labels.openChat(shownTitle)}\n className=\"min-w-0 max-w-full shrink\"><span className=\"truncate\">{shownTitle}</span></Button>\n {conversations && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-switch\" aria-label={labels.switchConversation} aria-haspopup=\"dialog\" aria-expanded={picking} title={labels.switchConversation} onClick={() => setPicking(open => !open)}>\n <icons.switchConversation data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {expanded ? <span className=\"flex-1\" /> : <Button variant=\"ghost\" tabIndex={-1} aria-hidden=\"true\" data-testid=\"ambient-header-fill\" onClick={expand} className=\"min-w-2 flex-1\" />}\n {systemNotifications && permission === 'default' && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-enable-notifications\" aria-label={labels.enableNotifications} title={labels.enableNotifications}\n onClick={() => { void requestNotifyPermission().then(setPermission); }}><icons.notify data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {headerTools}\n <BlockActions actions={headerActions} testId=\"ambient-action\" menuLabel={labels.moreActions} />\n {onDock && !phone && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-dock\" aria-label={labels.dock} title={labels.dockHint} onClick={() => { setPicking(false); onDock(); }}><icons.dock data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {!phone && grip()}\n </header>\n\n {expanded && <div className=\"flex min-h-0 flex-1 flex-col\">\n {(connectionKind === 'error' || connectionKind === 'closed') && <div className=\"px-4 pb-2\"><Notice tone={connectionKind === 'error' ? 'error' : 'info'} testid=\"connection-notice\">\n <span className=\"flex-1\">{connectionKind === 'error' ? labels.loadFailed : labels.disconnected}</span>\n {!state.disposed && <Button size=\"sm\" variant=\"outline\" onClick={() => act(controller.connect)}><icons.reconnect data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.reconnect}</Button>}</Notice></div>}\n {derived.pinned.length > 0 && <aside aria-label={labels.requiredActions} className=\"shrink-0 space-y-2 px-4 pb-2\">{derived.pinned.map(card => <div key={card.key}>{card.content}</div>)}</aside>}\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\n <MessageScroller>\n <MessageScrollerViewport ref={stick.scrollRef} data-testid=\"transcript-scroll\" aria-label={labels.messages} className=\"scrollbar-thumb-border scrollbar-track-transparent\">\n {!empty && <TranscriptHistory history={state.history} connected={!state.disposed && connected} onLoad={transcript.reveal} sentinelRef={transcript.sentinelRef} hidden={hidden} loaded={transcript.loaded} />}\n <div className=\"px-4 pt-4 pb-6\">\n <MessageScrollerContent aria-label={labels.messages} aria-live=\"polite\" data-testid=\"transcript\">\n {visibleRows.map(row => <MessageScrollerItem key={row.key} messageId={row.key} className=\"[content-visibility:visible]\"><RowView row={row} context={rowContext} /></MessageScrollerItem>)}\n {loading && <p role=\"status\" data-testid=\"loading\" className=\"m-0 flex items-center gap-2 text-sm text-muted-foreground\"><Loader2Icon className=\"size-4 animate-spin motion-reduce:animate-none\" aria-hidden=\"true\" />{labels.loading}</p>}\n {empty && !loading && <p data-testid=\"ambient-empty\" className=\"m-0 max-w-sm py-8 text-sm leading-6 text-muted-foreground\">{labels.empty}</p>}\n </MessageScrollerContent>\n </div>\n </MessageScrollerViewport>\n {!stick.isAtBottom && <MessageScrollerButton data-testid=\"jump-latest\" aria-label={labels.jumpToLatest} title={labels.jumpToLatest} onClick={transcript.reset}><icons.jumpToLatest aria-hidden=\"true\" /></MessageScrollerButton>}\n </MessageScroller>\n </div>\n </div>}\n\n {(expanded || !embedded) && <Separator />}\n <div className={cn('shrink-0', phone && 'pb-[env(safe-area-inset-bottom)]')}>\n <div className=\"space-y-2 px-3 empty:hidden [&:has(*)]:pt-2\">\n <ChatNotices state={state} error={error} onReconcile={() => act(controller.reconcile)} onRetry={() => act(controller.retrySameRequest)} />\n {!expanded && (connectionKind === 'error' || connectionKind === 'closed') && <Notice tone=\"info\" testid=\"connection-notice\">\n <span className=\"flex-1\">{connectionKind === 'error' ? labels.loadFailedShort : labels.disconnectedShort}</span>\n {!state.disposed && <Button size=\"sm\" variant=\"outline\" onClick={() => act(controller.connect)}>{labels.reconnect}</Button>}</Notice>}\n </div>\n <div className=\"px-3 pt-2 pb-3\">\n <ConversationFiles files={files} />\n <MessageQueue items={queued} sending={state.outbox} withdraw={actions?.withdraw} actions={queueActions} />\n <Composer {...composer} layout={composerLayout} placeholder={working || waitingForAnswer ? label : labels.placeholder} barStart={barStart} barNote={barNote} barEnd={tools} />\n {belowComposer !== undefined && <div data-testid=\"below-composer\" className=\"pt-2\">{belowComposer}</div>}\n </div>\n </div>\n </section>;\n return <div ref={root} data-boring={embedded ? 'pi-chat' : 'ambient-chat'} data-state={windowState} data-variant={variant} data-phone={phone ? 'true' : undefined} role=\"region\" aria-label={labels.region(shownTitle)}\n className={rootClass} style={rootStyle} onKeyDown={onKeyDown}>\n {!embedded && notifications !== false && !(phone && expanded) && <AgentNotifications store={store} variant={variant} placement=\"inline\" systemNotifications={systemNotifications} onActivate={onActivate} labels={labels}\n {...(autoDismissMs !== undefined ? { autoDismissMs } : {})} {...(maxToasts !== undefined ? { max: maxToasts } : {})} className=\"absolute right-0 bottom-full mb-3 w-full\" />}\n {picking && conversations && <ConversationHistory conversations={conversations} placement={expanded ? 'below' : 'above'} onClose={() => setPicking(false)} />}\n <div data-testid=\"ambient-surface\" className={surface}>\n {card && <Card aria-hidden=\"true\" data-boring=\"chrome\" className=\"pointer-events-none absolute inset-0 -z-10\" />}\n {artifactPanel ? <ArtifactWorkspace open={panelOpen} onClose={() => { setOpened(null); setFullscreen(false); }} fullscreen={fullscreen} onFullscreenChange={setFullscreen} storageKey=\"boring.ambient.panel-width\" panelLabel={labels.artifactPanel}\n defaultWidth={600} minPanel={320} minChat={300} sheetBelow={720}\n panel={api => shownArtifact ? artifactPanel(shownArtifact, { ...api, versions: known, follow: opened?.follow ?? true,\n select: version => setOpened(current => current ? (version === 'latest' ? { descriptor: known[0] ?? current.descriptor, follow: true } : { descriptor: version, follow: false }) : current) }) : null}\n chat={conversation} /> : conversation}\n </div>\n </div>;\n}\n",
31
+ "content": "'use client';\n\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\nimport type { EntryRecord } from '@earendil-works/pi-durable';\nimport type { ImageContent, ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';\nimport type { NativeChatController } from '@hachej/boring-ui-kit/native-chat';\nimport { BellIcon, ChevronDownIcon, CircleAlertIcon, CircleCheckIcon, ExternalLinkIcon, GripVerticalIcon, Loader2Icon, MessageCircleQuestionIcon, MinusIcon, PanelLeftIcon, ThumbsDownIcon, ThumbsUpIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { MessageScroller, MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem, MessageScrollerButton } from '@/components/ui/message-scroller';\nimport { Toggle } from '@/components/ui/toggle';\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar';\nimport { Badge } from '@/components/ui/badge';\nimport { Card } from '@/components/ui/card';\nimport { Separator } from '@/components/ui/separator';\nimport { BlockActions } from '../button/actions';\nimport type { BlockAction, BlockIcon } from '../button/actions';\nimport { useChatPlacement } from '../pi-chat/use-remote-chat';\nimport { CopyButton } from '../pi-chat/code-block';\nimport { Composer } from '../pi-chat/composer';\nimport type { ComposerFeedback, ComposerProps } from '../pi-chat/composer';\nimport { ConversationFiles } from '../pi-chat/conversation-files';\nimport type { AttachmentsConfig, ConversationsConfig, EffortConfig, MentionsConfig, ModelConfig, SlashConfig } from '../pi-chat/config';\nimport { notifyPermission, requestNotifyPermission } from './browser-notify';\nimport type { NotifyPermission } from './browser-notify';\nimport { ConversationHistory, TranscriptHistory } from '../pi-chat/history';\nimport type { CommandMentions } from '../pi-chat/markdown';\nimport { RowView } from '../pi-chat/message';\nimport type { RowContext } from '../pi-chat/message';\nimport { ChatNotices, Notice, PreparingStatus, preparing } from '../pi-chat/notice';\nimport { AgentNotifications, KindIcon, createNotificationStore, defaultNotificationLabels, finishNotice, pendingQuestion, useNotifications } from './notifications';\nimport type { AgentNotification, NotificationLabels, NotificationStore } from './notifications';\nimport { ChatTextProvider, defaultChatIcons, defaultChatLabels, useMergedText } from '../pi-chat/labels';\nimport type { ChatIcons, ChatLabels } from '../pi-chat/labels';\nimport { MessageQueue } from '../pi-chat/queue';\nimport { artifactKey, collectArtifacts } from '../pi-chat/artifact';\nimport type { ArtifactDescriptor, ArtifactsConfig } from '../pi-chat/artifact';\nimport type { ChatCard, Mode } from '../pi-chat/rows';\nimport { useChatSession, useTranscript } from '../pi-chat/session';\nimport type { ChatFeatureProps, ChatSession } from '../pi-chat/session';\nimport { ArtifactWorkspace } from '../pi-workspace/workspace';\nimport type { WorkspacePanelApi } from '../pi-workspace/workspace';\nimport { cn } from '../utils/utils';\n\nexport { AgentNotifications, createNotificationStore, defaultNotificationLabels, summaryOf, useNotifications, watchConversation } from './notifications';\nexport type { AgentNotification, AgentNotificationsProps, NewNotification, NotificationKind, NotificationLabels, NotificationStore } from './notifications';\n\nconst elapsedText = (seconds: number) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`;\n\n/** The floating window's own words, over the chat's (`defaultChatLabels`) and the toasts' (`defaultNotificationLabels`). */\nexport const defaultAmbientLabels = {\n ...defaultChatLabels,\n ...defaultNotificationLabels,\n /** Shown while the conversation has no title of its own (the active item of `conversations` provides one). */\n title: 'Agent',\n /** The input's placeholder while idle. */\n placeholder: 'Do anything',\n /** The input's placeholder while the agent works. */\n working: (seconds: number) => `Working for ${elapsedText(seconds)}`,\n region: (title: string) => `Agent: ${title}`,\n openAgent: (title: string, unread: boolean) => `Open agent: ${title}${unread ? ' (new activity)' : ''}`,\n openChat: (title: string) => `Open chat: ${title}`,\n minimize: 'Minimise',\n switchConversation: 'Switch conversation',\n statusWorking: 'Working',\n enableNotifications: 'Notify me when the page is in the background',\n dock: 'Dock chat',\n dockHint: 'Dock chat beside the panel',\n move: 'Move the agent window (arrow keys, Home resets)',\n moveHint: 'Drag to move',\n goodResponse: 'Good response',\n badResponse: 'Bad response',\n openFull: 'Open in full chat',\n empty: 'Ask the agent to do something.',\n loadFailedShort: 'Could not load the conversation.',\n disconnectedShort: 'Disconnected.',\n artifactPanel: 'Artifact panel',\n};\nexport type AmbientLabels = ChatLabels & NotificationLabels & typeof defaultAmbientLabels;\n\n/** The floating window's icons, over the chat's. */\nexport const defaultAmbientIcons = {\n ...defaultChatIcons,\n minimize: MinusIcon as BlockIcon,\n dock: PanelLeftIcon as BlockIcon,\n grip: GripVerticalIcon as BlockIcon,\n switchConversation: ChevronDownIcon as BlockIcon,\n notify: BellIcon as BlockIcon,\n openFull: ExternalLinkIcon as BlockIcon,\n goodResponse: ThumbsUpIcon as BlockIcon,\n badResponse: ThumbsDownIcon as BlockIcon,\n};\nexport type AmbientIcons = ChatIcons & typeof defaultAmbientIcons;\nconst AMBIENT_TEXT = { labels: defaultAmbientLabels as AmbientLabels, icons: defaultAmbientIcons as AmbientIcons };\n\n/** `bar` is the compact bar, `expanded` the same window grown into a chat, `minimized` a small pill. */\nexport type AmbientState = 'bar' | 'expanded' | 'minimized';\n\n/** What the artifact panel inside the window gets besides the full-screen and close controls: the versions of the open artifact and how to switch. */\nexport interface AmbientArtifactPanelApi extends WorkspacePanelApi {\n /** Every known version of the open artifact in this conversation, newest first. */\n readonly versions: readonly ArtifactDescriptor[];\n /** True while the panel follows the newest version; false when one older version is pinned. */\n readonly follow: boolean;\n /** `'latest'` follows the newest version; a descriptor from `versions` pins that one. */\n readonly select: (version: ArtifactDescriptor | 'latest') => void;\n}\n\nexport interface AmbientChatProps extends ChatFeatureProps {\n readonly controller: NativeChatController;\n /** Embed the expanded conversation in a host workspace. Switching this flag changes layout without remounting the transcript or composer. The host supplies the header. */\n readonly embedded?: boolean;\n /**\n * Every word the window shows, over `defaultAmbientLabels` (the chat's labels, the toasts' and its own): `title` while the conversation has\n * none of its own, `placeholder` while idle, `working(seconds)` while the agent works.\n */\n readonly labels?: Partial<AmbientLabels> | undefined;\n /** The window's icons (minimise, dock, grip, …) and the chat's, over `defaultAmbientIcons`. */\n readonly icons?: Partial<AmbientIcons> | undefined;\n /** The agent's avatar before the title in the bar. */\n readonly avatar?: ReactNode;\n /** Host actions in the bar, before Dock: `header` ones as buttons, `menu` ones in a \"…\" menu. Test ids `ambient-action-<id>`, the menu `ambient-action-more`. */\n readonly headerActions?: readonly BlockAction[] | undefined;\n /** Host-owned header controls, such as icon-anchored popovers, before the actions and Dock button. */\n readonly headerTools?: ReactNode;\n /** `contrast` is a dark window on any page: the root carries `.dark`, so it shows the host's own dark tokens; `surface` follows the host's theme. */\n readonly variant?: 'contrast' | 'surface';\n /** Controlled window state; otherwise `defaultState` starts it and the window keeps it. */\n readonly state?: AmbientState;\n readonly defaultState?: AmbientState;\n readonly onStateChange?: (state: AmbientState) => void;\n /** Where the bar was dragged is remembered under this key. */\n readonly storageKey?: string;\n /** `session` (default) remembers the position for this browser tab's session; `local` keeps it in `localStorage` across sessions. */\n readonly storage?: 'session' | 'local';\n /** Host controls in the composer row, between the effort picker and Send (for example a voice button the host guards itself). Absent: nothing is shown. */\n readonly tools?: ReactNode;\n /** Host content under the composer (for example a collapsed list of the person's requests). Absent: nothing is shown. */\n readonly belowComposer?: ReactNode;\n /** Two-row reference composer by default; older layouts require an explicit choice. */\n readonly composerLayout?: ComposerProps['layout'];\n /** Hand the conversation to a full `PiChat` or `ArtifactWorkspace`. Adds an \"Open in full chat\" action to replies. */\n readonly onOpenFull?: (conversationId: string) => void;\n /** Adds thumbs up and down to replies. `value` is `null` when the person takes the rating back. */\n readonly onFeedback?: (reply: { readonly key: string; readonly text: string }, value: 'up' | 'down' | null) => void;\n /** Toasts for finished, failed and waiting tasks. Pass a store to feed it for background conversations too; `false` turns them off. */\n readonly notifications?: NotificationStore | false;\n readonly autoDismissMs?: number;\n readonly maxToasts?: number;\n /** System notifications while the page is hidden. A bell in the header asks for permission when the person chooses to; nothing is asked on load. */\n readonly systemNotifications?: boolean;\n /**\n * The viewer shown inside the window when an artifact card is opened: the window widens into chat on the left and this panel on the right\n * (`ArtifactWorkspace`: draggable divider, full screen, a full-screen sheet on a phone). Build it from the viewer registry (`ViewerFrame`, ...),\n * wrapped in `ViewerWindowProvider` with `api.fullscreen` / `api.onFullscreenChange` so its bar shows the full screen button.\n */\n readonly artifactPanel?: (artifact: ArtifactDescriptor, api: AmbientArtifactPanelApi) => ReactNode;\n /** `window` (default when `artifactPanel` is given) opens artifacts inside the window; `host` calls `artifacts.open` instead, for hosts that show them in their own viewer. */\n readonly artifactTarget?: 'window' | 'host';\n /** The title's ⌄ opens this list to switch conversations or tasks. */\n readonly conversations?: ConversationsConfig;\n /** Adds a \"Dock chat\" action to the header, for a host that floats this window out of a docked chat (`ArtifactWorkspace` with `floatBelow`). It shares the controller with the docked `PiChat`, so nothing is lost. */\n readonly onDock?: () => void;\n /** The optional annotate toggle and notes chip in the composer (the registry `feedback` item's `useComposerFeedback`). Omit it and nothing changes. */\n readonly feedback?: ComposerFeedback;\n readonly className?: string;\n}\n\ninterface Position { readonly right: number; readonly bottom: number }\nconst MARGIN = 8, DEFAULT_POSITION: Position = { right: 16, bottom: 16 };\n\nfunction storageFor(persist: 'session' | 'local'): Storage | undefined { try { return persist === 'local' ? globalThis.localStorage : globalThis.sessionStorage; } catch { return undefined; } }\nfunction readPosition(key: string, persist: 'session' | 'local'): Position | undefined {\n try {\n const value: unknown = JSON.parse(storageFor(persist)?.getItem(key) ?? 'null');\n const { right, bottom } = (value ?? {}) as { right?: unknown; bottom?: unknown };\n return typeof right === 'number' && typeof bottom === 'number' && Number.isFinite(right) && Number.isFinite(bottom) ? { right, bottom } : undefined;\n } catch { return undefined; }\n}\nfunction writePosition(key: string, persist: 'session' | 'local', position: Position) {\n try { storageFor(persist)?.setItem(key, JSON.stringify(position)); } catch { /* the position is a convenience */ }\n}\nconst clamp = (position: Position, size: { w: number; h: number }, viewport: { w: number; h: number }): Position => ({\n right: Math.round(Math.min(Math.max(MARGIN, position.right), Math.max(MARGIN, viewport.w - size.w - MARGIN))),\n bottom: Math.round(Math.min(Math.max(MARGIN, position.bottom), Math.max(MARGIN, viewport.h - size.h - MARGIN))),\n});\n\n// The layout viewport that fixed positioning uses (without scrollbars), not the window's outer size.\nconst viewportSize = () => { const root = globalThis.document?.documentElement; return { w: root?.clientWidth || globalThis.innerWidth || 1024, h: root?.clientHeight || globalThis.innerHeight || 768 }; };\nfunction usePhone(): boolean {\n const query = '(max-width: 639px)';\n const [phone, setPhone] = useState(() => globalThis.matchMedia?.(query).matches === true);\n useEffect(() => {\n const list = globalThis.matchMedia?.(query);\n if (!list) return;\n const update = () => setPhone(list.matches);\n update();\n list.addEventListener('change', update);\n return () => list.removeEventListener('change', update);\n }, []);\n return phone;\n}\n\n/** Whole seconds since `active` became true; 0 while inactive. */\nfunction useElapsed(active: boolean): number {\n const [seconds, setSeconds] = useState(0);\n useEffect(() => {\n if (!active) { setSeconds(0); return; }\n const started = Date.now();\n setSeconds(0);\n const timer = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1000)), 1000);\n return () => clearInterval(timer);\n }, [active]);\n return seconds;\n}\n\n/** Pushes a toast when a run finishes or fails and when a question starts waiting; takes it back when the question is answered or a new run starts. */\nfunction useRunFeed({ enabled, store, conversationId, title, session, suppress, onOutcome, waiting }: {\n readonly enabled: boolean; readonly store: NotificationStore; readonly conversationId: string; readonly title: string; readonly session: ChatSession; readonly waiting: string;\n readonly suppress: () => boolean; readonly onOutcome: (outcome: 'done' | 'error' | null) => void;\n}) {\n const { working, waitingForAnswer, rows, connected } = session;\n const before = useRef({ working, waiting: waitingForAnswer });\n const latest = useRef({ rows, title, suppress, onOutcome, connected, waiting });\n latest.current = { rows, title, suppress, onOutcome, connected, waiting };\n useEffect(() => {\n const was = before.current;\n before.current = { working, waiting: waitingForAnswer };\n const now = latest.current;\n if (working && !was.working) { now.onOutcome(null); store.dismiss(`${conversationId}:done`); }\n if (!enabled) return;\n if (waitingForAnswer && !was.waiting && !now.suppress()) store.push({ id: `${conversationId}:input`, kind: 'input', title: now.title, summary: pendingQuestion(now.rows) ?? now.waiting, conversationId });\n if (!waitingForAnswer && was.waiting) store.dismiss(`${conversationId}:input`);\n if (was.working && !working && now.connected) {\n const finished = finishNotice(now.rows);\n if (!finished) return;\n now.onOutcome(finished.kind);\n if (!now.suppress()) store.push({ id: `${conversationId}:done`, kind: finished.kind, title: now.title, summary: finished.summary, conversationId });\n }\n }, [working, waitingForAnswer, enabled, store, conversationId]);\n}\n\n/** A compact floating agent bar over an existing page; it grows into a chat window in place. Borrows a concrete native controller like `PiChat`. */\nexport function AmbientChat(props: AmbientChatProps) {\n const active = useRef(props.controller); active.current = props.controller;\n const [mount, setMount] = useState({ controller: props.controller, sequence: 0 });\n if (mount.controller !== props.controller) setMount({ controller: props.controller, sequence: mount.sequence + 1 });\n // The window state and the toasts belong to the bar, not to one conversation: they survive switching conversations.\n const [local, setLocal] = useState<AmbientState>(props.defaultState ?? 'bar');\n if (props.embedded && local !== 'expanded') setLocal('expanded');\n const internalStore = useMemo(createNotificationStore, []);\n const text = useMergedText<AmbientLabels, AmbientIcons>(props.labels, props.icons, AMBIENT_TEXT);\n return <ChatTextProvider value={text}><MessageScrollerProvider key={mount.sequence} autoScroll><AmbientSession {...props} labels={text.labels} icons={text.icons} activeController={active} windowState={props.embedded ? 'expanded' : props.state ?? local} store={props.notifications || internalStore}\n onWindowState={next => { if (props.state === undefined) setLocal(next); props.onStateChange?.(next); }} /></MessageScrollerProvider></ChatTextProvider>;\n}\n\ntype SessionProps = Omit<AmbientChatProps, 'labels' | 'icons'> & { readonly labels: AmbientLabels; readonly icons: AmbientIcons; readonly activeController: { readonly current: NativeChatController }; readonly windowState: AmbientState; readonly store: NotificationStore; readonly onWindowState: (state: AmbientState) => void };\n\nfunction AmbientSession({ controller, embedded = false, labels, icons, avatar, headerActions, headerTools, messageActions, variant = 'contrast', mode = 'expert', presentation, actions,\n windowState, onWindowState, store, storageKey = 'boring.ambient.position', storage = 'session', tools, belowComposer, composerChips, composerLayout = 'card', onOpenFull, onFeedback, notifications, autoDismissMs, maxToasts, systemNotifications = false,\n renderEntry, hideLine, displayLimit, historicalMessageActions, unread: replyUnread, replyError, renderTool, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, fileAccept = 'image/*', slash, mentions, attachments, onOpenFile, fileChanges, pasteToFile, model, effort, replyFooter, artifacts: hostArtifacts, artifactPanel, artifactTarget, conversations, onDock, feedback: composerFeedback, className,\n activeController }: SessionProps) {\n const { title } = labels;\n useChatPlacement(controller, embedded ? 'docked' : 'floating');\n const stateRef = useRef(windowState); stateRef.current = windowState;\n // The artifact open inside the window, and whether it follows the newest version. It belongs to the expanded window: leaving that state closes it.\n const [opened, setOpened] = useState<{ readonly descriptor: ArtifactDescriptor; readonly follow: boolean } | null>(null);\n const [fullscreen, setFullscreen] = useState(false);\n const setWindowState = (next: AmbientState) => {\n if (next !== 'expanded') { setOpened(null); setFullscreen(false); }\n if (next !== stateRef.current) onWindowState(next);\n };\n const expanded = windowState === 'expanded', minimized = windowState === 'minimized';\n const phone = usePhone();\n const root = useRef<HTMLDivElement>(null), titleButton = useRef<HTMLButtonElement>(null), pill = useRef<HTMLButtonElement>(null);\n const [picking, setPicking] = useState(false);\n const [feedback, setFeedback] = useState<Readonly<Record<string, 'up' | 'down'>>>({});\n const [outcome, setOutcome] = useState<'done' | 'error' | null>(null);\n const [permission, setPermission] = useState<NotifyPermission>('unsupported');\n useEffect(() => { setPermission(notifyPermission()); }, []);\n useEffect(() => { if (!outcome) return; const timer = setTimeout(() => setOutcome(null), 8000); return () => clearTimeout(timer); }, [outcome]);\n\n // Replies get copy, optional feedback and the time; the host may hand the conversation to its full chat.\n const conversationRef = useRef('');\n const replyActions = useMemo<NonNullable<RowContext['replyActions']>>(() => (reply, host) => <div data-testid=\"reply-actions\" className=\"mt-2 -ml-2 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 max-sm:opacity-100 pointer-coarse:opacity-100 motion-reduce:transition-none\">\n <CopyButton text={reply.text} label={labels.copyResponse} iconOnly {...(onCopy ? { onCopy } : {})} />\n {onFeedback && <>\n {(['up', 'down'] as const).map(value => <Toggle key={value} data-testid={`feedback-${value}`} pressed={feedback[reply.key] === value} aria-label={value === 'up' ? labels.goodResponse : labels.badResponse}\n title={value === 'up' ? labels.goodResponse : labels.badResponse}\n onPressedChange={pressed => { const next = pressed ? value : null; setFeedback(current => { const { [reply.key]: _removed, ...rest } = current; return next ? { ...rest, [reply.key]: next } : rest; }); onFeedback({ key: reply.key, text: reply.text }, next); }}>\n {value === 'up' ? <icons.goodResponse data-icon=\"inline-start\" aria-hidden=\"true\" /> : <icons.badResponse data-icon=\"inline-start\" aria-hidden=\"true\" />}</Toggle>)}\n </>}\n {onOpenFull && <Button variant=\"ghost\" size=\"icon\" data-testid=\"open-full\" aria-label={labels.openFull} title={labels.openFull} onClick={() => onOpenFull(conversationRef.current)}><icons.openFull data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {host}\n {reply.timestamp !== undefined && <time dateTime={new Date(reply.timestamp).toISOString()} data-testid=\"reply-time\" className=\"ml-1.5 text-xs tabular-nums text-muted-foreground\">{new Date(reply.timestamp).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}</time>}\n </div>, [onCopy, onFeedback, onOpenFull, feedback, labels, icons]);\n const scrollToBottom = useRef<() => void>(() => {});\n const rowExtras = useMemo(() => ({ replyActions }), [replyActions]);\n const inWindow = Boolean(artifactPanel) && artifactTarget !== 'host';\n const detect = hostArtifacts?.detect;\n const versionsRef = useRef<readonly ArtifactDescriptor[]>([]);\n const artifacts = useMemo<ArtifactsConfig | undefined>(() => !inWindow ? hostArtifacts : {\n open: descriptor => { const newest = versionsRef.current.filter(version => artifactKey(version) === artifactKey(descriptor)).reduce((top, version) => Math.max(top, version.ordinal ?? 0), 0); setOpened({ descriptor, follow: (descriptor.ordinal ?? 0) >= newest }); },\n isOpen: descriptor => opened !== null && artifactKey(opened.descriptor) === artifactKey(descriptor) && (opened.follow ? (descriptor.ordinal ?? 0) >= (versionsRef.current.find(version => artifactKey(version) === artifactKey(descriptor))?.ordinal ?? 0) : descriptor.revision === opened.descriptor.revision),\n ...(detect ? { detect } : {}),\n ...(hostArtifacts?.kinds ? { kinds: hostArtifacts.kinds } : {}),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [inWindow, hostArtifacts, opened, detect]);\n const session = useChatSession({ controller, activeController, mode, presentation, actions, renderEntry, hideLine, historicalMessageActions, unread: replyUnread, replyError, renderTool, groupTool, commandMentions, onOpenImage, onCopy, onComposerKeyDown, fileAccept,\n slash, mentions, attachments, model, effort, artifacts, messageActions, replyFooter, afterSend: () => scrollToBottom.current(), rowExtras, feedback: composerFeedback, onOpenFile, fileChanges, pasteToFile, composerChips });\n const { state, derived, rows, queued, queueActions, working, waitingForAnswer, connected, error, act, textarea, rowContext, composer, files, loading } = session;\n const empty = session.empty;\n const transcript = useTranscript(rows, displayLimit ?? 60, 40, { state: state.history, connected: !state.disposed && connected, load: controller.loadEarlier }, displayLimit === undefined);\n const versions = useMemo(() => inWindow ? collectArtifacts(state.view, detect).sort((a, b) => (b.ordinal ?? 0) - (a.ordinal ?? 0)) : [], [inWindow, state.view, detect]);\n versionsRef.current = versions;\n const known = opened ? versions.filter(version => artifactKey(version) === artifactKey(opened.descriptor)) : [];\n const shownArtifact = opened ? (opened.follow ? known[0] ?? opened.descriptor : opened.descriptor) : undefined;\n const { stick, hidden } = transcript;\n scrollToBottom.current = () => { transcript.reset(); void stick.scrollToBottom('instant'); };\n const { barStart, barNote } = session.pickers();\n\n const conversationId = String(conversations?.activeId ?? state.conversationId);\n conversationRef.current = conversationId;\n const activeItem = conversations?.items.find(item => item.id === conversations.activeId);\n const shownTitle = activeItem?.title?.trim() || title;\n const toasts = useNotifications(store);\n useRunFeed({ enabled: notifications !== false, store, conversationId, title: shownTitle, session, onOutcome: setOutcome, waiting: labels.waitingForAnswer,\n suppress: () => stateRef.current === 'expanded' && globalThis.document?.visibilityState !== 'hidden' });\n const unread = !expanded && toasts.length > 0;\n const seconds = useElapsed(working);\n\n // Position: offsets from the bottom right corner, remembered for the session, clamped to the viewport at render so a smaller window restores them.\n const [position, setPosition] = useState<Position>(() => readPosition(storageKey, storage) ?? DEFAULT_POSITION);\n const [viewport, setViewport] = useState(viewportSize);\n const [size, setSize] = useState({ w: 0, h: 0 });\n useEffect(() => {\n const onResize = () => setViewport(viewportSize());\n globalThis.addEventListener('resize', onResize);\n return () => globalThis.removeEventListener('resize', onResize);\n }, []);\n useEffect(() => {\n const element = root.current;\n if (!element || typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(() => setSize({ w: element.offsetWidth, h: element.offsetHeight }));\n observer.observe(element);\n setSize({ w: element.offsetWidth, h: element.offsetHeight });\n return () => observer.disconnect();\n }, [minimized, expanded]);\n // The size and the viewport also follow every render, so the clamp below never works from a stale measurement (the title or status icon can change the pill's width; a scrollbar can appear without a resize event).\n useLayoutEffect(() => {\n const element = root.current;\n const fresh = viewportSize();\n setViewport(current => current.w === fresh.w && current.h === fresh.h ? current : fresh);\n if (element) setSize(current => current.w === element.offsetWidth && current.h === element.offsetHeight ? current : { w: element.offsetWidth, h: element.offsetHeight });\n });\n const placed = clamp(position, size, viewport);\n // Moves and the release are followed on the window, so a drag keeps working when the pointer leaves the handle (and when pointer capture is unavailable).\n // `threshold` is how far the pointer must travel before it is a drag: a click on the pill (threshold > 0) still restores it.\n const dragged = useRef(false);\n const beginDrag = (event: PointerEvent<HTMLElement>, threshold: number) => {\n if (phone || event.button !== 0) return;\n dragged.current = false;\n const origin = { pointer: event.pointerId, x: event.clientX, y: event.clientY, start: placed };\n let last = origin.start, moving = threshold === 0;\n const move = (next: globalThis.PointerEvent) => {\n if (next.pointerId !== origin.pointer) return;\n if (!moving) { if (Math.hypot(next.clientX - origin.x, next.clientY - origin.y) < threshold) return; moving = true; dragged.current = true; }\n last = clamp({ right: origin.start.right - (next.clientX - origin.x), bottom: origin.start.bottom - (next.clientY - origin.y) }, size, viewport);\n setPosition(last);\n };\n const end = (next: globalThis.PointerEvent) => {\n if (next.pointerId !== origin.pointer) return;\n globalThis.removeEventListener('pointermove', move); globalThis.removeEventListener('pointerup', end); globalThis.removeEventListener('pointercancel', end);\n if (moving) writePosition(storageKey, storage, last);\n };\n globalThis.addEventListener('pointermove', move); globalThis.addEventListener('pointerup', end); globalThis.addEventListener('pointercancel', end);\n };\n const onGripDown = (event: PointerEvent<HTMLButtonElement>) => { event.preventDefault(); beginDrag(event, 0); };\n // Arrow keys move the window (Shift for big steps); Home puts it back in the default corner.\n const onGripKey = (event: KeyboardEvent<HTMLElement>) => {\n if (phone) return;\n const step = event.shiftKey ? 64 : 16;\n const delta = event.key === 'ArrowLeft' ? { right: step, bottom: 0 } : event.key === 'ArrowRight' ? { right: -step, bottom: 0 } : event.key === 'ArrowUp' ? { right: 0, bottom: step } : event.key === 'ArrowDown' ? { right: 0, bottom: -step } : undefined;\n if (!delta && event.key !== 'Home') return;\n event.preventDefault();\n const next = delta ? clamp({ right: placed.right + delta.right, bottom: placed.bottom + delta.bottom }, size, viewport) : clamp(DEFAULT_POSITION, size, viewport);\n setPosition(next); writePosition(storageKey, storage, next);\n };\n const grip = (className?: string) => <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-grip\" aria-label={labels.move} title={labels.moveHint} onPointerDown={onGripDown} onKeyDown={onGripKey}\n className={cn('touch-none', className)}><icons.grip data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>;\n\n const expand = () => { setOutcome(null); store.dismissConversation(conversationId); setWindowState('expanded'); };\n useEffect(() => {\n if (expanded && !globalThis.matchMedia?.('(pointer: coarse)').matches) textarea.current?.focus();\n }, [expanded, textarea]);\n // The title is disabled while the window is open, so focus goes back to it once the bar is showing again.\n const refocusTitle = useRef(false);\n useEffect(() => { if (!expanded && refocusTitle.current) { refocusTitle.current = false; titleButton.current?.focus(); } }, [expanded]);\n const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (embedded || event.key !== 'Escape' || event.defaultPrevented || !expanded || picking) return;\n event.preventDefault();\n // Escape steps back: full screen, then the artifact, then the window.\n if (fullscreen) return setFullscreen(false);\n if (opened) return setOpened(null);\n refocusTitle.current = true;\n setWindowState('bar');\n };\n const onActivate = useCallback((item: AgentNotification) => {\n if (item.conversationId !== undefined && item.conversationId !== conversationId) conversations?.onSelect(item.conversationId);\n expand();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [conversationId, conversations]);\n\n const connectionKind = state.connection.kind;\n const status = waitingForAnswer ? 'input' : working ? 'working' : outcome;\n const label = waitingForAnswer ? labels.waitingForAnswer : labels.working(seconds);\n const panelOpen = expanded && shownArtifact !== undefined && artifactPanel !== undefined;\n // The floating window is drawn by the host's Card behind it (a sibling layer, so switching placement never remounts the chat);\n // embedded and full-screen phone windows are plain page regions.\n const card = !embedded && !(phone && expanded);\n const surface = embedded ? 'relative flex min-h-0 flex-1 flex-col overflow-clip bg-transparent text-foreground' : cn('relative flex flex-col', card ? 'isolate' : 'h-dvh bg-background pt-[env(safe-area-inset-top)] text-foreground',\n !phone && expanded && (panelOpen ? 'h-full overflow-hidden' : 'max-h-[70dvh]'));\n // With an artifact open the window widens (most of the viewport, at most 72rem) and takes a fixed height so the panel fills it.\n const rootClass = embedded ? cn('pi-chat relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden', className) : cn('pi-chat pi-ambient fixed z-50', variant === 'contrast' && 'dark scheme-dark', phone ? (expanded ? 'inset-0' : 'inset-x-2 bottom-[max(0.5rem,env(safe-area-inset-bottom))]')\n : cn('transition-[width] motion-reduce:transition-none', minimized ? 'w-auto' : panelOpen ? 'h-[min(46rem,calc(100dvh-1rem))] w-[min(72rem,calc(100vw-1rem))]' : expanded ? 'w-[min(35rem,calc(100vw-1rem))]' : 'w-[min(26rem,calc(100vw-1rem))]'), className);\n const rootStyle = phone || embedded ? undefined : { right: placed.right, bottom: placed.bottom };\n\n if (minimized) return <div ref={root} data-boring=\"ambient-chat\" data-state=\"minimized\" data-variant={variant} className={cn(rootClass, phone && 'flex justify-end')} style={rootStyle} onKeyDown={onKeyDown}>\n {notifications !== false && <AgentNotifications store={store} variant={variant} placement=\"inline\" systemNotifications={systemNotifications} onActivate={onActivate} labels={labels}\n {...(autoDismissMs !== undefined ? { autoDismissMs } : {})} {...(maxToasts !== undefined ? { max: maxToasts } : {})} className=\"absolute right-0 bottom-full mb-3 w-[min(26rem,calc(100vw-1rem))]\" />}\n <div data-testid=\"ambient-pill-box\" onPointerDown={event => beginDrag(event, 4)} onKeyDown={onGripKey} onClickCapture={event => { if (dragged.current) { dragged.current = false; event.stopPropagation(); event.preventDefault(); } }}\n className={cn('relative flex max-w-[min(20rem,calc(100vw-1rem))] items-center gap-0.5', !phone && 'cursor-grab touch-none select-none active:cursor-grabbing')}>\n {!phone && grip('ml-1.5')}\n <Button ref={pill} variant=\"outline\" size=\"lg\" data-testid=\"ambient-pill\" onClick={() => setWindowState('bar')} aria-label={labels.openAgent(shownTitle, unread)}\n className=\"relative min-w-0 gap-2.5\">\n {status === 'working' ? <Loader2Icon data-icon=\"inline-start\" className=\"animate-spin motion-reduce:animate-none\" aria-hidden=\"true\" />\n : status === 'input' ? <MessageCircleQuestionIcon data-icon=\"inline-start\" aria-hidden=\"true\" />\n : status === 'done' ? <CircleCheckIcon data-icon=\"inline-start\" aria-hidden=\"true\" />\n : status === 'error' ? <CircleAlertIcon data-icon=\"inline-start\" aria-hidden=\"true\" /> : null}\n <span className=\"min-w-0 truncate\">{shownTitle}</span>\n {unread && <Badge data-testid=\"ambient-unread\" aria-hidden=\"true\" className=\"absolute -top-1.5 -right-1.5\" />}\n </Button>\n </div>\n </div>;\n\n const visibleRows = transcript.visible(rows);\n const conversation = <section className=\"flex min-h-0 min-w-0 flex-1 flex-col\">\n <header hidden={embedded} data-testid=\"ambient-header\" className={cn(embedded && '!hidden','flex h-12 shrink-0 items-center gap-0.5 px-2 max-sm:h-14 max-sm:px-3')}>\n <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-minimize\" aria-label={labels.minimize} title={labels.minimize} onClick={() => { setPicking(false); setWindowState(expanded ? 'bar' : 'minimized'); }}><icons.minimize data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>\n <span data-testid=\"ambient-status\" data-status={status ?? 'idle'} role=\"img\" aria-label={status === 'working' ? labels.statusWorking : status === 'input' ? labels.needsInput : status === 'done' ? labels.finished : status === 'error' ? labels.failedRun : undefined}\n aria-hidden={status ? undefined : 'true'} className={cn('flex shrink-0 items-center justify-center', status ? 'mx-1.5 w-5' : 'w-1.5')}>\n {status === 'working' ? <Loader2Icon className=\"size-4 animate-spin text-muted-foreground motion-reduce:animate-none\" aria-hidden=\"true\" /> : status ? <KindIcon kind={status === 'input' ? 'input' : status} className=\"size-[1.125rem]\" /> : null}\n </span>\n {avatar && <Avatar size=\"sm\" data-testid=\"ambient-avatar\" className=\"overflow-hidden\"><AvatarFallback className=\"*:max-h-full *:max-w-full\">{avatar}</AvatarFallback></Avatar>}\n <Button ref={titleButton} variant=\"ghost\" data-testid=\"ambient-title\" onClick={expand} disabled={expanded} aria-label={expanded ? shownTitle : labels.openChat(shownTitle)}\n className=\"min-w-0 max-w-full shrink\"><span className=\"truncate\">{shownTitle}</span></Button>\n {conversations && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-switch\" aria-label={labels.switchConversation} aria-haspopup=\"dialog\" aria-expanded={picking} title={labels.switchConversation} onClick={() => setPicking(open => !open)}>\n <icons.switchConversation data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {expanded ? <span className=\"flex-1\" /> : <Button variant=\"ghost\" tabIndex={-1} aria-hidden=\"true\" data-testid=\"ambient-header-fill\" onClick={expand} className=\"min-w-2 flex-1\" />}\n {systemNotifications && permission === 'default' && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-enable-notifications\" aria-label={labels.enableNotifications} title={labels.enableNotifications}\n onClick={() => { void requestNotifyPermission().then(setPermission); }}><icons.notify data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {headerTools}\n <BlockActions actions={headerActions} testId=\"ambient-action\" menuLabel={labels.moreActions} />\n {onDock && !phone && <Button variant=\"ghost\" size=\"icon\" data-testid=\"ambient-dock\" aria-label={labels.dock} title={labels.dockHint} onClick={() => { setPicking(false); onDock(); }}><icons.dock data-icon=\"inline-start\" aria-hidden=\"true\" /></Button>}\n {!phone && grip()}\n </header>\n\n {expanded && <div className=\"flex min-h-0 flex-1 flex-col\">\n {(connectionKind === 'error' || connectionKind === 'closed') && <div className=\"px-4 pb-2\"><Notice tone={connectionKind === 'error' ? 'error' : 'info'} testid=\"connection-notice\">\n <span className=\"flex-1\">{connectionKind === 'error' ? labels.loadFailed : labels.disconnected}</span>\n {!state.disposed && <Button size=\"sm\" variant=\"outline\" onClick={() => act(controller.connect)}><icons.reconnect data-icon=\"inline-start\" aria-hidden=\"true\" />{labels.reconnect}</Button>}</Notice></div>}\n {derived.pinned.length > 0 && <aside aria-label={labels.requiredActions} className=\"shrink-0 space-y-2 px-4 pb-2\">{derived.pinned.map(card => <div key={card.key}>{card.content}</div>)}</aside>}\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\n <MessageScroller>\n <MessageScrollerViewport ref={stick.scrollRef} data-testid=\"transcript-scroll\" aria-label={labels.messages} className=\"scrollbar-thumb-border scrollbar-track-transparent\">\n {!empty && <TranscriptHistory history={state.history} connected={!state.disposed && connected} onLoad={transcript.reveal} sentinelRef={transcript.sentinelRef} hidden={hidden} loaded={transcript.loaded} />}\n <div className=\"px-4 pt-4 pb-6\">\n <MessageScrollerContent aria-label={labels.messages} aria-live=\"polite\" data-testid=\"transcript\">\n {visibleRows.map(row => <MessageScrollerItem key={row.key} messageId={row.key} className=\"[content-visibility:visible]\"><RowView row={row} context={rowContext} /></MessageScrollerItem>)}\n {loading && <p role=\"status\" data-testid=\"loading\" className=\"m-0 flex items-center gap-2 text-sm text-muted-foreground\"><Loader2Icon className=\"size-4 animate-spin motion-reduce:animate-none\" aria-hidden=\"true\" />{labels.loading}</p>}\n {empty && !loading && <p data-testid=\"ambient-empty\" className=\"m-0 max-w-sm py-8 text-sm leading-6 text-muted-foreground\">{labels.empty}</p>}\n </MessageScrollerContent>\n </div>\n </MessageScrollerViewport>\n {!stick.isAtBottom && <MessageScrollerButton data-testid=\"jump-latest\" aria-label={labels.jumpToLatest} title={labels.jumpToLatest} onClick={transcript.reset}><icons.jumpToLatest aria-hidden=\"true\" /></MessageScrollerButton>}\n </MessageScroller>\n </div>\n </div>}\n\n {(expanded || !embedded) && <Separator />}\n <div className={cn('shrink-0', phone && 'pb-[env(safe-area-inset-bottom)]')}>\n <div className=\"space-y-2 px-3 empty:hidden [&:has(*)]:pt-2\">\n {preparing({ natural: session.natural, waitingForAnswer, run: derived.run, retry: derived.retry, compacting: derived.compacting, stop: state.stop }) && <PreparingStatus>{derived.stageStatus}</PreparingStatus>}\n <ChatNotices natural={session.natural} state={state} error={error} onReconcile={() => act(controller.reconcile)} onRetry={() => act(controller.retrySameRequest)} />\n {!expanded && (connectionKind === 'error' || connectionKind === 'closed') && <Notice tone=\"info\" testid=\"connection-notice\">\n <span className=\"flex-1\">{connectionKind === 'error' ? labels.loadFailedShort : labels.disconnectedShort}</span>\n {!state.disposed && <Button size=\"sm\" variant=\"outline\" onClick={() => act(controller.connect)}>{labels.reconnect}</Button>}</Notice>}\n </div>\n <div className=\"px-3 pt-2 pb-3\">\n <ConversationFiles files={files} />\n <MessageQueue items={queued} sending={state.outbox} withdraw={actions?.withdraw} actions={queueActions} />\n <Composer {...composer} layout={composerLayout} placeholder={working || waitingForAnswer ? label : labels.placeholder} barStart={barStart} barNote={barNote} barEnd={tools} />\n {belowComposer !== undefined && <div data-testid=\"below-composer\" className=\"pt-2\">{belowComposer}</div>}\n </div>\n </div>\n </section>;\n return <div ref={root} data-boring={embedded ? 'pi-chat' : 'ambient-chat'} data-state={windowState} data-variant={variant} data-phone={phone ? 'true' : undefined} role=\"region\" aria-label={labels.region(shownTitle)}\n className={rootClass} style={rootStyle} onKeyDown={onKeyDown}>\n {!embedded && notifications !== false && !(phone && expanded) && <AgentNotifications store={store} variant={variant} placement=\"inline\" systemNotifications={systemNotifications} onActivate={onActivate} labels={labels}\n {...(autoDismissMs !== undefined ? { autoDismissMs } : {})} {...(maxToasts !== undefined ? { max: maxToasts } : {})} className=\"absolute right-0 bottom-full mb-3 w-full\" />}\n {picking && conversations && <ConversationHistory conversations={conversations} placement={expanded ? 'below' : 'above'} onClose={() => setPicking(false)} />}\n <div data-testid=\"ambient-surface\" className={surface}>\n {card && <Card aria-hidden=\"true\" data-boring=\"chrome\" className=\"pointer-events-none absolute inset-0 -z-10\" />}\n {artifactPanel ? <ArtifactWorkspace open={panelOpen} onClose={() => { setOpened(null); setFullscreen(false); }} fullscreen={fullscreen} onFullscreenChange={setFullscreen} storageKey=\"boring.ambient.panel-width\" panelLabel={labels.artifactPanel}\n defaultWidth={600} minPanel={320} minChat={300} sheetBelow={720}\n panel={api => shownArtifact ? artifactPanel(shownArtifact, { ...api, versions: known, follow: opened?.follow ?? true,\n select: version => setOpened(current => current ? (version === 'latest' ? { descriptor: known[0] ?? current.descriptor, follow: true } : { descriptor: version, follow: false }) : current) }) : null}\n chat={conversation} /> : conversation}\n </div>\n </div>;\n}\n",
32
32
  "type": "registry:component",
33
33
  "target": "components/pi-ambient/ambient.tsx"
34
34
  },