@hachej/boring-ui-kit 0.2.2 → 0.2.4

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.
@@ -8,13 +8,13 @@
8
8
  "react-dom@^19.0.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "@boring-ui/pi-chat",
12
- "@boring-ui/utils"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.4/r/pi-chat.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.4/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {
16
16
  "path": "registry/pi-workspace/workspace.tsx",
17
- "content": "'use client';\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\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: () => void;\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 readonly onClose: () => void;\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 /** Initial panel width in pixels, before the person drags the divider. */\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 /** 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 => { try { const value = Number(sessionStorage.getItem(key)); return Number.isFinite(value) && value > 0 ? value : undefined; } catch { return undefined; } };\nconst write = (key: string, value: number) => { try { sessionStorage.setItem(key, String(Math.round(value))); } catch { /* the width is a convenience */ } };\nconst readFlag = (key: string): boolean => { try { return sessionStorage.getItem(key) === '1'; } catch { return false; } };\nconst writeFlag = (key: string, on: boolean) => { try { if (on) sessionStorage.setItem(key, '1'); else sessionStorage.removeItem(key); } catch { /* the layout is a convenience */ } };\n\n/**\n * Chat with an artifact panel beside it, as in Claude's chat apps. The panel slides in from 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 = 640,\n minPanel = 360, minChat = 340, sheetBelow = 768, floatBelow, panelLabel = 'Artifact panel', labels, className }: ArtifactWorkspaceProps) {\n const root = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(0);\n const [stored, setStored] = useState<number>(() => read(storageKey) ?? defaultWidth);\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 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 // The panel slides in on the frame after it mounts.\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 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 const max = Math.max(minPanel, size - minChat);\n const width = Math.min(Math.max(stored, 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 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 backdrop-blur-[2px]\">\n <span className=\"rounded-full border border-border bg-popover px-4 py-2 text-sm font-medium text-popover-foreground shadow-lg\">{labels?.floatHint ?? 'Release to float the chat'}</span>\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.currentTarget.setPointerCapture(event.pointerId); setDragging(true); }} onPointerMove={drag}\n onPointerUp={release} onPointerCancel={() => { setDragging(false); setFloatHint(false); }}\n onKeyDown={keys} onDoubleClick={() => resize(defaultWidth)}\n className=\"group relative z-10 -mx-1 w-2 shrink-0 cursor-col-resize touch-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 transition-[width,background-color] group-hover:w-0.5 group-hover:bg-ring/70 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 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 bg-background text-foreground transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none',\n sheet || fullscreen ? 'fixed inset-0 z-50' : floating ? 'flex-1' : 'shrink-0 border-l border-border',\n entered ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0', dragging && 'pointer-events-none select-none')}>\n {typeof panel === 'function' ? panel(api) : panel}\n </aside>\n </>}\n </div>;\n}\n",
17
+ "content": "'use client';\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type { KeyboardEvent, PointerEvent, ReactNode } from 'react';\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: () => void;\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 readonly onClose: () => void;\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 /** Initial panel width in pixels, before the person drags the divider. */\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 /** 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 => { try { const value = Number(sessionStorage.getItem(key)); return Number.isFinite(value) && value > 0 ? value : undefined; } catch { return undefined; } };\nconst write = (key: string, value: number) => { try { sessionStorage.setItem(key, String(Math.round(value))); } catch { /* the width is a convenience */ } };\nconst readFlag = (key: string): boolean => { try { return sessionStorage.getItem(key) === '1'; } catch { return false; } };\nconst writeFlag = (key: string, on: boolean) => { try { if (on) sessionStorage.setItem(key, '1'); else sessionStorage.removeItem(key); } catch { /* the layout is a convenience */ } };\n\n/**\n * Chat with an artifact panel beside it, as in Claude's chat apps. The panel slides in from 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 = 640,\n minPanel = 360, minChat = 340, sheetBelow = 768, floatBelow, panelLabel = 'Artifact panel', labels, className }: ArtifactWorkspaceProps) {\n const root = useRef<HTMLDivElement>(null);\n const [size, setSize] = useState(0);\n const [stored, setStored] = useState<number>(() => read(storageKey) ?? defaultWidth);\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 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 // The panel slides in on the frame after it mounts.\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 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 const max = Math.max(minPanel, size - minChat);\n const width = Math.min(Math.max(stored, 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 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 backdrop-blur-[2px]\">\n <span className=\"rounded-full border border-border bg-popover px-4 py-2 text-sm font-medium text-popover-foreground shadow-lg\">{labels?.floatHint ?? 'Release to float the chat'}</span>\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.currentTarget.setPointerCapture(event.pointerId); setDragging(true); }} onPointerMove={drag}\n onPointerUp={release} onPointerCancel={() => { setDragging(false); setFloatHint(false); }}\n onKeyDown={keys} onDoubleClick={() => resize(defaultWidth)}\n className=\"group relative z-10 -mx-1 w-2 shrink-0 cursor-col-resize touch-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 transition-[width,background-color] group-hover:w-0.5 group-hover:bg-ring/70 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 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 bg-background text-foreground transition-[transform,opacity] motion-reduce:transition-none',\n sheet || fullscreen ? 'fixed inset-0 z-50' : floating ? 'flex-1' : 'shrink-0 border-l border-border',\n entered ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0', dragging && 'pointer-events-none select-none')}>\n {typeof panel === 'function' ? panel(api) : panel}\n </aside>\n </>}\n </div>;\n}\n",
18
18
  "type": "registry:component",
19
19
  "target": "components/pi-workspace/workspace.tsx"
20
20
  },
@@ -9,8 +9,8 @@
9
9
  "lucide-react@^1.51.0"
10
10
  ],
11
11
  "registryDependencies": [
12
- "@boring-ui/theme",
13
- "@boring-ui/utils"
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.4/r/theme.json",
13
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.4/r/utils.json"
14
14
  ],
15
15
  "files": [
16
16
  {