@hachej/boring-ui-kit 0.4.46 → 0.4.47

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 (62) hide show
  1. package/README.md +0 -4
  2. package/dist/chat-react-session.d.ts +1 -0
  3. package/dist/chat-react-session.js +12 -3
  4. package/dist/chat-view.d.ts +2 -1
  5. package/dist/native-chat.d.ts +39 -2
  6. package/dist/native-chat.js +135 -9
  7. package/dist/remote-chat.d.ts +6 -0
  8. package/dist/remote-chat.js +22 -0
  9. package/dist/shared-chat/index.d.ts +1 -1
  10. package/package.json +2 -6
  11. package/r/agent-target.json +4 -4
  12. package/r/app-shell.json +12 -12
  13. package/r/board-viewer.json +6 -6
  14. package/r/button.json +1 -1
  15. package/r/dock.json +1 -1
  16. package/r/documents.json +12 -12
  17. package/r/feedback.json +24 -12
  18. package/r/file-tree.json +4 -4
  19. package/r/highlights.json +3 -3
  20. package/r/html-viewer.json +3 -3
  21. package/r/looks-viewer.json +2 -2
  22. package/r/maquette-viewer.json +1 -1
  23. package/r/markdown-editor.json +3 -3
  24. package/r/mockup-viewer.json +1 -1
  25. package/r/pi-ambient.json +9 -9
  26. package/r/pi-app-kinds.json +10 -10
  27. package/r/pi-app.json +12 -12
  28. package/r/pi-artifacts.json +2 -2
  29. package/r/pi-chat-approval.json +2 -2
  30. package/r/pi-chat-attachments.json +1 -1
  31. package/r/pi-chat-markdown.json +2 -2
  32. package/r/pi-chat-mentions.json +1 -1
  33. package/r/pi-chat-pickers.json +1 -1
  34. package/r/pi-chat-plugins.json +11 -11
  35. package/r/pi-chat-question.json +1 -1
  36. package/r/pi-chat-slash.json +2 -2
  37. package/r/pi-chat-subagent.json +1 -1
  38. package/r/pi-chat-voice.json +9 -3
  39. package/r/pi-chat.json +11 -24
  40. package/r/pi-workspace.json +3 -3
  41. package/r/placed-agent.json +4 -4
  42. package/r/provider-setup.json +1 -1
  43. package/r/registry.json +1211 -1381
  44. package/r/shared-chat.json +6 -6
  45. package/r/spaces.json +44 -0
  46. package/r/task-list-viewer.json +2 -2
  47. package/r/version-history.json +2 -2
  48. package/r/viewer-code.json +2 -2
  49. package/r/viewer-frame.json +4 -4
  50. package/r/viewer-html.json +4 -4
  51. package/r/viewer-image.json +2 -2
  52. package/r/viewer-markdown.json +5 -5
  53. package/r/viewer-pdf.json +3 -3
  54. package/r/viewers.json +6 -6
  55. package/r/windows.json +1 -1
  56. package/dist/task-board.d.ts +0 -96
  57. package/dist/task-board.js +0 -48
  58. package/r/feedback-preview.json +0 -20
  59. package/r/pm-panel.json +0 -122
  60. package/r/requests-board.json +0 -33
  61. package/r/task-board.json +0 -41
  62. package/r/task-mentions.json +0 -25
@@ -4,16 +4,16 @@
4
4
  "title": "Shared chat",
5
5
  "description": "Optional multi-party host log presentation over PiChat's generic composition seams. No agent runtime or speaking policy.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.46",
7
+ "@hachej/boring-ui-kit@^0.4.47",
8
8
  "react@^19.0.0",
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/pi-chat.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/pi-chat-plugins.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/pi-chat-markdown.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/pi-chat-mentions.json",
16
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/pi-chat.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/pi-chat-plugins.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/pi-chat-markdown.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/pi-chat-mentions.json",
16
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json"
17
17
  ],
