@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 +12 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/markdown/code-block.js +30 -8
- package/dist/primitives/alert.d.ts +35 -0
- package/dist/primitives/alert.js +47 -0
- package/dist/primitives/empty-state.d.ts +3 -0
- package/dist/primitives/empty-state.js +2 -2
- package/dist/primitives/metric-strip.d.ts +3 -0
- package/dist/primitives/metric-strip.js +16 -8
- package/dist/primitives/pagination.d.ts +19 -0
- package/dist/primitives/pagination.js +81 -0
- package/dist/primitives/segmented-control.d.ts +4 -0
- package/dist/primitives/segmented-control.js +4 -1
- package/dist/primitives.d.ts +3 -1
- package/dist/primitives.js +3 -1
- package/package.json +2 -2
- package/src/markdown/code-block.test.tsx +42 -3
- package/src/markdown/code-block.tsx +36 -17
- package/src/primitives/alert.tsx +80 -0
- package/src/primitives/empty-state.test.tsx +18 -0
- package/src/primitives/empty-state.tsx +5 -2
- package/src/primitives/index.ts +6 -0
- package/src/primitives/metric-strip.test.tsx +15 -0
- package/src/primitives/metric-strip.tsx +12 -1
- package/src/primitives/pagination-alert.stories.tsx +51 -0
- package/src/primitives/pagination-alert.test.tsx +90 -0
- package/src/primitives/pagination.tsx +117 -0
- package/src/primitives/segmented-control.test.tsx +20 -0
- package/src/primitives/segmented-control.tsx +6 -0
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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(
|
|
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: [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
45
|
+
"data-testid": option.testId,
|
|
46
|
+
tabIndex: focusable ? 0 : -1,
|
|
44
47
|
onClick: () => {
|
|
45
48
|
if (!active) onValueChange(option.value);
|
|
46
49
|
},
|
package/dist/primitives.d.ts
CHANGED
|
@@ -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 };
|
package/dist/primitives.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
<
|
|
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}
|
package/src/primitives/index.ts
CHANGED
|
@@ -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)
|