@hachej/boring-ui-kit 0.4.41 → 0.4.42
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 +18 -24
- package/r/board-viewer.json +6 -6
- package/r/button.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +13 -13
- package/r/feedback-preview.json +1 -1
- package/r/feedback.json +6 -6
- package/r/file-tree.json +4 -4
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +2 -2
- package/r/maquette-viewer.json +1 -1
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +8 -8
- package/r/pi-app-kinds.json +10 -10
- package/r/pi-app.json +13 -13
- package/r/pi-artifacts.json +2 -2
- package/r/pi-chat-approval.json +2 -2
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +2 -2
- 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 +2 -2
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +3 -3
- package/r/pi-chat.json +5 -5
- package/r/pi-workspace.json +4 -4
- package/r/placed-agent.json +4 -4
- package/r/pm-panel.json +42 -28
- package/r/provider-setup.json +1 -1
- package/r/registry.json +206 -194
- package/r/requests-board.json +1 -1
- package/r/shared-chat.json +6 -6
- package/r/task-board.json +2 -2
- package/r/task-list-viewer.json +2 -2
- package/r/task-mentions.json +4 -4
- package/r/utils.json +6 -0
- package/r/version-history.json +2 -2
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +6 -6
- 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/pi-chat-subagent.json
CHANGED
package/r/pi-chat-voice.json
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"title": "Pi chat voice messages",
|
|
5
5
|
"description": "Tap-to-record voice messages: a compatibility export of the kit's chat-react/voice recorder hook, the composer's record button and the pending voice bubble, plus useHoldToRecord. Plug it in with `voiceMessage={{ ...config, ...voiceMessagePlugin }}`; without it pi-chat has no voice button.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.42",
|
|
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.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
package/r/pi-chat.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"title": "Pi chat",
|
|
5
5
|
"description": "A complete Tailwind chat over the borrowed native chat controller: composer, messages, one collapsible activity block per turn (tool calls and reasoning), message queue, history and feedback cards for the opt-in feedback tool (lists, and Show offers that the host page resolves). Chat only: no layout. Owned, editable source; no AI SDK. The question, approval, sub-agent and artifact cards are separate items (pi-chat-question, pi-chat-approval, pi-chat-subagent, pi-artifacts); without them a question or an approval shows a \"waiting for your answer\" notice, a sub-agent call is an ordinary tool step and no artifact is offered. The optional pi-ambient and pi-workspace blocks build on it.",
|
|
6
6
|
"dependencies": [
|
|
7
|
-
"@hachej/boring-ui-kit@^0.4.
|
|
8
|
-
"@hachej/boring-files@^0.4.
|
|
9
|
-
"@hachej/boring-feedback@^0.4.
|
|
7
|
+
"@hachej/boring-ui-kit@^0.4.42",
|
|
8
|
+
"@hachej/boring-files@^0.4.42",
|
|
9
|
+
"@hachej/boring-feedback@^0.4.42",
|
|
10
10
|
"react@^19.0.0",
|
|
11
11
|
"react-dom@^19.0.0",
|
|
12
12
|
"@base-ui/react@1.9.0",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"zod@^4.6.5"
|
|
20
20
|
],
|
|
21
21
|
"registryDependencies": [
|
|
22
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
23
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
22
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json",
|
|
23
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/button.json"
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
{
|
package/r/pi-workspace.json
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"react-dom@^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.
|
|
13
|
-
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-chat.json",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/dock.json"
|
|
14
14
|
],
|
|
15
15
|
"files": [
|
|
16
16
|
{
|
|
17
17
|
"path": "registry/pi-workspace/workspace.tsx",
|
|
18
|
-
"content": "'use client';\n\nimport { PHONE_BELOW } from '../utils/use-media';\nimport { readFlag, writeFlag, readPersisted, writePersisted } from '../utils/persisted';\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Card } from '@/components/ui/card';\nimport { cn } from '../utils/utils';\n\n/** What the open panel can do to its workspace. Pass `onFullscreenChange` on to the viewer's top bar; it is absent in the phone sheet. */\nexport interface WorkspacePanelApi {\n readonly fullscreen: boolean;\n /** Absent on a phone, where the panel is already a full-screen sheet. */\n readonly onFullscreenChange: ((next: boolean) => void) | undefined;\n readonly close: () => unknown;\n readonly sheet: boolean;\n /** Float the chat over the panel (see `floatBelow`). Absent without `floatBelow`, while the chat already floats and in the phone sheet. pi-app's `AgentWorkspace` turns it into the viewers' \"Float chat\" action (`ViewerWindow.actions`). */\n readonly floatChat: (() => void) | undefined;\n}\n\n/** What `chat` can render as a function: whether the chat floats over the full-width panel, and how to put it back beside it. */\nexport interface WorkspaceChatLayout {\n readonly floating: boolean;\n readonly dock: () => void;\n}\n\nexport interface ArtifactWorkspaceProps {\n /**\n * Usually a `PiChat`. It stays mounted while the panel opens, resizes, goes full screen and closes. With `floatBelow`, pass a function that\n * returns the docked chat or, while `floating`, an `AmbientChat` on the same controller (`onDock` there is `dock`): the chat is then a different surface over one session.\n */\n readonly chat: ReactNode | ((layout: WorkspaceChatLayout) => ReactNode);\n /** Whether the panel is open. Closing is the host's decision (`onClose`); the workspace never unmounts `chat`. */\n readonly open: boolean;\n /** The panel content, usually a viewer whose top bar is the shared `ViewerFrame`. Receives what it needs for its close and full screen buttons. */\n readonly panel: ReactNode | ((api: WorkspacePanelApi) => ReactNode);\n /** Return false when a synchronous host guard refuses to close; Back then restores the sheet entry. */\n readonly onClose: () => unknown;\n /** Full screen is controlled when both are given; otherwise the workspace keeps it. Escape leaves it. */\n readonly fullscreen?: boolean;\n readonly onFullscreenChange?: (next: boolean) => void;\n /** Width of the panel is remembered for this browser session under this key. */\n readonly storageKey?: string;\n /**\n * Initial panel width before the person drags the divider: pixels, or a fraction of the workspace's width when at most 1 (0.55: a bit\n * over half). Without it the panel takes all but 420px of chat. Double-clicking the divider comes back to it.\n */\n readonly defaultWidth?: number;\n readonly minPanel?: number;\n readonly minChat?: number;\n /** Below this container width the panel is a full-screen sheet and the divider is gone. */\n readonly sheetBelow?: number;\n /**\n * Opt in to a floating chat: when the divider is dragged until the chat is narrower than this many pixels, a hint appears and releasing floats the chat\n * (the panel takes the full width). Alt+Left on the divider and `api.floatChat` do the same without a pointer. Remembered for the session; ignored in the phone sheet.\n * Off by default.\n */\n readonly floatBelow?: number;\n /** The panel's accessible name. */\n readonly panelLabel?: string;\n /**\n * `split` (default): the panel is a full-height column with a hard border and a visible divider. `inset`: the panel is a rounded card inside the\n * workspace (a margin around it, a border and a soft shadow on the card only) and the divider remains visible and strengthens on hover, focus or drag, so the panel\n * reads as part of the chat's own surface, as in Claude's artifact panel. Resizing works the same.\n */\n readonly variant?: 'split' | 'inset';\n /** The divider's accessible name and the hint shown while a drag would float the chat. */\n readonly labels?: { readonly resize?: string | undefined; readonly floatHint?: string | undefined } | undefined;\n readonly className?: string;\n}\n\nconst STEP = 24, BIG_STEP = 96;\nconst read = (key: string): number | undefined => { const value = Number(readPersisted(key)); return Number.isFinite(value) && value > 0 ? value : undefined; };\nconst write = (key: string, value: number) => writePersisted(key, String(Math.round(value)));\n\n/**\n * Chat with an artifact panel beside it, as in Claude's chat apps. The panel opens on the right at full height and the chat\n * narrows; a divider (pointer or arrow keys) resizes it, the width is remembered for the session; the panel can go full screen\n * (Escape leaves) and on a phone it is a full-screen sheet. With `floatBelow` the chat can float over a full-width panel instead of sitting beside it. The workspace owns layout only: what is open, and the viewer in it, are the host's.\n */\nexport function ArtifactWorkspace({ chat, open, panel, onClose, fullscreen: controlledFullscreen, onFullscreenChange, storageKey = 'boring.artifact-panel.width', defaultWidth,\n minPanel = 360, minChat = 360, sheetBelow = PHONE_BELOW, floatBelow, panelLabel = 'Artifact panel', variant = 'split', labels, className }: ArtifactWorkspaceProps) {\n const root = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(0);\n const [stored, setStored] = useState<number | undefined>(() => read(storageKey) ?? (defaultWidth !== undefined && defaultWidth > 1 ? defaultWidth : undefined));\n const closeRef = useRef(onClose);\n const fullscreenRef = useRef(false);\n const opener = useRef<HTMLElement | null>(null);\n const swipe = useRef<{ x: number; y: number } | null>(null);\n const [local, setLocal] = useState(false);\n const [entered, setEntered] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [floatHint, setFloatHint] = useState(false);\n const [floatState, setFloatState] = useState(() => floatBelow !== undefined && readFlag(`${storageKey}.floating`));\n const controlled = controlledFullscreen !== undefined && onFullscreenChange !== undefined;\n const sheet = size > 0 && size < sheetBelow;\n const floating = open && !sheet && floatBelow !== undefined && floatState;\n const fullscreen = open && !sheet && (controlled ? controlledFullscreen : local);\n useLayoutEffect(() => { closeRef.current = onClose; fullscreenRef.current = fullscreen; });\n const setFullscreen = useCallback((next: boolean) => { if (controlled) onFullscreenChange!(next); else setLocal(next); }, [controlled, onFullscreenChange]);\n\n useLayoutEffect(() => {\n const element = root.current;\n if (!element) return;\n const measure = () => setSize(element.getBoundingClientRect().width);\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n const setFloating = useCallback((next: boolean) => { setFloatState(next); setFloatHint(false); writeFlag(`${storageKey}.floating`, next); }, [storageKey]);\n useEffect(() => {\n // With the panel closed there is nothing to float over: the chat docks back.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- closing resets the panel's entry, fullscreen and floating once, after commit\n if (!open) { setEntered(false); setLocal(false); setFloating(false); return; }\n const frame = requestAnimationFrame(() => setEntered(true));\n return () => cancelAnimationFrame(frame);\n }, [open, setFloating]);\n useEffect(() => {\n if (!fullscreen) return;\n const escape = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape' && !event.defaultPrevented) setFullscreen(false); };\n document.addEventListener('keydown', escape);\n return () => document.removeEventListener('keydown', escape);\n }, [fullscreen, setFullscreen]);\n\n useEffect(() => {\n if (!open) return;\n const element = root.current;\n opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const escape = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented || fullscreenRef.current) return;\n event.preventDefault(); closeRef.current();\n };\n document.addEventListener('keydown', escape);\n return () => {\n document.removeEventListener('keydown', escape);\n const previous = opener.current;\n const target = previous?.isConnected && previous !== document.body ? previous : element?.querySelector<HTMLElement>('[data-testid=composer-input]');\n target?.focus({ preventScroll: true });\n // A shadcn Button transitions its inherited visibility, so it stays hidden (unfocusable) briefly after the sheet closes: retry per frame.\n if (target) { let frames = 30; const retry = () => { if (document.activeElement === target || document.activeElement !== document.body || frames-- <= 0) return; target.focus({ preventScroll: true }); requestAnimationFrame(retry); }; requestAnimationFrame(retry); }\n };\n }, [open]);\n useEffect(() => {\n if (!open || !sheet) return;\n const key = `artifact-${Math.random()}`;\n const history = window.history;\n const enter = () => history.pushState({ ...history.state, boringArtifact: key }, '');\n enter();\n const back = () => {\n if (history.state?.boringArtifact === key) return;\n void Promise.resolve(closeRef.current()).then(closed => { if (closed === false) enter(); });\n };\n window.addEventListener('popstate', back);\n return () => { window.removeEventListener('popstate', back); if (history.state?.boringArtifact === key) history.back(); };\n }, [open, sheet]); // The mounted sheet keeps its own Back entry, independent of renders.\n\n const max = Math.max(minPanel, size - minChat);\n const initial = defaultWidth === undefined ? size - 420 : defaultWidth <= 1 ? size * defaultWidth : defaultWidth;\n const width = Math.min(Math.max(stored ?? Math.max(minPanel, initial), minPanel), max);\n const resize = (next: number) => { const clamped = Math.min(Math.max(next, minPanel), max); setStored(clamped); write(storageKey, clamped); };\n const drag = (event: PointerEvent<HTMLElement>) => {\n if (!dragging) return;\n const box = root.current?.getBoundingClientRect();\n if (!box) return;\n const target = box.right - event.clientX;\n // Past the threshold the width stays at the last one above it; releasing there floats the chat.\n const below = floatBelow !== undefined && size - target < floatBelow;\n setFloatHint(below);\n if (!below) resize(target);\n };\n const release = (event: PointerEvent<HTMLElement>) => { event.currentTarget.releasePointerCapture(event.pointerId); setDragging(false); if (floatHint) setFloating(true); setFloatHint(false); };\n const keys = (event: KeyboardEvent<HTMLElement>) => {\n if (floatBelow !== undefined && event.altKey && event.key === 'ArrowLeft') { event.preventDefault(); setFloating(true); return; }\n const step = event.shiftKey ? BIG_STEP : STEP;\n const next = event.key === 'ArrowLeft' ? width + step : event.key === 'ArrowRight' ? width - step : event.key === 'Home' ? max : event.key === 'End' ? minPanel : undefined;\n if (next === undefined) return;\n event.preventDefault(); resize(next);\n };\n\n const api: WorkspacePanelApi = { fullscreen, onFullscreenChange: sheet ? undefined : setFullscreen, close: onClose, sheet, floatChat: floatBelow !== undefined && !sheet && !floating ? () => setFloating(true) : undefined };\n const covering = open && (sheet || fullscreen);\n // The inset panel is drawn by the host's Card behind it (a sibling layer, so a placement change never remounts the panel);\n // every other placement is a plain region of the page.\n const inset = variant === 'inset' && !sheet && !fullscreen && !floating;\n return <div ref={root} data-boring=\"artifact-workspace\" data-open={open ? 'true' : 'false'} data-fullscreen={fullscreen ? 'true' : 'false'} data-sheet={sheet && open ? 'true' : 'false'}\n className={cn('relative flex h-full min-h-0 min-w-0 flex-1', className)}>\n <div data-testid=\"workspace-chat\" data-floating={floating ? 'true' : undefined} {...(covering ? { inert: true, 'aria-hidden': true } : {})}\n className={cn('relative flex min-h-0 min-w-0 flex-col', floating ? 'w-0 flex-none' : 'flex-1', covering && 'invisible')}>\n {typeof chat === 'function' ? chat({ floating, dock: () => setFloating(false) }) : chat}\n {floatHint && <div role=\"status\" data-testid=\"workspace-float-hint\" className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/75 p-4\">\n <Badge variant=\"secondary\">{labels?.floatHint ?? 'Release to float the chat'}</Badge>\n </div>}\n </div>\n {open && <>\n {!sheet && !fullscreen && !floating && <div role=\"separator\" tabIndex={0} aria-orientation=\"vertical\" aria-label={labels?.resize ?? 'Resize artifact panel'} aria-valuemin={minPanel} aria-valuemax={max} aria-valuenow={Math.round(width)}\n data-testid=\"workspace-divider\" data-dragging={dragging ? 'true' : undefined}\n onPointerDown={event => { event.preventDefault(); event.currentTarget.focus(); event.currentTarget.setPointerCapture(event.pointerId); setDragging(true); }} onPointerMove={drag}\n onPointerUp={release} onPointerCancel={() => { setDragging(false); setFloatHint(false); }}\n onKeyDown={keys} onDoubleClick={() => resize(initial)}\n className=\"group relative z-10 -mx-1.5 w-3 shrink-0 cursor-col-resize touch-none select-none outline-none\">\n <span aria-hidden=\"true\" className={cn('absolute inset-y-0 left-1/2 w-px -translate-x-1/2', 'bg-border/65', 'transition-colors group-hover:w-0.5 group-hover:bg-ring group-focus-visible:w-0.5 group-focus-visible:bg-ring motion-reduce:transition-none', dragging && 'w-0.5 bg-ring')} />\n </div>}\n <aside onTouchStart={event => { const point = event.touches[0]; swipe.current = sheet && point && point.clientX < 32 ? { x: point.clientX, y: point.clientY } : null; }} onTouchEnd={event => { const point = event.changedTouches[0], start = swipe.current; swipe.current = null; if (point && start && point.clientX - start.x > 80 && Math.abs(point.clientY - start.y) < 60) onClose(); }} data-testid=\"workspace-panel\" aria-label={panelLabel} data-state={entered ? 'open' : 'entering'} data-floating={floating ? 'true' : undefined} data-fullscreen={fullscreen ? 'true' : 'false'} data-sheet={sheet ? 'true' : 'false'}\n style={sheet || fullscreen || floating ? undefined : { width, minWidth: minPanel }}\n className={cn('flex min-h-0 min-w-0 flex-col overflow-hidden transition-opacity motion-reduce:transition-none', !inset && 'bg-background text-foreground',\n sheet || fullscreen ? 'fixed inset-0 z-50' : floating ? 'flex-1' : inset ? 'relative isolate my-3 me-3 ms-0 shrink-0' : 'shrink-0',\n entered ? 'opacity-100' : 'opacity-0', dragging && 'pointer-events-none select-none')}>\n {inset && <Card aria-hidden=\"true\" data-boring=\"chrome\" className=\"pointer-events-none absolute inset-0 -z-10\" />}\n {typeof panel === 'function' ? panel(api) : panel}\n </aside>\n </>}\n </div>;\n}\n",
|
|
18
|
+
"content": "'use client';\n\nimport { PHONE_BELOW } from '../utils/use-media';\nimport { useBackLayer } from '../utils/back';\nimport { readFlag, writeFlag, readPersisted, writePersisted } from '../utils/persisted';\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Card } from '@/components/ui/card';\nimport { cn } from '../utils/utils';\n\n/** What the open panel can do to its workspace. Pass `onFullscreenChange` on to the viewer's top bar; it is absent in the phone sheet. */\nexport interface WorkspacePanelApi {\n readonly fullscreen: boolean;\n /** Absent on a phone, where the panel is already a full-screen sheet. */\n readonly onFullscreenChange: ((next: boolean) => void) | undefined;\n readonly close: () => unknown;\n readonly sheet: boolean;\n /** Float the chat over the panel (see `floatBelow`). Absent without `floatBelow`, while the chat already floats and in the phone sheet. pi-app's `AgentWorkspace` turns it into the viewers' \"Float chat\" action (`ViewerWindow.actions`). */\n readonly floatChat: (() => void) | undefined;\n}\n\n/** What `chat` can render as a function: whether the chat floats over the full-width panel, and how to put it back beside it. */\nexport interface WorkspaceChatLayout {\n readonly floating: boolean;\n readonly dock: () => void;\n}\n\nexport interface ArtifactWorkspaceProps {\n /**\n * Usually a `PiChat`. It stays mounted while the panel opens, resizes, goes full screen and closes. With `floatBelow`, pass a function that\n * returns the docked chat or, while `floating`, an `AmbientChat` on the same controller (`onDock` there is `dock`): the chat is then a different surface over one session.\n */\n readonly chat: ReactNode | ((layout: WorkspaceChatLayout) => ReactNode);\n /** Whether the panel is open. Closing is the host's decision (`onClose`); the workspace never unmounts `chat`. */\n readonly open: boolean;\n /** The panel content, usually a viewer whose top bar is the shared `ViewerFrame`. Receives what it needs for its close and full screen buttons. */\n readonly panel: ReactNode | ((api: WorkspacePanelApi) => ReactNode);\n /** Return false when a synchronous host guard refuses to close; Back then restores the sheet entry. */\n readonly onClose: () => unknown;\n /** Full screen is controlled when both are given; otherwise the workspace keeps it. Escape leaves it. */\n readonly fullscreen?: boolean;\n readonly onFullscreenChange?: (next: boolean) => void;\n /** Width of the panel is remembered for this browser session under this key. */\n readonly storageKey?: string;\n /**\n * Initial panel width before the person drags the divider: pixels, or a fraction of the workspace's width when at most 1 (0.55: a bit\n * over half). Without it the panel takes all but 420px of chat. Double-clicking the divider comes back to it.\n */\n readonly defaultWidth?: number;\n readonly minPanel?: number;\n readonly minChat?: number;\n /** Below this container width the panel is a full-screen sheet and the divider is gone. */\n readonly sheetBelow?: number;\n /**\n * Opt in to a floating chat: when the divider is dragged until the chat is narrower than this many pixels, a hint appears and releasing floats the chat\n * (the panel takes the full width). Alt+Left on the divider and `api.floatChat` do the same without a pointer. Remembered for the session; ignored in the phone sheet.\n * Off by default.\n */\n readonly floatBelow?: number;\n /** The panel's accessible name. */\n readonly panelLabel?: string;\n /**\n * `split` (default): the panel is a full-height column with a hard border and a visible divider. `inset`: the panel is a rounded card inside the\n * workspace (a margin around it, a border and a soft shadow on the card only) and the divider remains visible and strengthens on hover, focus or drag, so the panel\n * reads as part of the chat's own surface, as in Claude's artifact panel. Resizing works the same.\n */\n readonly variant?: 'split' | 'inset';\n /** The divider's accessible name and the hint shown while a drag would float the chat. */\n readonly labels?: { readonly resize?: string | undefined; readonly floatHint?: string | undefined } | undefined;\n readonly className?: string;\n}\n\nconst STEP = 24, BIG_STEP = 96;\nconst read = (key: string): number | undefined => { const value = Number(readPersisted(key)); return Number.isFinite(value) && value > 0 ? value : undefined; };\nconst write = (key: string, value: number) => writePersisted(key, String(Math.round(value)));\n\n/**\n * Chat with an artifact panel beside it, as in Claude's chat apps. The panel opens on the right at full height and the chat\n * narrows; a divider (pointer or arrow keys) resizes it, the width is remembered for the session; the panel can go full screen\n * (Escape leaves) and on a phone it is a full-screen sheet. With `floatBelow` the chat can float over a full-width panel instead of sitting beside it. The workspace owns layout only: what is open, and the viewer in it, are the host's.\n */\nexport function ArtifactWorkspace({ chat, open, panel, onClose, fullscreen: controlledFullscreen, onFullscreenChange, storageKey = 'boring.artifact-panel.width', defaultWidth,\n minPanel = 360, minChat = 360, sheetBelow = PHONE_BELOW, floatBelow, panelLabel = 'Artifact panel', variant = 'split', labels, className }: ArtifactWorkspaceProps) {\n const root = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(0);\n const [stored, setStored] = useState<number | undefined>(() => read(storageKey) ?? (defaultWidth !== undefined && defaultWidth > 1 ? defaultWidth : undefined));\n const closeRef = useRef(onClose);\n const fullscreenRef = useRef(false);\n const opener = useRef<HTMLElement | null>(null);\n const swipe = useRef<{ x: number; y: number } | null>(null);\n const [local, setLocal] = useState(false);\n const [entered, setEntered] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [floatHint, setFloatHint] = useState(false);\n const [floatState, setFloatState] = useState(() => floatBelow !== undefined && readFlag(`${storageKey}.floating`));\n const controlled = controlledFullscreen !== undefined && onFullscreenChange !== undefined;\n const sheet = size > 0 && size < sheetBelow;\n const floating = open && !sheet && floatBelow !== undefined && floatState;\n const fullscreen = open && !sheet && (controlled ? controlledFullscreen : local);\n useLayoutEffect(() => { closeRef.current = onClose; fullscreenRef.current = fullscreen; });\n const setFullscreen = useCallback((next: boolean) => { if (controlled) onFullscreenChange!(next); else setLocal(next); }, [controlled, onFullscreenChange]);\n\n useLayoutEffect(() => {\n const element = root.current;\n if (!element) return;\n const measure = () => setSize(element.getBoundingClientRect().width);\n measure();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(measure);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n const setFloating = useCallback((next: boolean) => { setFloatState(next); setFloatHint(false); writeFlag(`${storageKey}.floating`, next); }, [storageKey]);\n useEffect(() => {\n // With the panel closed there is nothing to float over: the chat docks back.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- closing resets the panel's entry, fullscreen and floating once, after commit\n if (!open) { setEntered(false); setLocal(false); setFloating(false); return; }\n const frame = requestAnimationFrame(() => setEntered(true));\n return () => cancelAnimationFrame(frame);\n }, [open, setFloating]);\n useEffect(() => {\n if (!fullscreen) return;\n const escape = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape' && !event.defaultPrevented) setFullscreen(false); };\n document.addEventListener('keydown', escape);\n return () => document.removeEventListener('keydown', escape);\n }, [fullscreen, setFullscreen]);\n\n useEffect(() => {\n if (!open) return;\n const element = root.current;\n opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const escape = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape' || event.defaultPrevented || fullscreenRef.current) return;\n event.preventDefault(); closeRef.current();\n };\n document.addEventListener('keydown', escape);\n return () => {\n document.removeEventListener('keydown', escape);\n const previous = opener.current;\n const target = previous?.isConnected && previous !== document.body ? previous : element?.querySelector<HTMLElement>('[data-testid=composer-input]');\n target?.focus({ preventScroll: true });\n // A shadcn Button transitions its inherited visibility, so it stays hidden (unfocusable) briefly after the sheet closes: retry per frame.\n if (target) { let frames = 30; const retry = () => { if (document.activeElement === target || document.activeElement !== document.body || frames-- <= 0) return; target.focus({ preventScroll: true }); requestAnimationFrame(retry); }; requestAnimationFrame(retry); }\n };\n }, [open]);\n useBackLayer(open && sheet, onClose, 'artifact');\n\n const max = Math.max(minPanel, size - minChat);\n const initial = defaultWidth === undefined ? size - 420 : defaultWidth <= 1 ? size * defaultWidth : defaultWidth;\n const width = Math.min(Math.max(stored ?? Math.max(minPanel, initial), minPanel), max);\n const resize = (next: number) => { const clamped = Math.min(Math.max(next, minPanel), max); setStored(clamped); write(storageKey, clamped); };\n const drag = (event: PointerEvent<HTMLElement>) => {\n if (!dragging) return;\n const box = root.current?.getBoundingClientRect();\n if (!box) return;\n const target = box.right - event.clientX;\n // Past the threshold the width stays at the last one above it; releasing there floats the chat.\n const below = floatBelow !== undefined && size - target < floatBelow;\n setFloatHint(below);\n if (!below) resize(target);\n };\n const release = (event: PointerEvent<HTMLElement>) => { event.currentTarget.releasePointerCapture(event.pointerId); setDragging(false); if (floatHint) setFloating(true); setFloatHint(false); };\n const keys = (event: KeyboardEvent<HTMLElement>) => {\n if (floatBelow !== undefined && event.altKey && event.key === 'ArrowLeft') { event.preventDefault(); setFloating(true); return; }\n const step = event.shiftKey ? BIG_STEP : STEP;\n const next = event.key === 'ArrowLeft' ? width + step : event.key === 'ArrowRight' ? width - step : event.key === 'Home' ? max : event.key === 'End' ? minPanel : undefined;\n if (next === undefined) return;\n event.preventDefault(); resize(next);\n };\n\n const api: WorkspacePanelApi = { fullscreen, onFullscreenChange: sheet ? undefined : setFullscreen, close: onClose, sheet, floatChat: floatBelow !== undefined && !sheet && !floating ? () => setFloating(true) : undefined };\n const covering = open && (sheet || fullscreen);\n // The inset panel is drawn by the host's Card behind it (a sibling layer, so a placement change never remounts the panel);\n // every other placement is a plain region of the page.\n const inset = variant === 'inset' && !sheet && !fullscreen && !floating;\n return <div ref={root} data-boring=\"artifact-workspace\" data-open={open ? 'true' : 'false'} data-fullscreen={fullscreen ? 'true' : 'false'} data-sheet={sheet && open ? 'true' : 'false'}\n className={cn('relative flex h-full min-h-0 min-w-0 flex-1', className)}>\n <div data-testid=\"workspace-chat\" data-floating={floating ? 'true' : undefined} {...(covering ? { inert: true, 'aria-hidden': true } : {})}\n className={cn('relative flex min-h-0 min-w-0 flex-col', floating ? 'w-0 flex-none' : 'flex-1', covering && 'invisible')}>\n {typeof chat === 'function' ? chat({ floating, dock: () => setFloating(false) }) : chat}\n {floatHint && <div role=\"status\" data-testid=\"workspace-float-hint\" className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/75 p-4\">\n <Badge variant=\"secondary\">{labels?.floatHint ?? 'Release to float the chat'}</Badge>\n </div>}\n </div>\n {open && <>\n {!sheet && !fullscreen && !floating && <div role=\"separator\" tabIndex={0} aria-orientation=\"vertical\" aria-label={labels?.resize ?? 'Resize artifact panel'} aria-valuemin={minPanel} aria-valuemax={max} aria-valuenow={Math.round(width)}\n data-testid=\"workspace-divider\" data-dragging={dragging ? 'true' : undefined}\n onPointerDown={event => { event.preventDefault(); event.currentTarget.focus(); event.currentTarget.setPointerCapture(event.pointerId); setDragging(true); }} onPointerMove={drag}\n onPointerUp={release} onPointerCancel={() => { setDragging(false); setFloatHint(false); }}\n onKeyDown={keys} onDoubleClick={() => resize(initial)}\n className=\"group relative z-10 -mx-1.5 w-3 shrink-0 cursor-col-resize touch-none select-none outline-none\">\n <span aria-hidden=\"true\" className={cn('absolute inset-y-0 left-1/2 w-px -translate-x-1/2', 'bg-border/65', 'transition-colors group-hover:w-0.5 group-hover:bg-ring group-focus-visible:w-0.5 group-focus-visible:bg-ring motion-reduce:transition-none', dragging && 'w-0.5 bg-ring')} />\n </div>}\n <aside onTouchStart={event => { const point = event.touches[0]; swipe.current = sheet && point && point.clientX < 32 ? { x: point.clientX, y: point.clientY } : null; }} onTouchEnd={event => { const point = event.changedTouches[0], start = swipe.current; swipe.current = null; if (point && start && point.clientX - start.x > 80 && Math.abs(point.clientY - start.y) < 60) onClose(); }} data-testid=\"workspace-panel\" aria-label={panelLabel} data-state={entered ? 'open' : 'entering'} data-floating={floating ? 'true' : undefined} data-fullscreen={fullscreen ? 'true' : 'false'} data-sheet={sheet ? 'true' : 'false'}\n style={sheet || fullscreen || floating ? undefined : { width, minWidth: minPanel }}\n className={cn('flex min-h-0 min-w-0 flex-col overflow-hidden transition-opacity motion-reduce:transition-none', !inset && 'bg-background text-foreground',\n sheet ? (size >= window.innerWidth - 1 ? 'fixed inset-0 z-50' : 'absolute inset-0 z-50') : fullscreen ? 'fixed inset-0 z-50' : floating ? 'flex-1' : inset ? 'relative isolate my-3 me-3 ms-0 shrink-0' : 'shrink-0',\n entered ? 'opacity-100' : 'opacity-0', dragging && 'pointer-events-none select-none')}>\n {inset && <Card aria-hidden=\"true\" data-boring=\"chrome\" className=\"pointer-events-none absolute inset-0 -z-10\" />}\n {typeof panel === 'function' ? panel(api) : panel}\n </aside>\n </>}\n </div>;\n}\n",
|
|
19
19
|
"type": "registry:component",
|
|
20
20
|
"target": "components/pi-workspace/workspace.tsx"
|
|
21
21
|
},
|
package/r/placed-agent.json
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
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.
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-app.json",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/pi-ambient.json",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/app-shell.json",
|
|
15
|
+
"https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.4.42/r/utils.json"
|
|
16
16
|
],
|
|
17
17
|
"files": [
|
|
18
18
|
{
|