@hachej/boring-ui-kit 0.4.42 → 0.4.43

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 (50) hide show
  1. package/package.json +2 -2
  2. package/r/agent-target.json +4 -4
  3. package/r/app-shell.json +10 -10
  4. package/r/board-viewer.json +6 -6
  5. package/r/button.json +1 -1
  6. package/r/dock.json +1 -1
  7. package/r/documents.json +12 -12
  8. package/r/feedback-preview.json +1 -1
  9. package/r/feedback.json +6 -6
  10. package/r/file-tree.json +4 -4
  11. package/r/highlights.json +3 -3
  12. package/r/html-viewer.json +3 -3
  13. package/r/looks-viewer.json +2 -2
  14. package/r/maquette-viewer.json +1 -1
  15. package/r/markdown-editor.json +3 -3
  16. package/r/mockup-viewer.json +1 -1
  17. package/r/pi-ambient.json +8 -8
  18. package/r/pi-app-kinds.json +10 -10
  19. package/r/pi-app.json +10 -10
  20. package/r/pi-artifacts.json +2 -2
  21. package/r/pi-chat-approval.json +2 -2
  22. package/r/pi-chat-attachments.json +1 -1
  23. package/r/pi-chat-markdown.json +2 -2
  24. package/r/pi-chat-mentions.json +1 -1
  25. package/r/pi-chat-pickers.json +1 -1
  26. package/r/pi-chat-plugins.json +11 -11
  27. package/r/pi-chat-question.json +1 -1
  28. package/r/pi-chat-slash.json +2 -2
  29. package/r/pi-chat-subagent.json +1 -1
  30. package/r/pi-chat-voice.json +3 -3
  31. package/r/pi-chat.json +5 -5
  32. package/r/pi-workspace.json +3 -3
  33. package/r/placed-agent.json +4 -4
  34. package/r/pm-panel.json +18 -18
  35. package/r/provider-setup.json +1 -1
  36. package/r/registry.json +185 -185
  37. package/r/requests-board.json +1 -1
  38. package/r/shared-chat.json +6 -6
  39. package/r/task-board.json +4 -4
  40. package/r/task-list-viewer.json +2 -2
  41. package/r/task-mentions.json +4 -4
  42. package/r/version-history.json +2 -2
  43. package/r/viewer-code.json +2 -2
  44. package/r/viewer-frame.json +4 -4
  45. package/r/viewer-html.json +4 -4
  46. package/r/viewer-image.json +2 -2
  47. package/r/viewer-markdown.json +5 -5
  48. package/r/viewer-pdf.json +3 -3
  49. package/r/viewers.json +6 -6
  50. package/r/windows.json +1 -1
@@ -7,7 +7,7 @@
7
7
  "react@^19.0.0"
8
8
  ],
9
9
  "registryDependencies": [
10
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json"
10
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json"
11
11
  ],