18
18
  "files": [
19
19
  {
package/r/spaces.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "spaces",
4
+ "title": "Spaces",
5
+ "description": "A client for a host's Spaces service: useSpaces, SpaceSwitcher and MembersSheet over a transport the app gives. Membership and roles stay the host's; no route or app wording.",
6
+ "dependencies": [
7
+ "@hachej/boring-files@^0.4.47",
8
+ "react@^19.0.0",
9
+ "lucide-react@^1.51.0"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "registry/spaces/spaces.ts",
14
+ "content": "export { MembersSheet } from './members-sheet';\nexport type { MembersSheetProps } from './members-sheet';\nexport { SpaceSwitcher } from './space-switcher';\nexport type { SpaceSwitcherProps } from './space-switcher';\nexport { spaceErrorMessage, spacesLabels, spacesLabelsFr } from './spaces-labels';\nexport type { SpacesLabels } from './spaces-labels';\nexport { spaceInviteLink, useSpaces } from './use-spaces';\nexport type { SpaceAdminAccess, SpaceInvite, SpaceMember, SpaceRoleOption, SpacesTransport, SpaceSummary, SpaceView, UseSpacesOptions } from './use-spaces';\n",
15
+ "type": "registry:lib",
16
+ "target": "components/spaces/spaces.ts"
17
+ },
18
+ {
19
+ "path": "registry/spaces/use-spaces.ts",
20
+ "content": "'use client';\n\n// The client of a host's Spaces service. The host checks membership and role on every call; this only names the space.\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { randomUUID } from '@hachej/boring-files/platform';\nimport { spaceErrorMessage, spacesLabels } from './spaces-labels';\nimport type { SpacesLabels } from './spaces-labels';\n\nexport type SpaceSummary = { readonly id: string; readonly kind: string; readonly title: string; readonly role: string };\nexport type SpaceMember = { readonly person?: string; readonly name: string; readonly role: string; readonly since?: string };\nexport type SpaceInvite = { readonly id: string; readonly role: string; readonly expiresAt: string; readonly by: string };\nexport type SpaceAdminAccess = { readonly by: string; readonly at: string; readonly reason: string; readonly write: boolean; readonly until: string };\n/** A guest sees the managers only; managers also get the open invites and every administrator access. */\nexport type SpaceView = { readonly space: SpaceSummary & { readonly orphaned: boolean }; readonly members: readonly SpaceMember[]; readonly invites?: readonly SpaceInvite[]; readonly access?: readonly SpaceAdminAccess[] };\nexport type SpaceRoleOption = { readonly id: string; readonly label: string; readonly manage?: boolean };\n\n/** One operation of the host's Spaces service, for example `(operation, input) => api(`_spaces/${operation}`, { body: input })`. */\nexport type SpacesTransport = <T>(operation: string, input: Record<string, unknown>) => Promise<T>;\n\nexport interface UseSpacesOptions {\n /** The kind of space to list and create. */\n readonly kind: string;\n /** Keep it stable: a new function reads the list again. */\n readonly transport: SpacesTransport;\n readonly labels?: SpacesLabels;\n /** The address parameter of an invitation, accepted once and then removed (default `invite`). */\n readonly inviteParam?: string;\n}\n\n/** A change carries a fresh idempotency key: a retry of the same call is the same change. */\nconst change = (input: Record<string, unknown>) => ({ ...input, idempotencyKey: randomUUID() });\n\nconst rememberedKey = (kind: string) => `space:${kind}`;\nconst remembered = (kind: string): string | null => { try { return localStorage.getItem(rememberedKey(kind)); } catch { return null; } };\n\n/** The link a manager sends: this page with the invitation in its address. The host signs the person in first and keeps the query. */\nexport const spaceInviteLink = (token: string, inviteParam = 'invite'): string => {\n const url = new URL(window.location.href);\n url.hash = '';\n url.search = `?${inviteParam}=${encodeURIComponent(token)}`;\n return url.href;\n};\n\n/** The person's spaces of one kind: the list, the current one (remembered per kind on this device), create, members, and the invitation in the address. */\nexport function useSpaces({ kind, transport, labels = spacesLabels, inviteParam = 'invite' }: UseSpacesOptions) {\n const [listed, setListed] = useState<{ readonly kind: string; readonly spaces: readonly SpaceSummary[] } | null>(null);\n // One choice per kind; a kind not chosen yet starts from what this device remembers.\n const [picks, setPicks] = useState<Readonly<Record<string, string | null>>>({});\n const [listError, setListError] = useState<string | null>(null);\n const [inviteError, setInviteError] = useState<string | null>(null);\n const spaces = listed?.kind === kind ? listed.spaces : null;\n const picked = Object.hasOwn(picks, kind);\n const selected = picked ? picks[kind] ?? null : remembered(kind);\n const words = useRef(labels);\n useEffect(() => { words.current = labels; }, [labels]);\n const accepting = useRef<Promise<unknown>>(Promise.resolve());\n const latest = useRef(0);\n const refresh = useCallback(async () => {\n const ticket = ++latest.current;\n // A list read before an invitation is accepted would not hold the space it opens, and only the last list asked for counts.\n await accepting.current;\n try {\n const { spaces: list } = await transport<{ spaces: SpaceSummary[] }>('spaces.list', { kind });\n if (ticket !== latest.current) return;\n setListError(null);\n setListed({ kind, spaces: list });\n setPicks(current => {\n const id = Object.hasOwn(current, kind) ? current[kind] ?? null : remembered(kind);\n return { ...current, [kind]: id && list.some(space => space.id === id) ? id : list[0]?.id ?? null };\n });\n } catch (problem) { if (ticket === latest.current) setListError(spaceErrorMessage(problem, words.current)); }\n }, [kind, transport]);\n const select = useCallback((id: string) => setPicks(current => ({ ...current, [kind]: id })), [kind]);\n useEffect(() => {\n const token = new URLSearchParams(window.location.search).get(inviteParam);\n // Removed from the address at once, so a second run of this effect (Strict Mode, a new transport) cannot use the one-time token again.\n if (token) {\n const url = new URL(window.location.href);\n url.searchParams.delete(inviteParam);\n window.history.replaceState(window.history.state, '', url.href);\n accepting.current = transport<{ space: SpaceSummary }>('invites.accept', change({ token }))\n .then(({ space }) => { setInviteError(null); select(space.id); }, problem => setInviteError(spaceErrorMessage(problem, words.current)));\n }\n // eslint-disable-next-line react-hooks/set-state-in-effect -- reads the host's list on mount: its state is set after the await, never synchronously\n void refresh();\n }, [refresh, select, transport, inviteParam]);\n useEffect(() => { try { if (picked && selected) localStorage.setItem(rememberedKey(kind), selected); } catch { /* private mode: not remembered */ } }, [picked, selected, kind]);\n const create = useCallback(async (title: string) => {\n const { space } = await transport<{ space: SpaceSummary }>('spaces.create', change({ kind, title }));\n await refresh();\n select(space.id);\n return space;\n }, [kind, transport, refresh, select]);\n const read = useCallback((space: string) => transport<SpaceView>('spaces.get', { space }), [transport]);\n const invite = useCallback(async (space: string, email: string, role: string) => {\n const { invite: created } = await transport<{ invite: { token: string; expiresAt: string } }>('members.invite', change({ space, email, role }));\n return { ...created, link: spaceInviteLink(created.token, inviteParam) };\n }, [transport, inviteParam]);\n const setRole = useCallback((space: string, person: string, role: string) => transport<unknown>('members.setRole', change({ space, person, role })), [transport]);\n const remove = useCallback((space: string, person: string) => transport<unknown>('members.remove', change({ space, person })), [transport]);\n const leave = useCallback((space: string) => transport<unknown>('members.leave', change({ space })), [transport]);\n const error = listError ?? inviteError;\n return useMemo(() => ({\n spaces, current: spaces?.find(space => space.id === selected) ?? null, error, select, create, refresh, read, invite, setRole, remove, leave,\n }), [spaces, selected, error, select, create, refresh, read, invite, setRole, remove, leave]);\n}\n",
21
+ "type": "registry:hook",
22
+ "target": "components/spaces/use-spaces.ts"
23
+ },
24
+ {
25
+ "path": "registry/spaces/spaces-labels.ts",
26
+ "content": "// The Spaces block's words, in English and French. A host names what a space is through its own labels.\n\nexport interface SpacesLabels {\n readonly choose: (label: string) => string;\n readonly noSpaces: string;\n readonly membersDescription: string;\n readonly members: string;\n readonly you: string;\n readonly role: string;\n readonly remove: string;\n readonly invite: string;\n readonly email: string;\n readonly emailPlaceholder: string;\n readonly createLink: string;\n readonly copy: string;\n readonly inviteHint: string;\n readonly adminAccess: string;\n readonly adminAccessEntry: (entry: { readonly at: string; readonly reason: string; readonly write: boolean }) => string;\n readonly leave: string;\n /** What a refusal of the host means, by its code. A code missing here shows the error's own message. */\n readonly errors: Readonly<Record<string, string>>;\n}\n\nexport const spacesLabels: SpacesLabels = {\n choose: label => `Choose: ${label.toLowerCase()}`,\n noSpaces: 'None yet.',\n membersDescription: 'Who can open this space, and with which role.',\n members: 'Members',\n you: ' (you)',\n role: 'Role',\n remove: 'Remove',\n invite: 'Invite someone',\n email: 'Email',\n emailPlaceholder: 'name@example.com',\n createLink: 'Create the link',\n copy: 'Copy',\n inviteHint: 'Send it yourself: it works once, for this email.',\n adminAccess: 'Administrator access',\n adminAccessEntry: ({ at, reason, write }) => `Administrator access on ${new Date(at).toLocaleString('en-GB')}: ${reason}${write ? ' (with changes)' : ''}`,\n leave: 'Leave this space',\n errors: {\n 'last-manager': 'A space needs at least one manager: name another one before leaving.',\n 'invite-used': 'This invitation link has already been used. Ask for a new one.',\n 'invite-expired': 'This invitation link has expired. Ask for a new one.',\n 'not-found': 'This space does not exist, or you have no access to it.',\n role: 'Your role in this space does not allow it.',\n manage: 'Only a manager of this space can do this.',\n limit: 'This space has reached its limit.',\n member: 'This person is already a member of this space.',\n },\n};\n\nexport const spacesLabelsFr: SpacesLabels = {\n choose: label => `Choisir : ${label.toLowerCase()}`,\n noSpaces: 'Aucun pour l’instant.',\n membersDescription: 'Qui peut ouvrir cet espace, et avec quel rôle.',\n members: 'Membres',\n you: ' (vous)',\n role: 'Rôle',\n remove: 'Retirer',\n invite: 'Inviter quelqu’un',\n email: 'Email',\n emailPlaceholder: 'nom@exemple.fr',\n createLink: 'Créer le lien',\n copy: 'Copier',\n inviteHint: 'Envoyez-le vous-même : il sert une fois, pour cet email.',\n adminAccess: 'Accès administrateur',\n adminAccessEntry: ({ at, reason, write }) => `Accès administrateur le ${new Date(at).toLocaleString('fr-FR')} : ${reason}${write ? ' (avec modifications)' : ''}`,\n leave: 'Quitter cet espace',\n errors: {\n 'last-manager': 'Il faut au moins un responsable : nommez-en un autre avant de partir.',\n 'invite-used': 'Ce lien d’invitation a déjà servi. Demandez-en un nouveau.',\n 'invite-expired': 'Ce lien d’invitation a expiré. Demandez-en un nouveau.',\n 'not-found': 'Cet espace n’existe pas, ou vous n’y avez pas accès.',\n role: 'Votre rôle dans cet espace ne le permet pas.',\n manage: 'Seul un responsable de cet espace peut le faire.',\n limit: 'La limite est atteinte pour cet espace.',\n member: 'Cette personne fait déjà partie de cet espace.',\n },\n};\n\n/** The words for an error of the host: by its code, else its own message. */\nexport const spaceErrorMessage = (error: unknown, labels: SpacesLabels = spacesLabels): string => {\n const code = (error as { code?: string } | null)?.code;\n return (code && labels.errors[code]) || (error as Error | null)?.message || String(error);\n};\n",
27
+ "type": "registry:lib",
28
+ "target": "components/spaces/spaces-labels.ts"
29
+ },
30
+ {
31
+ "path": "registry/spaces/space-switcher.tsx",
32
+ "content": "'use client';\n\n// Choosing the open space among the person's own.\nimport { CheckIcon, ChevronsUpDownIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';\nimport { spacesLabels } from './spaces-labels';\nimport type { SpacesLabels } from './spaces-labels';\nimport type { SpaceSummary } from './use-spaces';\n\nexport interface SpaceSwitcherProps {\n /** What a space is called here, as a heading (\"Projects\"). */\n readonly label: string;\n readonly spaces: readonly SpaceSummary[];\n readonly current: SpaceSummary | null;\n readonly onSelect: (id: string) => void;\n /** The word for a role (default: the role's id). */\n readonly roleLabel?: (role: string) => string;\n readonly labels?: SpacesLabels;\n}\n\nexport function SpaceSwitcher({ label, spaces, current, onSelect, roleLabel = role => role, labels = spacesLabels }: SpaceSwitcherProps) {\n return <div className=\"flex items-center gap-2\" data-testid=\"space-switcher\">\n <DropdownMenu>\n <DropdownMenuTrigger render={<Button variant=\"outline\" className=\"max-w-72 justify-between gap-2\" />} data-testid=\"space-switcher-trigger\">\n <span className=\"truncate\">{current?.title ?? labels.choose(label)}</span>\n <ChevronsUpDownIcon className=\"size-4 text-muted-foreground\" aria-hidden=\"true\" />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-72\">\n <DropdownMenuGroup>\n <DropdownMenuLabel>{label}</DropdownMenuLabel>\n {spaces.length === 0 && <div className=\"px-2 py-1.5 text-sm text-muted-foreground\">{labels.noSpaces}</div>}\n {spaces.map(space => <DropdownMenuItem key={space.id} onClick={() => onSelect(space.id)} data-testid={`space-${space.id}`}>\n <CheckIcon className={space.id === current?.id ? 'size-4' : 'size-4 invisible'} aria-hidden=\"true\" />\n <span className=\"flex-1 truncate\">{space.title}</span>\n <span className=\"text-xs text-muted-foreground\">{roleLabel(space.role)}</span>\n </DropdownMenuItem>)}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>;\n}\n",
33
+ "type": "registry:component",
34
+ "target": "components/spaces/space-switcher.tsx"
35
+ },
36
+ {
37
+ "path": "registry/spaces/members-sheet.tsx",
38
+ "content": "'use client';\n\n// The open space's members and roles, the invite link, remove, leave, and every administrator access to it.\n// What a person may do is the host's to decide: the sheet only hides what would be refused.\nimport { useState } from 'react';\nimport { copyToClipboard } from '@hachej/boring-files/platform';\nimport { CopyIcon, LogOutIcon, ShieldAlertIcon, UserMinusIcon } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Item, ItemActions, ItemContent } from '@/components/ui/item';\nimport { NativeSelect, NativeSelectOption } from '@/components/ui/native-select';\nimport { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';\nimport { spaceErrorMessage, spacesLabels } from './spaces-labels';\nimport type { SpacesLabels } from './spaces-labels';\nimport type { SpaceMember, SpaceRoleOption, SpaceView } from './use-spaces';\n\nexport interface MembersSheetProps {\n readonly open: boolean;\n readonly onOpenChange: (open: boolean) => void;\n readonly view: SpaceView | null;\n readonly roles: readonly SpaceRoleOption[];\n /** The person looking: never offered to remove themselves. */\n readonly me: string;\n readonly onInvite: (email: string, role: string) => Promise<{ readonly link: string }>;\n readonly onSetRole: (person: string, role: string) => Promise<unknown>;\n readonly onRemove: (person: string) => Promise<unknown>;\n readonly onLeave: () => Promise<unknown>;\n readonly labels?: SpacesLabels;\n}\n\ntype Act = (run: () => Promise<unknown>) => Promise<void>;\n\nfunction RoleSelect({ roles, value, onChange, className, label }: { readonly roles: readonly SpaceRoleOption[]; readonly value: string; readonly onChange: (role: string) => void; readonly className: string; readonly label: string }) {\n return <NativeSelect aria-label={label} value={value} onChange={event => onChange(event.target.value)} className={className}>\n {roles.map(role => <NativeSelectOption key={role.id} value={role.id}>{role.label}</NativeSelectOption>)}\n </NativeSelect>;\n}\n\nfunction MemberRow({ member, editable, me, roles, labels, onSetRole, onRemove }: { readonly member: SpaceMember; readonly editable: boolean; readonly me: string; readonly roles: readonly SpaceRoleOption[]; readonly labels: SpacesLabels; readonly onSetRole: (role: string) => void; readonly onRemove: () => void }) {\n const managed = editable && member.person && member.person !== me;\n return <li className=\"flex items-center gap-2 px-3 py-2\" data-testid={`member-${member.person ?? member.name}`}>\n <span className=\"flex-1 truncate text-sm\">{member.name || member.person}{member.person === me ? labels.you : ''}</span>\n {managed\n ? <RoleSelect roles={roles} value={member.role} onChange={onSetRole} className=\"w-36\" label={labels.role} />\n : <span className=\"text-xs text-muted-foreground\">{roles.find(role => role.id === member.role)?.label ?? member.role}</span>}\n {managed && <Button size=\"icon\" variant=\"ghost\" aria-label={labels.remove} title={labels.remove} onClick={onRemove}><UserMinusIcon className=\"size-4\" /></Button>}\n </li>;\n}\n\nfunction InviteForm({ roles, labels, onInvite, act }: { readonly roles: readonly SpaceRoleOption[]; readonly labels: SpacesLabels; readonly onInvite: MembersSheetProps['onInvite']; readonly act: Act }) {\n const [email, setEmail] = useState('');\n const [role, setRole] = useState(roles.find(option => !option.manage)?.id ?? roles[0]?.id ?? '');\n const [link, setLink] = useState<string | null>(null);\n const submit = () => act(async () => { setLink((await onInvite(email.trim(), role)).link); setEmail(''); });\n return <section className=\"flex flex-col gap-2\" aria-label={labels.invite}>\n <h3 className=\"text-sm font-medium\">{labels.invite}</h3>\n <form className=\"flex flex-col gap-2\" onSubmit={event => { event.preventDefault(); void submit(); }}>\n <Input type=\"email\" required value={email} onChange={event => setEmail(event.target.value)} placeholder={labels.emailPlaceholder} aria-label={labels.email} data-testid=\"invite-email\" />\n <div className=\"flex gap-2\">\n <RoleSelect roles={roles} value={role} onChange={setRole} className=\"flex-1\" label={labels.role} />\n <Button type=\"submit\" data-testid=\"invite-submit\">{labels.createLink}</Button>\n </div>\n </form>\n {link && <Item variant=\"muted\" size=\"sm\" data-testid=\"invite-link\">\n <ItemContent className=\"min-w-0\"><span className=\"truncate text-xs\">{link}</span></ItemContent>\n <ItemActions><Button size=\"icon\" variant=\"ghost\" aria-label={labels.copy} onClick={() => void copyToClipboard(link)}><CopyIcon className=\"size-4\" /></Button></ItemActions>\n </Item>}\n {link && <p className=\"text-xs text-muted-foreground\">{labels.inviteHint}</p>}\n </section>;\n}\n\nexport function MembersSheet({ open, onOpenChange, view, roles, me, onInvite, onSetRole, onRemove, onLeave, labels = spacesLabels }: MembersSheetProps) {\n const [error, setError] = useState<string | null>(null);\n const myRole = view?.space.role ?? null;\n const manager = Boolean(roles.find(role => role.id === myRole)?.manage);\n const access = manager ? view?.access ?? [] : [];\n const act: Act = async run => { setError(null); try { await run(); } catch (problem) { setError(spaceErrorMessage(problem, labels)); } };\n return <Sheet open={open} onOpenChange={next => onOpenChange(next)}>\n <SheetContent side=\"right\" className=\"w-full sm:max-w-md\" data-testid=\"members-sheet\">\n <SheetHeader>\n <SheetTitle>{view?.space.title ?? '…'}</SheetTitle>\n <SheetDescription>{labels.membersDescription}</SheetDescription>\n </SheetHeader>\n <div className=\"flex flex-col gap-6 overflow-y-auto px-4 pb-6\">\n {error && <p role=\"alert\" className=\"text-sm text-destructive\" data-testid=\"members-error\">{error}</p>}\n <section className=\"flex flex-col gap-2\" aria-label={labels.members}>\n <h3 className=\"text-sm font-medium\">{labels.members}</h3>\n <ul className=\"flex flex-col divide-y\">\n {(view?.members ?? []).map(member => <MemberRow key={member.person ?? member.name} member={member} editable={manager} me={me} roles={roles} labels={labels}\n onSetRole={role => void act(() => onSetRole(member.person!, role))} onRemove={() => void act(() => onRemove(member.person!))} />)}\n </ul>\n </section>\n {manager && <InviteForm key={view?.space.id} roles={roles} labels={labels} onInvite={onInvite} act={act} />}\n {access.length > 0 && <section className=\"flex flex-col gap-2\" aria-label={labels.adminAccess} data-testid=\"admin-access\">\n <h3 className=\"text-sm font-medium\">{labels.adminAccess}</h3>\n <ul className=\"flex flex-col gap-1\">\n {access.map(entry => <li key={entry.at} className=\"flex items-start gap-2 text-sm\">\n <ShieldAlertIcon className=\"mt-0.5 size-4 text-muted-foreground\" aria-hidden=\"true\" />\n <span>{labels.adminAccessEntry(entry)}</span>\n </li>)}\n </ul>\n </section>}\n {myRole && <Button variant=\"outline\" className=\"self-start\" onClick={() => void act(onLeave)} data-testid=\"leave-space\"><LogOutIcon className=\"size-4\" />{labels.leave}</Button>}\n </div>\n </SheetContent>\n </Sheet>;\n}\n",
39
+ "type": "registry:component",
40
+ "target": "components/spaces/members-sheet.tsx"
41
+ }
42
+ ],
43
+ "type": "registry:component"
44
+ }
@@ -4,8 +4,8 @@
4
4
  "title": "Consumer task-list viewer",
