@hachej/boring-ui-kit 0.2.2 → 0.2.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-ui-kit",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -110,8 +110,8 @@
110
110
  "./file-tree.css": "./styles/file-tree.css"
111
111
  },
112
112
  "peerDependencies": {
113
- "@hachej/boring-files": "^0.2.2",
114
- "@earendil-works/pi-durable": "^1.1.0",
113
+ "@hachej/boring-files": "^0.2.3",
114
+ "@earendil-works/pi-durable": "~1.1.0",
115
115
  "react": "^19.0.0",
116
116
  "react-dom": "^19.0.0",
117
117
  "marked": "^17.0.6",
@@ -127,8 +127,8 @@
127
127
  "@tiptap/extension-highlight": "^3.31.4",
128
128
  "@tiptap/extension-bubble-menu": "^3.31.4",
129
129
  "@tiptap/extension-floating-menu": "^3.31.4",
130
- "@earendil-works/chord": "^1.1.0",
131
- "@earendil-works/pi-ai": "^1.1.0",
130
+ "@earendil-works/chord": "~1.1.0",
131
+ "@earendil-works/pi-ai": "~1.1.0",
132
132
  "tldraw": "^5.5.2",
133
133
  "@tldraw/editor": "^5.5.2",
134
134
  "@tldraw/store": "^5.5.2",
package/r/button.json CHANGED
@@ -8,8 +8,8 @@
8
8
  "lucide-react@^1.51.0"
9
9
  ],
10
10
  "registryDependencies": [
11
- "@boring-ui/theme",
12
- "@boring-ui/utils"
11
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.3/r/theme.json",
12
+ "https://cdn.jsdelivr.net/npm/@hachej/boring-ui-kit@0.2.3/r/utils.json"
13
13
  ],
14
14
  "files": [
15
15
  {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  {
22
22
  "path": "registry/button/actions.tsx",
23
- "content": "'use client';\n\nimport { useEffect, useId, useRef, useState } from 'react';\nimport type { ComponentType, KeyboardEvent, ReactNode } from 'react';\nimport { CheckIcon, EllipsisIcon } from 'lucide-react';\nimport { Button, IconButton } from './button';\nimport { cn } from '../utils/utils';\n\n/** An icon the host may pass for any of the blocks' icons: any component that takes a class name (every lucide icon does). */\nexport type BlockIcon = ComponentType<{ readonly className?: string | undefined; readonly 'aria-hidden'?: boolean | 'true' | 'false' | undefined }>;\n\n/**\n * A host action added to a block's bar or row (the chat header, the floating chat's bar, the artifact panel, a conversation row, a reply),\n * next to the block's own buttons. `header` (the default) is a button in the bar: icon only when `icon` is given (the label is then its\n * accessible name and tooltip), otherwise the label. `menu` puts it in the block's \"…\" menu. Its test id is `<surface>-<id>` (see each block).\n */\nexport interface BlockAction {\n readonly id: string;\n readonly label: string;\n readonly icon?: BlockIcon | undefined;\n readonly onSelect: () => void;\n readonly placement?: 'header' | 'menu' | undefined;\n readonly disabled?: boolean | undefined;\n}\n\n/** One row of a menu. With `checked` set (true or false) the row is a radio choice, otherwise a plain action. */\nexport interface MenuItem {\n readonly id: string;\n readonly label: string;\n readonly icon?: ReactNode;\n /** Small text after the label, for example \"Latest\". */\n readonly hint?: string;\n readonly checked?: boolean;\n readonly disabled?: boolean;\n readonly testId?: string;\n readonly onSelect: () => void;\n}\n\nexport interface ActionMenuProps {\n /** Accessible name and tooltip of the trigger; also the name of the menu. */\n readonly label: string;\n readonly icon: ReactNode;\n readonly items: readonly MenuItem[];\n readonly testId?: string;\n /** Which edge of the trigger the menu lines up with. */\n readonly align?: 'start' | 'end';\n /** The trigger's size: the viewer bar's (default) or a small chat button. */\n readonly size?: 'icon-bar' | 'icon-sm';\n readonly className?: string;\n}\n\nconst ITEM = 'flex h-9 w-full cursor-pointer items-center gap-2 rounded-md px-2.5 text-left text-sm outline-none hover:bg-muted focus-visible:bg-muted disabled:pointer-events-none disabled:opacity-45 max-sm:h-11 pointer-coarse:h-11';\n\n/**\n * An icon button that opens a small menu: arrow keys, Home/End and roving focus, Escape (focus returns to the trigger), Tab and an outside\n * press close it. The one menu of the blocks' \"…\" buttons and the viewers' version history.\n */\nexport function ActionMenu({ label, icon, items, testId = 'menu', align = 'end', size = 'icon-bar', className }: ActionMenuProps) {\n const [open, setOpen] = useState(false);\n const root = useRef<HTMLDivElement>(null);\n const trigger = useRef<HTMLButtonElement>(null);\n const menuId = useId();\n const radio = items.some(item => item.checked !== undefined);\n\n useEffect(() => {\n if (!open) return;\n const list = root.current?.querySelectorAll<HTMLElement>('[role^=menuitem]:not(:disabled)');\n const wanted = root.current?.querySelector<HTMLElement>('[aria-checked=true]:not(:disabled)') ?? list?.[0];\n wanted?.focus();\n const away = (event: Event) => { if (!(event.target instanceof Node) || !root.current?.contains(event.target)) setOpen(false); };\n document.addEventListener('pointerdown', away);\n return () => document.removeEventListener('pointerdown', away);\n }, [open]);\n\n const close = (refocus: boolean) => { setOpen(false); if (refocus) trigger.current?.focus(); };\n const keys = (event: KeyboardEvent<HTMLElement>) => {\n const rows = [...(root.current?.querySelectorAll<HTMLElement>('[role^=menuitem]:not(:disabled)') ?? [])];\n const at = rows.indexOf(document.activeElement as HTMLElement);\n const go = (index: number) => { event.preventDefault(); rows[(index + rows.length) % rows.length]?.focus(); };\n if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(true); }\n else if (event.key === 'Tab') close(false);\n else if (event.key === 'ArrowDown') go(at + 1);\n else if (event.key === 'ArrowUp') go(at < 0 ? -1 : at - 1);\n else if (event.key === 'Home') go(0);\n else if (event.key === 'End') go(-1);\n };\n\n return <div ref={root} data-viewer-menu className={cn('relative', className)} onKeyDown={event => { if (open) keys(event); }}>\n <IconButton ref={trigger} label={label} size={size} data-testid={testId} aria-haspopup=\"menu\" aria-expanded={open} aria-controls={open ? menuId : undefined}\n onClick={() => setOpen(value => !value)}\n onKeyDown={event => { if (!open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { event.preventDefault(); setOpen(true); } }}>{icon}</IconButton>\n {open && <div id={menuId} role=\"menu\" aria-label={label} data-testid={`${testId}-list`}\n className={cn('absolute top-full z-50 mt-1 grid max-h-72 min-w-48 gap-0.5 overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg', 'max-sm:fixed max-sm:inset-x-2 max-sm:top-auto max-sm:bottom-2 max-sm:mt-0 max-sm:max-h-[70vh] max-sm:rounded-xl max-sm:p-1.5', align === 'end' ? 'right-0' : 'left-0')}>\n {items.map(item => <button key={item.id} type=\"button\" tabIndex={-1} role={radio ? 'menuitemradio' : 'menuitem'} {...(radio ? { 'aria-checked': item.checked === true } : {})}\n disabled={item.disabled} data-testid={item.testId ?? `${testId}-${item.id}`} data-item={item.id} className={ITEM}\n onClick={() => { close(true); item.onSelect(); }}>\n {item.icon ?? (radio ? <span className=\"size-4 shrink-0\">{item.checked && <CheckIcon className=\"size-4\" aria-hidden=\"true\" />}</span> : null)}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground\">{item.hint}</span>}\n </button>)}\n </div>}\n </div>;\n}\n\n/** A host action as a menu row (for a block that already has a \"…\" menu of its own). */\nexport function actionMenuItem(action: BlockAction, testId: string): MenuItem {\n const Icon = action.icon;\n return { id: action.id, label: action.label, ...(Icon ? { icon: <Icon className=\"size-4\" aria-hidden=\"true\" /> } : {}), ...(action.disabled ? { disabled: true } : {}), testId: `${testId}-${action.id}`, onSelect: action.onSelect };\n}\n\n/** The `header` actions of a list. */\nexport const headerActions = (actions: readonly BlockAction[] | undefined): readonly BlockAction[] => (actions ?? []).filter(action => action.placement !== 'menu');\n/** The `menu` actions of a list. */\nexport const menuActions = (actions: readonly BlockAction[] | undefined): readonly BlockAction[] => (actions ?? []).filter(action => action.placement === 'menu');\n\n/** One `header` action as a bar button: the icon with the label as its name and tooltip, or the label as text. */\nexport function ActionButton({ action, testId, size = 'icon-sm', className }: { readonly action: BlockAction; readonly testId: string; readonly size?: 'icon-sm' | 'icon-bar'; readonly className?: string }) {\n const Icon = action.icon;\n if (Icon) return <IconButton label={action.label} size={size} data-testid={`${testId}-${action.id}`} data-action={action.id} disabled={action.disabled} onClick={action.onSelect} className={className}>\n <Icon className=\"size-4\" aria-hidden=\"true\" /></IconButton>;\n return <Button size=\"sm\" variant=\"ghost\" data-testid={`${testId}-${action.id}`} data-action={action.id} disabled={action.disabled} onClick={action.onSelect} title={action.label}\n className={cn('max-w-40 min-w-0 text-muted-foreground', className)}><span className=\"truncate\">{action.label}</span></Button>;\n}\n\n/**\n * A block's host actions where it has no \"…\" menu of its own: the `header` ones as buttons, then one \"…\" menu (test id `<testId>-more`)\n * holding the `menu` ones. Each action's test id is `<testId>-<id>`. Nothing is rendered for an empty list.\n */\nexport function BlockActions({ actions, testId, menuLabel, size = 'icon-sm', className }: {\n readonly actions: readonly BlockAction[] | undefined;\n readonly testId: string;\n /** The \"…\" button's name. */\n readonly menuLabel: string;\n readonly size?: 'icon-sm' | 'icon-bar';\n readonly className?: string;\n}) {\n const buttons = headerActions(actions), menu = menuActions(actions);\n if (!buttons.length && !menu.length) return null;\n return <div data-boring=\"block-actions\" data-actions={testId} className={cn('flex shrink-0 items-center gap-0.5', className)}>\n {buttons.map(action => <ActionButton key={action.id} action={action} testId={testId} size={size} />)}\n {menu.length > 0 && <ActionMenu label={menuLabel} size={size} testId={`${testId}-more`} icon={<EllipsisIcon className=\"size-4\" aria-hidden=\"true\" />}\n items={menu.map(action => actionMenuItem(action, testId))} />}\n </div>;\n}\n",
23
+ "content": "'use client';\n\nimport { useEffect, useId, useRef, useState } from 'react';\nimport type { ComponentType, KeyboardEvent, ReactNode } from 'react';\nimport { CheckIcon, EllipsisIcon } from 'lucide-react';\nimport { Button, IconButton } from './button';\nimport { cn } from '../utils/utils';\n\n/** An icon the host may pass for any of the blocks' icons: any component that takes a class name (every lucide icon does). */\nexport type BlockIcon = ComponentType<{ readonly className?: string | undefined; readonly 'aria-hidden'?: boolean | 'true' | 'false' | undefined }>;\n\n/**\n * A host action added to a block's bar or row (the chat header, the floating chat's bar, the artifact panel, a conversation row, a reply),\n * next to the block's own buttons. `header` (the default) is a button in the bar: icon only when `icon` is given (the label is then its\n * accessible name and tooltip), otherwise the label. `menu` puts it in the block's \"…\" menu. Its test id is `<surface>-<id>` (see each block).\n */\nexport interface BlockAction {\n readonly id: string;\n readonly label: string;\n readonly icon?: BlockIcon | undefined;\n readonly onSelect: () => void;\n readonly placement?: 'header' | 'menu' | undefined;\n readonly disabled?: boolean | undefined;\n /** A toggle: set, the button carries `aria-pressed` with this state. */\n readonly pressed?: boolean | undefined;\n}\n\n/** One row of a menu. With `checked` set (true or false) the row is a radio choice, otherwise a plain action. */\nexport interface MenuItem {\n readonly id: string;\n readonly label: string;\n readonly icon?: ReactNode;\n /** Small text after the label, for example \"Latest\". */\n readonly hint?: string;\n readonly checked?: boolean;\n readonly disabled?: boolean;\n readonly testId?: string;\n readonly onSelect: () => void;\n}\n\nexport interface ActionMenuProps {\n /** Accessible name and tooltip of the trigger; also the name of the menu. */\n readonly label: string;\n readonly icon: ReactNode;\n readonly items: readonly MenuItem[];\n readonly testId?: string;\n /** Which edge of the trigger the menu lines up with. */\n readonly align?: 'start' | 'end';\n /** The trigger's size: the viewer bar's (default) or a small chat button. */\n readonly size?: 'icon-bar' | 'icon-sm';\n readonly className?: string;\n}\n\nconst ITEM = 'flex h-9 w-full cursor-pointer items-center gap-2 rounded-md px-2.5 text-left text-sm outline-none hover:bg-muted focus-visible:bg-muted disabled:pointer-events-none disabled:opacity-45 max-sm:h-11 pointer-coarse:h-11';\n\n/**\n * An icon button that opens a small menu: arrow keys, Home/End and roving focus, Escape (focus returns to the trigger), Tab and an outside\n * press close it. The one menu of the blocks' \"…\" buttons and the viewers' version history.\n */\nexport function ActionMenu({ label, icon, items, testId = 'menu', align = 'end', size = 'icon-bar', className }: ActionMenuProps) {\n const [open, setOpen] = useState(false);\n const root = useRef<HTMLDivElement>(null);\n const trigger = useRef<HTMLButtonElement>(null);\n const menuId = useId();\n const radio = items.some(item => item.checked !== undefined);\n\n useEffect(() => {\n if (!open) return;\n const list = root.current?.querySelectorAll<HTMLElement>('[role^=menuitem]:not(:disabled)');\n const wanted = root.current?.querySelector<HTMLElement>('[aria-checked=true]:not(:disabled)') ?? list?.[0];\n wanted?.focus();\n const away = (event: Event) => { if (!(event.target instanceof Node) || !root.current?.contains(event.target)) setOpen(false); };\n document.addEventListener('pointerdown', away);\n return () => document.removeEventListener('pointerdown', away);\n }, [open]);\n\n const close = (refocus: boolean) => { setOpen(false); if (refocus) trigger.current?.focus(); };\n const keys = (event: KeyboardEvent<HTMLElement>) => {\n const rows = [...(root.current?.querySelectorAll<HTMLElement>('[role^=menuitem]:not(:disabled)') ?? [])];\n const at = rows.indexOf(document.activeElement as HTMLElement);\n const go = (index: number) => { event.preventDefault(); rows[(index + rows.length) % rows.length]?.focus(); };\n if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(true); }\n else if (event.key === 'Tab') close(false);\n else if (event.key === 'ArrowDown') go(at + 1);\n else if (event.key === 'ArrowUp') go(at < 0 ? -1 : at - 1);\n else if (event.key === 'Home') go(0);\n else if (event.key === 'End') go(-1);\n };\n\n return <div ref={root} data-viewer-menu className={cn('relative', className)} onKeyDown={event => { if (open) keys(event); }}>\n <IconButton ref={trigger} label={label} size={size} data-testid={testId} aria-haspopup=\"menu\" aria-expanded={open} aria-controls={open ? menuId : undefined}\n onClick={() => setOpen(value => !value)}\n onKeyDown={event => { if (!open && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) { event.preventDefault(); setOpen(true); } }}>{icon}</IconButton>\n {open && <div id={menuId} role=\"menu\" aria-label={label} data-testid={`${testId}-list`}\n className={cn('absolute top-full z-50 mt-1 grid max-h-72 min-w-48 gap-0.5 overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg', 'max-sm:fixed max-sm:inset-x-2 max-sm:top-auto max-sm:bottom-2 max-sm:mt-0 max-sm:max-h-[70vh] max-sm:rounded-xl max-sm:p-1.5', align === 'end' ? 'right-0' : 'left-0')}>\n {items.map(item => <button key={item.id} type=\"button\" tabIndex={-1} role={radio ? 'menuitemradio' : 'menuitem'} {...(radio ? { 'aria-checked': item.checked === true } : {})}\n disabled={item.disabled} data-testid={item.testId ?? `${testId}-${item.id}`} data-item={item.id} className={ITEM}\n onClick={() => { close(true); item.onSelect(); }}>\n {item.icon ?? (radio ? <span className=\"size-4 shrink-0\">{item.checked && <CheckIcon className=\"size-4\" aria-hidden=\"true\" />}</span> : null)}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground\">{item.hint}</span>}\n </button>)}\n </div>}\n </div>;\n}\n\n/** A host action as a menu row (for a block that already has a \"…\" menu of its own). */\nexport function actionMenuItem(action: BlockAction, testId: string): MenuItem {\n const Icon = action.icon;\n return { id: action.id, label: action.label, ...(Icon ? { icon: <Icon className=\"size-4\" aria-hidden=\"true\" /> } : {}), ...(action.disabled ? { disabled: true } : {}), testId: `${testId}-${action.id}`, onSelect: action.onSelect };\n}\n\n/** The `header` actions of a list. */\nexport const headerActions = (actions: readonly BlockAction[] | undefined): readonly BlockAction[] => (actions ?? []).filter(action => action.placement !== 'menu');\n/** The `menu` actions of a list. */\nexport const menuActions = (actions: readonly BlockAction[] | undefined): readonly BlockAction[] => (actions ?? []).filter(action => action.placement === 'menu');\n\n/** One `header` action as a bar button: the icon with the label as its name and tooltip, or the label as text. */\nexport function ActionButton({ action, testId, size = 'icon-sm', className }: { readonly action: BlockAction; readonly testId: string; readonly size?: 'icon-sm' | 'icon-bar'; readonly className?: string }) {\n const Icon = action.icon;\n if (Icon) return <IconButton label={action.label} size={size} data-testid={`${testId}-${action.id}`} data-action={action.id} disabled={action.disabled} {...(action.pressed === undefined ? {} : { 'aria-pressed': action.pressed })} onClick={action.onSelect} className={className}>\n <Icon className=\"size-4\" aria-hidden=\"true\" /></IconButton>;\n return <Button size=\"sm\" variant=\"ghost\" data-testid={`${testId}-${action.id}`} data-action={action.id} disabled={action.disabled} {...(action.pressed === undefined ? {} : { 'aria-pressed': action.pressed })} onClick={action.onSelect} title={action.label}\n className={cn('max-w-40 min-w-0 text-muted-foreground', className)}><span className=\"truncate\">{action.label}</span></Button>;\n}\n\n/**\n * A block's host actions where it has no \"…\" menu of its own: the `header` ones as buttons, then one \"…\" menu (test id `<testId>-more`)\n * holding the `menu` ones. Each action's test id is `<testId>-<id>`. Nothing is rendered for an empty list.\n */\nexport function BlockActions({ actions, testId, menuLabel, size = 'icon-sm', className }: {\n readonly actions: readonly BlockAction[] | undefined;\n readonly testId: string;\n /** The \"…\" button's name. */\n readonly menuLabel: string;\n readonly size?: 'icon-sm' | 'icon-bar';\n readonly className?: string;\n}) {\n const buttons = headerActions(actions), menu = menuActions(actions);\n if (!buttons.length && !menu.length) return null;\n return <div data-boring=\"block-actions\" data-actions={testId} className={cn('flex shrink-0 items-center gap-0.5', className)}>\n {buttons.map(action => <ActionButton key={action.id} action={action} testId={testId} size={size} />)}\n {menu.length > 0 && <ActionMenu label={menuLabel} size={size} testId={`${testId}-more`} icon={<EllipsisIcon className=\"size-4\" aria-hidden=\"true\" />}\n items={menu.map(action => actionMenuItem(action, testId))} />}\n </div>;\n}\n",
24
24
  "type": "registry:ui",
25
25
  "target": "components/button/actions.tsx"
26
26
  }
@@ -4,8 +4,8 @@
4
4
  "title": "Feedback preview",
5
5
  "description": "PreviewBanner: a live, reversible preview of a change in the page (Approve / Discard) over `@hachej/boring-feedback/preview`, whose subagent calls models through a host gateway. Separate from `feedback` so annotation-only apps do not install the model SDK; install `feedback` first (its styles cover the banner).",
6
6
  "dependencies": [
7
- "@hachej/boring-feedback@^0.2.2",
8
- "@earendil-works/pi-ai@^1.1.0",
7
+ "@hachej/boring-feedback@^0.2.3",
8
+ "@earendil-works/pi-ai@~1.1.0",
9
9
  "react@^19.0.0"
10
10
  ],
11
11
  "files": [