12
12
  "files": [
13
13
  {
@@ -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.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/pi-chat.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat-plugins.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat-markdown.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat-mentions.json",
16
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat-plugins.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat-markdown.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat-mentions.json",
16
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json"
17
17
  ],
18
18
  "files": [
19
19
  {
package/r/task-board.json CHANGED
@@ -4,12 +4,12 @@
4
4
  "title": "Task board",
5
5
  "description": "Host-owned business requests: board, stacked task/file overlays and compact own-request rows. No agent or task backend required.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/utils.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  {
22
22
  "path": "registry/task-board/task-board.tsx",
23
- "content": "'use client';\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { MouseEvent, ReactNode } from 'react';\nimport { selectTasks, taskFromUrl, taskReference, taskUrl } from '@hachej/boring-ui-kit/task-board';\nimport type { TaskFile, TaskFilter, TaskRecord } from '@hachej/boring-ui-kit/task-board';\nimport { ChevronLeftIcon } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';\nimport { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';\nimport { Separator } from '@/components/ui/separator';\nimport { cn } from '../utils/utils';\nimport { TaskRequestCard, TaskStatusPill, taskMeta, taskTokens } from './task-request-card';\nimport type { TaskLocale, TaskViewerAction } from './task-request-card';\nimport { TaskDetail } from './task-detail';\nimport type { TaskDetailProps } from './task-detail';\nimport { columnLabels, compactLabels, compactTaskStatus, TASK_COLUMNS, taskColumn } from './compact-presentation';\nimport type { TaskColumn } from './compact-presentation';\n\nexport { TaskRequestCard, TaskRequestCard as TaskCard, TaskStatusPill, TaskStatusChip, TaskAttention, taskMeta, taskTokens } from './task-request-card';\nexport { TaskDetail } from './task-detail';\nexport type { TaskRequestCardProps, TaskRequestCardProps as TaskCardProps, TaskLocale, TaskViewerAction } from './task-request-card';\nexport type { TaskDetailProps } from './task-detail';\nexport type { TaskFile, TaskRecord, TaskFilter, TaskState } from '@hachej/boring-ui-kit/task-board';\n\nconst navigation = 'boring:task-navigation';\nconst subscribe = (changed: () => void) => {\n window.addEventListener('popstate', changed); window.addEventListener(navigation, changed);\n return () => { window.removeEventListener('popstate', changed); window.removeEventListener(navigation, changed); };\n};\nconst readSelection = () => taskFromUrl(window.location.href);\nconst serverSelection = () => null;\nconst FOLDED = ['finished', 'technical'] as const;\n\nexport interface TaskFileViewApi {\n readonly fullscreen: boolean;\n readonly onFullscreenChange: (fullscreen: boolean) => void;\n readonly onClose: () => void;\n readonly onBack: () => void;\n /** The host's PM pointing service; absent unless onShowFile was supplied. */\n readonly onShowFile?: (() => void) | undefined;\n}\nexport interface TaskBoardProps {\n readonly tasks: readonly TaskRecord[];\n /** Controlled Mine/All. Uncontrolled, the tasks bar starts on Mine when `currentUser` is known, otherwise All. */\n readonly filter?: TaskFilter;\n readonly onFilterChange?: ((filter: TaskFilter) => void) | undefined;\n readonly currentUser?: string;\n readonly limit?: number;\n readonly variant?: 'board' | 'compact';\n /** Compact-only presentation, never server authorization. Supply only records the viewer may already read. */\n readonly compactPresentation?: { readonly includeTechnical?: boolean };\n readonly title?: string;\n /** @deprecated Ignored: columns follow who acts next and `locale`. */\n readonly labels?: unknown;\n /** The tasks bar (host content, app filter, Mine/All). Hosts that draw it in their own header pass false. */\n readonly bar?: boolean;\n /** Host content at the start of the tasks bar, e.g. the collapsed Studio header's app name and « Show the app ». */\n readonly barStart?: ReactNode;\n readonly locale?: TaskLocale;\n /** Explicit host context for compact French labels (« À vous » or « À confirmer »). */\n readonly viewerAction?: TaskViewerAction;\n /** Omit to use ?task= with native history. A router may instead supply controlled selection. */\n readonly selectedTask?: string | null;\n readonly onSelectedTaskChange?: ((id: string | null) => void) | undefined;\n /** Optional host navigation/present path, particularly for compact lists inside the PM panel. */\n readonly onOpen?: ((task: TaskRecord) => void) | undefined;\n /** Host content under the detail's title card. */\n readonly actions?: ((task: TaskRecord) => ReactNode) | undefined;\n /** Host-authorized compact card actions (« Modifier », « Confirmer »). */\n readonly cardActions?: ((task: TaskRecord) => ReactNode) | undefined;\n readonly onAnswer?: TaskDetailProps['onAnswer'];\n readonly onOther?: TaskDetailProps['onOther'];\n readonly onDiscussQuestion?: TaskDetailProps['onDiscussQuestion'];\n readonly onAsk?: TaskDetailProps['onAsk'];\n /** Adapter paths only. The host reauthorizes them when opening the regular viewer. */\n readonly onOpenFile?: TaskDetailProps['onOpenFile'];\n readonly onShowFile?: TaskDetailProps['onOpenFile'];\n /** Supply the host's regular FileViewer; it opens 760px wide over the task detail. No second store or composer. */\n readonly renderFile?: ((task: TaskRecord, file: TaskFile, api: TaskFileViewApi) => ReactNode) | undefined;\n /** Host-owned overlay appearance (for example the host's overlay shadow token). */\n readonly overlayClassName?: string;\n readonly className?: string;\n}\n\n/**\n * The one task board: columns by who acts next (needs you, blocked, in progress, recently live), finished and technical folded. The\n * panel's own width decides the layout, so a narrow pane or phone gets the same sections stacked. The detail opens 400px wide over the\n * board, whole-pane when narrower; a file opens 760px wide over the detail. `variant=\"compact\"` stacks C7 cards.\n */\nexport function TaskBoard({ tasks, filter, onFilterChange, currentUser, limit, variant = 'board', compactPresentation, title = 'Tasks', bar = true, barStart, locale = 'en', viewerAction,\n selectedTask, onSelectedTaskChange, onOpen, actions, cardActions, onAnswer, onOther, onDiscussQuestion, onAsk, onOpenFile, onShowFile, renderFile, overlayClassName, className }: TaskBoardProps) {\n const urlSelection = useSyncExternalStore(subscribe, readSelection, serverSelection);\n const [ownFilter, setOwnFilter] = useState<TaskFilter>(currentUser === undefined ? 'all' : 'mine');\n const [unfolded, setUnfolded] = useState<ReadonlySet<TaskColumn>>(() => new Set());\n const [app, setApp] = useState<string | null>(null);\n const shownFilter = filter ?? ownFilter;\n const selected = selectedTask === undefined ? onOpen ? null : urlSelection : selectedTask;\n const detail = selected === null ? undefined : tasks.find(task => task.id === selected || taskReference(task) === selected);\n const shown = selectTasks(tasks, { filter: shownFilter, ...(currentUser === undefined ? {} : { currentUser }), ...(limit === undefined ? {} : { limit }) });\n const [fileSelection, setFileSelection] = useState<{ readonly taskId: string; readonly path: string } | null>(null);\n const filePath = fileSelection?.path;\n const file = detail?.id === fileSelection?.taskId ? detail?.files?.find(candidate => candidate.path === filePath) : undefined;\n const fileOpen = !!file && !!renderFile;\n const [fileFullscreen, setFileFullscreen] = useState(false);\n const closeFile = () => { setFileSelection(null); setFileFullscreen(false); };\n const openFile = (task: TaskRecord, file: TaskFile) => { setFileSelection({ taskId: task.id, path: file.path }); onOpenFile?.(task, file); };\n const root = useRef<HTMLElement>(null);\n const back = useRef<HTMLButtonElement>(null);\n const previous = useRef<{ readonly id: string; readonly column: TaskColumn | undefined } | null>(null);\n const detailColumn = detail && taskColumn(detail);\n const pendingFocus = useRef<string | null>(null);\n const cardLink = (id: string) => [...root.current?.querySelectorAll<HTMLAnchorElement>('[data-task-open]') ?? []].find(candidate => candidate.dataset['taskOpen'] === id);\n const unfold = (column: TaskColumn, open: boolean) => setUnfolded(current => { const next = new Set(current); if (open) next.add(column); else next.delete(column); return next; });\n useEffect(() => {\n if (selected !== null) back.current?.focus();\n else if (previous.current !== null) {\n const { id, column } = previous.current, link = cardLink(id);\n if (link) link.focus();\n else if (column) { pendingFocus.current = id; unfold(column, true); }\n }\n previous.current = selected === null ? null : { id: detail?.id ?? selected, column: detailColumn };\n }, [selected, detail?.id, detailColumn]);\n useEffect(() => { if (pendingFocus.current !== null) { cardLink(pendingFocus.current)?.focus(); pendingFocus.current = null; } }, [unfolded]);\n const select = (id: string | null) => {\n closeFile();\n if (selectedTask === undefined) {\n window.history.pushState(null, '', taskUrl(window.location.href, id));\n window.dispatchEvent(new Event(navigation));\n }\n onSelectedTaskChange?.(id);\n };\n const open = (task: TaskRecord) => { if (onOpen) onOpen(task); else select(task.id); };\n const href = (task: TaskRecord) => typeof window === 'undefined' ? `?task=${encodeURIComponent(task.id)}` : taskUrl(window.location.href, task.id);\n const card = (task: TaskRecord) => <TaskRequestCard key={task.id} task={task} variant={variant === 'compact' ? 'compact' : 'card'} selected={task.id === detail?.id} href={href(task)} onOpen={open}\n locale={locale} viewerAction={viewerAction} actions={variant === 'compact' ? cardActions?.(task) : undefined} />;\n const apps = [...new Set(shown.flatMap(task => task.app ? [task.app] : []))].sort();\n const listed = app === null ? shown : shown.filter(task => task.app === app);\n const words = columnLabels[locale];\n const of = (column: TaskColumn) => listed.filter(task => taskColumn(task) === column).sort((a, b) => (Date.parse(b.updated_at ?? '') || 0) - (Date.parse(a.updated_at ?? '') || 0));\n const appChip = (value: string | null, text: string) => <Badge key={text} data-task-app-filter={text} variant={app === value ? 'default' : 'secondary'} render={<a href=\"#\" aria-current={app === value ? 'true' : undefined} onClick={(event: MouseEvent) => { event.preventDefault(); setApp(value); }} />}>{text}</Badge>;\n const chooseFilter = (next: TaskFilter) => (event: MouseEvent) => { event.preventDefault(); if (filter === undefined) setOwnFilter(next); onFilterChange?.(next); };\n if (variant === 'compact') {\n const strings = compactLabels[locale];\n // Filter hidden records and prioritize current viewer needs before applying the visible limit.\n const compact = selectTasks(tasks.filter(task => task.technical !== true || compactPresentation?.includeTechnical === true), { filter: shownFilter, ...(currentUser === undefined ? {} : { currentUser }) })\n .slice().sort((a, b) => Number(compactTaskStatus(b) === 'needs-you') - Number(compactTaskStatus(a) === 'needs-you'))\n .slice(0, limit);\n const ordinary = compact.filter(task => task.technical !== true && compactTaskStatus(task) !== 'closed');\n const closed = compact.filter(task => task.technical !== true && compactTaskStatus(task) === 'closed');\n const technical = compact.filter(task => task.technical === true);\n const group = (kind: 'closed' | 'technical', records: readonly TaskRecord[]) => records.length > 0 && <Collapsible data-task-group={kind}>\n <Separator />\n <CollapsibleTrigger render={<Button variant=\"ghost\" />}>{kind === 'closed' ? strings.finished : strings.technical} · {records.length}</CollapsibleTrigger>\n <CollapsibleContent><div role=\"list\" aria-label={kind === 'closed' ? strings.finished : strings.technical} className=\"flex flex-col gap-2\">{records.map(card)}</div></CollapsibleContent>\n </Collapsible>;\n return <section ref={root} data-boring=\"task-board\" data-variant=\"compact\" aria-label={title} className={cn('flex min-w-0 flex-col gap-2', className)}>\n <div data-task-list role=\"list\" aria-label={title} className=\"flex flex-col gap-2\">{ordinary.map(card)}</div>\n {!compact.length && <Empty><EmptyHeader><EmptyTitle>{strings.empty}</EmptyTitle><EmptyDescription>{strings.emptyDescription}</EmptyDescription></EmptyHeader></Empty>}\n {group('closed', closed)}\n {group('technical', technical)}\n </section>;\n }\n return <section ref={root} data-boring=\"task-board\" data-variant={variant} aria-label={title} className={cn('flex min-h-0 min-w-0 flex-col', className)} onKeyDown={event => {\n if (event.key === 'Escape' && selected !== null && !event.defaultPrevented) { event.preventDefault(); if (fileOpen) closeFile(); else select(null); }\n }}>\n {bar && <nav data-task-bar aria-label=\"Task lists\" className=\"flex h-13 shrink-0 items-center gap-2.5 px-3.5 text-[13px] text-muted-foreground\">\n {barStart}\n {apps.length > 1 && <span className={cn('flex min-w-0 flex-wrap items-center gap-1.5', taskTokens.pill)} data-task-apps=\"\">{appChip(null, locale === 'fr' ? 'Toutes' : 'All apps')}{apps.map(name => appChip(name, name))}</span>}\n {currentUser !== undefined && <span className={cn('ml-auto flex items-center gap-2.5', taskTokens.pill)} data-task-filter={shownFilter}>\n {(['mine', 'all'] as const).map(value => <Badge key={value} variant={shownFilter === value ? 'default' : 'secondary'} render={<a href=\"#\" aria-current={shownFilter === value ? 'true' : undefined} onClick={chooseFilter(value)} />}>{value === 'mine' ? 'Mine' : 'All'}</Badge>)}\n </span>}\n </nav>}\n <div data-task-stage className=\"relative min-h-0 flex-1 overflow-hidden\">\n <div data-task-list className=\"@container/task-rails h-full min-w-0 overflow-auto\">\n <div className=\"grid min-w-0 content-start gap-2.5 p-3.5 @3xl/task-rails:grid-cols-4\">\n {TASK_COLUMNS.map(column => {\n const rows = of(column);\n // Stacked, an empty section is noise; side by side, a column keeps its place so the board does not shift.\n return <section key={column} data-task-column={column} aria-label={words[column]} className={cn('min-w-0 flex-col gap-[7px] self-start bg-muted p-2', rows.length ? 'flex' : 'hidden @3xl/task-rails:flex', taskTokens.column)}>\n <h3 className=\"flex min-h-6 items-center gap-1.5 px-0.5 pb-0.5 text-xs font-semibold text-muted-foreground\">{words[column]}<span className=\"font-normal\">{rows.length}</span></h3>\n <div role=\"list\" className=\"flex flex-col gap-[7px]\">{rows.map(card)}</div>\n </section>;\n })}\n </div>\n {!listed.length && <Empty><EmptyHeader><EmptyTitle>{compactLabels[locale].empty}</EmptyTitle><EmptyDescription>{compactLabels[locale].emptyDescription}</EmptyDescription></EmptyHeader></Empty>}\n {FOLDED.map(column => {\n const rows = of(column);\n return rows.length > 0 && <div key={column} className=\"px-3.5 pb-2\"><Collapsible data-task-group={column} open={unfolded.has(column)} onOpenChange={open => unfold(column, open)}>\n <CollapsibleTrigger render={<Button variant=\"ghost\" size=\"sm\" />}>{words[column]} · {rows.length}</CollapsibleTrigger>\n <CollapsibleContent><div role=\"list\" aria-label={words[column]} className=\"grid gap-[7px] pt-1.5 @3xl/task-rails:grid-cols-4\">{rows.map(card)}</div></CollapsibleContent>\n </Collapsible></div>;\n })}\n </div>\n {selected !== null && <aside aria-label=\"Task detail\" data-task-side className={cn('absolute inset-y-0 right-0 flex w-[400px] max-w-full flex-col bg-popover text-popover-foreground', overlayClassName)} aria-live=\"polite\">\n <Separator orientation=\"vertical\" className=\"absolute inset-y-0 left-0\" />\n <div className=\"flex h-10 shrink-0 items-center gap-2 pr-1.5 pl-3.5\">\n <Button ref={back} variant=\"ghost\" size=\"icon\" data-task-back=\"\" aria-label=\"Back to the tasks\" title=\"Back to the tasks\" onClick={() => select(null)}><ChevronLeftIcon aria-hidden=\"true\" /></Button>\n {detail && <><span className=\"text-[11px] text-muted-foreground\">{[detail.app, taskMeta(detail)].filter(Boolean).join(' · ')}</span><TaskStatusPill task={detail} locale={locale} viewerAction={viewerAction} /></>}\n </div>\n <Separator />\n <div className=\"min-h-0 flex-1 overflow-auto p-3.5\">\n {detail ? <TaskDetail key={detail.id} task={detail} locale={locale} actions={actions?.(detail)} onAnswer={onAnswer} onOther={onOther} onDiscussQuestion={onDiscussQuestion} onAsk={onAsk} onOpenFile={renderFile || onOpenFile ? openFile : undefined} />\n : <Empty><EmptyHeader><EmptyTitle>Task unavailable</EmptyTitle><EmptyDescription>The host has not supplied this task.</EmptyDescription></EmptyHeader></Empty>}\n </div>\n </aside>}\n {fileOpen && detail && file && <section aria-label=\"Task file viewer\" data-task-file-pane={detail.id} className={cn('flex min-w-0 flex-col bg-background', fileFullscreen ? 'fixed inset-0 z-50' : 'absolute inset-y-0 right-0 w-[760px] max-w-full', overlayClassName)}>\n {renderFile!(detail, file, { fullscreen: fileFullscreen, onFullscreenChange: setFileFullscreen, onClose: closeFile, onBack: closeFile, onShowFile: onShowFile ? () => onShowFile(detail, file) : undefined })}\n </section>}\n </div>\n </section>;\n}\n",
23
+ "content": "'use client';\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport type { MouseEvent, ReactNode } from 'react';\nimport { selectTasks, taskFromUrl, taskReference, taskUrl } from '@hachej/boring-ui-kit/task-board';\nimport type { TaskFile, TaskFilter, TaskRecord } from '@hachej/boring-ui-kit/task-board';\nimport { ChevronLeftIcon } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';\nimport { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';\nimport { Separator } from '@/components/ui/separator';\nimport { cn } from '../utils/utils';\nimport { TaskRequestCard, TaskStatusPill, taskMeta, taskTokens } from './task-request-card';\nimport type { TaskLocale, TaskViewerAction } from './task-request-card';\nimport { TaskDetail } from './task-detail';\nimport type { TaskDetailProps } from './task-detail';\nimport { columnLabels, compactLabels, TASK_COLUMNS, taskColumn } from './compact-presentation';\nimport type { TaskColumn } from './compact-presentation';\n\nexport { TaskRequestCard, TaskRequestCard as TaskCard, TaskStatusPill, TaskStatusChip, TaskAttention, taskMeta, taskTokens } from './task-request-card';\nexport { TaskDetail } from './task-detail';\nexport type { TaskRequestCardProps, TaskRequestCardProps as TaskCardProps, TaskLocale, TaskViewerAction } from './task-request-card';\nexport type { TaskDetailProps } from './task-detail';\nexport type { TaskFile, TaskRecord, TaskFilter, TaskState } from '@hachej/boring-ui-kit/task-board';\n\nconst navigation = 'boring:task-navigation';\nconst subscribe = (changed: () => void) => {\n window.addEventListener('popstate', changed); window.addEventListener(navigation, changed);\n return () => { window.removeEventListener('popstate', changed); window.removeEventListener(navigation, changed); };\n};\nconst readSelection = () => taskFromUrl(window.location.href);\nconst serverSelection = () => null;\nconst FOLDED = ['finished', 'technical'] as const;\n\nexport interface TaskFileViewApi {\n readonly fullscreen: boolean;\n readonly onFullscreenChange: (fullscreen: boolean) => void;\n readonly onClose: () => void;\n readonly onBack: () => void;\n /** The host's PM pointing service; absent unless onShowFile was supplied. */\n readonly onShowFile?: (() => void) | undefined;\n}\nexport interface TaskBoardProps {\n readonly tasks: readonly TaskRecord[];\n /** Controlled Mine/All. Uncontrolled, the tasks bar starts on Mine when `currentUser` is known, otherwise All. */\n readonly filter?: TaskFilter;\n readonly onFilterChange?: ((filter: TaskFilter) => void) | undefined;\n readonly currentUser?: string;\n readonly limit?: number;\n readonly title?: string;\n /** @deprecated Ignored: columns follow who acts next and `locale`. */\n readonly labels?: unknown;\n /** The tasks bar (host content, app filter, Mine/All). Hosts that draw it in their own header pass false. */\n readonly bar?: boolean;\n /** Host content at the start of the tasks bar, e.g. the collapsed Studio header's app name and « Show the app ». */\n readonly barStart?: ReactNode;\n readonly locale?: TaskLocale;\n /** Explicit host context for compact French labels (« À vous » or « À confirmer »). */\n readonly viewerAction?: TaskViewerAction;\n /** Omit to use ?task= with native history. A router may instead supply controlled selection. */\n readonly selectedTask?: string | null;\n readonly onSelectedTaskChange?: ((id: string | null) => void) | undefined;\n /** Optional host navigation/present path, particularly for compact lists inside the PM panel. */\n readonly onOpen?: ((task: TaskRecord) => void) | undefined;\n /** Host content in the detail, after the question and the request. */\n readonly actions?: ((task: TaskRecord) => ReactNode) | undefined;\n readonly onAnswer?: TaskDetailProps['onAnswer'];\n readonly onOther?: TaskDetailProps['onOther'];\n readonly onDiscussQuestion?: TaskDetailProps['onDiscussQuestion'];\n readonly onAsk?: TaskDetailProps['onAsk'];\n /** Adapter paths only. The host reauthorizes them when opening the regular viewer. */\n readonly onOpenFile?: TaskDetailProps['onOpenFile'];\n readonly onShowFile?: TaskDetailProps['onOpenFile'];\n /** Supply the host's regular FileViewer; it opens 760px wide over the task detail. No second store or composer. */\n readonly renderFile?: ((task: TaskRecord, file: TaskFile, api: TaskFileViewApi) => ReactNode) | undefined;\n /** Host-owned overlay appearance (for example the host's overlay shadow token). */\n readonly overlayClassName?: string;\n readonly className?: string;\n}\n\n/**\n * The one task board: columns by who acts next (needs you, blocked, in progress, recently live), finished and technical folded. The\n * panel's own width decides the layout, so a narrow pane or phone gets the same sections stacked. The detail opens 400px wide over the\n * board, whole-pane when narrower; a file opens 760px wide over the detail.\n */\nexport function TaskBoard({ tasks, filter, onFilterChange, currentUser, limit, title = 'Tasks', bar = true, barStart, locale = 'en', viewerAction,\n selectedTask, onSelectedTaskChange, onOpen, actions, onAnswer, onOther, onDiscussQuestion, onAsk, onOpenFile, onShowFile, renderFile, overlayClassName, className }: TaskBoardProps) {\n const urlSelection = useSyncExternalStore(subscribe, readSelection, serverSelection);\n const [ownFilter, setOwnFilter] = useState<TaskFilter>(currentUser === undefined ? 'all' : 'mine');\n const [unfolded, setUnfolded] = useState<ReadonlySet<TaskColumn>>(() => new Set());\n const [app, setApp] = useState<string | null>(null);\n const shownFilter = filter ?? ownFilter;\n const selected = selectedTask === undefined ? onOpen ? null : urlSelection : selectedTask;\n const detail = selected === null ? undefined : tasks.find(task => task.id === selected || taskReference(task) === selected);\n const shown = selectTasks(tasks, { filter: shownFilter, ...(currentUser === undefined ? {} : { currentUser }), ...(limit === undefined ? {} : { limit }) });\n const [fileSelection, setFileSelection] = useState<{ readonly taskId: string; readonly path: string } | null>(null);\n const filePath = fileSelection?.path;\n const file = detail?.id === fileSelection?.taskId ? detail?.files?.find(candidate => candidate.path === filePath) : undefined;\n const fileOpen = !!file && !!renderFile;\n const [fileFullscreen, setFileFullscreen] = useState(false);\n const closeFile = () => { setFileSelection(null); setFileFullscreen(false); };\n const openFile = (task: TaskRecord, file: TaskFile) => { setFileSelection({ taskId: task.id, path: file.path }); onOpenFile?.(task, file); };\n const root = useRef<HTMLElement>(null);\n const back = useRef<HTMLButtonElement>(null);\n const previous = useRef<{ readonly id: string; readonly column: TaskColumn | undefined } | null>(null);\n const detailColumn = detail && taskColumn(detail);\n const pendingFocus = useRef<string | null>(null);\n const cardLink = (id: string) => [...root.current?.querySelectorAll<HTMLAnchorElement>('[data-task-open]') ?? []].find(candidate => candidate.dataset['taskOpen'] === id);\n const unfold = (column: TaskColumn, open: boolean) => setUnfolded(current => { const next = new Set(current); if (open) next.add(column); else next.delete(column); return next; });\n useEffect(() => {\n if (selected !== null) back.current?.focus();\n else if (previous.current !== null) {\n const { id, column } = previous.current, link = cardLink(id);\n if (link) link.focus();\n else if (column) { pendingFocus.current = id; unfold(column, true); }\n }\n previous.current = selected === null ? null : { id: detail?.id ?? selected, column: detailColumn };\n }, [selected, detail?.id, detailColumn]);\n useEffect(() => { if (pendingFocus.current !== null) { cardLink(pendingFocus.current)?.focus(); pendingFocus.current = null; } }, [unfolded]);\n const select = (id: string | null) => {\n closeFile();\n if (selectedTask === undefined) {\n window.history.pushState(null, '', taskUrl(window.location.href, id));\n window.dispatchEvent(new Event(navigation));\n }\n onSelectedTaskChange?.(id);\n };\n const open = (task: TaskRecord) => { if (onOpen) onOpen(task); else select(task.id); };\n const href = (task: TaskRecord) => typeof window === 'undefined' ? `?task=${encodeURIComponent(task.id)}` : taskUrl(window.location.href, task.id);\n const card = (task: TaskRecord) => <TaskRequestCard key={task.id} task={task} selected={task.id === detail?.id} href={href(task)} onOpen={open}\n locale={locale} viewerAction={viewerAction} />;\n const apps = [...new Set(shown.flatMap(task => task.app ? [task.app] : []))].sort();\n const listed = app === null ? shown : shown.filter(task => task.app === app);\n const words = columnLabels[locale];\n const of = (column: TaskColumn) => listed.filter(task => taskColumn(task) === column).sort((a, b) => (Date.parse(b.updated_at ?? '') || 0) - (Date.parse(a.updated_at ?? '') || 0));\n const appChip = (value: string | null, text: string) => <Badge key={text} data-task-app-filter={text} variant={app === value ? 'default' : 'secondary'} render={<a href=\"#\" aria-current={app === value ? 'true' : undefined} onClick={(event: MouseEvent) => { event.preventDefault(); setApp(value); }} />}>{text}</Badge>;\n const chooseFilter = (next: TaskFilter) => (event: MouseEvent) => { event.preventDefault(); if (filter === undefined) setOwnFilter(next); onFilterChange?.(next); };\n return <section ref={root} data-boring=\"task-board\" aria-label={title} className={cn('flex min-h-0 min-w-0 flex-col', className)} onKeyDown={event => {\n if (event.key === 'Escape' && selected !== null && !event.defaultPrevented) { event.preventDefault(); if (fileOpen) closeFile(); else select(null); }\n }}>\n {bar && (barStart || apps.length > 1 || currentUser !== undefined) && <nav data-task-bar aria-label=\"Task lists\" className=\"flex h-13 shrink-0 items-center gap-2.5 px-3.5 text-[13px] text-muted-foreground\">\n {barStart}\n {apps.length > 1 && <span className={cn('flex min-w-0 items-center gap-1.5 overflow-x-auto [scrollbar-width:none] *:shrink-0', taskTokens.pill)} data-task-apps=\"\">{appChip(null, locale === 'fr' ? 'Toutes' : 'All apps')}{apps.map(name => appChip(name, name))}</span>}\n {currentUser !== undefined && <span className={cn('ml-auto flex items-center gap-2.5', taskTokens.pill)} data-task-filter={shownFilter}>\n {(['mine', 'all'] as const).map(value => <Badge key={value} variant={shownFilter === value ? 'default' : 'secondary'} render={<a href=\"#\" aria-current={shownFilter === value ? 'true' : undefined} onClick={chooseFilter(value)} />}>{value === 'mine' ? 'Mine' : 'All'}</Badge>)}\n </span>}\n </nav>}\n <div data-task-stage className=\"relative min-h-0 flex-1 overflow-hidden\">\n <div data-task-list className=\"@container/task-rails h-full min-w-0 overflow-auto\">\n {!!listed.length && <div className=\"grid min-w-0 content-start gap-2.5 p-3.5 @3xl/task-rails:grid-cols-4\">\n {TASK_COLUMNS.map(column => {\n const rows = of(column);\n // Stacked, an empty section is noise; side by side, a column keeps its place so the board does not shift.\n return <section key={column} data-task-column={column} aria-label={words[column]} className={cn('min-w-0 flex-col gap-[7px] self-start bg-muted p-2', rows.length ? 'flex' : 'hidden @3xl/task-rails:flex', taskTokens.column)}>\n <h3 className=\"flex min-h-6 items-center gap-1.5 px-0.5 pb-0.5 text-xs font-semibold text-muted-foreground\">{words[column]}<span className=\"font-normal\">{rows.length}</span></h3>\n <div role=\"list\" className=\"flex flex-col gap-[7px]\">{rows.map(card)}</div>\n </section>;\n })}\n </div>}\n {!listed.length && <Empty><EmptyHeader><EmptyTitle>{compactLabels[locale].empty}</EmptyTitle><EmptyDescription>{compactLabels[locale].emptyDescription}</EmptyDescription></EmptyHeader></Empty>}\n {FOLDED.map(column => {\n const rows = of(column);\n return rows.length > 0 && <div key={column} className=\"px-3.5 pb-2\"><Collapsible data-task-group={column} open={unfolded.has(column)} onOpenChange={open => unfold(column, open)}>\n <CollapsibleTrigger render={<Button variant=\"ghost\" size=\"sm\" />}>{words[column]} · {rows.length}</CollapsibleTrigger>\n <CollapsibleContent><div role=\"list\" aria-label={words[column]} className=\"grid gap-[7px] pt-1.5 @3xl/task-rails:grid-cols-4\">{rows.map(card)}</div></CollapsibleContent>\n </Collapsible></div>;\n })}\n </div>\n {selected !== null && <aside aria-label=\"Task detail\" data-task-side className={cn('absolute inset-y-0 right-0 flex w-[400px] max-w-full flex-col bg-popover text-popover-foreground', overlayClassName)} aria-live=\"polite\">\n <Separator orientation=\"vertical\" className=\"absolute inset-y-0 left-0\" />\n <div className=\"flex h-10 shrink-0 items-center gap-2 pr-1.5 pl-3.5\">\n <Button ref={back} variant=\"ghost\" size=\"icon\" data-task-back=\"\" aria-label=\"Back to the tasks\" title=\"Back to the tasks\" onClick={() => select(null)}><ChevronLeftIcon aria-hidden=\"true\" /></Button>\n {detail && <><span className=\"text-[11px] text-muted-foreground\">{[detail.app, taskMeta(detail)].filter(Boolean).join(' · ')}</span><TaskStatusPill task={detail} locale={locale} viewerAction={viewerAction} /></>}\n </div>\n <Separator />\n <div className=\"min-h-0 flex-1 overflow-auto p-3.5\">\n {detail ? <TaskDetail key={detail.id} task={detail} locale={locale} actions={actions?.(detail)} onAnswer={onAnswer} onOther={onOther} onDiscussQuestion={onDiscussQuestion} onAsk={onAsk} onOpenFile={renderFile || onOpenFile ? openFile : undefined} />\n : <Empty><EmptyHeader><EmptyTitle>Task unavailable</EmptyTitle><EmptyDescription>The host has not supplied this task.</EmptyDescription></EmptyHeader></Empty>}\n </div>\n </aside>}\n {fileOpen && detail && file && <section aria-label=\"Task file viewer\" data-task-file-pane={detail.id} className={cn('flex min-w-0 flex-col bg-background', fileFullscreen ? 'fixed inset-0 z-50' : 'absolute inset-y-0 right-0 w-[760px] max-w-full', overlayClassName)}>\n {renderFile!(detail, file, { fullscreen: fileFullscreen, onFullscreenChange: setFileFullscreen, onClose: closeFile, onBack: closeFile, onShowFile: onShowFile ? () => onShowFile(detail, file) : undefined })}\n </section>}\n </div>\n </section>;\n}\n",
24
24
  "type": "registry:component",
25
25
  "target": "components/task-board/task-board.tsx"
26
26
  },
@@ -32,7 +32,7 @@
32
32
  },
33
33
  {
34
34
  "path": "registry/task-board/task-detail.tsx",
35
- "content": "'use client';\n\nimport { useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { AppWindowIcon, ChevronRightIcon, FileIcon, ImageIcon } from 'lucide-react';\nimport type { TaskFile, TaskQuestion, TaskRecord } from '@hachej/boring-ui-kit/task-board';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Item } from '@/components/ui/item';\nimport { TaskAttention, taskTokens } from './task-request-card';\nimport type { TaskLocale } from './task-request-card';\nimport { compactLabels, compactTaskStatus, compactTaskTitle, taskWaitingText } from './compact-presentation';\n\nexport interface TaskDetailProps {\n readonly task: TaskRecord;\n readonly locale?: TaskLocale | undefined;\n /** Host content placed after the title card (for example a mount report or host-authorized request actions). */\n readonly actions?: ReactNode;\n /** The host reauthorizes and resolves the original question. Rendering never supplies approval. */\n readonly onAnswer?: ((task: TaskRecord, question: TaskQuestion, value: string) => void) | undefined;\n /** « Other… » opens an inline field; Send passes the typed text. No synthetic option value or approval is generated. */\n readonly onOther?: ((task: TaskRecord, question: TaskQuestion, text: string) => void) | undefined;\n /** « Discuss in chat »: the host stages `@ D-201 · <title>` and the question in its one composer, without sending. */\n readonly onDiscussQuestion?: ((task: TaskRecord, question: TaskQuestion) => void) | undefined;\n readonly onOpenFile?: ((task: TaskRecord, file: TaskFile) => void) | undefined;\n /** « Ask in the conversation »: the host focuses its composer with the task mention. */\n readonly onAsk?: ((task: TaskRecord) => void) | undefined;\n /** @deprecated Ignored; kept for source compatibility. */\n readonly labels?: unknown;\n}\nconst detailWords = {\n en: { question: 'QUESTION FOR YOU', waiting: 'WAITING ON YOU', elsewhere: 'Reply in the conversation.', request: 'THE REQUEST', yourAnswer: 'Your answer', send: 'Send', other: 'Other…', discuss: 'Discuss in chat', ask: 'Ask in the conversation' },\n fr: { question: 'QUESTION POUR VOUS', waiting: 'ATTEND VOTRE AVIS', elsewhere: 'Répondez dans la conversation.', request: 'LA DEMANDE', yourAnswer: 'Votre réponse', send: 'Envoyer', other: 'Autre…', discuss: 'En parler dans la conversation', ask: 'Demander dans la conversation' },\n} as const;\nconst preview = (value: string) => value.length > 160 ? `${value.slice(0, 160)}…` : value;\nconst sectionLabel = 'text-[11px] font-semibold tracking-[0.06em] text-muted-foreground';\nconst kinds = { markdown: ['Markdown', FileIcon], mockup: ['Mockup', AppWindowIcon], capture: ['Capture', ImageIcon] } as const;\nconst fileKind = (file: TaskFile) => file.kind ?? (/\\.(?:md|markdown)$/i.test(file.path) ? 'markdown' : /\\.html?$/i.test(file.path) ? 'mockup' : /\\.(?:png|jpe?g|gif|webp|svg)$/i.test(file.path) ? 'capture' : undefined);\n/** ISO times show as local HH:MM; host-formatted strings show as given. */\nconst clock = (value: string) => /^\\d{4}-\\d\\d-\\d\\dT/.test(value) && Number.isFinite(Date.parse(value)) ? new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) : value;\n\nfunction AnsweredQuestion({ question }: { readonly question: TaskQuestion }) {\n const [expanded, setExpanded] = useState(false);\n return <li data-task-answered={question.id} className=\"min-w-0\">\n <Button variant=\"ghost\" className=\"grid h-auto w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] justify-normal gap-x-2 gap-y-0.5 text-start whitespace-normal\" aria-expanded={expanded} onClick={() => setExpanded(value => !value)}>\n <span className=\"truncate text-[13px] font-normal\">{preview(question.text)}</span>\n <ChevronRightIcon aria-hidden=\"true\" className=\"text-muted-foreground\" />\n <span data-task-answer={question.id} className=\"col-span-full truncate text-[13px] font-normal text-muted-foreground\">{question.answeredBy ? `${preview(question.answeredBy)}: ` : ''}{preview(question.answer ?? '')}</span>\n </Button>\n {expanded && <div data-task-answer-full className=\"flex flex-col gap-1 px-2 pb-2 text-[13px] break-words whitespace-pre-wrap\"><p>{question.text}</p><p className=\"text-muted-foreground\">{question.answeredBy && <>{question.answeredBy}: </>}{question.answer}</p></div>}\n </li>;\n}\n\nfunction OpenQuestion({ task, question, locale = 'en', onAnswer, onOther, onDiscussQuestion }: Pick<TaskDetailProps, 'task' | 'locale' | 'onAnswer' | 'onOther' | 'onDiscussQuestion'> & { readonly question: TaskQuestion }) {\n // Without options typing is the only way to answer, so the field is simply there.\n const typed = !question.options?.length;\n const [other, setOther] = useState<string | null>(typed && onOther ? '' : null);\n const words = detailWords[locale];\n const send = () => { const text = other?.trim(); if (text) { onOther?.(task, question, text); setOther(typed ? '' : null); } };\n const asked = [question.askedBy, question.askedAt && clock(question.askedAt)].filter(Boolean).join(' · ');\n return <TaskAttention active>\n <Item variant=\"outline\" className=\"flex-col items-stretch gap-2\" data-task-question={question.id}>\n <div className=\"flex items-baseline justify-between gap-2\"><span className={sectionLabel}>{words.question}</span>{asked && <span className=\"text-[11px] text-muted-foreground\">{asked}</span>}</div>\n <p className=\"text-sm\">{question.text}</p>\n <div className={`flex items-center gap-2 ${taskTokens.control}`}>\n {question.options?.map((option, index) => {\n const value = typeof option === 'string' ? option : option.value;\n const label = typeof option === 'string' ? option : option.label;\n return <Button key={index} variant={index === 0 ? 'default' : 'outline'} disabled={!onAnswer} onClick={() => onAnswer?.(task, question, value)}>{label}</Button>;\n })}\n {onOther && !typed && <Button variant=\"outline\" data-task-other={question.id} aria-expanded={other !== null} onClick={() => setOther(value => value === null ? '' : null)}>{words.other}</Button>}\n {onDiscussQuestion && <Button variant=\"link\" className=\"h-auto min-w-0 shrink text-center leading-tight whitespace-normal underline\" data-task-discuss={question.id} onClick={() => onDiscussQuestion(task, question)}>{words.discuss}</Button>}\n </div>\n {other !== null && <form className={`flex gap-2 ${taskTokens.control}`} data-task-other-form={question.id} onSubmit={event => { event.preventDefault(); send(); }}>\n <Input autoFocus={!typed} aria-label={words.yourAnswer} maxLength={4096} value={other} onChange={event => setOther(event.target.value)} onKeyDown={event => { if (event.key === 'Escape' && !typed) { event.preventDefault(); setOther(null); } }} />\n <Button type=\"submit\" disabled={!other.trim()}>{words.send}</Button>\n </form>}\n </Item>\n </TaskAttention>;\n}\n\n/** Task detail (spec 3 §4), in order: title, the open question for the viewer, Answered, Files, « Ask in the conversation ». */\nexport function TaskDetail({ task, locale = 'en', actions, onAnswer, onOther, onDiscussQuestion, onOpenFile, onAsk }: TaskDetailProps) {\n const [showAll, setShowAll] = useState(false);\n const open = task.waiting_on_you === true ? task.questions?.find(question => question.answer === undefined) : undefined;\n const words = detailWords[locale];\n const status = compactTaskStatus(task);\n // Nothing openable may say it needs someone without saying what: the projected reason shows when there is no full question.\n const reason = !open && (status === 'needs-you' || status === 'blocked') ? taskWaitingText(task) : undefined;\n const answered = task.questions?.filter(question => question.answer !== undefined).toReversed().sort((a, b) => (Date.parse(b.answeredAt ?? '') || 0) - (Date.parse(a.answeredAt ?? '') || 0)) ?? [];\n const meta = [task.screen && `Screen: ${task.screen}`, task.requester && `asked by ${task.requester}`, task.refiner && `refined by ${task.refiner}`].filter(Boolean).join(' · ');\n const folder = task.folder?.replace(/\\/?$/, '/');\n return <div className=\"flex min-w-0 flex-col gap-2.5\" data-task-detail={task.id}>\n <div className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\" data-task-title-card=\"\">\n <h2 className=\"text-base font-semibold break-words\">{compactTaskTitle(task)}</h2>\n {compactTaskTitle(task) !== task.title.trim() && <p className=\"text-[13px] break-words text-muted-foreground\">{task.title}</p>}\n {meta && <p className=\"text-xs text-muted-foreground\">{meta}</p>}\n </Item></div>\n {actions}\n {open && <div className=\"rounded-[var(--radius-lg)] bg-card\"><OpenQuestion key={open.id} task={task} question={open} locale={locale} onAnswer={onAnswer} onOther={onOther} onDiscussQuestion={onDiscussQuestion} /></div>}\n {(reason || status === 'needs-you' && !open) && <div className=\"rounded-[var(--radius-lg)] bg-card\" data-task-reason=\"\"><TaskAttention active={status === 'needs-you'}><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\">\n <span className={sectionLabel}>{status === 'needs-you' ? words.waiting : compactLabels[locale].blocked}</span>\n {reason && <p className=\"text-sm break-words\">{reason}</p>}\n {status === 'needs-you' && <p className=\"text-[13px] text-muted-foreground\">{words.elsewhere}</p>}\n </Item></TaskAttention></div>}\n {task.spec && task.spec.trim() !== task.title.trim() && <section aria-label={words.request} className=\"rounded-[var(--radius-lg)] bg-card\" data-task-spec=\"\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\">\n <h3 className={sectionLabel}>{words.request}</h3>\n <p className=\"text-sm break-words whitespace-pre-wrap\">{task.spec}</p>\n </Item></section>}\n {!!answered.length && <section aria-label=\"Answered questions\" className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-0\">\n <div className=\"flex items-baseline justify-between gap-2 pl-2.5\"><h3 className={sectionLabel}>ANSWERED · {answered.length}</h3>\n {answered.length > 3 && <Button variant=\"link\" size=\"xs\" className=\"underline\" data-task-show-answers aria-expanded={showAll} onClick={() => setShowAll(value => !value)}>{showAll ? 'Show latest' : 'Show all'}</Button>}</div>\n <ul className=\"flex min-w-0 flex-col\">{(showAll ? answered : answered.slice(0, 3)).map(question => <AnsweredQuestion key={question.id} question={question} />)}</ul>\n </Item></section>}\n {!!task.files?.length && <section aria-label=\"Task files\" className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-0.5\">\n <h3 className={`${sectionLabel} px-2.5 pb-1`}>FILES{folder && ` · ${folder}`}</h3>\n {task.files.map(file => {\n const kind = fileKind(file), [label, Icon] = kind ? kinds[kind] : ['', FileIcon];\n const name = file.name ?? (folder && file.path.startsWith(folder) ? file.path.slice(folder.length) : file.path.split('/').pop());\n const content = <><Icon aria-hidden=\"true\" /><span className=\"min-w-0 flex-1 truncate text-start text-[13px] font-normal\">{name}</span>{label && <span className=\"text-[11px] font-normal text-muted-foreground\">{label}</span>}</>;\n return onOpenFile ? <Button key={file.path} variant=\"ghost\" className=\"w-full justify-start\" data-task-file={file.path} onClick={() => onOpenFile(task, file)}>{content}</Button> : <p key={file.path} className=\"flex items-center gap-2\">{content}</p>;\n })}\n </Item></section>}\n {onAsk && <Button variant=\"link\" className=\"self-start underline\" data-task-ask={task.id} onClick={() => onAsk(task)}>{words.ask}</Button>}\n </div>;\n}\n",
35
+ "content": "'use client';\n\nimport { useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { AppWindowIcon, ChevronRightIcon, FileIcon, ImageIcon } from 'lucide-react';\nimport type { TaskFile, TaskQuestion, TaskRecord } from '@hachej/boring-ui-kit/task-board';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Item } from '@/components/ui/item';\nimport { TaskAttention, taskTokens } from './task-request-card';\nimport type { TaskLocale } from './task-request-card';\nimport { compactLabels, compactTaskStatus, compactTaskTitle, taskWaitingText } from './compact-presentation';\n\nexport interface TaskDetailProps {\n readonly task: TaskRecord;\n readonly locale?: TaskLocale | undefined;\n /** Host content after the question and the request (for example a mount report or host-authorized request actions). */\n readonly actions?: ReactNode;\n /** The host reauthorizes and resolves the original question. Rendering never supplies approval. */\n readonly onAnswer?: ((task: TaskRecord, question: TaskQuestion, value: string) => void) | undefined;\n /** « Other… » opens an inline field; Send passes the typed text. No synthetic option value or approval is generated. */\n readonly onOther?: ((task: TaskRecord, question: TaskQuestion, text: string) => void) | undefined;\n /** « Discuss in chat »: the host stages `@ D-201 · <title>` and the question in its one composer, without sending. */\n readonly onDiscussQuestion?: ((task: TaskRecord, question: TaskQuestion) => void) | undefined;\n readonly onOpenFile?: ((task: TaskRecord, file: TaskFile) => void) | undefined;\n /** « Ask in the conversation »: the host focuses its composer with the task mention. */\n readonly onAsk?: ((task: TaskRecord) => void) | undefined;\n /** @deprecated Ignored; kept for source compatibility. */\n readonly labels?: unknown;\n}\nconst detailWords = {\n en: { question: 'QUESTION FOR YOU', waiting: 'WAITING ON YOU', elsewhere: 'Reply in the conversation.', request: 'THE REQUEST', yourAnswer: 'Your answer', send: 'Send', other: 'Other…', discuss: 'Discuss in chat', ask: 'Ask in the conversation' },\n fr: { question: 'QUESTION POUR VOUS', waiting: 'ATTEND VOTRE AVIS', elsewhere: 'Répondez dans la conversation.', request: 'LA DEMANDE', yourAnswer: 'Votre réponse', send: 'Envoyer', other: 'Autre…', discuss: 'En parler dans la conversation', ask: 'Demander dans la conversation' },\n} as const;\nconst preview = (value: string) => value.length > 160 ? `${value.slice(0, 160)}…` : value;\nconst sectionLabel = 'text-[11px] font-semibold tracking-[0.06em] text-muted-foreground';\nconst kinds = { markdown: ['Markdown', FileIcon], mockup: ['Mockup', AppWindowIcon], capture: ['Capture', ImageIcon] } as const;\nconst fileKind = (file: TaskFile) => file.kind ?? (/\\.(?:md|markdown)$/i.test(file.path) ? 'markdown' : /\\.html?$/i.test(file.path) ? 'mockup' : /\\.(?:png|jpe?g|gif|webp|svg)$/i.test(file.path) ? 'capture' : undefined);\n/** ISO times show as local HH:MM; host-formatted strings show as given. */\nconst clock = (value: string) => /^\\d{4}-\\d\\d-\\d\\dT/.test(value) && Number.isFinite(Date.parse(value)) ? new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) : value;\n\nfunction AnsweredQuestion({ question }: { readonly question: TaskQuestion }) {\n const [expanded, setExpanded] = useState(false);\n return <li data-task-answered={question.id} className=\"min-w-0\">\n <Button variant=\"ghost\" className=\"grid h-auto w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] justify-normal gap-x-2 gap-y-0.5 text-start whitespace-normal\" aria-expanded={expanded} onClick={() => setExpanded(value => !value)}>\n <span className=\"truncate text-[13px] font-normal\">{preview(question.text)}</span>\n <ChevronRightIcon aria-hidden=\"true\" className=\"text-muted-foreground\" />\n <span data-task-answer={question.id} className=\"col-span-full truncate text-[13px] font-normal text-muted-foreground\">{question.answeredBy ? `${preview(question.answeredBy)}: ` : ''}{preview(question.answer ?? '')}</span>\n </Button>\n {expanded && <div data-task-answer-full className=\"flex flex-col gap-1 px-2 pb-2 text-[13px] break-words whitespace-pre-wrap\"><p>{question.text}</p><p className=\"text-muted-foreground\">{question.answeredBy && <>{question.answeredBy}: </>}{question.answer}</p></div>}\n </li>;\n}\n\nfunction OpenQuestion({ task, question, locale = 'en', onAnswer, onOther, onDiscussQuestion }: Pick<TaskDetailProps, 'task' | 'locale' | 'onAnswer' | 'onOther' | 'onDiscussQuestion'> & { readonly question: TaskQuestion }) {\n // Without options typing is the only way to answer, so the field is simply there.\n const typed = !question.options?.length;\n const [other, setOther] = useState<string | null>(typed && onOther ? '' : null);\n const words = detailWords[locale];\n const send = () => { const text = other?.trim(); if (text) { onOther?.(task, question, text); setOther(typed ? '' : null); } };\n const asked = [question.askedBy, question.askedAt && clock(question.askedAt)].filter(Boolean).join(' · ');\n return <TaskAttention active>\n <Item variant=\"outline\" className=\"flex-col items-stretch gap-2\" data-task-question={question.id}>\n <div className=\"flex items-baseline justify-between gap-2\"><span className={sectionLabel}>{words.question}</span>{asked && <span className=\"text-[11px] text-muted-foreground\">{asked}</span>}</div>\n <p className=\"text-sm\">{question.text}</p>\n <div className={`flex items-center gap-2 ${taskTokens.control}`}>\n {question.options?.map((option, index) => {\n const value = typeof option === 'string' ? option : option.value;\n const label = typeof option === 'string' ? option : option.label;\n return <Button key={index} variant={index === 0 ? 'default' : 'outline'} disabled={!onAnswer} onClick={() => onAnswer?.(task, question, value)}>{label}</Button>;\n })}\n {onOther && !typed && <Button variant=\"outline\" data-task-other={question.id} aria-expanded={other !== null} onClick={() => setOther(value => value === null ? '' : null)}>{words.other}</Button>}\n {onDiscussQuestion && <Button variant=\"link\" className=\"h-auto min-w-0 shrink text-center leading-tight whitespace-normal underline\" data-task-discuss={question.id} onClick={() => onDiscussQuestion(task, question)}>{words.discuss}</Button>}\n </div>\n {other !== null && <form className={`flex gap-2 ${taskTokens.control}`} data-task-other-form={question.id} onSubmit={event => { event.preventDefault(); send(); }}>\n <Input autoFocus={!typed} aria-label={words.yourAnswer} maxLength={4096} value={other} onChange={event => setOther(event.target.value)} onKeyDown={event => { if (event.key === 'Escape' && !typed) { event.preventDefault(); setOther(null); } }} />\n <Button type=\"submit\" disabled={!other.trim()}>{words.send}</Button>\n </form>}\n </Item>\n </TaskAttention>;\n}\n\n/** Task detail (spec 3 §4), in order: title, the open question for the viewer, Answered, Files, « Ask in the conversation ». */\nexport function TaskDetail({ task, locale = 'en', actions, onAnswer, onOther, onDiscussQuestion, onOpenFile, onAsk }: TaskDetailProps) {\n const [showAll, setShowAll] = useState(false);\n const open = task.waiting_on_you === true ? task.questions?.find(question => question.answer === undefined) : undefined;\n const words = detailWords[locale];\n const status = compactTaskStatus(task);\n // Nothing openable may say it needs someone without saying what: the projected reason shows when there is no full question.\n const reason = !open && (status === 'needs-you' || status === 'blocked') ? taskWaitingText(task) : undefined;\n const answered = task.questions?.filter(question => question.answer !== undefined).toReversed().sort((a, b) => (Date.parse(b.answeredAt ?? '') || 0) - (Date.parse(a.answeredAt ?? '') || 0)) ?? [];\n const meta = [task.screen && `Screen: ${task.screen}`, task.requester && `asked by ${task.requester}`, task.refiner && `refined by ${task.refiner}`].filter(Boolean).join(' · ');\n const folder = task.folder?.replace(/\\/?$/, '/');\n return <div className=\"flex min-w-0 flex-col gap-2.5\" data-task-detail={task.id}>\n <div className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\" data-task-title-card=\"\">\n <h2 className=\"text-base font-semibold break-words\">{compactTaskTitle(task)}</h2>\n {compactTaskTitle(task) !== task.title.trim() && <p className=\"text-[13px] break-words text-muted-foreground\">{task.title}</p>}\n {meta && <p className=\"text-xs text-muted-foreground\">{meta}</p>}\n </Item></div>\n {open && <div className=\"rounded-[var(--radius-lg)] bg-card\"><OpenQuestion key={open.id} task={task} question={open} locale={locale} onAnswer={onAnswer} onOther={onOther} onDiscussQuestion={onDiscussQuestion} /></div>}\n {(reason || status === 'needs-you' && !open) && <div className=\"rounded-[var(--radius-lg)] bg-card\" data-task-reason=\"\"><TaskAttention active={status === 'needs-you'}><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\">\n <span className={sectionLabel}>{status === 'needs-you' ? words.waiting : compactLabels[locale].blocked}</span>\n {reason && <p className=\"text-sm break-words\">{reason}</p>}\n {status === 'needs-you' && <p className=\"text-[13px] text-muted-foreground\">{words.elsewhere}</p>}\n </Item></TaskAttention></div>}\n {task.spec && task.spec.trim() !== task.title.trim() && <section aria-label={words.request} className=\"rounded-[var(--radius-lg)] bg-card\" data-task-spec=\"\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\">\n <h3 className={sectionLabel}>{words.request}</h3>\n <p className=\"text-sm break-words whitespace-pre-wrap\">{task.spec}</p>\n </Item></section>}\n {actions}\n {!!answered.length && <section aria-label=\"Answered questions\" className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-0\">\n <div className=\"flex items-baseline justify-between gap-2 pl-2.5\"><h3 className={sectionLabel}>ANSWERED · {answered.length}</h3>\n {answered.length > 3 && <Button variant=\"link\" size=\"xs\" className=\"underline\" data-task-show-answers aria-expanded={showAll} onClick={() => setShowAll(value => !value)}>{showAll ? 'Show latest' : 'Show all'}</Button>}</div>\n <ul className=\"flex min-w-0 flex-col\">{(showAll ? answered : answered.slice(0, 3)).map(question => <AnsweredQuestion key={question.id} question={question} />)}</ul>\n </Item></section>}\n {!!task.files?.length && <section aria-label=\"Task files\" className=\"rounded-[var(--radius-lg)] bg-card\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-0.5\">\n <h3 className={`${sectionLabel} px-2.5 pb-1`}>FILES{folder && ` · ${folder}`}</h3>\n {task.files.map(file => {\n const kind = fileKind(file), [label, Icon] = kind ? kinds[kind] : ['', FileIcon];\n const name = file.name ?? (folder && file.path.startsWith(folder) ? file.path.slice(folder.length) : file.path.split('/').pop());\n const content = <><Icon aria-hidden=\"true\" /><span className=\"min-w-0 flex-1 truncate text-start text-[13px] font-normal\">{name}</span>{label && <span className=\"text-[11px] font-normal text-muted-foreground\">{label}</span>}</>;\n return onOpenFile ? <Button key={file.path} variant=\"ghost\" className=\"w-full justify-start\" data-task-file={file.path} onClick={() => onOpenFile(task, file)}>{content}</Button> : <p key={file.path} className=\"flex items-center gap-2\">{content}</p>;\n })}\n </Item></section>}\n {!!task.history?.length && <section aria-label=\"Task history\" className=\"rounded-[var(--radius-lg)] bg-card\" data-task-history=\"\"><Item variant=\"outline\" className=\"flex-col items-stretch gap-1\">\n <h3 className={sectionLabel}>HISTORY</h3>\n {task.history.map((item, index) => <p key={item.id ?? index} className=\"text-[13px] break-words text-muted-foreground\">{item.note}</p>)}\n </Item></section>}\n {onAsk && <Button variant=\"link\" className=\"self-start underline\" data-task-ask={task.id} onClick={() => onAsk(task)}>{words.ask}</Button>}\n </div>;\n}\n",
36
36
  "type": "registry:component",
37
37
  "target": "components/task-board/task-detail.tsx"
38
38
  }
@@ -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.42",
8
- "@hachej/boring-files@^0.4.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
8
+ "@hachej/boring-files@^0.4.43",
9
9
  "react@^19.0.0",
10
10
  "react-dom@^19.0.0",
11
11
  "zod@^4.6.5"
@@ -4,14 +4,14 @@
4
4
  "title": "Task mentions",
5
5
  "description": "Optional authorized task catalog for the existing PM composer, composed with host file search.",
6
6
  "dependencies": [
7
- "@hachej/boring-ui-kit@^0.4.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/task-board.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat-mentions.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/task-board.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/pi-chat-mentions.json"
15
15
  ],
16
16
  "files": [
17
17
  {
@@ -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.42",
7
+ "@hachej/boring-files@^0.4.43",
8
8
  "react@^19.0.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/button.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42/r/viewer-frame.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/button.json",
15
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/version-history.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/button.json",
15
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/viewer-frame.json",
13
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/button.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42/r/viewer-frame.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42",
7
+ "@hachej/boring-ui-kit@^0.4.43",
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.42/r/viewer-frame.json",
26
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/markdown-editor.json",
27
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json",
28
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/button.json"
25
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
26
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/markdown-editor.json",
27
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json",
28
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42",
7
+ "@hachej/boring-files@^0.4.43",
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.42/r/viewer-frame.json",
14
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-image.json"
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
14
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42/r/viewer-frame.json",
8
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-markdown.json",
9
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-html.json",
10
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-image.json",
11
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-pdf.json",
12
- "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/viewer-code.json"
7
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-frame.json",
8
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-markdown.json",
9
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-html.json",
10
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-image.json",
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/viewer-pdf.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/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.42/r/utils.json"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.43/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {