═══════════════════════════════════════════════════════════════════════
@bytelyst/ai-ui bump 0.1.0 → 0.4.0
═══════════════════════════════════════════════════════════════════════
Folds three more roadmap milestones into the flagship package.
0.2: <ToolCallCard> — disclosure card; status pill, JSON preview
<CitationChip> — inline citation marker + hover preview
useToolCalls() — per-turn tool-invocation state machine
(begin/update/settle/clear); preserves insertion
order across updates; auto-computes durationMs
0.3: <AgentTimeline> — vertical think→act→observe→respond trace;
embeds ToolCallCard for kind='tool_call' steps
<ModelPicker> — model dropdown with capability chips, cost,
latency, context window, disabled gating
0.4: <ToolPalette> — searchable tool list with MCP-style discovery
(source can be ToolDescriptor[] OR an
'mcp://...' URL resolved via a discover
adapter; default adapter is fetch+JSON)
Types extended:
- ToolInvocation, ToolCallStatus, Citation added
- Message gains optional toolInvocations + citations
Tests: 53/53 (27 old + 26 new) · typecheck clean · 7.65 KB / 35 KB
═══════════════════════════════════════════════════════════════════════
NEW PACKAGE: @bytelyst/command-palette@0.1.0
═══════════════════════════════════════════════════════════════════════
Wave 3 deliverable — Cmd-K dialog with three modes and pluggable command
registration. Roadmap §Wave 3 of ROADMAP_2026.md.
What's exported:
<CommandRegistryProvider> — wrap your app once
<CommandPalette> — the dialog (Cmd-K / Ctrl-K)
useRegisterCommands() — contribute commands for component lifetime
useCommands() — read snapshot
useCommandRegistry() — imperative access
useCommandPalette() — open/close state + global hotkey
fuzzyScore / scoreCommand — exposed for tests + custom UIs
Three modes:
actions — invoke a registered run()
navigate — jump to href via onNavigate or window.location
ask-ai — host-supplied askAiPanel; default renders an 'Ask AI: <q>'
suggestion that products can wire to <ChatStream>
Keyboard:
↑ ↓ navigate selection
Enter activate
Tab cycle mode tabs (Shift+Tab reverses)
Esc close
Niceties:
- Fuzzy matcher (substring + subsequence with light scoring)
- localStorage-backed recents float to top of actions mode
- requires() gate hides commands wholesale (auth / feature-flag)
- aria-haspopup, role=dialog, role=listbox, role=option, aria-selected
- Backdrop click closes; Esc handler at document level
- Hotkey suppressed by Cmd-K / Ctrl-K default; configurable
Tests: 26/26 · typecheck clean · 3.91 KB / 15 KB
═══════════════════════════════════════════════════════════════════════
CI plumbing
═══════════════════════════════════════════════════════════════════════
- .size-limit.cjs gains @bytelyst/command-palette entry
- .gitea/workflows/size-limit.yml build filter expanded
- All 8 measured packages comfortably under budget
Refs:
learning_ai_uxui_web/docs/ROADMAP_2026.md §Wave 2 (0.2/0.3/0.4)
learning_ai_uxui_web/docs/ROADMAP_2026.md §Wave 3 (Command palette)
docs/ROADMAP_2026_DECISIONS.md §10 (Vercel AI SDK shape continues)
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
import { useState, type CSSProperties, type ReactNode } from 'react';
|
|
import type { Citation } from './types.js';
|
|
|
|
export interface CitationChipProps {
|
|
citation: Citation;
|
|
/** Override the chip body — useful for icons. Defaults to `[n]`. */
|
|
children?: ReactNode;
|
|
/** Suppress the popover preview (e.g. when used inline in dense lists). */
|
|
noPreview?: boolean;
|
|
className?: string;
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
/**
|
|
* Inline citation marker. Renders a small `[n]` chip; on hover or focus
|
|
* a preview pops up with the title, snippet, and an outbound link.
|
|
*
|
|
* The preview is a CSS-only popover (no portal, no JS layout) so it
|
|
* stays cheap to embed dozens of times in a single message body.
|
|
*/
|
|
export function CitationChip({
|
|
citation,
|
|
children,
|
|
noPreview = false,
|
|
className,
|
|
style,
|
|
}: CitationChipProps) {
|
|
const [hover, setHover] = useState(false);
|
|
const [focus, setFocus] = useState(false);
|
|
const open = !noPreview && (hover || focus);
|
|
|
|
const Wrapper = citation.url ? 'a' : 'span';
|
|
const wrapperProps = citation.url
|
|
? {
|
|
href: citation.url,
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
}
|
|
: {};
|
|
|
|
return (
|
|
<span
|
|
style={{ position: 'relative', display: 'inline-block', ...style }}
|
|
className={className}
|
|
onMouseEnter={() => setHover(true)}
|
|
onMouseLeave={() => setHover(false)}
|
|
>
|
|
<Wrapper
|
|
data-testid={`bl-citation-${citation.id}`}
|
|
data-citation-index={citation.index}
|
|
{...wrapperProps}
|
|
onFocus={() => setFocus(true)}
|
|
onBlur={() => setFocus(false)}
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minWidth: 18,
|
|
height: 18,
|
|
padding: '0 4px',
|
|
marginInline: 2,
|
|
fontSize: 11,
|
|
fontWeight: 700,
|
|
lineHeight: 1,
|
|
color: 'var(--bl-accent, #6366f1)',
|
|
background: 'var(--bl-accent-muted, rgba(99,102,241,0.12))',
|
|
borderRadius: 'var(--bl-radius-pill, 999px)',
|
|
textDecoration: 'none',
|
|
cursor: citation.url ? 'pointer' : 'help',
|
|
verticalAlign: 'super',
|
|
}}
|
|
>
|
|
{children ?? citation.index}
|
|
</Wrapper>
|
|
|
|
{open && (
|
|
<span
|
|
role="tooltip"
|
|
data-testid={`bl-citation-preview-${citation.id}`}
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: 'calc(100% + 6px)',
|
|
left: 0,
|
|
zIndex: 50,
|
|
display: 'block',
|
|
width: 280,
|
|
padding: 'var(--bl-space-3, 12px)',
|
|
background: 'var(--bl-surface-card, #fff)',
|
|
color: 'var(--bl-text-primary, inherit)',
|
|
border: '1px solid var(--bl-border, rgba(0,0,0,0.08))',
|
|
borderRadius: 'var(--bl-radius-card, 10px)',
|
|
boxShadow: '0 10px 30px rgba(0,0,0,0.18)',
|
|
fontSize: '0.8rem',
|
|
lineHeight: 1.45,
|
|
fontWeight: 400,
|
|
textAlign: 'left',
|
|
whiteSpace: 'normal',
|
|
pointerEvents: 'none', // hover-only preview; avoids flicker
|
|
}}
|
|
>
|
|
{citation.source && (
|
|
<div
|
|
style={{
|
|
fontSize: '0.65rem',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.06em',
|
|
color: 'var(--bl-text-tertiary, #888)',
|
|
marginBottom: 2,
|
|
}}
|
|
>
|
|
{citation.source}
|
|
</div>
|
|
)}
|
|
<div style={{ fontWeight: 600, marginBottom: 4 }}>{citation.title}</div>
|
|
{citation.snippet && (
|
|
<div style={{ color: 'var(--bl-text-secondary, #444)' }}>
|
|
{citation.snippet}
|
|
</div>
|
|
)}
|
|
{citation.url && (
|
|
<div
|
|
style={{
|
|
marginTop: 6,
|
|
fontSize: '0.7rem',
|
|
color: 'var(--bl-text-tertiary, #888)',
|
|
wordBreak: 'break-all',
|
|
}}
|
|
>
|
|
{truncateUrl(citation.url)}
|
|
</div>
|
|
)}
|
|
</span>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function truncateUrl(url: string, max = 56): string {
|
|
return url.length <= max ? url : `${url.slice(0, max - 1)}…`;
|
|
}
|