@hachej/boring-ui-kit 0.4.15 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +9 -9
- package/r/board-viewer.json +5 -5
- package/r/button.json +1 -1
- package/r/diagram-viewer.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +12 -12
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +6 -6
- package/r/file-tree.json +5 -5
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +1 -1
- package/r/maquette-viewer.json +2 -2
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +10 -10
- package/r/pi-app.json +20 -20
- package/r/pi-artifacts.json +1 -1
- package/r/pi-chat-approval.json +1 -1
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +1 -1
- package/r/pi-chat-mentions.json +1 -1
- package/r/pi-chat-pickers.json +1 -1
- package/r/pi-chat-plugins.json +11 -11
- package/r/pi-chat-question.json +1 -1
- package/r/pi-chat-slash.json +1 -1
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +2 -2
- package/r/pi-chat.json +8 -8
- package/r/pi-workspace.json +4 -4
- package/r/placed-agent.json +4 -4
- package/r/provider-setup.json +2 -2
- package/r/record-viewer.json +1 -1
- package/r/registry.json +166 -166
- package/r/requests-board.json +1 -1
- package/r/shared-chat.json +10 -10
- package/r/task-list-viewer.json +2 -2
- package/r/version-history.json +3 -3
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +5 -5
- package/r/viewer-html.json +4 -4
- package/r/viewer-image.json +2 -2
- package/r/viewer-markdown.json +5 -5
- package/r/viewer-pdf.json +3 -3
- package/r/viewers.json +6 -6
- package/r/windows.json +1 -1
package/r/requests-board.json
CHANGED
package/r/shared-chat.json
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
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.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.16",
|
|
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.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
16
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
17
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/pi-chat.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/pi-chat-plugins.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/pi-chat-markdown.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/pi-chat-mentions.json",
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/theme.json",
|
|
17
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/utils.json",
|
|
18
18
|
"avatar",
|
|
19
19
|
"badge",
|
|
20
20
|
"button",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "registry/shared-chat/use-shared-chat.tsx",
|
|
41
|
-
"content": "'use client';\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ReactNode } from 'react';\nimport { UserIcon } from 'lucide-react';\nimport { createSharedChat } from '@hachej/boring-ui-kit/shared-chat';\nimport type { ChannelSource, Conversation, SharedChatController } from '@hachej/boring-ui-kit/shared-chat';\nimport type { PiChatProps } from '../pi-chat/pi-chat';\nimport type { ConversationsConfig, MentionsConfig } from '../pi-chat/config';\nimport { MentionPicker } from '../pi-chat-mentions/mention-menu';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';\nimport { agentTone, MemberAvatar, SharedMembers, ReplyQuote, sharedQuote } from './messages';\nimport { SharedTimeline } from './timeline';\nimport type { TimelineApi } from './timeline';\nimport { SharedTextProvider } from './text';\nimport type { SharedTextOptions } from './text';\nimport { defaultSharedLabels } from './shared-labels';\nimport { frenchSharedLabels } from './shared-labels-fr';\n\nconst subscribeNothing = () => () => {};\n/** Explicit host composition through PiChat's generic transcript, composer and catalog seams. */\nexport function useSharedChatProps(controller: SharedChatController | undefined, options: SharedTextOptions = {}): Omit<PiChatProps, 'controller'> {\n const state = useSyncExternalStore(controller?.subscribe ?? subscribeNothing, () => controller?.getSnapshot(), () => controller?.getSnapshot());\n const api = useRef<TimelineApi | undefined>(undefined);\n const members = state?.shared.members ?? [];\n const labels = { ...(options.locale === 'fr' ? frenchSharedLabels : defaultSharedLabels), ...options.labels };\n const mentions = useMemo<MentionsConfig>(() => ({\n listLabel: labels.sharedMembers, icon: <UserIcon aria-hidden=\"true\" />, Picker: MentionPicker,\n label: id => members.find(member => member.id === id)?.name ?? id,\n search: async query => members.filter(member => `${member.name} ${member.id}`.toLowerCase().includes(query.toLowerCase()))\n .sort((a, b) => Number(b.kind === 'agent') - Number(a.kind === 'agent')).map(member => ({ path: member.id, label: member.name })),\n render: id => <Badge variant=\"secondary\">@{members.find(member => member.id === id)?.name ?? id}</Badge>,\n renderSent: id => {\n const member = members.find(member => member.id === id) ?? { id, name: id, kind: 'person' };\n return <span data-testid=\"message-mention\" data-path={id} data-agent-color={agentTone(member)?.token} className={`inline-flex items-baseline gap-1 align-baseline font-medium ${agentTone(member)?.text ?? ''}`}><span aria-hidden=\"true\"><MemberAvatar member={member} compact interactive={false} /></span><span data-testid=\"mention-name\">{member.name}</span></span>;\n },\n renderOption: item => <><ItemMedia variant=\"icon\"><UserIcon aria-hidden=\"true\" /></ItemMedia><ItemContent className=\"min-w-0\"><ItemTitle>{item.label}</ItemTitle><ItemDescription className=\"m-0 truncate\">{item.path}</ItemDescription></ItemContent></>,\n }), [members, labels.sharedMembers]);\n const wrap = (children: ReactNode) => <SharedTextProvider {...options}>{children}</SharedTextProvider>;\n return {\n locale: options.locale,\n timeline: { count: state?.shared.messages.length ?? 0,\n render: context => controller ? wrap(<SharedTimeline controller={controller} api={api} mentions={mentions} {...context} />) : null,\n afterSend: () => api.current?.reset() },\n controls: controller && options.presence !== false ? wrap(<SharedMembers controller={controller} />) : undefined,\n mentions,\n composerHeader: controller && state?.shared.replyTo ? wrap(<ReplyQuote targetId={state.shared.replyTo} quote={sharedQuote(state.shared, state.shared.replyTo)} onReveal={id => api.current?.reveal(id)} onRemove={() => controller.setReplyTo(undefined)} />) : undefined,\n composerHasContent: Boolean(state?.shared.files.length),\n onPickFiles: files => { if (controller) controller.setFiles([...controller.getSnapshot().shared.files, ...files]); },\n composerChips: controller && wrap(state?.shared.files.map((file, index) => <Badge key={index} variant=\"secondary\">{file.name}<Button variant=\"ghost\" size=\"icon\" aria-label={`Remove ${file.name}`} onClick={() => controller.setFiles(controller.getSnapshot().shared.files.filter((_, at) => at !== index))}><span aria-hidden=\"true\">×</span></Button></Badge>)),\n renderConnectionError: error => error instanceof Error ? error.message : String(error),\n // WhatsApp's composer; a host spreading these props can still override either.\n composerLayout: 'pill', voiceReplacesSend: true,\n };\n}\n\n/** Owns only the adapters it creates; each adapter borrows the host source. */\nexport function useSharedChat(source: ChannelSource, options: SharedTextOptions = {}) {\n const [list, setList] = useState<{ source: ChannelSource; items: Conversation[] }>();\n const [selection, setSelection] = useState<{ source: ChannelSource; id: string }>();\n const [error, setError] = useState<string>();\n const items = list?.source === source ? list.items : [];\n const conversationId = selection?.source === source ? selection.id : items[0]?.id;\n const [binding, setBinding] = useState<{ source: ChannelSource; id: string; controller: SharedChatController }>();\n useEffect(() => {\n let off = false; setError(undefined);\n void source.conversations().then(items => { if (!off) setList({ source, items }); }, cause => { if (!off) setError(String(cause)); });\n return () => { off = true; };\n }, [source]);\n useEffect(() => {\n if (conversationId === undefined) return;\n const controller = createSharedChat(source, { conversationId });\n setBinding({ source, id: conversationId, controller });\n return () => { void controller.dispose(); };\n }, [source, conversationId]);\n const controller = binding?.source === source && binding.id === conversationId ? binding.controller : undefined;\n const conversations: ConversationsConfig = { items, activeId: conversationId, loading: list?.source !== source, onSelect: id => setSelection({ source, id }) };\n const chat = useSharedChatProps(controller, options);\n return { controller, conversationId, conversations, error,\n chatProps: controller ? { ...chat, controller, conversations } : undefined,\n workspaceProps: { controller, conversationId, conversations, locale: options.locale, controls: chat.controls, chat },\n };\n}\n",
|
|
41
|
+
"content": "'use client';\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ReactNode } from 'react';\nimport { UserIcon } from 'lucide-react';\nimport { createSharedChat } from '@hachej/boring-ui-kit/shared-chat';\nimport type { ChannelSource, Conversation, SharedChatController } from '@hachej/boring-ui-kit/shared-chat';\nimport type { PiChatProps } from '../pi-chat/pi-chat';\nimport type { ConversationsConfig, MentionsConfig } from '../pi-chat/config';\nimport { MentionPicker } from '../pi-chat-mentions/mention-menu';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';\nimport { agentTone, MemberAvatar, SharedMembers, ReplyQuote, sharedQuote } from './messages';\nimport { SharedTimeline } from './timeline';\nimport type { TimelineApi } from './timeline';\nimport { SharedTextProvider } from './text';\nimport type { SharedTextOptions } from './text';\nimport { defaultSharedLabels } from './shared-labels';\nimport { frenchSharedLabels } from './shared-labels-fr';\n\nconst subscribeNothing = () => () => {};\n/** Explicit host composition through PiChat's generic transcript, composer and catalog seams. */\nexport function useSharedChatProps(controller: SharedChatController | undefined, options: SharedTextOptions = {}): Omit<PiChatProps, 'controller'> {\n const state = useSyncExternalStore(controller?.subscribe ?? subscribeNothing, () => controller?.getSnapshot(), () => controller?.getSnapshot());\n const api = useRef<TimelineApi | undefined>(undefined);\n const members = state?.shared.members ?? [];\n const labels = { ...(options.locale === 'fr' ? frenchSharedLabels : defaultSharedLabels), ...options.labels };\n const mentions = useMemo<MentionsConfig>(() => ({\n listLabel: labels.sharedMembers, icon: <UserIcon aria-hidden=\"true\" />, Picker: MentionPicker,\n label: id => members.find(member => member.id === id)?.name ?? id,\n search: async query => members.filter(member => `${member.name} ${member.id}`.toLowerCase().includes(query.toLowerCase()))\n .sort((a, b) => Number(b.kind === 'agent') - Number(a.kind === 'agent')).map(member => ({ path: member.id, label: member.name })),\n render: id => <Badge variant=\"secondary\">@{members.find(member => member.id === id)?.name ?? id}</Badge>,\n renderSent: id => {\n const member = members.find(member => member.id === id) ?? { id, name: id, kind: 'person' };\n return <span data-testid=\"message-mention\" data-path={id} data-agent-color={agentTone(member)?.token} className={`inline-flex items-baseline gap-1 align-baseline font-medium ${agentTone(member)?.text ?? ''}`}><span aria-hidden=\"true\"><MemberAvatar member={member} compact interactive={false} /></span><span data-testid=\"mention-name\">{member.name}</span></span>;\n },\n renderOption: item => <><ItemMedia variant=\"icon\"><UserIcon aria-hidden=\"true\" /></ItemMedia><ItemContent className=\"min-w-0\"><ItemTitle>{item.label}</ItemTitle><ItemDescription className=\"m-0 truncate\">{item.path}</ItemDescription></ItemContent></>,\n }), [members, labels.sharedMembers]);\n const wrap = (children: ReactNode) => <SharedTextProvider {...options}>{children}</SharedTextProvider>;\n return {\n locale: options.locale,\n timeline: { count: state?.shared.messages.length ?? 0,\n render: context => controller ? wrap(<SharedTimeline controller={controller} api={api} mentions={mentions} {...context} />) : null,\n afterSend: () => api.current?.reset() },\n controls: controller && options.presence !== false ? wrap(<SharedMembers controller={controller} />) : undefined,\n mentions,\n composerHeader: controller && state?.shared.replyTo ? wrap(<ReplyQuote targetId={state.shared.replyTo} quote={sharedQuote(state.shared, state.shared.replyTo)} onReveal={id => api.current?.reveal(id)} onRemove={() => controller.setReplyTo(undefined)} />) : undefined,\n composerHasContent: Boolean(state?.shared.files.length),\n onPickFiles: files => { if (controller) controller.setFiles([...controller.getSnapshot().shared.files, ...files]); },\n composerChips: controller && wrap(state?.shared.files.map((file, index) => <Badge key={index} variant=\"secondary\">{file.name}<Button variant=\"ghost\" size=\"icon\" aria-label={`Remove ${file.name}`} onClick={() => controller.setFiles(controller.getSnapshot().shared.files.filter((_, at) => at !== index))}><span aria-hidden=\"true\">×</span></Button></Badge>)),\n renderConnectionError: error => error instanceof Error ? error.message : String(error),\n // WhatsApp's composer; a host spreading these props can still override either.\n composerLayout: 'pill', voiceReplacesSend: true,\n };\n}\n\n/** Owns only the adapters it creates; each adapter borrows the host source. `limit`: messages per history page read from the source (default 60). */\nexport function useSharedChat(source: ChannelSource, options: SharedTextOptions & { readonly limit?: number | undefined } = {}) {\n const [list, setList] = useState<{ source: ChannelSource; items: Conversation[] }>();\n const [selection, setSelection] = useState<{ source: ChannelSource; id: string }>();\n const [error, setError] = useState<string>();\n const items = list?.source === source ? list.items : [];\n const conversationId = selection?.source === source ? selection.id : items[0]?.id;\n const [binding, setBinding] = useState<{ source: ChannelSource; id: string; controller: SharedChatController }>();\n useEffect(() => {\n let off = false; setError(undefined);\n void source.conversations().then(items => { if (!off) setList({ source, items }); }, cause => { if (!off) setError(String(cause)); });\n return () => { off = true; };\n }, [source]);\n useEffect(() => {\n if (conversationId === undefined) return;\n const controller = createSharedChat(source, { conversationId, ...(options.limit === undefined ? {} : { limit: options.limit }) });\n setBinding({ source, id: conversationId, controller });\n return () => { void controller.dispose(); };\n }, [source, conversationId, options.limit]);\n const controller = binding?.source === source && binding.id === conversationId ? binding.controller : undefined;\n const conversations: ConversationsConfig = { items, activeId: conversationId, loading: list?.source !== source, onSelect: id => setSelection({ source, id }) };\n const chat = useSharedChatProps(controller, options);\n return { controller, conversationId, conversations, error,\n chatProps: controller ? { ...chat, controller, conversations } : undefined,\n workspaceProps: { controller, conversationId, conversations, locale: options.locale, controls: chat.controls, chat },\n };\n}\n",
|
|
42
42
|
"type": "registry:component",
|
|
43
43
|
"target": "components/shared-chat/use-shared-chat.tsx"
|
|
44
44
|
},
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"path": "registry/shared-chat/timeline.tsx",
|
|
53
|
-
"content": "'use client';\nimport { Children, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ReactNode, RefObject } from 'react';\nimport type { SharedChatController } from '@hachej/boring-ui-kit/shared-chat';\nimport type { MentionsConfig } from '../pi-chat/config';\nimport { MessageText } from '../pi-chat/message';\nimport { Markdown } from '../pi-chat-markdown/markdown';\nimport { useTranscript } from '../pi-chat/session';\nimport { TranscriptHistory } from '../pi-chat/history';\nimport { Message, MessageContent } from '@/components/ui/message';\nimport { Bubble, BubbleContent } from '@/components/ui/bubble';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { MessageScroller, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem } from '@/components/ui/message-scroller';\nimport { AuthorName, BubbleTail, ClockSpacer, MessageClock, PinnedComment, ReplyQuote, RequestCard, SharedMessage, SystemLines, clockRoom, sharedItems } from './messages';\nimport type { SharedItem } from './messages';\nimport { HostTrace } from './host-trace';\nimport { useChatText, useSharedView } from './text';\n\nexport interface TimelineApi { readonly reset: () => void; readonly reveal: (id: string) => void }\n/**\n * WhatsApp's room: bubbles hug the bottom, a day pill starts each day, host notices collapse into one small line, and a\n * « ↓ 3 new messages » pill appears below the list (never over it) while the reader is scrolled up.\n */\nexport function SharedTimeline({ controller, api, focusComposer, openFile, mentions, pendingVoice }: {\n readonly controller: SharedChatController; readonly api: RefObject<TimelineApi | undefined>;\n readonly focusComposer: () => void; readonly openFile?: ((path: string) => void) | undefined; readonly mentions: MentionsConfig; readonly pendingVoice?: ReactNode;\n}) {\n const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);\n const { labels, icons } = useChatText();\n const view = useSharedView();\n const items = useMemo(() => sharedItems(state.shared), [state.shared]);\n const connected = state.connection.kind === 'connected';\n const transcript = useTranscript(items, 60, 40, { state: state.history, connected: !state.disposed && connected, load: controller.loadEarlier });\n const { stick, hidden } = transcript;\n const root = useRef<HTMLDivElement>(null);\n const [revealing, setRevealing] = useState<string>();\n // The last message the reader has seen at the bottom; what others posted after it while scrolled up is « new ».\n const latest = state.shared.messages.at(-1)?.id;\n const [seen, setSeen] = useState(latest);\n useEffect(() => { if (stick.isAtBottom && seen !== latest) setSeen(latest); }, [stick.isAtBottom, latest, seen]);\n // The pill's row shows only once the reader scrolls up, so loading a room (which scrolls down) never shifts the list under a tap.\n const lastTop = useRef(0);\n const [scrolledUp, setScrolledUp] = useState(false);\n useEffect(() => { if (stick.isAtBottom) setScrolledUp(false); }, [stick.isAtBottom]);\n const seenAt = state.shared.messages.findIndex(message => message.id === seen);\n const unseen = stick.isAtBottom ? 0 : state.shared.messages.slice(seenAt + 1).filter(message => message.author !== state.shared.me.id).length;\n useEffect(() => {\n const value = { reset: () => { transcript.reset(); void stick.scrollToBottom('instant'); }, reveal: setRevealing };\n api.current = value;\n return () => { if (api.current === value) api.current = undefined; };\n });\n useEffect(() => {\n if (!revealing) return;\n const key = items.find(item => item.message.id === revealing || item.system?.some(line => line.id === revealing))?.key;\n if (key) {\n transcript.revealTo(key);\n const frame = requestAnimationFrame(() => {\n const parent = [...(root.current?.querySelectorAll<HTMLElement>('[data-shared-message]') ?? [])].find(element => element.dataset.sharedMessage === revealing);\n if (parent) { parent.scrollIntoView({ block: 'center', behavior: 'instant' }); setRevealing(undefined); }\n });\n return () => cancelAnimationFrame(frame);\n }\n if (state.history.kind === 'loading') return;\n if (state.history.kind === 'ready' && state.history.hasMore) void controller.loadEarlier().catch(() => setRevealing(undefined));\n else setRevealing(undefined);\n }, [revealing, items, hidden, state.history]);\n const hasContent = items.length > 0 || Children.count(pendingVoice) > 0;\n // Avatars and names only where they tell people apart: more than two members.\n const multi = view.presence !== false && state.shared.members.length > 2;\n const context = { pieces: { mentions: true, skills: [], renderMention: mentions.renderSent }, openFile };\n const renderMention = (path: string, text: string) => mentions.renderSent?.(path) ?? text;\n const reply = (id: string) => { controller.setReplyTo(id); focusComposer(); };\n const bubble = (item: SharedItem) => {\n const { message, author, own, first } = item;\n const person = own || author.kind !== 'agent' || message.via !== undefined;\n const head = first && !own && multi ? <AuthorName member={author} via={message.via} /> : undefined;\n const traced = view.steps !== 'hidden' && !message.via && author.kind === 'agent' && Boolean(message.streaming || message.steps?.length);\n const quote = message.replyTo ? <ReplyQuote targetId={message.replyTo} quote={item.quote} onReveal={setRevealing} /> : undefined;\n if (message.card) return <article data-row-id={item.key} data-role=\"event\" className=\"flex w-full min-w-0 flex-col\">\n <Bubble variant={own ? 'default' : 'muted'} align={own ? 'end' : 'start'} data-testid=\"card-bubble\">\n {first && <BubbleTail side={own ? 'end' : 'start'} />}\n <BubbleContent className=\"relative min-w-52\">{head}{quote}\n {message.card.kind === 'pin' ? <PinnedComment card={message.card} own={own} at={message.at} /> : <RequestCard card={message.card} action={controller.action} at={message.at} own={own} />}\n </BubbleContent>\n </Bubble></article>;\n return <article data-row-id={item.key} data-role={person ? 'user' : 'assistant'} data-streaming={message.streaming ? 'true' : undefined} className=\"group/message\"><Message align={own ? 'end' : 'start'}><MessageContent>\n {own ? <div data-testid=\"own-message\" className=\"contents [&_[data-testid=message-mention]]:text-inherit\">\n <MessageText text={message.text ?? ''} context={context} variant=\"default\" decoration={first ? <BubbleTail side=\"end\" /> : undefined} header={quote} footer={<><ClockSpacer /><MessageClock at={message.at} tone=\"primary\" /></>} /></div>\n : view.answerStyle === 'plain' && !person ? <div data-testid=\"plain-answer\" className=\"min-w-0\">\n {head}{quote}\n {message.text && <Markdown text={message.text} streaming={Boolean(message.streaming)} renderMention={renderMention} />}\n {traced && <HostTrace participant={author.name} steps={message.steps ?? []} streaming={Boolean(message.streaming)} active={item.showSpinner} />}\n </div>\n : <Bubble variant=\"muted\">{first && <BubbleTail side=\"start\" />}<BubbleContent className=\"relative\">\n {head}{quote}\n {message.text && <div className={traced ? undefined : clockRoom}><Markdown text={message.text} streaming={Boolean(message.streaming)} renderMention={renderMention} /></div>}\n {traced ? <>\n <HostTrace participant={author.name} steps={message.steps ?? []} streaming={Boolean(message.streaming)} active={item.showSpinner} />\n <div className=\"flex justify-end\"><MessageClock at={message.at} tone=\"muted\" floating={false} /></div>\n </> : <MessageClock at={message.at} tone=\"muted\" />}\n </BubbleContent></Bubble>}\n </MessageContent></Message></article>;\n };\n return <MessageScroller>\n <MessageScrollerViewport ref={node => { root.current = node; stick.scrollRef(node); }} data-testid=\"transcript-scroll\" aria-label={labels.messages} className=\"min-h-0 flex-1 scrollbar-thumb-border scrollbar-track-transparent\"\n onScroll={event => { const top = event.currentTarget.scrollTop; if (top < lastTop.current - 4) setScrolledUp(true); lastTop.current = top; }}>\n {items.length > 0 && (hidden > 0 || state.history.kind === 'loading' || state.history.kind === 'error' || state.history.kind === 'ready' && state.history.hasMore) && <TranscriptHistory history={state.history} connected={!state.disposed && connected} onLoad={transcript.reveal} sentinelRef={transcript.sentinelRef} hidden={hidden} loaded={transcript.loaded} />}\n <div data-testid=\"transcript-bottom\" className=\"mx-auto flex min-h-full w-full max-w-[720px] flex-col justify-end px-4 pt-4 pb-3 sm:px-6\">\n <MessageScrollerContent aria-label={labels.messages} aria-live=\"polite\" data-testid=\"transcript\" className=\"h-auto min-h-0 gap-0 [&>[data-local-voice]]:mt-3\">\n {transcript.visible(items).map((item, index) => {\n const gap = index === 0 || item.separator ? '' : item.system || item.first ? ' mt-3' : ' mt-0.5';\n return <MessageScrollerItem key={item.key} messageId={item.key} data-group-start={item.first ? 'true' : undefined} className={`[content-visibility:visible]${gap}`}>\n {item.separator && <div data-testid=\"day-separator\" className={`flex justify-center pb-3${index > 0 ? ' pt-4' : ''}`}><Badge variant=\"secondary\"><time dateTime={item.message.at} title={labels.sharedTimestamp(item.message.at)}>{labels.sharedDay(item.message.at)}</time></Badge></div>}\n {item.system ? <SystemLines lines={item.system} />\n : <SharedMessage shared={item} avatars={multi} remove={controller.remove} canRemove={controller.canRemove} openFile={openFile} onReply={reply} onReveal={setRevealing}>{bubble(item)}</SharedMessage>}\n </MessageScrollerItem>;\n })}\n {pendingVoice}\n </MessageScrollerContent>\n </div>\n </MessageScrollerViewport>\n {hasContent && !stick.isAtBottom && scrolledUp && <div data-testid=\"transcript-navigation\" className=\"flex shrink-0 items-center justify-center py-1.5\">\n <Button variant=\"secondary\" size=\"sm\" data-testid=\"jump-latest\" data-unseen={unseen || undefined} aria-label={unseen ? labels.sharedNewMessages(unseen) : labels.jumpToLatest} title={labels.jumpToLatest} className=\"max-sm:min-h-11 pointer-coarse:min-h-11\" onClick={() => api.current?.reset()}>\n <icons.jumpToLatest data-icon=\"inline-start\" aria-hidden=\"true\" />{unseen ? labels.sharedNewMessages(unseen) : labels.jumpToLatest}</Button>\n </div>}\n </MessageScroller>;\n}\n",
|
|
53
|
+
"content": "'use client';\nimport { Children, memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport type { ReactNode, RefObject } from 'react';\nimport type { SharedChatController } from '@hachej/boring-ui-kit/shared-chat';\nimport type { MentionsConfig } from '../pi-chat/config';\nimport { MessageText } from '../pi-chat/message';\nimport { Markdown } from '../pi-chat-markdown/markdown';\nimport { PAGE_FIRST, PAGE_MORE, useRowChunks, useTranscript } from '../pi-chat/session';\nimport type { ChatPaging } from '../pi-chat/session';\nimport { TranscriptHistory } from '../pi-chat/history';\nimport { Message, MessageContent } from '@/components/ui/message';\nimport { Bubble, BubbleContent } from '@/components/ui/bubble';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { MessageScroller, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem } from '@/components/ui/message-scroller';\nimport { AuthorName, BubbleTail, ClockSpacer, MessageClock, PinnedComment, ReplyQuote, RequestCard, SharedMessage, SystemLines, clockRoom, sharedItems } from './messages';\nimport type { SharedItem } from './messages';\nimport { HostTrace } from './host-trace';\nimport { useChatText, useSharedView } from './text';\n\nexport interface TimelineApi { readonly reset: () => void; readonly reveal: (id: string) => void }\n/**\n * WhatsApp's room: bubbles hug the bottom, a day pill starts each day, host notices collapse into one small line, and a\n * « ↓ 3 new messages » pill appears below the list (never over it) while the reader is scrolled up.\n */\nexport function SharedTimeline({ controller, api, focusComposer, openFile, mentions, pendingVoice, paging }: {\n readonly controller: SharedChatController; readonly api: RefObject<TimelineApi | undefined>;\n readonly focusComposer: () => void; readonly openFile?: ((path: string) => void) | undefined; readonly mentions: MentionsConfig; readonly pendingVoice?: ReactNode; readonly paging?: ChatPaging | undefined;\n}) {\n const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);\n const { labels, icons } = useChatText();\n const view = useSharedView();\n const items = useMemo(() => sharedItems(state.shared), [state.shared]);\n const connected = state.connection.kind === 'connected';\n const transcript = useTranscript(items, paging?.first ?? PAGE_FIRST, paging?.more ?? PAGE_MORE, { state: state.history, connected: !state.disposed && connected, load: controller.loadEarlier });\n const { stick, hidden } = transcript;\n const chunks = useRowChunks(useMemo(() => transcript.visible(items), [items, hidden]));\n const root = useRef<HTMLDivElement>(null);\n const [revealing, setRevealing] = useState<string>();\n // The last message the reader has seen at the bottom; what others posted after it while scrolled up is « new ».\n const latest = state.shared.messages.at(-1)?.id;\n const [seen, setSeen] = useState(latest);\n useEffect(() => { if (stick.isAtBottom && seen !== latest) setSeen(latest); }, [stick.isAtBottom, latest, seen]);\n // The pill's row shows only once the reader scrolls up, so loading a room (which scrolls down) never shifts the list under a tap.\n const lastTop = useRef(0);\n const [scrolledUp, setScrolledUp] = useState(false);\n useEffect(() => { if (stick.isAtBottom) setScrolledUp(false); }, [stick.isAtBottom]);\n const seenAt = state.shared.messages.findIndex(message => message.id === seen);\n const unseen = stick.isAtBottom ? 0 : state.shared.messages.slice(seenAt + 1).filter(message => message.author !== state.shared.me.id).length;\n useEffect(() => {\n const value = { reset: () => { transcript.reset(); void stick.scrollToBottom('instant'); }, reveal: setRevealing };\n api.current = value;\n return () => { if (api.current === value) api.current = undefined; };\n });\n useEffect(() => {\n if (!revealing) return;\n const key = items.find(item => item.message.id === revealing || item.system?.some(line => line.id === revealing))?.key;\n if (key) {\n transcript.revealTo(key);\n const frame = requestAnimationFrame(() => {\n const parent = [...(root.current?.querySelectorAll<HTMLElement>('[data-shared-message]') ?? [])].find(element => element.dataset.sharedMessage === revealing);\n if (parent) { parent.scrollIntoView({ block: 'center', behavior: 'instant' }); setRevealing(undefined); }\n });\n return () => cancelAnimationFrame(frame);\n }\n if (state.history.kind === 'loading') return;\n if (state.history.kind === 'ready' && state.history.hasMore) void controller.loadEarlier().catch(() => setRevealing(undefined));\n else setRevealing(undefined);\n }, [revealing, items, hidden, state.history]);\n const hasContent = items.length > 0 || Children.count(pendingVoice) > 0;\n // Avatars and names only where they tell people apart: more than two members.\n const multi = view.presence !== false && state.shared.members.length > 2;\n const focus = useRef(focusComposer); focus.current = focusComposer;\n const reply = useCallback((id: string) => { controller.setReplyTo(id); focus.current(); }, [controller]);\n const open = useRef(openFile); open.current = openFile;\n const hasOpen = Boolean(openFile);\n const stableOpen = useMemo(() => hasOpen ? (path: string) => open.current?.(path) : undefined, [hasOpen]);\n return <MessageScroller>\n <MessageScrollerViewport ref={node => { root.current = node; stick.scrollRef(node); }} data-testid=\"transcript-scroll\" aria-label={labels.messages} className=\"min-h-0 flex-1 scrollbar-thumb-border scrollbar-track-transparent\"\n onScroll={event => { const top = event.currentTarget.scrollTop; if (top < lastTop.current - 4) setScrolledUp(true); lastTop.current = top; }}>\n {items.length > 0 && (hidden > 0 || state.history.kind === 'loading' || state.history.kind === 'error' || state.history.kind === 'ready' && state.history.hasMore) && <TranscriptHistory history={state.history} connected={!state.disposed && connected} onLoad={transcript.reveal} sentinelRef={transcript.sentinelRef} hidden={hidden} loaded={transcript.loaded} />}\n <div data-testid=\"transcript-bottom\" className=\"mx-auto flex min-h-full w-full max-w-[720px] flex-col justify-end px-4 pt-4 pb-3 sm:px-6\">\n <MessageScrollerContent aria-label={labels.messages} aria-live=\"polite\" data-testid=\"transcript\" className=\"h-auto min-h-0 gap-0 [&>[data-local-voice]]:mt-3\">\n {chunks.map((run, index) => <TimelineChunk key={run[0]!.key} items={run} top={index === 0} multi={multi} controller={controller} mentions={mentions} openFile={stableOpen} onReply={reply} onReveal={setRevealing} />)}\n {pendingVoice}\n </MessageScrollerContent>\n </div>\n </MessageScrollerViewport>\n {hasContent && !stick.isAtBottom && scrolledUp && <div data-testid=\"transcript-navigation\" className=\"flex shrink-0 items-center justify-center py-1.5\">\n <Button variant=\"secondary\" size=\"sm\" data-testid=\"jump-latest\" data-unseen={unseen || undefined} aria-label={unseen ? labels.sharedNewMessages(unseen) : labels.jumpToLatest} title={labels.jumpToLatest} className=\"max-sm:min-h-11 pointer-coarse:min-h-11\" onClick={() => api.current?.reset()}>\n <icons.jumpToLatest data-icon=\"inline-start\" aria-hidden=\"true\" />{unseen ? labels.sharedNewMessages(unseen) : labels.jumpToLatest}</Button>\n </div>}\n </MessageScroller>;\n}\n\ntype RowProps = {\n readonly multi: boolean; readonly controller: SharedChatController; readonly mentions: MentionsConfig;\n readonly openFile: ((path: string) => void) | undefined; readonly onReply: (id: string) => void; readonly onReveal: (id: string) => void;\n};\n/** A run of timeline rows as one scroller item (`useRowChunks`). Memoized, like each row: it renders again only when its own items change. */\nconst TimelineChunk = memo(function TimelineChunk({ items, top, ...props }: RowProps & { readonly items: readonly SharedItem[]; readonly top: boolean }) {\n return <MessageScrollerItem messageId={items[0]!.key} data-transcript-chunk=\"\" className=\"flex flex-col [content-visibility:visible] [gap:inherit]\">\n {items.map((item, index) => <TimelineRow key={item.key} item={item} top={top && index === 0} {...props} />)}\n </MessageScrollerItem>;\n});\nconst TimelineRow = memo(function TimelineRow({ item, top, multi, controller, mentions, openFile, onReply, onReveal }: RowProps & { readonly item: SharedItem; readonly top: boolean }) {\n const { labels } = useChatText();\n const view = useSharedView();\n const context = useMemo(() => ({ pieces: { mentions: true, skills: [], renderMention: mentions.renderSent }, openFile }), [mentions.renderSent, openFile]);\n const renderMention = (path: string, text: string) => mentions.renderSent?.(path) ?? text;\n const bubble = () => {\n const { message, author, own, first } = item;\n const person = own || author.kind !== 'agent' || message.via !== undefined;\n const head = first && !own && multi ? <AuthorName member={author} via={message.via} /> : undefined;\n const traced = view.steps !== 'hidden' && !message.via && author.kind === 'agent' && Boolean(message.streaming || message.steps?.length);\n const quote = message.replyTo ? <ReplyQuote targetId={message.replyTo} quote={item.quote} onReveal={onReveal} /> : undefined;\n if (message.card) return <article data-row-id={item.key} data-role=\"event\" className=\"flex w-full min-w-0 flex-col\">\n <Bubble variant={own ? 'default' : 'muted'} align={own ? 'end' : 'start'} data-testid=\"card-bubble\">\n {first && <BubbleTail side={own ? 'end' : 'start'} />}\n <BubbleContent className=\"relative min-w-52\">{head}{quote}\n {message.card.kind === 'pin' ? <PinnedComment card={message.card} own={own} at={message.at} /> : <RequestCard card={message.card} action={controller.action} at={message.at} own={own} />}\n </BubbleContent>\n </Bubble></article>;\n return <article data-row-id={item.key} data-role={person ? 'user' : 'assistant'} data-streaming={message.streaming ? 'true' : undefined} className=\"group/message\"><Message align={own ? 'end' : 'start'}><MessageContent>\n {own ? <div data-testid=\"own-message\" className=\"contents [&_[data-testid=message-mention]]:text-inherit\">\n <MessageText text={message.text ?? ''} context={context} variant=\"default\" decoration={first ? <BubbleTail side=\"end\" /> : undefined} header={quote} footer={<><ClockSpacer /><MessageClock at={message.at} tone=\"primary\" /></>} /></div>\n : view.answerStyle === 'plain' && !person ? <div data-testid=\"plain-answer\" className=\"min-w-0\">\n {head}{quote}\n {message.text && <Markdown text={message.text} streaming={Boolean(message.streaming)} renderMention={renderMention} />}\n {traced && <HostTrace participant={author.name} steps={message.steps ?? []} streaming={Boolean(message.streaming)} active={item.showSpinner} />}\n </div>\n : <Bubble variant=\"muted\">{first && <BubbleTail side=\"start\" />}<BubbleContent className=\"relative\">\n {head}{quote}\n {message.text && <div className={traced ? undefined : clockRoom}><Markdown text={message.text} streaming={Boolean(message.streaming)} renderMention={renderMention} /></div>}\n {traced ? <>\n <HostTrace participant={author.name} steps={message.steps ?? []} streaming={Boolean(message.streaming)} active={item.showSpinner} />\n <div className=\"flex justify-end\"><MessageClock at={message.at} tone=\"muted\" floating={false} /></div>\n </> : <MessageClock at={message.at} tone=\"muted\" />}\n </BubbleContent></Bubble>}\n </MessageContent></Message></article>;\n };\n const gap = top || item.separator ? '' : item.system || item.first ? ' mt-3' : ' mt-0.5';\n return <div data-row-key={item.key} data-group-start={item.first ? 'true' : undefined} className={`min-w-0 shrink-0${gap}`}>\n {item.separator && <div data-testid=\"day-separator\" className={`flex justify-center pb-3${top ? '' : ' pt-4'}`}><Badge variant=\"secondary\"><time dateTime={item.message.at} title={labels.sharedTimestamp(item.message.at)}>{labels.sharedDay(item.message.at)}</time></Badge></div>}\n {item.system ? <SystemLines lines={item.system} />\n : <SharedMessage shared={item} avatars={multi} remove={controller.remove} canRemove={controller.canRemove} openFile={openFile} onReply={onReply} onReveal={onReveal}>{bubble()}</SharedMessage>}\n </div>;\n});\n",
|
|
54
54
|
"type": "registry:component",
|
|
55
55
|
"target": "components/shared-chat/timeline.tsx"
|
|
56
56
|
},
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"path": "registry/shared-chat/shared-labels.ts",
|
|
71
|
-
"content": "
|
|
71
|
+
"content": "// Building an Intl.DateTimeFormat costs far more than formatting with one; every bubble's clock reuses the same few.\nconst formats = new Map<string, Intl.DateTimeFormat>();\nfunction format(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale}|${JSON.stringify(options)}`;\n let found = formats.get(key);\n if (!found) { found = new Intl.DateTimeFormat(locale, options); formats.set(key, found); }\n return found;\n}\n\nexport function formatTimestamp(at: string, locale: string) {\n const date = new Date(at);\n if (!Number.isFinite(date.getTime())) return at;\n return `${format(locale, { weekday: 'short', day: 'numeric', month: 'short' }).format(date)}, ${format(locale, { hour: '2-digit', minute: '2-digit' }).format(date)}`;\n}\n/** The time inside a bubble (« 14:02 », « 2:02 PM »). */\nexport function formatClock(at: string, locale: string) {\n const date = new Date(at);\n return Number.isFinite(date.getTime()) ? format(locale, { hour: 'numeric', minute: '2-digit' }).format(date) : '';\n}\n/** A day separator: today and yesterday by name, otherwise « Tue 22 Oct » (with the year when it is not this year). */\nexport function formatDay(at: string, locale: string, words: { readonly today: string; readonly yesterday: string }, now = Date.now()) {\n const date = new Date(at);\n if (!Number.isFinite(date.getTime())) return at;\n const start = (time: number) => { const day = new Date(time); day.setHours(0, 0, 0, 0); return day.getTime(); };\n const days = Math.round((start(now) - start(date.getTime())) / 86_400_000);\n if (days === 0) return words.today;\n if (days === 1) return words.yesterday;\n const sameYear = new Date(now).getFullYear() === date.getFullYear();\n return format(locale, { weekday: 'short', day: 'numeric', month: 'short', ...(sameYear ? {} : { year: 'numeric' }) }).format(date).replace(/,/g, '');\n}\ntype RoleOf = { readonly kind: 'person' | 'agent' | 'admin'; readonly role?: string | undefined };\n/** owner, validator and member read as themselves; every agent is the app's agent; the admin keeps its badge instead. */\nexport function roleLine(member: RoleOf, words: { readonly owner: string; readonly validator: string; readonly member: string; readonly agent: string }): string | undefined {\n if (member.kind === 'admin' || member.role === 'admin') return undefined;\n if (member.kind === 'agent' || member.role?.startsWith('agent:')) return words.agent;\n return member.role === 'owner' || member.role === 'validator' || member.role === 'member' ? words[member.role] : undefined;\n}\nexport const defaultSharedLabels = {\n /** The full date and time, as the tooltip of a bubble's time. */\n sharedTimestamp: (at: string) => formatTimestamp(at, 'en'),\n /** The time inside a bubble, bottom right. */\n sharedClock: (at: string) => formatClock(at, 'en'),\n /** The centred day pill before the first message of each day. */\n sharedDay: (at: string) => formatDay(at, 'en-GB', { today: 'Today', yesterday: 'Yesterday' }),\n sharedNewMessages: (count: number) => count === 1 ? '1 new message' : `${count} new messages`,\n sharedMoreUpdates: (count: number) => count === 1 ? '1 more update' : `${count} more updates`, sharedFewerUpdates: 'Show less',\n sharedPinned: 'Pinned comment',\n sharedMembers: 'Members', sharedMemberCount: (count: number) => `${count} members`,\n sharedAgent: 'Agent', sharedVia: (name: string) => `via ${name}`,\n sharedWorking: (name: string) => `${name} is working…`,\n sharedReply: 'Reply', sharedCancelReply: 'Cancel reply', sharedEarlierMessage: 'Earlier message',\n sharedAdmin: 'Admin', sharedHere: 'Here', sharedAway: 'Away', sharedOffline: 'Offline',\n sharedRemoved: 'Message removed', sharedSince: (time: string) => `Since ${time}`,\n /** The line beside an author (« Léa · member »). Return undefined for none; a member's own `roleLabel` wins. */\n sharedRole: (member: RoleOf): string | undefined => roleLine(member, { owner: 'owner', validator: 'validator', member: 'member', agent: 'the app\\u2019s agent' }),\n};\nexport type SharedLabels = typeof defaultSharedLabels;\n",
|
|
72
72
|
"type": "registry:lib",
|
|
73
73
|
"target": "components/shared-chat/shared-labels.ts"
|
|
74
74
|
},
|
package/r/task-list-viewer.json
CHANGED
|
@@ -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.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.16",
|
|
8
|
+
"@hachej/boring-files@^0.4.16",
|
|
9
9
|
"react@^19.0.0",
|
|
10
10
|
"react-dom@^19.0.0",
|
|
11
11
|
"zod@^4.6.5"
|
package/r/version-history.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
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.
|
|
7
|
+
"@hachej/boring-files@^0.4.16",
|
|
8
8
|
"react@^19.0.0"
|
|
9
9
|
],
|
|
10
10
|
"registryDependencies": [
|
|
11
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/theme.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/button.json",
|
|
13
13
|
"button"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
package/r/viewer-code.json
CHANGED
|
@@ -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.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/pi-chat.json",
|
|
13
13
|
"table"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
package/r/viewer-frame.json
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
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.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.16",
|
|
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.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
15
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/theme.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/utils.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/button.json",
|
|
16
16
|
"button",
|
|
17
17
|
"toggle",
|
|
18
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/version-history.json",
|
|
19
19
|
"toggle-group",
|
|
20
20
|
"dialog",
|
|
21
21
|
"textarea",
|
package/r/viewer-html.json
CHANGED
|
@@ -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.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.16",
|
|
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.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/utils.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/button.json",
|
|
15
15
|
"button"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
package/r/viewer-image.json
CHANGED
|
@@ -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.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/utils.json",
|
|
13
13
|
"button"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
package/r/viewer-markdown.json
CHANGED
|
@@ -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.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.16",
|
|
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.
|
|
26
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
27
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
28
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
25
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
26
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/markdown-editor.json",
|
|
27
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/utils.json",
|
|
28
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/button.json",
|
|
29
29
|
"button",
|
|
30
30
|
"dropdown-menu"
|
|
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.
|
|
7
|
+
"@hachej/boring-files@^0.4.16",
|
|
8
8
|
"react@^19.0.0",
|
|
9
9
|
"lucide-react@^1.51.0",
|
|
10
10
|
"pdfjs-dist@6.3.289"
|
|
11
11
|
],
|
|
12
12
|
"registryDependencies": [
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
14
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-image.json",
|
|
15
15
|
"badge",
|
|
16
16
|
"button"
|
|
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.
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
10
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
12
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
7
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-frame.json",
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-markdown.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-html.json",
|
|
10
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-image.json",
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-pdf.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.16/r/viewer-code.json"
|
|
13
13
|
],
|
|
14
14
|
"files": [],
|
|
15
15
|
"type": "registry:component"
|
package/r/windows.json
CHANGED