5
5
  "description": "A consumer-defined task-list schema and controller over the shared text buffer, with a restylable borrowed React renderer.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.46",
8
- "@hachej/boring-files@^0.4.46",
7
+ "@hachej/boring-ui-kit@^0.4.47",
8
+ "@hachej/boring-files@^0.4.47",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0",
11
11
  "zod@^4.6.5"
@@ -4,11 +4,11 @@
4
4
  "title": "Version history rows",
5
5
  "description": "Shared view-only version rows and menu metadata with relative time, provenance and Current. No file reads, publication or restoration.",
6
6
  "dependencies": [
7
- "@hachej/boring-files@^0.4.46",
7
+ "@hachej/boring-files@^0.4.47",
8
8
  "react@^19.0.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/button.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/button.json"
12
12
  ],
13
13
  "files": [
14
14
  {
@@ -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.46/r/viewer-frame.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/pi-chat.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/pi-chat.json"
13
13
  ],
14
14
  "files": [
15
15
  {
@@ -4,15 +4,15 @@
4
4
  "title": "Viewer frame",
5
5
  "description": "The standard viewer top bar every viewer draws in (title, status, viewer controls, Refresh, Share, Copy, Download, Open in new tab, Close, overflow menu), its window labels, download, share and media helpers and the sandboxed interactive HTML frame. The viewers' styles live here. Each viewer is its own item (viewer-markdown, viewer-html, viewer-image, viewer-pdf, viewer-code) with a cheap kind module and a view loaded on first open.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.46",
7
+ "@hachej/boring-ui-kit@^0.4.47",
8
8
  "react@^19.0.0",
9
9
  "react-dom@^19.0.0",
10
10
  "lucide-react@^1.51.0"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/button.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/version-history.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/button.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/version-history.json"
16
16
  ],
17
17
  "files": [
18
18
  {
@@ -4,14 +4,14 @@
4
4
  "title": "HTML viewer",
5
5
  "description": "The HTML kind: `htmlKind` (match, loader) and `HtmlPane`, the passive HTML preview (sandboxed when the host opts in) and source editor, in the viewer frame. The pane loads the first time an HTML file opens.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.46",
7
+ "@hachej/boring-ui-kit@^0.4.47",
8
8
  "react@^19.0.0",
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-frame.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/button.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/button.json"
15
15
  ],
16
16
  "files": [
17
17
  {
@@ -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.46/r/viewer-frame.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {
@@ -4,7 +4,7 @@
4
4
  "title": "Markdown viewer",
5
5
  "description": "The Markdown kind: `markdownKind` (match, loader) and `MarkdownPane`, the tiptap editor with a formatting toolbar and prose styling over the kit's revisioned Markdown controller, in the viewer frame. The pane and tiptap load the first time a Markdown file opens.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.46",
7
+ "@hachej/boring-ui-kit@^0.4.47",
8
8
  "react@^19.0.0",
9
9
  "lucide-react@^1.51.0",
10
10
  "marked@^17.0.6",
@@ -22,10 +22,10 @@
22
22
  "@tiptap/extension-floating-menu@^3.31.4"
23
23
  ],
24
24
  "registryDependencies": [
25
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-frame.json",
26
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/markdown-editor.json",
27
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json",
28
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/button.json"
25
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
26
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/markdown-editor.json",
27
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json",
28
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/button.json"
29
29
  ],
30
30
  "files": [
31
31
  {
package/r/viewer-pdf.json CHANGED
@@ -4,14 +4,14 @@
4
4
  "title": "PDF viewer",
5
5
  "description": "The PDF kind: `pdfKind` (match, loader) and `PdfPane` in the viewer frame (the browser's viewer, or pdf.js pages where there is none), and LocalFileViewer for browser-only files. The pane loads the first time a PDF opens.",
6
6
  "dependencies": [
7
- "@hachej/boring-files@^0.4.46",
7
+ "@hachej/boring-files@^0.4.47",
8
8
  "react@^19.0.0",
9
9
  "lucide-react@^1.51.0",
10
10
  "pdfjs-dist@6.4.299"
11
11
  ],
12
12
  "registryDependencies": [
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-frame.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-image.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-image.json"
15
15
  ],
16
16
  "files": [
17
17
  {
package/r/viewers.json CHANGED
@@ -4,12 +4,12 @@
4
4
  "title": "Viewers",
5
5
  "description": "Deprecated aggregate: installs every viewer item (viewer-frame, viewer-markdown, viewer-html, viewer-image, viewer-pdf, viewer-code). Add the items you use instead.",
6
6
  "registryDependencies": [
7
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-frame.json",
8
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-markdown.json",
9
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-html.json",
10
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-image.json",
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-pdf.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/viewer-code.json"
7
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-frame.json",
8
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-markdown.json",
9
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-html.json",
10
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-image.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-pdf.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/viewer-code.json"
13
13
  ],
14
14
  "files": [],
15
15
  "type": "registry:component"
package/r/windows.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.46/r/utils.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.47/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {
@@ -1,96 +0,0 @@
1
- /** Business requests supplied by the host, not Pi execution tasks. No fetching, mutation or authorization lives here. */
2
- export declare const TASK_STATES: readonly ["waiting", "working", "open", "done", "closed"];
3
- export type TaskState = typeof TASK_STATES[number];
4
- export type TaskFilter = 'all' | 'mine' | 'waiting-on-me';
5
- export interface TaskQuestion {
6
- readonly id: string;
7
- readonly text: string;
8
- /** Host-supplied display identity and timestamp for the question. */
9
- readonly askedBy?: string;
10
- readonly askedAt?: string;
11
- readonly options?: readonly (string | {
12
- readonly label: string;
13
- readonly value: string;
14
- })[];
15
- readonly answer?: string;
16
- /** Display identity supplied by the host, not inferred from the current viewer. */
17
- readonly answeredBy?: string;
18
- /** Optional host answer timestamp; newest answers first, otherwise latest source entries first. */
19
- readonly answeredAt?: string;
20
- }
21
- export interface TaskFile {
22
- /** Authorized workspace path supplied by the host adapter; never derived from the task id. */
23
- readonly path: string;
24
- readonly name?: string;
25
- /** Presentation metadata, not permission to execute or load a file. */
26
- readonly kind?: 'markdown' | 'mockup' | 'capture';
27
- }
28
- export interface TaskHistory {
29
- readonly id?: string;
30
- readonly at?: string;
31
- readonly actor?: string;
32
- readonly note: string;
33
- }
34
- export interface TaskRecord {
35
- readonly id: string;
36
- readonly number?: number | string;
37
- readonly title: string;
38
- /** Display name of the task's app, set by a host showing several apps; cards then carry it and the bar filters by it. */
39
- readonly app?: string;
40
- /** Short host-authored display title; the original title remains the description. */
41
- readonly display?: string;
42
- /** Canonical host classification, never inferred from prose or an actor name. */
43
- readonly technical?: boolean;
44
- readonly state: TaskState;
45
- /** Host wording such as “À essayer”, independent of the five board columns. */
46
- readonly status?: string;
47
- /** The host adapter resolves the request's author; never inferred from an assignee. */
48
- readonly requesterId?: string;
49
- /** Host-formatted display metadata; identities here never determine ownership or filtering. */
50
- readonly requester?: string;
51
- readonly refiner?: string;
52
- readonly age?: string;
53
- /** Native viewer-relative projection, not inferred from waiting_on.who. */
54
- readonly waiting_on_you?: boolean;
55
- readonly waiting_on?: {
56
- readonly who: string;
57
- readonly question?: string;
58
- readonly type?: 'builder' | 'person' | 'other';
59
- };
60
- readonly risk?: string;
61
- readonly spec?: string;
62
- readonly screen?: string;
63
- readonly capture?: string;
64
- /** Number of captures attached to the request, independent of the legacy capture description. */
65
- readonly captureCount?: number;
66
- /** Authorized workspace folder supplied by the host; never synthesized from the task reference. */
67
- readonly folder?: string;
68
- readonly questions?: readonly TaskQuestion[];
69
- readonly files?: readonly TaskFile[];
70
- readonly history?: readonly TaskHistory[];
71
- readonly updated_at?: string;
72
- readonly created_at?: string;
73
- /** When the requester confirmed the request (« Confirmer »), from the host; absent until then. */
74
- readonly confirmed_at?: string;
75
- }
76
- export type TaskSemanticStatus = 'waiting-on-viewer' | 'waiting-on-builder' | 'high-risk' | 'sent' | 'none';
77
- /**
78
- * Single-pill presentation precedence: viewer, explicit builder, high risk, then « sent » (confirmed, still `open`: nobody started)
79
- * when the rendering asks for it (`sent`: the requester's compact card). No workflow or authority is changed.
80
- */
81
- export declare function deriveTaskStatus(task: Pick<TaskRecord, 'waiting_on_you' | 'waiting_on' | 'risk'> & Partial<Pick<TaskRecord, 'state' | 'confirmed_at'>>, { sent }?: {
82
- readonly sent?: boolean;
83
- }): TaskSemanticStatus;
84
- /** Confirmation wording is explicit host context, never inferred from a question, title or free-form status. */
85
- export declare function taskStatusLabel(status: TaskSemanticStatus, locale?: 'en' | 'fr', viewerAction?: 'question' | 'confirmation'): string | undefined;
86
- export interface TaskSelection {
87
- readonly filter?: TaskFilter;
88
- readonly currentUser?: string;
89
- readonly limit?: number;
90
- }
91
- /** Newest first, stable at equal/absent dates. Filtering is presentation, not an access boundary. */
92
- export declare function selectTasks(tasks: readonly TaskRecord[], { filter, currentUser, limit }?: TaskSelection): readonly TaskRecord[];
93
- export declare function taskReference(task: TaskRecord): string;
94
- export declare function taskFromUrl(url: string): string | null;
95
- export declare function taskUrl(url: string, id: string | null): string;
96
- //# sourceMappingURL=task-board.d.ts.map
@@ -1,48 +0,0 @@
1
- /** Business requests supplied by the host, not Pi execution tasks. No fetching, mutation or authorization lives here. */
2
- export const TASK_STATES = ['waiting', 'working', 'open', 'done', 'closed'];
3
- /**
4
- * Single-pill presentation precedence: viewer, explicit builder, high risk, then « sent » (confirmed, still `open`: nobody started)
5
- * when the rendering asks for it (`sent`: the requester's compact card). No workflow or authority is changed.
6
- */
7
- export function deriveTaskStatus(task, { sent = false } = {}) {
8
- if (task.waiting_on_you === true)
9
- return 'waiting-on-viewer';
10
- if (task.waiting_on?.type === 'builder')
11
- return 'waiting-on-builder';
12
- if (task.risk === 'high')
13
- return 'high-risk';
14
- if (sent && task.state === 'open' && task.confirmed_at)
15
- return 'sent';
16
- return 'none';
17
- }
18
- /** Confirmation wording is explicit host context, never inferred from a question, title or free-form status. */
19
- export function taskStatusLabel(status, locale = 'en', viewerAction = 'question') {
20
- switch (status) {
21
- case 'waiting-on-viewer': return locale === 'fr' ? viewerAction === 'confirmation' ? 'À confirmer' : 'À vous' : 'Waiting on you';
22
- case 'waiting-on-builder': return locale === 'fr' ? "Chez l'équipe" : 'Waiting on the builder';
23
- case 'high-risk': return locale === 'fr' ? 'Risque élevé' : 'High risk';
24
- case 'sent': return locale === 'fr' ? 'Envoyée' : 'Sent';
25
- default: return undefined;
26
- }
27
- }
28
- /** Newest first, stable at equal/absent dates. Filtering is presentation, not an access boundary. */
29
- export function selectTasks(tasks, { filter = 'all', currentUser, limit } = {}) {
30
- if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0))
31
- throw new RangeError('Task limit must be a nonnegative integer');
32
- const date = (task) => { const value = Date.parse(task.updated_at ?? task.created_at ?? ''); return Number.isFinite(value) ? value : 0; };
33
- const selected = tasks.filter(task => filter === 'mine' ? currentUser !== undefined && task.requesterId === currentUser : filter === 'waiting-on-me' ? task.state === 'waiting' && task.waiting_on_you === true : true)
34
- .sort((a, b) => date(b) - date(a));
35
- return limit === undefined ? selected : selected.slice(0, limit);
36
- }
37
- export function taskReference(task) {
38
- return task.number === undefined ? task.id : String(task.number).startsWith('D-') ? String(task.number) : `D-${task.number}`;
39
- }
40
- export function taskFromUrl(url) { return new URL(url, 'https://task.invalid').searchParams.get('task'); }
41
- export function taskUrl(url, id) {
42
- const next = new URL(url, 'https://task.invalid');
43
- if (id === null)
44
- next.searchParams.delete('task');
45
- else
46
- next.searchParams.set('task', id);
47
- return next.href;
48
- }
@@ -1,20 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "feedback-preview",
4
- "title": "Feedback preview",
5
- "description": "PreviewBanner: a live, reversible preview of a change in the page (Approve / Discard) over `@hachej/boring-feedback/preview`, whose subagent calls models through a host gateway. Separate from `feedback` so annotation-only apps do not install the model SDK; install `feedback` first (its styles cover the banner).",
6
- "dependencies": [
7
- "@hachej/boring-feedback@^0.4.46",
8
- "@earendil-works/pi-ai@~1.4.0",
9
- "react@^19.0.0"
10
- ],
11
- "files": [
12
- {
13
- "path": "registry/feedback-preview/preview-banner.tsx",
14
- "content": "// The preview banner (FEEDBACK.md, \"Preview\"): while the in-page preview subagent changes the live page for the builder's\n// `browser_preview` call, a fixed, clearly labelled banner says nothing is saved, lists the changes, lets the person keep talking to\n// the subagent (\"darker\"), and offers Approve and Discard. Everything it shows and does is the headless `createPreviewSession` from\n// `@hachej/boring-feedback/preview`; this file is how it looks. It is `data-feedback-ignore`, so the preview tools and every snapshot skip it.\nimport { useState, useSyncExternalStore, type FormEvent } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Card } from '@/components/ui/card';\nimport { Input } from '@/components/ui/input';\nimport { readableElement } from '@hachej/boring-feedback/format';\nimport type { PreviewSession, PreviewSnapshot } from '@hachej/boring-feedback/preview';\n\nconst STATUS: Readonly<Record<PreviewSnapshot['status'], string>> = {\n starting: 'Preparing the preview…', working: 'Previewing…', ready: 'Preview ready', failed: 'Preview failed',\n answering: 'Sending…', approved: 'Approved', discarded: 'Discarded',\n};\n\nexport interface PreviewBannerProps {\n readonly session: PreviewSession;\n}\n\nexport function PreviewBanner({ session }: PreviewBannerProps) {\n const state = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);\n const [text, setText] = useState('');\n const busy = state.status === 'starting' || state.status === 'working' || state.status === 'answering';\n const done = state.status === 'approved' || state.status === 'discarded';\n const submit = (event: FormEvent) => {\n event.preventDefault();\n const said = text.trim();\n if (!said || busy) return;\n setText('');\n void session.say(said);\n };\n return <Card size=\"sm\" data-boring=\"feedback\" data-feedback-ignore=\"\" role=\"region\" aria-label=\"Preview of a change\" aria-live=\"polite\"\n data-testid=\"preview-banner\" data-status={state.status} className=\"boring-feedback-preview\">\n <div className=\"boring-feedback-preview-head\">\n <Badge>Preview</Badge>\n <span className=\"boring-feedback-preview-notice\">these changes are not saved</span>\n <span className=\"boring-feedback-preview-status\" data-testid=\"preview-status\">{STATUS[state.status]}</span>\n </div>\n {state.summary && <p className=\"boring-feedback-preview-summary\" data-testid=\"preview-summary\">{state.summary}</p>}\n {state.changes.length > 0 && <ul className=\"boring-feedback-preview-changes\" data-testid=\"preview-changes\">\n {state.changes.map((change, index) => {\n const element = readableElement(change.element, change.source);\n return <li key={index} data-testid=\"preview-change\">\n <span title={element.path}>{element.name}{element.file ? ` · ${element.file}` : ''}</span>{' '}\n <code>{change.text ? 'text' : change.property}</code> {change.from || '—'} → <strong>{change.to}</strong>\n </li>;\n })}\n </ul>}\n {state.error && <p className=\"boring-feedback-preview-error\" role=\"alert\" data-testid=\"preview-error\">{state.error}</p>}\n {!done && <form className=\"boring-feedback-preview-actions\" onSubmit={submit}>\n <Input className=\"boring-feedback-preview-input\" data-testid=\"preview-input\" value={text} maxLength={300} disabled={busy}\n aria-label=\"Tell the preview what to change\" placeholder=\"Adjust it… (e.g. darker)\" onChange={event => setText(event.currentTarget.value)} />\n <Button type=\"submit\" variant=\"default\" data-testid=\"preview-say\" disabled={busy || !text.trim()}>Send</Button>\n <Button variant=\"default\" data-testid=\"preview-approve\" disabled={busy || state.changes.length === 0}\n onClick={() => { void session.approve(); }}>Approve</Button>\n <Button variant=\"ghost\" data-testid=\"preview-discard\" disabled={state.status === 'answering'}\n onClick={() => { void session.discard(); }}>Discard</Button>\n </form>}\n </Card>;\n}\n",
15
- "type": "registry:component",
16
- "target": "components/feedback-preview/preview-banner.tsx"
17
- }
18
- ],
19
- "type": "registry:component"
20
- }