@tangle-network/ui 11.20.0 → 11.22.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 17162d7: Metric accepts an optional decorative `icon` before its label. EmptyState accepts `titleAs` (h2, h3 or h4) so the title follows the page outline. CopyButton is a `type="button"` with an `aria-label` and a polite status announcement when the copy succeeds, and its check icon uses the success text token. The CodeBlock header label renders at `text-xs` (12px) instead of one pixel below it.
8
+
9
+ ## 11.21.0
10
+
11
+ ### Minor Changes
12
+
13
+ - a69c72f: Add `Pagination` (zero-based `page`, `pageCount`, `onPageChange`; a constant number of page slots with first and last always reachable) and `Alert` with `AlertTitle` and `AlertDescription` (Brand status tones plus `neutral`). `SegmentedControl` options take `testId`, and the group takes `data-testid`.
14
+
3
15
  ## 11.20.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -76,6 +76,8 @@ import { Heading, HeadingProps, HeadingVariant } from "./primitives/heading.js";
76
76
  import { PageShell, PageShellProps } from "./primitives/page-shell.js";
77
77
  import { PageHeader, PageHeaderProps } from "./primitives/page-header.js";
78
78
  import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
79
+ import { Alert, AlertDescription, AlertProps, AlertTitle, AlertTone } from "./primitives/alert.js";
80
+ import { Pagination, PaginationProps } from "./primitives/pagination.js";
79
81
  import { CATEGORY_TONES, CategoryTone, TONE_CLASSES, Tone, ToneClasses, toneFor } from "./primitives/tone.js";
80
82
  import { Tag, TagProps } from "./primitives/tag.js";
81
83
  import { Chip, ChipProps } from "./primitives/chip.js";
@@ -112,4 +114,4 @@ import { QuestionPreview, QuestionPreviewProps } from "./tool-previews/question-
112
114
  import { DiffPreview, DiffPreviewProps } from "./tool-previews/diff-preview.js";
113
115
  import { RedactedDocSegment, RedactedDocument, RedactedDocumentData, RedactedDocumentProps, RevealResult } from "./redaction/redacted-document.js";
114
116
  import "./redaction.js";
115
- export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AppendUserMessageOptions, ApplySdkEventOptions, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, CustomToolRenderer, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, FileArtifactPane, type FileArtifactPaneProps, type FileFormat, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
117
+ export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, AppendUserMessageOptions, ApplySdkEventOptions, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, CustomToolRenderer, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, FileArtifactPane, type FileArtifactPaneProps, type FileFormat, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
package/dist/index.js CHANGED
@@ -39,6 +39,8 @@ import { InlineCode } from "./primitives/code-block.js";
39
39
  import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
40
40
  import { PageShell } from "./primitives/page-shell.js";
41
41
  import { PageHeader } from "./primitives/page-header.js";
42
+ import { Alert, AlertDescription, AlertTitle } from "./primitives/alert.js";
43
+ import { Pagination } from "./primitives/pagination.js";
42
44
  import { StatusPill } from "./primitives/status-pill.js";
43
45
  import { CATEGORY_TONES, TONE_CLASSES, toneFor } from "./primitives/tone.js";
44
46
  import { Tag } from "./primitives/tag.js";
@@ -96,4 +98,4 @@ import { addMessage, addParts, clearChat, isStreamingAtom, messagesAtom, partMap
96
98
  import { copyText } from "./utils/copy-text.js";
97
99
  import { timeAgo } from "./utils/time-ago.js";
98
100
  import { RedactedDocument } from "./redaction/redacted-document.js";
99
- export { AgentTimeline, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, FileArtifactPane, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunRowShell, RunRowStatusDot, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
101
+ export { AgentTimeline, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, FileArtifactPane, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunRowShell, RunRowStatusDot, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
@@ -1,7 +1,7 @@
1
1
  import { focusRing, focusRingInset } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { memo, useCallback, useEffect, useRef, useState } from "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Check, Copy } from "lucide-react";
6
6
  //#region src/markdown/code-block.tsx
7
7
  const syntaxTheme = (() => {
@@ -107,7 +107,7 @@ const CodeBlock = memo(({ code, language, label, showLineNumbers = false, light,
107
107
  headerLabel && /* @__PURE__ */ jsxs("div", {
108
108
  className: "flex items-center justify-between border-b border-border px-3 py-1 bg-[var(--code-header-bg,hsl(var(--background)))]",
109
109
  children: [/* @__PURE__ */ jsx("span", {
110
- className: "text-[calc(var(--font-size-xs)-1px)] font-mono font-medium uppercase tracking-widest text-muted-foreground",
110
+ className: "text-xs font-mono font-medium uppercase tracking-widest text-muted-foreground",
111
111
  children: headerLabel
112
112
  }), children]
113
113
  }),
@@ -150,6 +150,7 @@ CodeBlock.displayName = "CodeBlock";
150
150
  /** Copy-to-clipboard button for use inside CodeBlock. */
151
151
  const CopyButton = memo(({ text }) => {
152
152
  const [copied, setCopied] = useState(false);
153
+ const [status, setStatus] = useState("");
153
154
  const timerRef = useRef(null);
154
155
  useEffect(() => {
155
156
  return () => {
@@ -157,21 +158,42 @@ const CopyButton = memo(({ text }) => {
157
158
  };
158
159
  }, []);
159
160
  const handleCopy = useCallback(async () => {
161
+ setStatus("");
162
+ if (timerRef.current) clearTimeout(timerRef.current);
160
163
  try {
161
164
  await navigator.clipboard.writeText(text);
162
165
  setCopied(true);
163
- if (timerRef.current) clearTimeout(timerRef.current);
164
- timerRef.current = setTimeout(() => setCopied(false), 2e3);
166
+ setStatus("Copied to clipboard");
165
167
  } catch (err) {
166
168
  console.warn("Clipboard write failed:", err);
169
+ setCopied(false);
170
+ setStatus("Copy failed");
167
171
  }
172
+ timerRef.current = setTimeout(() => {
173
+ setCopied(false);
174
+ setStatus("");
175
+ }, 2e3);
168
176
  }, [text]);
169
- return /* @__PURE__ */ jsx("button", {
177
+ const label = copied ? "Copied" : "Copy to clipboard";
178
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
179
+ type: "button",
170
180
  onClick: handleCopy,
171
181
  className: cn("flex items-center justify-center w-6 h-6 rounded-md bg-muted border border-border hover:border-[var(--border-strong)] transition-colors", focusRing),
172
- title: "Copy to clipboard",
173
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5 text-emerald-500" }) : /* @__PURE__ */ jsx(Copy, { className: "w-3.5 h-3.5 text-muted-foreground" })
174
- });
182
+ "aria-label": label,
183
+ title: label,
184
+ children: copied ? /* @__PURE__ */ jsx(Check, {
185
+ "aria-hidden": "true",
186
+ className: "w-3.5 h-3.5 text-[var(--surface-success-text)]"
187
+ }) : /* @__PURE__ */ jsx(Copy, {
188
+ "aria-hidden": "true",
189
+ className: "w-3.5 h-3.5 text-muted-foreground"
190
+ })
191
+ }), /* @__PURE__ */ jsx("span", {
192
+ className: "sr-only",
193
+ role: "status",
194
+ "aria-live": "polite",
195
+ children: status
196
+ })] });
175
197
  });
176
198
  CopyButton.displayName = "CopyButton";
177
199
  //#endregion
@@ -0,0 +1,35 @@
1
+ import { StatusTone } from "./status-pill.js";
2
+ import * as React from "react";
3
+ //#region src/primitives/alert.d.ts
4
+ /**
5
+ * An inline message that stays on the page: a policy note above a form, a
6
+ * failed save beside the field it concerns. For a message that interrupts, use
7
+ * `AlertDialog`; for one that goes away by itself, use a toast.
8
+ *
9
+ * Each tone paints its fill, border and text from Brand's matched status
10
+ * triple, the same one `StatusPill` uses, so the text is solved against its
11
+ * own background on every plane. `neutral` is a card with body text.
12
+ *
13
+ * `danger` and `warning` announce themselves (`role="alert"`); the quieter
14
+ * tones are `role="status"` so a screen reader is not interrupted by a note.
15
+ */
16
+ export type AlertTone = Exclude<StatusTone, "running">;
17
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
18
+ tone?: AlertTone;
19
+ /** A leading icon. It takes the tone's colour and sits beside the title. */
20
+ icon?: React.ReactNode;
21
+ }
22
+ export declare const Alert: React.ForwardRefExoticComponent<AlertProps & React.RefAttributes<HTMLDivElement>>;
23
+ export declare function AlertTitle({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>): React.JSX.Element;
24
+ export declare namespace AlertTitle {
25
+ var displayName: string;
26
+ }
27
+ /**
28
+ * Body text. Under a toned alert it keeps the tone's ink, which is the colour
29
+ * tested against the tone's fill; under `neutral` it is the muted tier.
30
+ */
31
+ export declare function AlertDescription({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element;
32
+ export declare namespace AlertDescription {
33
+ var displayName: string;
34
+ }
35
+ //#endregion
@@ -0,0 +1,47 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import * as React from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/primitives/alert.tsx
5
+ const TONE = {
6
+ neutral: "border-border bg-card text-card-foreground",
7
+ info: "border-[var(--surface-info-border)] bg-[var(--surface-info-bg)] text-[var(--surface-info-text)]",
8
+ success: "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
9
+ warning: "border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)]",
10
+ danger: "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]"
11
+ };
12
+ const Alert = React.forwardRef(({ tone = "neutral", icon, className, children, role, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
13
+ ref,
14
+ role: role ?? (tone === "danger" || tone === "warning" ? "alert" : "status"),
15
+ "data-tone": tone,
16
+ className: cn("flex w-full min-w-0 gap-3 rounded-lg border px-4 py-3 text-sm", TONE[tone], className),
17
+ ...props,
18
+ children: [icon ? /* @__PURE__ */ jsx("span", {
19
+ "aria-hidden": "true",
20
+ className: "mt-0.5 flex shrink-0 [&_svg]:size-4",
21
+ children: icon
22
+ }) : null, /* @__PURE__ */ jsx("div", {
23
+ className: "grid min-w-0 flex-1 gap-1",
24
+ children
25
+ })]
26
+ }));
27
+ Alert.displayName = "Alert";
28
+ function AlertTitle({ className, ...props }) {
29
+ return /* @__PURE__ */ jsx("p", {
30
+ className: cn("font-medium leading-snug [overflow-wrap:anywhere]", className),
31
+ ...props
32
+ });
33
+ }
34
+ AlertTitle.displayName = "AlertTitle";
35
+ /**
36
+ * Body text. Under a toned alert it keeps the tone's ink, which is the colour
37
+ * tested against the tone's fill; under `neutral` it is the muted tier.
38
+ */
39
+ function AlertDescription({ className, ...props }) {
40
+ return /* @__PURE__ */ jsx("div", {
41
+ className: cn("leading-relaxed [overflow-wrap:anywhere] [[data-tone=neutral]_&]:text-muted-foreground", className),
42
+ ...props
43
+ });
44
+ }
45
+ AlertDescription.displayName = "AlertDescription";
46
+ //#endregion
47
+ export { Alert, AlertDescription, AlertTitle };
@@ -5,6 +5,9 @@ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  title: string;
6
6
  description?: string;
7
7
  action?: React.ReactNode;
8
+ /** Heading element for the title. Pick the level that follows the page's
9
+ * outline, e.g. h2 directly under a page h1. Defaults to h3. */
10
+ titleAs?: "h2" | "h3" | "h4";
8
11
  }
9
12
  declare const EmptyState: React.ForwardRefExoticComponent<EmptyStateProps & React.RefAttributes<HTMLDivElement>>;
10
13
  //#endregion
@@ -2,7 +2,7 @@ import { cn } from "../lib/utils.js";
2
2
  import * as React from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/primitives/empty-state.tsx
5
- const EmptyState = React.forwardRef(({ className, icon, title, description, action, ...props }, ref) => {
5
+ const EmptyState = React.forwardRef(({ className, icon, title, description, action, titleAs: Title = "h3", ...props }, ref) => {
6
6
  return /* @__PURE__ */ jsxs("div", {
7
7
  ref,
8
8
  className: cn("flex flex-col items-center justify-center px-4 py-16 text-center", className),
@@ -12,7 +12,7 @@ const EmptyState = React.forwardRef(({ className, icon, title, description, acti
12
12
  className: "mb-4 rounded-full bg-muted p-4 text-muted-foreground",
13
13
  children: icon
14
14
  }),
15
- /* @__PURE__ */ jsx("h3", {
15
+ /* @__PURE__ */ jsx(Title, {
16
16
  className: "font-semibold text-lg",
17
17
  children: title
18
18
  }),
@@ -19,6 +19,9 @@ export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
19
19
  tone: StatusTone;
20
20
  label: string;
21
21
  };
22
+ /** Decorative glyph before the label; the label carries the meaning. A
23
+ * direct svg child is sized to 16px; wrap other content to size it yourself. */
24
+ icon?: React.ReactNode;
22
25
  }
23
26
  /** A described term; the strip, not this item, decides where separators belong. */
24
27
  declare const Metric: React.ForwardRefExoticComponent<MetricProps & React.RefAttributes<HTMLDivElement>>;
@@ -17,20 +17,28 @@ const MetricStrip = React.forwardRef(({ className, children, columns = 4, ...pro
17
17
  }));
18
18
  MetricStrip.displayName = "MetricStrip";
19
19
  /** A described term; the strip, not this item, decides where separators belong. */
20
- const Metric = React.forwardRef(({ className, label, value, hint, attention, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
20
+ const Metric = React.forwardRef(({ className, label, value, hint, attention, icon, ...props }, ref) => /* @__PURE__ */ jsxs("div", {
21
21
  ref,
22
22
  className: cn("min-w-0 border-border p-4 sm:p-5", className),
23
23
  ...props,
24
24
  children: [
25
25
  /* @__PURE__ */ jsxs("dt", {
26
26
  className: "flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm",
27
- children: [/* @__PURE__ */ jsx("span", {
28
- className: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
29
- children: label
30
- }), attention && /* @__PURE__ */ jsx(StatusPill, {
31
- tone: attention.tone,
32
- children: attention.label
33
- })]
27
+ children: [
28
+ icon != null && icon !== false && /* @__PURE__ */ jsx("span", {
29
+ "aria-hidden": "true",
30
+ className: "inline-flex shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4",
31
+ children: icon
32
+ }),
33
+ /* @__PURE__ */ jsx("span", {
34
+ className: "min-w-0 whitespace-normal [overflow-wrap:anywhere]",
35
+ children: label
36
+ }),
37
+ attention && /* @__PURE__ */ jsx(StatusPill, {
38
+ tone: attention.tone,
39
+ children: attention.label
40
+ })
41
+ ]
34
42
  }),
35
43
  /* @__PURE__ */ jsx("dd", {
36
44
  className: cn("mt-1 whitespace-normal [overflow-wrap:anywhere] font-semibold text-xl tabular-nums tracking-tight lg:text-2xl", attention?.tone === "danger" && "text-[var(--surface-danger-text)]"),
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ //#region src/primitives/pagination.d.ts
3
+ /**
4
+ * Page controls for a paged list or table: previous, the page numbers around
5
+ * the current one with the first and last always reachable, and next.
6
+ *
7
+ * Pages are zero-based in the API and one-based on screen. With one page or
8
+ * none there is nothing to choose, so it renders nothing.
9
+ */
10
+ export interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "children"> {
11
+ /** The current page, zero-based. */
12
+ page: number;
13
+ pageCount: number;
14
+ onPageChange: (page: number) => void;
15
+ /** Pages shown on each side of the current one before an ellipsis. */
16
+ siblings?: number;
17
+ }
18
+ export declare function Pagination({ page, pageCount, onPageChange, siblings, className, "aria-label": ariaLabel, ...rest }: PaginationProps): React.JSX.Element | null;
19
+ //#endregion
@@ -0,0 +1,81 @@
1
+ import { cn } from "../lib/utils.js";
2
+ import { buttonVariants } from "./button.js";
3
+ import "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ //#region src/primitives/pagination.tsx
7
+ /**
8
+ * The visible slots for `page` of `pageCount`. The number of slots stays the
9
+ * same as the current page moves, so the controls do not shift under the
10
+ * pointer while a person clicks through.
11
+ */
12
+ function pageSlots(page, pageCount, siblings = 1) {
13
+ const last = pageCount - 1;
14
+ if (pageCount <= siblings * 2 + 5) return Array.from({ length: pageCount }, (_, i) => i);
15
+ const current = Math.min(Math.max(page, 0), last);
16
+ const start = Math.max(Math.min(current - siblings, last - siblings * 2 - 2), 2);
17
+ const end = Math.min(Math.max(current + siblings, siblings * 2 + 2), last - 2);
18
+ const slots = [0, start > 2 ? "gap-start" : 1];
19
+ for (let i = start; i <= end; i++) slots.push(i);
20
+ slots.push(end < last - 2 ? "gap-end" : last - 1, last);
21
+ return slots;
22
+ }
23
+ const pageButton = cn(buttonVariants({
24
+ variant: "ghost",
25
+ size: "sm"
26
+ }), "min-w-8 px-2 tabular-nums");
27
+ function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "aria-label": ariaLabel = "Pagination", ...rest }) {
28
+ if (pageCount <= 1) return null;
29
+ const last = pageCount - 1;
30
+ const current = Math.min(Math.max(page, 0), last);
31
+ const go = (next) => {
32
+ const target = Math.min(Math.max(next, 0), last);
33
+ if (target !== current) onPageChange(target);
34
+ };
35
+ return /* @__PURE__ */ jsx("nav", {
36
+ "aria-label": ariaLabel,
37
+ className: cn("flex items-center", className),
38
+ ...rest,
39
+ children: /* @__PURE__ */ jsxs("ul", {
40
+ className: "flex flex-wrap items-center gap-1",
41
+ children: [
42
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
43
+ type: "button",
44
+ "aria-label": "Previous page",
45
+ disabled: current === 0,
46
+ onClick: () => go(current - 1),
47
+ className: cn(pageButton, "gap-1 pl-1.5"),
48
+ children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
49
+ className: "hidden sm:inline",
50
+ children: "Previous"
51
+ })]
52
+ }) }),
53
+ pageSlots(current, pageCount, siblings).map((slot) => typeof slot === "number" ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
54
+ type: "button",
55
+ "aria-label": `Page ${slot + 1}`,
56
+ "aria-current": slot === current ? "page" : void 0,
57
+ onClick: () => go(slot),
58
+ className: cn(pageButton, slot === current && "border-border bg-card font-semibold text-foreground hover:bg-card"),
59
+ children: slot + 1
60
+ }) }, slot) : /* @__PURE__ */ jsx("li", {
61
+ "aria-hidden": "true",
62
+ className: "flex min-w-8 justify-center text-sm text-muted-foreground",
63
+ children: "…"
64
+ }, slot)),
65
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
66
+ type: "button",
67
+ "aria-label": "Next page",
68
+ disabled: current === last,
69
+ onClick: () => go(current + 1),
70
+ className: cn(pageButton, "gap-1 pr-1.5"),
71
+ children: [/* @__PURE__ */ jsx("span", {
72
+ className: "hidden sm:inline",
73
+ children: "Next"
74
+ }), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })]
75
+ }) })
76
+ ]
77
+ })
78
+ });
79
+ }
80
+ //#endregion
81
+ export { Pagination, pageSlots };
@@ -23,6 +23,8 @@ export interface SegmentedControlOption<T extends string = string> {
23
23
  label: React.ReactNode;
24
24
  /** Rendered right of the label, typically a count or status pill. */
25
25
  adornment?: React.ReactNode;
26
+ /** Rendered as the option button's `data-testid`, for end-to-end selectors. */
27
+ testId?: string;
26
28
  }
27
29
  export interface SegmentedControlProps<T extends string = string> extends Pick<React.HTMLAttributes<HTMLDivElement>, "id" | "className" | "aria-label" | "aria-labelledby"> {
28
30
  value: T;
@@ -35,6 +37,8 @@ export interface SegmentedControlProps<T extends string = string> extends Pick<R
35
37
  * reads as a classic tab (used on the Team page)
36
38
  */
37
39
  variant?: "row" | "tabs";
40
+ /** Rendered as the group's `data-testid`. */
41
+ "data-testid"?: string;
38
42
  }
39
43
  export declare function SegmentedControl<T extends string = string>({ value, onValueChange, options, variant, className, ...rest }: SegmentedControlProps<T>): React.JSX.Element;
40
44
  //#endregion
@@ -28,10 +28,12 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
28
28
  id: rest.id,
29
29
  "aria-label": rest["aria-label"],
30
30
  "aria-labelledby": rest["aria-labelledby"],
31
+ "data-testid": rest["data-testid"],
31
32
  onKeyDown: handleKeyDown,
32
33
  className: cn("flex gap-1", variant === "row" && "flex-wrap items-center rounded-lg border border-border bg-card p-1", variant === "tabs" && "flex-nowrap items-end border-b border-border pb-0 overflow-x-auto", className),
33
34
  children: options.map((option, i) => {
34
35
  const active = option.value === value;
36
+ const focusable = active || !hasMatch && i === 0;
35
37
  return /* @__PURE__ */ jsxs("button", {
36
38
  ref: (el) => {
37
39
  if (el) optionRefs.current.set(option.value, el);
@@ -40,7 +42,8 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
40
42
  type: "button",
41
43
  role: "radio",
42
44
  "aria-checked": active,
43
- tabIndex: active || !hasMatch && i === 0 ? 0 : -1,
45
+ "data-testid": option.testId,
46
+ tabIndex: focusable ? 0 : -1,
44
47
  onClick: () => {
45
48
  if (!active) onValueChange(option.value);
46
49
  },
@@ -38,6 +38,8 @@ import { Heading, HeadingProps, HeadingVariant } from "./primitives/heading.js";
38
38
  import { PageShell, PageShellProps } from "./primitives/page-shell.js";
39
39
  import { PageHeader, PageHeaderProps } from "./primitives/page-header.js";
40
40
  import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
41
+ import { Alert, AlertDescription, AlertProps, AlertTitle, AlertTone } from "./primitives/alert.js";
42
+ import { Pagination, PaginationProps } from "./primitives/pagination.js";
41
43
  import { CATEGORY_TONES, CategoryTone, TONE_CLASSES, Tone, ToneClasses, toneFor } from "./primitives/tone.js";
42
44
  import { Tag, TagProps } from "./primitives/tag.js";
43
45
  import { Chip, ChipProps } from "./primitives/chip.js";
@@ -45,4 +47,4 @@ import { IconTile, IconTileProps, initialsOf } from "./primitives/icon-tile.js";
45
47
  import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
46
48
  import { FilterField, FilterFieldProps, Toolbar, ToolbarProps } from "./primitives/toolbar.js";
47
49
  import { Logo, LogoProps, TangleKnot } from "@tangle-network/brand";
48
- export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
50
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
@@ -37,6 +37,8 @@ import { InlineCode } from "./primitives/code-block.js";
37
37
  import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
38
38
  import { PageShell } from "./primitives/page-shell.js";
39
39
  import { PageHeader } from "./primitives/page-header.js";
40
+ import { Alert, AlertDescription, AlertTitle } from "./primitives/alert.js";
41
+ import { Pagination } from "./primitives/pagination.js";
40
42
  import { StatusPill } from "./primitives/status-pill.js";
41
43
  import { CATEGORY_TONES, TONE_CLASSES, toneFor } from "./primitives/tone.js";
42
44
  import { Tag } from "./primitives/tag.js";
@@ -45,4 +47,4 @@ import { IconTile, initialsOf } from "./primitives/icon-tile.js";
45
47
  import { Metric, MetricStrip } from "./primitives/metric-strip.js";
46
48
  import { FilterField, Toolbar } from "./primitives/toolbar.js";
47
49
  import { Logo, TangleKnot } from "@tangle-network/brand";
48
- export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
50
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, badgeVariants, buttonVariants, initialsOf, toneFor, useTheme, useToast };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.20.0",
3
+ "version": "11.22.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -149,7 +149,7 @@
149
149
  "peerDependencies": {
150
150
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
151
151
  "@nanostores/react": ">=0.8.4 <3.0.0",
152
- "@tangle-network/brand": "^1.15.1",
152
+ "@tangle-network/brand": "^1.15.2",
153
153
  "@tiptap/core": ">=3.20.4 <4.0.0",
154
154
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
155
155
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -1,10 +1,17 @@
1
- import { cleanup, render, screen, waitFor } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
2
2
  import { renderToString } from "react-dom/server";
3
3
  import SyntaxHighlighter from "react-syntax-highlighter";
4
- import { afterEach, describe, expect, it } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { CodeBlock, CopyButton } from "./code-block";
6
6
 
7
- afterEach(cleanup);
7
+ const originalClipboard = Object.getOwnPropertyDescriptor(navigator, "clipboard");
8
+
9
+ afterEach(() => {
10
+ cleanup();
11
+ vi.restoreAllMocks();
12
+ if (originalClipboard) Object.defineProperty(navigator, "clipboard", originalClipboard);
13
+ else delete (navigator as { clipboard?: unknown }).clipboard;
14
+ });
8
15
 
9
16
  describe("CodeBlock scoped semantic colors", () => {
10
17
  it("keeps code readable before the renderer loads, then applies live semantic colors and line numbers", async () => {
@@ -137,4 +144,36 @@ describe("CodeBlock", () => {
137
144
  expect(copyButton?.closest(".absolute")).not.toBeNull();
138
145
  expect(copyButton?.closest(".border-b")).toBeNull();
139
146
  });
147
+
148
+ it("names the copy button, keeps it out of forms, and announces the copy", async () => {
149
+ const writeText = vi.fn().mockResolvedValue(undefined);
150
+ Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
151
+ render(<CopyButton text="secret" />);
152
+ const button = screen.getByRole("button", { name: "Copy to clipboard" });
153
+ expect(button.getAttribute("type")).toBe("button");
154
+ const status = screen.getByRole("status");
155
+ expect(status.getAttribute("aria-live")).toBe("polite");
156
+ expect(status.textContent).toBe("");
157
+ fireEvent.click(button);
158
+ await waitFor(() => expect(status.textContent).toBe("Copied to clipboard"));
159
+ expect(writeText).toHaveBeenCalledWith("secret");
160
+ expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy();
161
+ });
162
+
163
+ it("renders the header label at the 12px text-xs step", () => {
164
+ const { getByText } = render(<CodeBlock code={"x"} label="YAML" />);
165
+ expect(getByText("YAML").className).toContain("text-xs");
166
+ expect(getByText("YAML").className).not.toContain("calc(");
167
+ });
168
+
169
+ it("announces a failed copy", async () => {
170
+ const writeText = vi.fn().mockRejectedValue(new Error("denied"));
171
+ Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
172
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
173
+ render(<CopyButton text="secret" />);
174
+ fireEvent.click(screen.getByRole("button", { name: "Copy to clipboard" }));
175
+ await waitFor(() => expect(screen.getByRole("status").textContent).toBe("Copy failed"));
176
+ expect(screen.getByRole("button", { name: "Copy to clipboard" })).toBeTruthy();
177
+ warn.mockRestore();
178
+ });
140
179
  });
@@ -138,7 +138,7 @@ export const CodeBlock = memo(
138
138
  >
139
139
  {headerLabel && (
140
140
  <div className="flex items-center justify-between border-b border-border px-3 py-1 bg-[var(--code-header-bg,hsl(var(--background)))]">
141
- <span className="text-[calc(var(--font-size-xs)-1px)] font-mono font-medium uppercase tracking-widest text-muted-foreground">
141
+ <span className="text-xs font-mono font-medium uppercase tracking-widest text-muted-foreground">
142
142
  {headerLabel}
143
143
  </span>
144
144
  {children}
@@ -180,6 +180,9 @@ CodeBlock.displayName = "CodeBlock";
180
180
  /** Copy-to-clipboard button for use inside CodeBlock. */
181
181
  export const CopyButton = memo(({ text }: { text: string }) => {
182
182
  const [copied, setCopied] = useState(false);
183
+ // Status text for the live region. Cleared before each attempt so a repeat
184
+ // copy or a failure is announced again.
185
+ const [status, setStatus] = useState("");
183
186
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
184
187
 
185
188
  useEffect(() => {
@@ -187,31 +190,47 @@ export const CopyButton = memo(({ text }: { text: string }) => {
187
190
  }, []);
188
191
 
189
192
  const handleCopy = useCallback(async () => {
193
+ setStatus("");
194
+ if (timerRef.current) clearTimeout(timerRef.current);
190
195
  try {
191
196
  await navigator.clipboard.writeText(text);
192
197
  setCopied(true);
193
- if (timerRef.current) clearTimeout(timerRef.current);
194
- timerRef.current = setTimeout(() => setCopied(false), 2000);
198
+ setStatus("Copied to clipboard");
195
199
  } catch (err) {
196
200
  console.warn("Clipboard write failed:", err);
201
+ setCopied(false);
202
+ setStatus("Copy failed");
197
203
  }
204
+ timerRef.current = setTimeout(() => {
205
+ setCopied(false);
206
+ setStatus("");
207
+ }, 2000);
198
208
  }, [text]);
199
209
 
210
+ const label = copied ? "Copied" : "Copy to clipboard";
200
211
  return (
201
- <button
202
- onClick={handleCopy}
203
- className={cn(
204
- "flex items-center justify-center w-6 h-6 rounded-md bg-muted border border-border hover:border-[var(--border-strong)] transition-colors",
205
- focusRing,
206
- )}
207
- title="Copy to clipboard"
208
- >
209
- {copied ? (
210
- <Check className="w-3.5 h-3.5 text-emerald-500" />
211
- ) : (
212
- <Copy className="w-3.5 h-3.5 text-muted-foreground" />
213
- )}
214
- </button>
212
+ <>
213
+ <button
214
+ type="button"
215
+ onClick={handleCopy}
216
+ className={cn(
217
+ "flex items-center justify-center w-6 h-6 rounded-md bg-muted border border-border hover:border-[var(--border-strong)] transition-colors",
218
+ focusRing,
219
+ )}
220
+ aria-label={label}
221
+ title={label}
222
+ >
223
+ {copied ? (
224
+ <Check aria-hidden="true" className="w-3.5 h-3.5 text-[var(--surface-success-text)]" />
225
+ ) : (
226
+ <Copy aria-hidden="true" className="w-3.5 h-3.5 text-muted-foreground" />
227
+ )}
228
+ </button>
229
+ {/* Announce the result; the icon swap alone is silent to screen readers. */}
230
+ <span className="sr-only" role="status" aria-live="polite">
231
+ {status}
232
+ </span>
233
+ </>
215
234
  );
216
235
  });
217
236
  CopyButton.displayName = "CopyButton";
@@ -0,0 +1,80 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import type { StatusTone } from "./status-pill";
4
+
5
+ /**
6
+ * An inline message that stays on the page: a policy note above a form, a
7
+ * failed save beside the field it concerns. For a message that interrupts, use
8
+ * `AlertDialog`; for one that goes away by itself, use a toast.
9
+ *
10
+ * Each tone paints its fill, border and text from Brand's matched status
11
+ * triple, the same one `StatusPill` uses, so the text is solved against its
12
+ * own background on every plane. `neutral` is a card with body text.
13
+ *
14
+ * `danger` and `warning` announce themselves (`role="alert"`); the quieter
15
+ * tones are `role="status"` so a screen reader is not interrupted by a note.
16
+ */
17
+ export type AlertTone = Exclude<StatusTone, "running">;
18
+
19
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
20
+ tone?: AlertTone;
21
+ /** A leading icon. It takes the tone's colour and sits beside the title. */
22
+ icon?: React.ReactNode;
23
+ }
24
+
25
+ const TONE: Record<AlertTone, string> = {
26
+ neutral: "border-border bg-card text-card-foreground",
27
+ info: "border-[var(--surface-info-border)] bg-[var(--surface-info-bg)] text-[var(--surface-info-text)]",
28
+ success:
29
+ "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
30
+ warning:
31
+ "border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)] text-[var(--surface-warning-text)]",
32
+ danger:
33
+ "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]",
34
+ };
35
+
36
+ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
37
+ ({ tone = "neutral", icon, className, children, role, ...props }, ref) => (
38
+ <div
39
+ ref={ref}
40
+ role={role ?? (tone === "danger" || tone === "warning" ? "alert" : "status")}
41
+ data-tone={tone}
42
+ className={cn(
43
+ "flex w-full min-w-0 gap-3 rounded-lg border px-4 py-3 text-sm",
44
+ TONE[tone],
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ {icon ? (
50
+ <span aria-hidden="true" className="mt-0.5 flex shrink-0 [&_svg]:size-4">
51
+ {icon}
52
+ </span>
53
+ ) : null}
54
+ <div className="grid min-w-0 flex-1 gap-1">{children}</div>
55
+ </div>
56
+ ),
57
+ );
58
+ Alert.displayName = "Alert";
59
+
60
+ export function AlertTitle({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
61
+ return <p className={cn("font-medium leading-snug [overflow-wrap:anywhere]", className)} {...props} />;
62
+ }
63
+ AlertTitle.displayName = "AlertTitle";
64
+
65
+ /**
66
+ * Body text. Under a toned alert it keeps the tone's ink, which is the colour
67
+ * tested against the tone's fill; under `neutral` it is the muted tier.
68
+ */
69
+ export function AlertDescription({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
70
+ return (
71
+ <div
72
+ className={cn(
73
+ "leading-relaxed [overflow-wrap:anywhere] [[data-tone=neutral]_&]:text-muted-foreground",
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+ AlertDescription.displayName = "AlertDescription";
@@ -0,0 +1,18 @@
1
+ import { cleanup, render, screen } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { EmptyState } from "./empty-state";
4
+
5
+ afterEach(cleanup);
6
+
7
+ describe("EmptyState", () => {
8
+ it("renders the title as h3 by default", () => {
9
+ render(<EmptyState title="No runs yet" />);
10
+ expect(screen.getByRole("heading", { level: 3, name: "No runs yet" })).toBeTruthy();
11
+ });
12
+
13
+ it("uses the requested heading level", () => {
14
+ render(<EmptyState title="No suites yet" titleAs="h2" description="Add one." />);
15
+ expect(screen.getByRole("heading", { level: 2, name: "No suites yet" })).toBeTruthy();
16
+ expect(screen.queryByRole("heading", { level: 3 })).toBeNull();
17
+ });
18
+ });
@@ -6,10 +6,13 @@ export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
6
6
  title: string;
7
7
  description?: string;
8
8
  action?: React.ReactNode;
9
+ /** Heading element for the title. Pick the level that follows the page's
10
+ * outline, e.g. h2 directly under a page h1. Defaults to h3. */
11
+ titleAs?: "h2" | "h3" | "h4";
9
12
  }
10
13
 
11
14
  const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
12
- ({ className, icon, title, description, action, ...props }, ref) => {
15
+ ({ className, icon, title, description, action, titleAs: Title = "h3", ...props }, ref) => {
13
16
  return (
14
17
  <div
15
18
  ref={ref}
@@ -24,7 +27,7 @@ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
24
27
  {icon}
25
28
  </div>
26
29
  )}
27
- <h3 className="font-semibold text-lg">{title}</h3>
30
+ <Title className="font-semibold text-lg">{title}</Title>
28
31
  {description && (
29
32
  <p className="mt-2 max-w-sm text-muted-foreground text-sm">
30
33
  {description}
@@ -190,6 +190,12 @@ export type { PageShellProps } from "./page-shell";
190
190
  export { PageHeader } from "./page-header";
191
191
  export type { PageHeaderProps } from "./page-header";
192
192
 
193
+ export { Alert, AlertDescription, AlertTitle } from "./alert";
194
+ export type { AlertProps, AlertTone } from "./alert";
195
+
196
+ export { Pagination } from "./pagination";
197
+ export type { PaginationProps } from "./pagination";
198
+
193
199
  export { StatusPill } from "./status-pill";
194
200
  export type { StatusPillProps, StatusTone } from "./status-pill";
195
201
 
@@ -116,4 +116,19 @@ describe("MetricStrip", () => {
116
116
  expect(strip.current!.className).not.toContain("sm:grid-cols-4");
117
117
  expect(Array.from(strip.current!.querySelectorAll("dt")).map((node) => node.textContent)).toEqual(["Spend", "Calls", "Average"]);
118
118
  });
119
+
120
+ it("renders an optional icon before the label without naming the term", () => {
121
+ render(
122
+ <MetricStrip>
123
+ <Metric label="Steps" value="4/5" icon={<svg data-testid="steps-icon" />} />
124
+ <Metric label="Cost" value="$0.12" />
125
+ </MetricStrip>,
126
+ );
127
+ const icon = screen.getByTestId("steps-icon");
128
+ const dt = screen.getByText("Steps").closest("dt")!;
129
+ expect(dt.contains(icon)).toBe(true);
130
+ expect(icon.parentElement!.getAttribute("aria-hidden")).toBe("true");
131
+ expect(dt.firstElementChild).toBe(icon.parentElement);
132
+ expect(screen.getByText("Cost").closest("dt")!.querySelector('[aria-hidden="true"]')).toBeNull();
133
+ });
119
134
  });
@@ -45,17 +45,28 @@ export interface MetricProps extends React.HTMLAttributes<HTMLDivElement> {
45
45
  hint?: React.ReactNode;
46
46
  /** Attention is a caller-owned action requirement, never inferred from zero. */
47
47
  attention?: { tone: StatusTone; label: string };
48
+ /** Decorative glyph before the label; the label carries the meaning. A
49
+ * direct svg child is sized to 16px; wrap other content to size it yourself. */
50
+ icon?: React.ReactNode;
48
51
  }
49
52
 
50
53
  /** A described term; the strip, not this item, decides where separators belong. */
51
54
  const Metric = React.forwardRef<HTMLDivElement, MetricProps>(
52
- ({ className, label, value, hint, attention, ...props }, ref) => (
55
+ ({ className, label, value, hint, attention, icon, ...props }, ref) => (
53
56
  <div
54
57
  ref={ref}
55
58
  className={cn("min-w-0 border-border p-4 sm:p-5", className)}
56
59
  {...props}
57
60
  >
58
61
  <dt className="flex min-w-0 flex-wrap items-center gap-2 text-muted-foreground text-sm">
62
+ {icon != null && icon !== false && (
63
+ <span
64
+ aria-hidden="true"
65
+ className="inline-flex shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4"
66
+ >
67
+ {icon}
68
+ </span>
69
+ )}
59
70
  <span className="min-w-0 whitespace-normal [overflow-wrap:anywhere]">{label}</span>
60
71
  {attention && (
61
72
  <StatusPill tone={attention.tone}>{attention.label}</StatusPill>
@@ -0,0 +1,51 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { Info, TriangleAlert } from 'lucide-react'
3
+ import * as React from 'react'
4
+ import { Alert, AlertDescription, AlertTitle } from './alert'
5
+ import { Pagination } from './pagination'
6
+
7
+ const meta: Meta = {
8
+ title: 'Primitives/Pagination and Alert',
9
+ }
10
+ export default meta
11
+
12
+ function PagedTable({ pageCount }: { pageCount: number }) {
13
+ const [page, setPage] = React.useState(0)
14
+ return <Pagination page={page} pageCount={pageCount} onPageChange={setPage} />
15
+ }
16
+
17
+ export const PaginationLengths: StoryObj = {
18
+ render: () => (
19
+ <div className="flex flex-col gap-6 bg-background p-6 text-foreground">
20
+ <PagedTable pageCount={1} />
21
+ <PagedTable pageCount={4} />
22
+ <PagedTable pageCount={48} />
23
+ <PagedTable pageCount={1200} />
24
+ </div>
25
+ ),
26
+ }
27
+
28
+ export const AlertTones: StoryObj = {
29
+ render: () => (
30
+ <div className="flex max-w-xl flex-col gap-3 bg-background p-6 text-foreground">
31
+ <Alert icon={<Info />}>
32
+ <AlertTitle>Public events</AlertTitle>
33
+ <AlertDescription>Anyone with the link can join a public event.</AlertDescription>
34
+ </Alert>
35
+ <Alert tone="info" icon={<Info />}>
36
+ <AlertTitle>Applies from the next billing cycle</AlertTitle>
37
+ </Alert>
38
+ <Alert tone="success">
39
+ <AlertDescription>Changes saved.</AlertDescription>
40
+ </Alert>
41
+ <Alert tone="warning" icon={<TriangleAlert />}>
42
+ <AlertTitle>A_very_long_policy_identifier_without_any_spaces_that_has_to_wrap_inside_the_alert</AlertTitle>
43
+ <AlertDescription>Seats above the limit are removed when the cycle closes.</AlertDescription>
44
+ </Alert>
45
+ <Alert tone="danger" icon={<TriangleAlert />}>
46
+ <AlertTitle>Could not save the policy</AlertTitle>
47
+ <AlertDescription>The server refused the change. Try again.</AlertDescription>
48
+ </Alert>
49
+ </div>
50
+ ),
51
+ }
@@ -0,0 +1,90 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { Alert, AlertDescription, AlertTitle } from "./alert";
5
+ import { Pagination, pageSlots } from "./pagination";
6
+
7
+ const PALETTE = /\b(?:bg|text|border)-(?:slate|zinc|gray|neutral|blue|green|red|amber|emerald|black|white)(?:-\d|\b)/;
8
+
9
+ describe("pageSlots", () => {
10
+ it("lists every page when they fit", () => {
11
+ expect(pageSlots(0, 1)).toEqual([0]);
12
+ expect(pageSlots(3, 7)).toEqual([0, 1, 2, 3, 4, 5, 6]);
13
+ });
14
+
15
+ it("keeps the same number of slots as the current page moves", () => {
16
+ expect(pageSlots(0, 10)).toEqual([0, 1, 2, 3, 4, "gap-end", 9]);
17
+ expect(pageSlots(5, 10)).toEqual([0, "gap-start", 4, 5, 6, "gap-end", 9]);
18
+ expect(pageSlots(9, 10)).toEqual([0, "gap-start", 5, 6, 7, 8, 9]);
19
+ for (let page = 0; page < 40; page++) expect(pageSlots(page, 40)).toHaveLength(7);
20
+ });
21
+
22
+ it("clamps a page outside the range", () => {
23
+ expect(pageSlots(-3, 10)).toEqual(pageSlots(0, 10));
24
+ expect(pageSlots(99, 10)).toEqual(pageSlots(9, 10));
25
+ });
26
+ });
27
+
28
+ describe("Pagination", () => {
29
+ it("renders nothing for a single page", () => {
30
+ const { container } = render(<Pagination page={0} pageCount={1} onPageChange={vi.fn()} />);
31
+ expect(container).toBeEmptyDOMElement();
32
+ });
33
+
34
+ it("marks the current page and moves with previous, next and a number", async () => {
35
+ const user = userEvent.setup();
36
+ const onPageChange = vi.fn();
37
+ render(<Pagination page={4} pageCount={12} onPageChange={onPageChange} />);
38
+
39
+ expect(screen.getByRole("navigation", { name: "Pagination" })).toBeInTheDocument();
40
+ const current = screen.getByRole("button", { name: "Page 5" });
41
+ expect(current).toHaveAttribute("aria-current", "page");
42
+ expect(current.className).not.toMatch(PALETTE);
43
+
44
+ await user.click(screen.getByRole("button", { name: "Previous page" }));
45
+ await user.click(screen.getByRole("button", { name: "Next page" }));
46
+ await user.click(screen.getByRole("button", { name: "Page 12" }));
47
+ expect(onPageChange.mock.calls).toEqual([[3], [5], [11]]);
48
+ });
49
+
50
+ it("disables previous on the first page and next on the last", () => {
51
+ const { rerender } = render(<Pagination page={0} pageCount={3} onPageChange={vi.fn()} />);
52
+ expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
53
+ rerender(<Pagination page={2} pageCount={3} onPageChange={vi.fn()} />);
54
+ expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
55
+ });
56
+
57
+ it("does not report the page that is already current", async () => {
58
+ const user = userEvent.setup();
59
+ const onPageChange = vi.fn();
60
+ render(<Pagination page={1} pageCount={3} onPageChange={onPageChange} />);
61
+ await user.click(screen.getByRole("button", { name: "Page 2" }));
62
+ expect(onPageChange).not.toHaveBeenCalled();
63
+ });
64
+ });
65
+
66
+ describe("Alert", () => {
67
+ it("paints a tone from the matched Brand status triple", () => {
68
+ render(
69
+ <Alert tone="warning">
70
+ <AlertTitle>Policy is changing</AlertTitle>
71
+ <AlertDescription>Seats above the limit are removed next cycle.</AlertDescription>
72
+ </Alert>,
73
+ );
74
+ const alert = screen.getByRole("alert");
75
+ expect(alert.className).toContain("bg-[var(--surface-warning-bg)]");
76
+ expect(alert.className).toContain("text-[var(--surface-warning-text)]");
77
+ expect(alert.className).not.toMatch(PALETTE);
78
+ });
79
+
80
+ it("uses role status for quiet tones and keeps an explicit role", () => {
81
+ const { rerender } = render(<Alert tone="info">Saved</Alert>);
82
+ expect(screen.getByRole("status")).toHaveTextContent("Saved");
83
+ rerender(
84
+ <Alert tone="neutral" role="note">
85
+ Note
86
+ </Alert>,
87
+ );
88
+ expect(screen.getByRole("note")).toHaveAttribute("data-tone", "neutral");
89
+ });
90
+ });
@@ -0,0 +1,117 @@
1
+ import { ChevronLeft, ChevronRight } from "lucide-react";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/utils";
4
+ import { buttonVariants } from "./button";
5
+
6
+ /**
7
+ * Page controls for a paged list or table: previous, the page numbers around
8
+ * the current one with the first and last always reachable, and next.
9
+ *
10
+ * Pages are zero-based in the API and one-based on screen. With one page or
11
+ * none there is nothing to choose, so it renders nothing.
12
+ */
13
+ export interface PaginationProps
14
+ extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "children"> {
15
+ /** The current page, zero-based. */
16
+ page: number;
17
+ pageCount: number;
18
+ onPageChange: (page: number) => void;
19
+ /** Pages shown on each side of the current one before an ellipsis. */
20
+ siblings?: number;
21
+ }
22
+
23
+ type PageSlot = number | "gap-start" | "gap-end";
24
+
25
+ /**
26
+ * The visible slots for `page` of `pageCount`. The number of slots stays the
27
+ * same as the current page moves, so the controls do not shift under the
28
+ * pointer while a person clicks through.
29
+ */
30
+ export function pageSlots(page: number, pageCount: number, siblings = 1): PageSlot[] {
31
+ const last = pageCount - 1;
32
+ if (pageCount <= siblings * 2 + 5) return Array.from({ length: pageCount }, (_, i) => i);
33
+
34
+ const current = Math.min(Math.max(page, 0), last);
35
+ const start = Math.max(Math.min(current - siblings, last - siblings * 2 - 2), 2);
36
+ const end = Math.min(Math.max(current + siblings, siblings * 2 + 2), last - 2);
37
+
38
+ const slots: PageSlot[] = [0, start > 2 ? "gap-start" : 1];
39
+ for (let i = start; i <= end; i++) slots.push(i);
40
+ slots.push(end < last - 2 ? "gap-end" : last - 1, last);
41
+ return slots;
42
+ }
43
+
44
+ const pageButton = cn(buttonVariants({ variant: "ghost", size: "sm" }), "min-w-8 px-2 tabular-nums");
45
+
46
+ export function Pagination({
47
+ page,
48
+ pageCount,
49
+ onPageChange,
50
+ siblings = 1,
51
+ className,
52
+ "aria-label": ariaLabel = "Pagination",
53
+ ...rest
54
+ }: PaginationProps) {
55
+ if (pageCount <= 1) return null;
56
+
57
+ const last = pageCount - 1;
58
+ const current = Math.min(Math.max(page, 0), last);
59
+ const go = (next: number) => {
60
+ const target = Math.min(Math.max(next, 0), last);
61
+ if (target !== current) onPageChange(target);
62
+ };
63
+
64
+ return (
65
+ <nav aria-label={ariaLabel} className={cn("flex items-center", className)} {...rest}>
66
+ <ul className="flex flex-wrap items-center gap-1">
67
+ <li>
68
+ <button
69
+ type="button"
70
+ aria-label="Previous page"
71
+ disabled={current === 0}
72
+ onClick={() => go(current - 1)}
73
+ className={cn(pageButton, "gap-1 pl-1.5")}
74
+ >
75
+ <ChevronLeft aria-hidden="true" />
76
+ <span className="hidden sm:inline">Previous</span>
77
+ </button>
78
+ </li>
79
+ {pageSlots(current, pageCount, siblings).map((slot) =>
80
+ typeof slot === "number" ? (
81
+ <li key={slot}>
82
+ <button
83
+ type="button"
84
+ aria-label={`Page ${slot + 1}`}
85
+ aria-current={slot === current ? "page" : undefined}
86
+ onClick={() => go(slot)}
87
+ className={cn(
88
+ pageButton,
89
+ slot === current &&
90
+ "border-border bg-card font-semibold text-foreground hover:bg-card",
91
+ )}
92
+ >
93
+ {slot + 1}
94
+ </button>
95
+ </li>
96
+ ) : (
97
+ <li key={slot} aria-hidden="true" className="flex min-w-8 justify-center text-sm text-muted-foreground">
98
+ …
99
+ </li>
100
+ ),
101
+ )}
102
+ <li>
103
+ <button
104
+ type="button"
105
+ aria-label="Next page"
106
+ disabled={current === last}
107
+ onClick={() => go(current + 1)}
108
+ className={cn(pageButton, "gap-1 pr-1.5")}
109
+ >
110
+ <span className="hidden sm:inline">Next</span>
111
+ <ChevronRight aria-hidden="true" />
112
+ </button>
113
+ </li>
114
+ </ul>
115
+ </nav>
116
+ );
117
+ }
@@ -326,3 +326,23 @@ describe("SegmentedControl", () => {
326
326
  expect(screen.getByRole("radiogroup")).toHaveAttribute("id", "scope-control")
327
327
  })
328
328
  })
329
+
330
+ describe("SegmentedControl test ids", () => {
331
+ it("renders option and group test ids for end-to-end selectors", () => {
332
+ render(
333
+ <SegmentedControl
334
+ aria-label="Workbench view"
335
+ data-testid="workbench-tabs"
336
+ value="code"
337
+ onValueChange={vi.fn()}
338
+ options={[
339
+ { value: "code", label: "Code", testId: "workbench-tab-code" },
340
+ { value: "preview", label: "Preview", testId: "workbench-tab-preview" },
341
+ ]}
342
+ />,
343
+ )
344
+ expect(screen.getByTestId("workbench-tabs")).toHaveAttribute("role", "radiogroup")
345
+ expect(screen.getByTestId("workbench-tab-preview")).toHaveAttribute("aria-checked", "false")
346
+ expect(screen.getByTestId("workbench-tab-code")).toHaveAttribute("aria-checked", "true")
347
+ })
348
+ })
@@ -29,6 +29,8 @@ export interface SegmentedControlOption<T extends string = string> {
29
29
  label: React.ReactNode
30
30
  /** Rendered right of the label, typically a count or status pill. */
31
31
  adornment?: React.ReactNode
32
+ /** Rendered as the option button's `data-testid`, for end-to-end selectors. */
33
+ testId?: string
32
34
  }
33
35
 
34
36
  export interface SegmentedControlProps<T extends string = string>
@@ -43,6 +45,8 @@ export interface SegmentedControlProps<T extends string = string>
43
45
  * reads as a classic tab (used on the Team page)
44
46
  */
45
47
  variant?: "row" | "tabs"
48
+ /** Rendered as the group's `data-testid`. */
49
+ "data-testid"?: string
46
50
  }
47
51
 
48
52
  export function SegmentedControl<T extends string = string>({
@@ -93,6 +97,7 @@ export function SegmentedControl<T extends string = string>({
93
97
  id={rest.id}
94
98
  aria-label={rest["aria-label"]}
95
99
  aria-labelledby={rest["aria-labelledby"]}
100
+ data-testid={rest["data-testid"]}
96
101
  onKeyDown={handleKeyDown}
97
102
  className={cn(
98
103
  "flex gap-1",
@@ -116,6 +121,7 @@ export function SegmentedControl<T extends string = string>({
116
121
  type="button"
117
122
  role="radio"
118
123
  aria-checked={active}
124
+ data-testid={option.testId}
119
125
  tabIndex={focusable ? 0 : -1}
120
126
  onClick={() => {
121
127
  if (!active) onValueChange(option.value)