@tangle-network/ui 11.31.3 → 11.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.32.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c94bde7: Fix shared defects from the Hospitality design audit.
8
+
9
+ Brand: dark `--destructive` is now a light red carrying dark ink (`0 90% 77%` on `0 60% 10%`), so `text-destructive` clears AA (at least 4.5:1) on the canvas, card, popover and muted planes of every dark theme; it measured 2.5:1 on Hospitality's dark card. Destructive fills keep their foreground at 8:1. The `--font-sans` and `--font-display` stacks name `"Inter Variable"` ahead of `"Inter"`, and the README now recommends `@fontsource-variable/inter` so apps render every weight the scale uses. Brand still loads no fonts itself.
10
+
11
+ UI: the `tabs` SegmentedControl scrolls the selected tab into view inside the strip (never the page) and fades the edge that has more tabs past it; tab items take a 44px hit height on coarse pointers; labels keep one weight in both variants, so changing the selection no longer shifts the row. SelectTrigger values end in an ellipsis with a gap before the chevron instead of clipping mid-glyph. Plain-text code fences (`text`, `txt`, `plain`, `plaintext`) show no language label.
12
+
13
+ ## 11.32.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 02b2d30: One shell header height and one transcript type scale. Brand adds `--shell-header-height` (56px, utility `h-shell-header`) and `--shell-inset-gutter` so every shell header row shares one bottom divider, and `--transcript-font-size` / `--transcript-line-height` with the `tangle-transcript-text` rule. UI's `AgentTimeline`, `UserMessage` and `ChatMessage` set the person's and the agent's message text with the same `TRANSCRIPT_TEXT` treatment (exported), so they differ only by alignment and surface; an agent message no longer renders at a different leading from the person's. `tangle-drift` gates two new metrics: `header_height_literal` (a header row given its own height) and `transcript_type_override` (product CSS restyling transcript text).
18
+
3
19
  ## 11.31.3
4
20
 
5
21
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  import { focusRing } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
3
  import { Markdown } from "../markdown/markdown.js";
4
+ import "./transcript-text.js";
4
5
  import { MessageAuthor } from "./message-author.js";
5
6
  import { UserMessage } from "./user-message.js";
6
7
  import { ThinkingIndicator } from "./thinking-indicator.js";
@@ -62,7 +63,7 @@ function AssistantMessage({ item, author }) {
62
63
  className: "mb-1.5"
63
64
  }),
64
65
  item.content && /* @__PURE__ */ jsx(Markdown, {
65
- className: cn("tangle-prose text-[length:var(--font-size-base)] leading-[1.5]", item.isStreaming && STREAMING_CARET),
66
+ className: cn("tangle-prose", "tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]", item.isStreaming && STREAMING_CARET),
66
67
  children: item.content
67
68
  }),
68
69
  item.isStreaming && !item.content && /* @__PURE__ */ jsx("span", {
@@ -1,8 +1,18 @@
1
1
  import { cn } from "../lib/utils.js";
2
2
  import { Markdown } from "../markdown/markdown.js";
3
+ import { TRANSCRIPT_TEXT } from "./transcript-text.js";
3
4
  import { MessageAuthor, isViewerMessage } from "./message-author.js";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  //#region src/chat/chat-message.tsx
7
+ /**
8
+ * ChatMessage — one message in the conversation.
9
+ *
10
+ * Supports user messages (plain text) and assistant messages
11
+ * (rich markdown with inline tool call activity). With `author` and
12
+ * `viewerId` it renders a transcript shared by several people: the reader's
13
+ * own messages stay at the end, everyone else's sit at the start under
14
+ * their author.
15
+ */
6
16
  function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, className, userLabel = "You", assistantLabel = "Agent", hideRoleLabel, author, viewerId }) {
7
17
  const isUser = role === "user";
8
18
  const isAssistant = role === "assistant";
@@ -23,10 +33,10 @@ function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, classNa
23
33
  }), /* @__PURE__ */ jsxs("div", {
24
34
  className: cn("min-w-0 max-w-[85%] space-y-1", fromViewer ? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background" : fromOtherPerson ? "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-foreground" : isAssistant ? "text-foreground" : "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"),
25
35
  children: [isUser ? /* @__PURE__ */ jsx("div", {
26
- className: "whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
36
+ className: cn("whitespace-pre-wrap", TRANSCRIPT_TEXT),
27
37
  children: content
28
38
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [content && /* @__PURE__ */ jsx(Markdown, {
29
- className: "tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
39
+ className: cn("tangle-prose", "tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"),
30
40
  children: content
31
41
  }), isStreaming && /* @__PURE__ */ jsx("span", { className: "ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" })] }), toolCalls]
32
42
  })]
@@ -0,0 +1,11 @@
1
+ //#region src/chat/transcript-text.d.ts
2
+ /**
3
+ * The one text treatment for every chat message body, the person's and the
4
+ * agent's alike, so the two differ only by alignment and surface.
5
+ * `tangle-transcript-text` (brand globals.css) reads `--transcript-font-size`
6
+ * and `--transcript-line-height`; the utilities repeat the same tokens for a
7
+ * host that has not loaded brand's globals. `length:` types the token: a bare
8
+ * `text-[var(--x)]` compiles to a color.
9
+ */
10
+ export declare const TRANSCRIPT_TEXT = "tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]";
11
+ //#endregion
@@ -0,0 +1,12 @@
1
+ //#region src/chat/transcript-text.ts
2
+ /**
3
+ * The one text treatment for every chat message body, the person's and the
4
+ * agent's alike, so the two differ only by alignment and surface.
5
+ * `tangle-transcript-text` (brand globals.css) reads `--transcript-font-size`
6
+ * and `--transcript-line-height`; the utilities repeat the same tokens for a
7
+ * host that has not loaded brand's globals. `length:` types the token: a bare
8
+ * `text-[var(--x)]` compiles to a color.
9
+ */
10
+ const TRANSCRIPT_TEXT = "tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]";
11
+ //#endregion
12
+ export { TRANSCRIPT_TEXT };
@@ -1,4 +1,5 @@
1
1
  import { SyntheticText, userTextSegments } from "./synthetic-text.js";
2
+ import { TRANSCRIPT_TEXT } from "./transcript-text.js";
2
3
  import { MessageAuthor, isViewerMessage } from "./message-author.js";
3
4
  import { memo } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -27,7 +28,7 @@ function UserBubble({ text, timestamp, actions }) {
27
28
  className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
28
29
  children: formatTime(timestamp)
29
30
  }) : null, /* @__PURE__ */ jsx("div", {
30
- className: "break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]",
31
+ className: `break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`,
31
32
  children: text
32
33
  })]
33
34
  }), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
@@ -53,7 +54,7 @@ function OtherPersonBubble({ text, timestamp, actions, author }) {
53
54
  className: "pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
54
55
  children: formatTime(timestamp)
55
56
  }) : null, /* @__PURE__ */ jsx("div", {
56
- className: "break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]",
57
+ className: `break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`,
57
58
  children: text
58
59
  })]
59
60
  }),
package/dist/chat.d.ts CHANGED
@@ -6,4 +6,5 @@ import { ChatMessage, ChatMessageProps, MessageRole } from "./chat/chat-message.
6
6
  import { MessageAuthor, MessageAuthorProps, isViewerMessage } from "./chat/message-author.js";
7
7
  import { ThinkingIndicator, ThinkingIndicatorProps } from "./chat/thinking-indicator.js";
8
8
  import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem } from "./chat/agent-timeline.js";
9
- export { AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, type ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps, isViewerMessage };
9
+ import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
10
+ export { AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, type ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, TRANSCRIPT_TEXT, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps, isViewerMessage };
package/dist/chat.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
1
2
  import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
2
3
  import { UserMessage } from "./chat/user-message.js";
3
4
  import { MessageList } from "./chat/message-list.js";
@@ -5,4 +6,4 @@ import { ThinkingIndicator } from "./chat/thinking-indicator.js";
5
6
  import { AgentTimeline } from "./chat/agent-timeline.js";
6
7
  import { ChatContainer } from "./chat/chat-container.js";
7
8
  import { ChatMessage } from "./chat/chat-message.js";
8
- export { AgentTimeline, ChatContainer, ChatMessage, MessageAuthor, MessageList, ThinkingIndicator, UserMessage, isViewerMessage };
9
+ export { AgentTimeline, ChatContainer, ChatMessage, MessageAuthor, MessageList, TRANSCRIPT_TEXT, ThinkingIndicator, UserMessage, isViewerMessage };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ import { ThinkingIndicator, ThinkingIndicatorProps } from "./chat/thinking-indic
18
18
  import { ToolCallStatus, ToolCallType } from "./run/tool-call-step.js";
19
19
  import { FeedSegment, ToolCallData, ToolCallFeed, ToolCallFeedProps, parseToolEvent } from "./run/tool-call-feed.js";
20
20
  import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem } from "./chat/agent-timeline.js";
21
+ import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
21
22
  import "./chat.js";
22
23
  import { ArtifactPane, ArtifactPaneProps } from "./primitives/artifact-pane.js";
23
24
  import { FileNode, FileTree, FileTreeProps, FileTreeVisibilityOptions, filterFileTree } from "./files/file-tree.js";
@@ -129,4 +130,4 @@ import { QuestionPreview, QuestionPreviewProps } from "./tool-previews/question-
129
130
  import { DiffPreview, DiffPreviewProps } from "./tool-previews/diff-preview.js";
130
131
  import { RedactedDocSegment, RedactedDocument, RedactedDocumentData, RedactedDocumentProps, RevealResult } from "./redaction/redacted-document.js";
131
132
  import "./redaction.js";
132
- 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, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, 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, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, ChatAuthor, 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, DatePicker, type DatePickerProps, 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, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, 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 MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, 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, PdfViewer, type PdfViewerConfig, type PdfViewerProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, 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, type RunPhase, RunPhaseList, 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, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, 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, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, localToday, messagesAtom, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, tableCsv, taskProgress, taskStatus, 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 };
133
+ 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, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, 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, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, ChatAuthor, 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, DatePicker, type DatePickerProps, 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, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, 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 MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, 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, PdfViewer, type PdfViewerConfig, type PdfViewerProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, 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, type RunPhase, RunPhaseList, 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, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, 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, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, localToday, messagesAtom, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, tableCsv, taskProgress, taskStatus, 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
@@ -78,6 +78,7 @@ import { OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES } from "./openui/schema-data
78
78
  import { findUnsupportedOpenUINode, validateOpenUIJsonArtifact, validateOpenUIJsonNode } from "./openui-schema.js";
79
79
  import { OpenUIArtifactRenderer } from "./openui/openui-artifact-renderer.js";
80
80
  import { RunGroup } from "./run/run-group.js";
81
+ import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
81
82
  import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
82
83
  import { UserMessage } from "./chat/user-message.js";
83
84
  import { MessageList } from "./chat/message-list.js";
@@ -113,4 +114,4 @@ import { addMessage, addParts, clearChat, isStreamingAtom, messagesAtom, partMap
113
114
  import { copyText } from "./utils/copy-text.js";
114
115
  import { timeAgo } from "./utils/time-ago.js";
115
116
  import { RedactedDocument } from "./redaction/redacted-document.js";
116
- export { AgentTimeline, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, 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, DatePicker, 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, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, 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, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, localToday, messagesAtom, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, tableCsv, taskProgress, taskStatus, 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 { AgentTimeline, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, 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, DatePicker, 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, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, 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, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, localToday, messagesAtom, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, tableCsv, taskProgress, taskStatus, 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 };
@@ -28,6 +28,11 @@ const controlSquare = {
28
28
  * page on focus for anything smaller.
29
29
  */
30
30
  const fieldTouchText = "pointer-coarse:text-base";
31
+ /**
32
+ * A control that is shorter than 44px on a fine pointer still gets a 44px hit
33
+ * height on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
34
+ */
35
+ const touchTarget = "pointer-coarse:min-h-11";
31
36
  /** Field sizes: the control scale with field padding. */
32
37
  const fieldSizes = {
33
38
  sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, "px-2.5"),
@@ -40,4 +45,4 @@ const controlSizes = {
40
45
  };
41
46
  const fieldPresentation = cn(focusField, controlMotion, "min-w-0 bg-[var(--field-surface,var(--bg-input))] text-foreground", "aria-invalid:border-[var(--surface-danger-border)] aria-invalid:hover:border-[var(--surface-danger-border)] aria-invalid:focus:border-[var(--focus-border-danger)] aria-invalid:focus:ring-[var(--focus-halo-danger)]", "autofill:shadow-[inset_0_0_0_1000px_var(--bg-input)] autofill:[-webkit-text-fill-color:var(--text-primary)] autofill:caret-[var(--text-primary)]");
42
47
  //#endregion
43
- export { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText };
48
+ export { controlHeight, controlMotion, controlRadius, controlSizes, controlSquare, controlText, fieldPresentation, fieldSizes, fieldTouchText, touchTarget };
@@ -1,6 +1,5 @@
1
1
  import { HTMLAttributes, ReactNode } from "react";
2
2
  //#region src/markdown/code-block.d.ts
3
- /** The Shiki grammar for a fence language, or undefined for plain text. */
4
3
  export declare function codeBlockGrammar(language: string | undefined): string | undefined;
5
4
  export interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
6
5
  code: string;
@@ -123,6 +123,12 @@ const LANGUAGE_NAMES = {
123
123
  dockerfile: "Dockerfile"
124
124
  };
125
125
  /** The Shiki grammar for a fence language, or undefined for plain text. */
126
+ const PLAIN_TEXT = /* @__PURE__ */ new Set([
127
+ "text",
128
+ "txt",
129
+ "plain",
130
+ "plaintext"
131
+ ]);
126
132
  function codeBlockGrammar(language) {
127
133
  if (!language) return void 0;
128
134
  const id = language.toLowerCase();
@@ -191,7 +197,7 @@ const CodeBlock = memo(({ code, language, label, filename, showLineNumbers = fal
191
197
  active = false;
192
198
  };
193
199
  }, [code, grammar]);
194
- const languageName = grammar ? LANGUAGE_NAMES[grammar] : language;
200
+ const languageName = grammar ? LANGUAGE_NAMES[grammar] : PLAIN_TEXT.has(language?.toLowerCase() ?? "") ? void 0 : language;
195
201
  const headerLabel = label ?? (filename ? void 0 : languageName);
196
202
  const hasHeader = Boolean(filename || headerLabel);
197
203
  const mode = light === void 0 ? {} : { "data-theme": light ? "light" : "dark" };
@@ -17,6 +17,12 @@ import * as React from "react";
17
17
  * that darken on hover. This keeps the selected segment as the
18
18
  * single visual anchor instead of the whole group competing with
19
19
  * itself.
20
+ * - Every label keeps one weight. Selection is colour plus surface or
21
+ * underline, so changing the value never reflows the row.
22
+ *
23
+ * The tabs variant scrolls sideways when its options outgrow the row: the
24
+ * selected tab is kept in view, and a faded edge shows that more tabs sit
25
+ * past it.
20
26
  */
21
27
  export interface SegmentedControlOption<T extends string = string> {
22
28
  value: T;
@@ -1,12 +1,61 @@
1
1
  "use client";
2
2
  import { focusRing, focusRingInset } from "../lib/focus.js";
3
3
  import { cn } from "../lib/utils.js";
4
+ import "../lib/control-presentation.js";
4
5
  import * as React from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/primitives/segmented-control.tsx
8
+ /** Width of the faded edge that marks more tabs past the strip, in px. */
9
+ const TAB_EDGE_FADE = 24;
10
+ const tabEdgeFade = [
11
+ "data-[overflow-start]:[mask-image:linear-gradient(to_right,transparent,#000_24px)]",
12
+ "data-[overflow-end]:[mask-image:linear-gradient(to_right,#000_calc(100%-24px),transparent)]",
13
+ "data-[overflow-start]:data-[overflow-end]:[mask-image:linear-gradient(to_right,transparent,#000_24px,#000_calc(100%-24px),transparent)]"
14
+ ];
7
15
  function SegmentedControl({ value, onValueChange, options, variant = "row", className, ...rest }) {
8
16
  if (typeof process !== "undefined" && process?.env?.NODE_ENV !== "production" && !rest["aria-label"] && !rest["aria-labelledby"]) console.warn("[SegmentedControl] role=\"radiogroup\" requires either aria-label or aria-labelledby for accessibility.");
9
17
  const optionRefs = React.useRef(/* @__PURE__ */ new Map());
18
+ const stripRef = React.useRef(null);
19
+ const [overflow, setOverflow] = React.useState({
20
+ start: false,
21
+ end: false
22
+ });
23
+ const tabs = variant === "tabs";
24
+ const measureOverflow = React.useCallback(() => {
25
+ const strip = stripRef.current;
26
+ if (!strip) return;
27
+ const start = strip.scrollLeft > 1;
28
+ const end = strip.scrollLeft + strip.clientWidth < strip.scrollWidth - 1;
29
+ setOverflow((prev) => prev.start === start && prev.end === end ? prev : {
30
+ start,
31
+ end
32
+ });
33
+ }, []);
34
+ React.useLayoutEffect(() => {
35
+ if (!tabs) return;
36
+ const strip = stripRef.current;
37
+ const option = optionRefs.current.get(value);
38
+ if (strip && option) {
39
+ const start = option.offsetLeft;
40
+ const end = start + option.offsetWidth;
41
+ if (start < strip.scrollLeft + TAB_EDGE_FADE) strip.scrollLeft = Math.max(0, start - TAB_EDGE_FADE);
42
+ else if (end > strip.scrollLeft + strip.clientWidth - TAB_EDGE_FADE) strip.scrollLeft = end - strip.clientWidth + TAB_EDGE_FADE;
43
+ }
44
+ measureOverflow();
45
+ }, [
46
+ tabs,
47
+ value,
48
+ options.length,
49
+ measureOverflow
50
+ ]);
51
+ React.useEffect(() => {
52
+ if (!tabs || typeof ResizeObserver === "undefined") return;
53
+ const strip = stripRef.current;
54
+ if (!strip) return;
55
+ const observer = new ResizeObserver(measureOverflow);
56
+ observer.observe(strip);
57
+ return () => observer.disconnect();
58
+ }, [tabs, measureOverflow]);
10
59
  const hasMatch = options.some((o) => o.value === value);
11
60
  const handleKeyDown = (e) => {
12
61
  if (options.length === 0) return;
@@ -29,8 +78,12 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
29
78
  "aria-label": rest["aria-label"],
30
79
  "aria-labelledby": rest["aria-labelledby"],
31
80
  "data-testid": rest["data-testid"],
81
+ "data-overflow-start": overflow.start || void 0,
82
+ "data-overflow-end": overflow.end || void 0,
83
+ ref: stripRef,
32
84
  onKeyDown: handleKeyDown,
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),
85
+ onScroll: tabs ? measureOverflow : void 0,
86
+ className: cn("flex gap-1", variant === "row" && "flex-wrap items-center rounded-lg border border-border bg-card p-1", variant === "tabs" && ["relative flex-nowrap items-end border-b border-border pb-0 overflow-x-auto", tabEdgeFade], className),
34
87
  children: options.map((option, i) => {
35
88
  const active = option.value === value;
36
89
  const focusable = active || !hasMatch && i === 0;
@@ -47,7 +100,11 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
47
100
  onClick: () => {
48
101
  if (!active) onValueChange(option.value);
49
102
  },
50
- className: cn("relative inline-flex items-center gap-2 whitespace-nowrap text-sm transition-[color,background-color,border-color,box-shadow]", variant === "row" && ["rounded-md px-3 py-1.5", focusRing], variant === "tabs" && ["rounded-none border-b-2 -mb-px px-4 py-2", focusRingInset], active ? variant === "row" ? "border border-transparent bg-[var(--accent-surface)] text-[var(--accent-text)] font-semibold" : "border-[var(--accent-text)] text-[var(--accent-text)] font-semibold" : variant === "row" ? "border border-transparent text-muted-foreground hover:text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"),
103
+ className: cn("relative inline-flex items-center gap-2 whitespace-nowrap text-sm font-medium transition-[color,background-color,border-color,box-shadow]", variant === "row" && ["rounded-md px-3 py-1.5", focusRing], variant === "tabs" && [
104
+ "shrink-0 rounded-none border-b-2 -mb-px px-4 py-2",
105
+ "pointer-coarse:min-h-11",
106
+ focusRingInset
107
+ ], active ? variant === "row" ? "border border-transparent bg-[var(--accent-surface)] text-[var(--accent-text)]" : "border-[var(--accent-text)] text-[var(--accent-text)]" : variant === "row" ? "border border-transparent text-muted-foreground hover:text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"),
51
108
  children: [/* @__PURE__ */ jsx("span", { children: option.label }), option.adornment && /* @__PURE__ */ jsx("span", {
52
109
  className: cn("text-xs", active ? "text-[var(--accent-text)]/80" : "text-muted-foreground"),
53
110
  children: option.adornment
@@ -11,7 +11,7 @@ const SelectGroup = SelectPrimitive.Group;
11
11
  const SelectValue = SelectPrimitive.Value;
12
12
  const SelectTrigger = React.forwardRef(({ className, children, size = "default", ...props }, ref) => /* @__PURE__ */ jsxs(SelectPrimitive.Trigger, {
13
13
  ref,
14
- className: cn("flex w-full items-center justify-between whitespace-nowrap border py-1 shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", fieldPresentation, fieldTouchText, "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:text-left", "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform", size === "compact" || size === "touch" ? cn(fieldSizes.md, controlSizes[size]) : fieldSizes[size === "default" ? "md" : size], className),
14
+ className: cn("flex w-full items-center justify-between gap-2 whitespace-nowrap border py-1 shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50", fieldPresentation, fieldTouchText, "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:truncate [&>span]:text-left", "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform", size === "compact" || size === "touch" ? cn(fieldSizes.md, controlSizes[size]) : fieldSizes[size === "default" ? "md" : size], className),
15
15
  ...props,
16
16
  children: [children, /* @__PURE__ */ jsx(SelectPrimitive.Icon, {
17
17
  asChild: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.31.3",
3
+ "version": "11.32.1",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -150,7 +150,7 @@
150
150
  "peerDependencies": {
151
151
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
152
152
  "@nanostores/react": ">=0.8.4 <3.0.0",
153
- "@tangle-network/brand": "^1.20.0",
153
+ "@tangle-network/brand": "^1.22.0",
154
154
  "@tiptap/core": ">=3.20.4 <4.0.0",
155
155
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
156
156
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"
3
3
  import { describe, expect, it, vi } from "vitest"
4
4
  import type { ToolPart } from "../types/parts"
5
5
  import { AgentTimeline, type AgentTimelineItem } from "./agent-timeline"
6
+ import { TRANSCRIPT_TEXT } from "./transcript-text"
6
7
 
7
8
  const editPart: ToolPart = {
8
9
  type: "tool",
@@ -287,3 +288,22 @@ describe("AgentTimeline tool actions", () => {
287
288
  expect(screen.getByText("Output")).toBeInTheDocument()
288
289
  })
289
290
  })
291
+
292
+ describe("transcript type scale", () => {
293
+ it("sets the person's and the agent's message text in one family, size and leading", () => {
294
+ render(
295
+ <AgentTimeline
296
+ items={[
297
+ { id: "u", kind: "message", role: "user", content: "Can Luz take the 3 pm massage?" },
298
+ { id: "a", kind: "message", role: "assistant", content: "Yes, Luz is free at 3 pm." },
299
+ ]}
300
+ />,
301
+ )
302
+ const user = screen.getByText("Can Luz take the 3 pm massage?")
303
+ const agent = screen.getByText("Yes, Luz is free at 3 pm.").closest(".tangle-prose")
304
+ const textClasses = (element: Element | null) =>
305
+ [...(element?.classList ?? [])].filter((name) => /^(tangle-transcript-text|text-|leading-|font-)/.test(name)).sort()
306
+ expect(textClasses(user)).toEqual(TRANSCRIPT_TEXT.split(" ").sort())
307
+ expect(textClasses(agent)).toEqual(textClasses(user))
308
+ })
309
+ })
@@ -1,3 +1,4 @@
1
+ import { TRANSCRIPT_TEXT } from "./transcript-text";
1
2
  import { useState, type KeyboardEvent, type ReactNode } from "react";
2
3
  import {
3
4
  AlertTriangle,
@@ -188,10 +189,9 @@ function AssistantMessage({ item, author }: { item: AgentTimelineMessageItem; au
188
189
  {author && <MessageAuthor author={author} className="mb-1.5" />}
189
190
  {item.content && (
190
191
  <Markdown
191
- // `length:` types the token: a bare `text-[var(--x)]` compiles to a
192
- // color, which left the reading size at the Markdown default.
193
192
  className={cn(
194
- "tangle-prose text-[length:var(--font-size-base)] leading-[1.5]",
193
+ "tangle-prose",
194
+ TRANSCRIPT_TEXT,
195
195
  item.isStreaming && STREAMING_CARET,
196
196
  )}
197
197
  >
@@ -8,6 +8,7 @@
8
8
  * their author.
9
9
  */
10
10
 
11
+ import { TRANSCRIPT_TEXT } from "./transcript-text";
11
12
  import { type ReactNode } from "react";
12
13
  import { cn } from "../lib/utils";
13
14
  import { Markdown } from "../markdown/markdown";
@@ -102,12 +103,12 @@ export function ChatMessage({
102
103
  )}
103
104
  >
104
105
  {isUser ? (
105
- <div className="whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">
106
+ <div className={cn("whitespace-pre-wrap", TRANSCRIPT_TEXT)}>
106
107
  {content}
107
108
  </div>
108
109
  ) : (
109
110
  <>
110
- {content && <Markdown className="tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">{content}</Markdown>}
111
+ {content && <Markdown className={cn("tangle-prose", TRANSCRIPT_TEXT)}>{content}</Markdown>}
111
112
  {isStreaming && (
112
113
  <span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-[var(--brand-cool)] align-text-bottom" />
113
114
  )}
package/src/chat/index.ts CHANGED
@@ -17,3 +17,4 @@ export {
17
17
  type AgentTimelineCustomItem,
18
18
  type AgentTimelineTone,
19
19
  } from "./agent-timeline";
20
+ export { TRANSCRIPT_TEXT } from "./transcript-text";
@@ -22,18 +22,18 @@ const segmentedParts: SessionPart[] = [
22
22
  // existed. A transcript that names no author must keep that exact markup.
23
23
  describe("a transcript without authors", () => {
24
24
  it("renders ChatMessage as before", () => {
25
- expect(html(<ChatMessage role="user" content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-end"><div class="flex items-center gap-2 px-1 flex-row-reverse"><span class="font-medium text-foreground text-xs">You</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background"><div class="whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]">Can I break my lease early?</div></div></div>"`)
26
- expect(html(<ChatMessage role="assistant" content="It depends on the lease terms." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 text-foreground"><div class="tangle-prose max-w-none tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]"><p>It depends on the lease terms.</p></div></div></div>"`)
27
- expect(html(<ChatMessage role="system" content="Session started." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"><div class="tangle-prose max-w-none tangle-prose text-[length:var(--font-size-base)] leading-[var(--line-height-base)]"><p>Session started.</p></div></div></div>"`)
25
+ expect(html(<ChatMessage role="user" content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-end"><div class="flex items-center gap-2 px-1 flex-row-reverse"><span class="font-medium text-foreground text-xs">You</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background"><div class="whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div>"`)
26
+ expect(html(<ChatMessage role="assistant" content="It depends on the lease terms." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 text-foreground"><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>It depends on the lease terms.</p></div></div></div>"`)
27
+ expect(html(<ChatMessage role="system" content="Session started." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>Session started.</p></div></div></div>"`)
28
28
  })
29
29
 
30
30
  it("renders UserMessage actions and application notes as before", () => {
31
- expect(html(<UserMessage content="Can I break my lease early?" actions={<button type="button">Copy</button>} />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">Can I break my lease early?</div></div><div class="flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground"><button type="button">Copy</button></div></div></div>"`)
32
- expect(html(<UserMessage parts={segmentedParts} actions={<button type="button">Copy</button>} />)).toMatchInlineSnapshot(`"<div class="space-y-2"><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">I signed the lease in March.</div></div></div></div><div role="note" aria-label="Application note" data-synthetic-text="" class="min-w-0 rounded-md border border-dashed border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground"><div class="mb-1 font-medium">Application note</div><div class="break-words whitespace-pre-wrap">Lease uploaded: lease.pdf</div></div><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">Here it is.</div></div></div></div><div class="flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground"><button type="button">Copy</button></div></div>"`)
31
+ expect(html(<UserMessage content="Can I break my lease early?" actions={<button type="button">Copy</button>} />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div><div class="flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground"><button type="button">Copy</button></div></div></div>"`)
32
+ expect(html(<UserMessage parts={segmentedParts} actions={<button type="button">Copy</button>} />)).toMatchInlineSnapshot(`"<div class="space-y-2"><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">I signed the lease in March.</div></div></div></div><div role="note" aria-label="Application note" data-synthetic-text="" class="min-w-0 rounded-md border border-dashed border-border bg-muted/30 px-3 py-2 text-sm text-muted-foreground"><div class="mb-1 font-medium">Application note</div><div class="break-words whitespace-pre-wrap">Lease uploaded: lease.pdf</div></div><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Here it is.</div></div></div></div><div class="flex flex-wrap items-center justify-end gap-1.5 text-xs text-muted-foreground"><button type="button">Copy</button></div></div>"`)
33
33
  })
34
34
 
35
35
  it("renders UserMessage as before", () => {
36
- expect(html(<UserMessage content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">Can I break my lease early?</div></div></div></div>"`)
36
+ expect(html(<UserMessage content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div></div>"`)
37
37
  })
38
38
 
39
39
  it("renders AgentTimeline messages as before", () => {
@@ -46,7 +46,7 @@ describe("a transcript without authors", () => {
46
46
  ]}
47
47
  />,
48
48
  ),
49
- ).toMatchInlineSnapshot(`"<div class="mx-auto flex w-full max-w-5xl flex-col px-4 py-4"><div data-timeline-step="user" class=""><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">Can I break my lease early?</div></div></div></div></div><div data-timeline-step="prose" class="mt-4"><div><div class="tangle-prose max-w-none tangle-prose text-[length:var(--font-size-base)] leading-[1.5]"><p>It depends on the lease terms.</p></div></div></div></div>"`)
49
+ ).toMatchInlineSnapshot(`"<div class="mx-auto flex w-full max-w-5xl flex-col px-4 py-4"><div data-timeline-step="user" class=""><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div></div></div><div data-timeline-step="prose" class="mt-4"><div><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>It depends on the lease terms.</p></div></div></div></div>"`)
50
50
  })
51
51
  })
52
52
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one text treatment for every chat message body, the person's and the
3
+ * agent's alike, so the two differ only by alignment and surface.
4
+ * `tangle-transcript-text` (brand globals.css) reads `--transcript-font-size`
5
+ * and `--transcript-line-height`; the utilities repeat the same tokens for a
6
+ * host that has not loaded brand's globals. `length:` types the token: a bare
7
+ * `text-[var(--x)]` compiles to a color.
8
+ */
9
+ export const TRANSCRIPT_TEXT =
10
+ "tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]";
@@ -1,3 +1,4 @@
1
+ import { TRANSCRIPT_TEXT } from "./transcript-text";
1
2
  import { memo, type ReactNode } from "react";
2
3
  import type { ChatAuthor, SessionMessage } from "../types/message";
3
4
  import type { SessionPart } from "../types/parts";
@@ -53,7 +54,7 @@ function UserBubble({ text, timestamp, actions }: {
53
54
  {formatTime(timestamp)}
54
55
  </span>
55
56
  ) : null}
56
- <div className="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">
57
+ <div className={`break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`}>
57
58
  {text}
58
59
  </div>
59
60
  </div>
@@ -88,7 +89,7 @@ function OtherPersonBubble({ text, timestamp, actions, author }: {
88
89
  {formatTime(timestamp)}
89
90
  </span>
90
91
  ) : null}
91
- <div className="break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]">
92
+ <div className={`break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`}>
92
93
  {text}
93
94
  </div>
94
95
  </div>
@@ -46,6 +46,12 @@ export const controlSquare = {
46
46
  */
47
47
  export const fieldTouchText = "pointer-coarse:text-base";
48
48
 
49
+ /**
50
+ * A control that is shorter than 44px on a fine pointer still gets a 44px hit
51
+ * height on a coarse one (WCAG 2.5.5, Apple HIG). Same floor as `touch`.
52
+ */
53
+ export const touchTarget = "pointer-coarse:min-h-11";
54
+
49
55
  /** Field sizes: the control scale with field padding. */
50
56
  export const fieldSizes = {
51
57
  sm: cn(controlHeight.sm, controlText.sm, controlRadius.sm, "px-2.5"),
@@ -14,6 +14,13 @@ afterEach(() => {
14
14
 
15
15
  describe("CodeBlock scoped semantic colors", () => {
16
16
  it("keeps code readable before the renderer loads, then applies live semantic colors and line numbers", async () => {
17
+ // Shiki stops tokenizing a line after 500ms and paints the rest in the last
18
+ // token's color. The first pass over a cold grammar also compiles its
19
+ // patterns, which on a loaded host can take longer than that. Warm the
20
+ // grammar first: this test checks the color mapping, not cold-start time.
21
+ const warm = render(<CodeBlock code={'const message = "hello";'} language="javascript" />);
22
+ await waitFor(() => expect(warm.container.querySelector("[data-highlighted]")).not.toBeNull(), { timeout: 10_000 });
23
+ warm.unmount();
17
24
  const { container } = render(<CodeBlock code={'const message = "hello";'} language="javascript" showLineNumbers />);
18
25
  // Plain code already carries its line numbers, so highlighting shifts no layout.
19
26
  expect(container.querySelector("pre code")?.textContent?.replace(/^1/, "")).toBe('const message = "hello";');
@@ -116,6 +123,17 @@ describe("CodeBlock", () => {
116
123
  expect(getByText("Python")).toBeInTheDocument();
117
124
  });
118
125
 
126
+ it.each(["text", "TXT", "plain", "plaintext"])("shows no %s label on a plain-text fence, only its copy control", (language) => {
127
+ const { container, queryByText } = render(
128
+ <CodeBlock code={"x"} language={language}>
129
+ <CopyButton text="x" />
130
+ </CodeBlock>,
131
+ );
132
+ expect(container.querySelector(".border-b")).toBeNull();
133
+ expect(queryByText(language)).toBeNull();
134
+ expect(container.querySelector('button[title="Copy to clipboard"]')).not.toBeNull();
135
+ });
136
+
119
137
  it("renders no header when neither label nor language is set", () => {
120
138
  const { container } = render(<CodeBlock code={"x"} />);
121
139
  // The header is the only element carrying a bottom border.
@@ -94,6 +94,8 @@ const LANGUAGE_NAMES: Record<string, string> = {
94
94
  };
95
95
 
96
96
  /** The Shiki grammar for a fence language, or undefined for plain text. */
97
+ const PLAIN_TEXT = new Set(["text", "txt", "plain", "plaintext"]);
98
+
97
99
  export function codeBlockGrammar(language: string | undefined): string | undefined {
98
100
  if (!language) return undefined;
99
101
  const id = language.toLowerCase();
@@ -177,7 +179,8 @@ export const CodeBlock = memo(
177
179
  return () => { active = false; };
178
180
  }, [code, grammar]);
179
181
 
180
- const languageName = grammar ? LANGUAGE_NAMES[grammar] : language;
182
+ // A plain-text fence names no language worth showing; it keeps its copy control.
183
+ const languageName = grammar ? LANGUAGE_NAMES[grammar] : PLAIN_TEXT.has(language?.toLowerCase() ?? "") ? undefined : language;
181
184
  const headerLabel = label ?? (filename ? undefined : languageName);
182
185
  const hasHeader = Boolean(filename || headerLabel);
183
186
  const mode = light === undefined ? {} : { "data-theme": light ? "light" : "dark" };
@@ -117,3 +117,19 @@ describe("shared control presentation", () => {
117
117
  expect(buttonVariants()).toContain("motion-reduce:active:scale-100");
118
118
  });
119
119
  });
120
+
121
+ describe("SelectTrigger with a value longer than the trigger", () => {
122
+ it("ends the value in an ellipsis with a gap before the chevron", () => {
123
+ const value = "Luna — ReCenter Guest Concierge & Practitioner Desk";
124
+ render(<Select value="concierge"><SelectTrigger aria-label="Agent" className="w-40"><SelectValue>{value}</SelectValue></SelectTrigger></Select>);
125
+ const trigger = screen.getByLabelText("Agent");
126
+ const classes = trigger.className.split(" ");
127
+ // line-clamp cannot ellipsize a nowrap line: it clipped mid-glyph under the chevron.
128
+ expect(trigger.className).not.toContain("line-clamp");
129
+ expect(classes).toContain("[&>span]:truncate");
130
+ expect(classes).toContain("[&>span]:min-w-0");
131
+ expect(classes).toContain("gap-2");
132
+ // The rule targets the value span, which Radix renders as the trigger's direct child.
133
+ expect(trigger.querySelector(":scope > span")?.textContent).toBe(value);
134
+ });
135
+ });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from "vitest"
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
2
  import { render, screen } from "@testing-library/react"
3
3
  import userEvent from "@testing-library/user-event"
4
4
  import { SegmentedControl, type SegmentedControlOption } from "./segmented-control"
@@ -346,3 +346,90 @@ describe("SegmentedControl test ids", () => {
346
346
  expect(screen.getByTestId("workbench-tab-code")).toHaveAttribute("aria-checked", "true")
347
347
  })
348
348
  })
349
+
350
+ describe("SegmentedControl tabs on a narrow strip", () => {
351
+ // jsdom has no layout. Model a 250px strip holding six 100px tabs, so the
352
+ // strip scrolls and some tabs sit past its edges.
353
+ const STRIP = 250
354
+ const TAB = 100
355
+ const scroll = new WeakMap<Element, number>()
356
+ const overrides: Array<[object, string, PropertyDescriptor | undefined]> = []
357
+ const define = (proto: object, key: string, descriptor: PropertyDescriptor) => {
358
+ overrides.push([proto, key, Object.getOwnPropertyDescriptor(proto, key)])
359
+ Object.defineProperty(proto, key, { configurable: true, ...descriptor })
360
+ }
361
+ const isStrip = (el: Element) => el.getAttribute("role") === "radiogroup"
362
+ const index = (el: Element) => Array.from(el.parentElement?.children ?? []).indexOf(el)
363
+
364
+ beforeEach(() => {
365
+ define(HTMLElement.prototype, "offsetLeft", { get(this: HTMLElement) { return isStrip(this) ? 0 : index(this) * TAB } })
366
+ define(HTMLElement.prototype, "offsetWidth", { get(this: HTMLElement) { return isStrip(this) ? STRIP : TAB } })
367
+ define(Element.prototype, "clientWidth", { get(this: Element) { return isStrip(this) ? STRIP : TAB } })
368
+ define(Element.prototype, "scrollWidth", { get(this: Element) { return isStrip(this) ? this.children.length * TAB : TAB } })
369
+ define(Element.prototype, "scrollLeft", {
370
+ get(this: Element) { return scroll.get(this) ?? 0 },
371
+ set(this: Element, v: number) { scroll.set(this, v) },
372
+ })
373
+ })
374
+ afterEach(() => {
375
+ for (const [proto, key, original] of overrides.reverse()) {
376
+ if (original) Object.defineProperty(proto, key, original)
377
+ else delete (proto as Record<string, unknown>)[key]
378
+ }
379
+ overrides.length = 0
380
+ })
381
+
382
+ const work: SegmentedControlOption[] = ["Today", "Messages", "Tasks", "Operations", "Payments", "People"].map(
383
+ (label) => ({ value: label.toLowerCase(), label }),
384
+ )
385
+
386
+ it("scrolls the strip, not the page, so the selected tab is in view", () => {
387
+ const { rerender } = render(
388
+ <SegmentedControl aria-label="Work" variant="tabs" value="payments" onValueChange={vi.fn()} options={work} />,
389
+ )
390
+ const strip = screen.getByRole("radiogroup")
391
+ // Payments spans 400-500px: its right edge plus the 24px fade fits the 250px view.
392
+ expect(strip.scrollLeft).toBe(500 - STRIP + 24)
393
+
394
+ rerender(<SegmentedControl aria-label="Work" variant="tabs" value="today" onValueChange={vi.fn()} options={work} />)
395
+ expect(strip.scrollLeft).toBe(0)
396
+ })
397
+
398
+ it("fades the edge that has more tabs past it", () => {
399
+ const { rerender } = render(
400
+ <SegmentedControl aria-label="Work" variant="tabs" value="today" onValueChange={vi.fn()} options={work} />,
401
+ )
402
+ const strip = screen.getByRole("radiogroup")
403
+ expect(strip).toHaveAttribute("data-overflow-end", "true")
404
+ expect(strip).not.toHaveAttribute("data-overflow-start")
405
+ expect(strip.className).toContain("data-[overflow-end]:[mask-image:linear-gradient(to_right,#000_calc(100%-24px),transparent)]")
406
+
407
+ rerender(<SegmentedControl aria-label="Work" variant="tabs" value="people" onValueChange={vi.fn()} options={work} />)
408
+ expect(strip).toHaveAttribute("data-overflow-start", "true")
409
+ expect(strip).not.toHaveAttribute("data-overflow-end")
410
+ })
411
+
412
+ it("leaves a strip that fits unmasked", () => {
413
+ render(<SegmentedControl aria-label="Work" variant="tabs" value="all" onValueChange={vi.fn()} options={options.slice(0, 2)} />)
414
+ const strip = screen.getByRole("radiogroup")
415
+ expect(strip).not.toHaveAttribute("data-overflow-start")
416
+ expect(strip).not.toHaveAttribute("data-overflow-end")
417
+ })
418
+ })
419
+
420
+ describe("SegmentedControl stable labels and touch targets", () => {
421
+ it.each(["row", "tabs"] as const)("keeps one label weight whichever %s option is selected", (variant) => {
422
+ render(<SegmentedControl aria-label="Scope" variant={variant} value="personal" onValueChange={vi.fn()} options={options} />)
423
+ for (const radio of screen.getAllByRole("radio")) {
424
+ expect(radio.className).toContain("font-medium")
425
+ expect(radio.className).not.toMatch(/font-(semibold|bold)/)
426
+ }
427
+ })
428
+
429
+ it("gives each tab a 44px hit height on a coarse pointer", () => {
430
+ render(<SegmentedControl aria-label="Scope" variant="tabs" value="all" onValueChange={vi.fn()} options={options} />)
431
+ for (const radio of screen.getAllByRole("radio")) {
432
+ expect(radio.className).toContain("pointer-coarse:min-h-11")
433
+ }
434
+ })
435
+ })
@@ -3,6 +3,7 @@
3
3
  declare const process: { env: { NODE_ENV?: string } } | undefined
4
4
 
5
5
  import * as React from "react"
6
+ import { touchTarget } from "../lib/control-presentation"
6
7
  import { focusRing, focusRingInset } from "../lib/focus"
7
8
  import { cn } from "../lib/utils"
8
9
 
@@ -23,6 +24,12 @@ import { cn } from "../lib/utils"
23
24
  * that darken on hover. This keeps the selected segment as the
24
25
  * single visual anchor instead of the whole group competing with
25
26
  * itself.
27
+ * - Every label keeps one weight. Selection is colour plus surface or
28
+ * underline, so changing the value never reflows the row.
29
+ *
30
+ * The tabs variant scrolls sideways when its options outgrow the row: the
31
+ * selected tab is kept in view, and a faded edge shows that more tabs sit
32
+ * past it.
26
33
  */
27
34
  export interface SegmentedControlOption<T extends string = string> {
28
35
  value: T
@@ -49,6 +56,15 @@ export interface SegmentedControlProps<T extends string = string>
49
56
  "data-testid"?: string
50
57
  }
51
58
 
59
+ /** Width of the faded edge that marks more tabs past the strip, in px. */
60
+ const TAB_EDGE_FADE = 24
61
+ // Fade whichever edge has tabs past it; the two-attribute rule wins on specificity.
62
+ const tabEdgeFade = [
63
+ "data-[overflow-start]:[mask-image:linear-gradient(to_right,transparent,#000_24px)]",
64
+ "data-[overflow-end]:[mask-image:linear-gradient(to_right,#000_calc(100%-24px),transparent)]",
65
+ "data-[overflow-start]:data-[overflow-end]:[mask-image:linear-gradient(to_right,transparent,#000_24px,#000_calc(100%-24px),transparent)]",
66
+ ]
67
+
52
68
  export function SegmentedControl<T extends string = string>({
53
69
  value,
54
70
  onValueChange,
@@ -69,6 +85,44 @@ export function SegmentedControl<T extends string = string>({
69
85
  }
70
86
 
71
87
  const optionRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map())
88
+ const stripRef = React.useRef<HTMLDivElement>(null)
89
+ const [overflow, setOverflow] = React.useState({ start: false, end: false })
90
+ const tabs = variant === "tabs"
91
+
92
+ const measureOverflow = React.useCallback(() => {
93
+ const strip = stripRef.current
94
+ if (!strip) return
95
+ const start = strip.scrollLeft > 1
96
+ const end = strip.scrollLeft + strip.clientWidth < strip.scrollWidth - 1
97
+ setOverflow((prev) => (prev.start === start && prev.end === end ? prev : { start, end }))
98
+ }, [])
99
+
100
+ // Keep the selected tab inside the strip. Only the strip scrolls: the page
101
+ // never moves, which scrollIntoView cannot promise.
102
+ React.useLayoutEffect(() => {
103
+ if (!tabs) return
104
+ const strip = stripRef.current
105
+ const option = optionRefs.current.get(value)
106
+ if (strip && option) {
107
+ const start = option.offsetLeft
108
+ const end = start + option.offsetWidth
109
+ if (start < strip.scrollLeft + TAB_EDGE_FADE) {
110
+ strip.scrollLeft = Math.max(0, start - TAB_EDGE_FADE)
111
+ } else if (end > strip.scrollLeft + strip.clientWidth - TAB_EDGE_FADE) {
112
+ strip.scrollLeft = end - strip.clientWidth + TAB_EDGE_FADE
113
+ }
114
+ }
115
+ measureOverflow()
116
+ }, [tabs, value, options.length, measureOverflow])
117
+
118
+ React.useEffect(() => {
119
+ if (!tabs || typeof ResizeObserver === "undefined") return
120
+ const strip = stripRef.current
121
+ if (!strip) return
122
+ const observer = new ResizeObserver(measureOverflow)
123
+ observer.observe(strip)
124
+ return () => observer.disconnect()
125
+ }, [tabs, measureOverflow])
72
126
 
73
127
  const hasMatch = options.some((o) => o.value === value)
74
128
 
@@ -98,13 +152,17 @@ export function SegmentedControl<T extends string = string>({
98
152
  aria-label={rest["aria-label"]}
99
153
  aria-labelledby={rest["aria-labelledby"]}
100
154
  data-testid={rest["data-testid"]}
155
+ data-overflow-start={overflow.start || undefined}
156
+ data-overflow-end={overflow.end || undefined}
157
+ ref={stripRef}
101
158
  onKeyDown={handleKeyDown}
159
+ onScroll={tabs ? measureOverflow : undefined}
102
160
  className={cn(
103
161
  "flex gap-1",
104
162
  variant === "row" &&
105
163
  "flex-wrap items-center rounded-lg border border-border bg-card p-1",
106
164
  variant === "tabs" &&
107
- "flex-nowrap items-end border-b border-border pb-0 overflow-x-auto",
165
+ ["relative flex-nowrap items-end border-b border-border pb-0 overflow-x-auto", tabEdgeFade],
108
166
  className,
109
167
  )}
110
168
  >
@@ -127,17 +185,17 @@ export function SegmentedControl<T extends string = string>({
127
185
  if (!active) onValueChange(option.value)
128
186
  }}
129
187
  className={cn(
130
- "relative inline-flex items-center gap-2 whitespace-nowrap text-sm transition-[color,background-color,border-color,box-shadow]",
188
+ "relative inline-flex items-center gap-2 whitespace-nowrap text-sm font-medium transition-[color,background-color,border-color,box-shadow]",
131
189
  variant === "row" && ["rounded-md px-3 py-1.5", focusRing],
132
190
  // The strip scrolls horizontally, which would clip an outer ring.
133
- variant === "tabs" && ["rounded-none border-b-2 -mb-px px-4 py-2", focusRingInset],
191
+ variant === "tabs" && ["shrink-0 rounded-none border-b-2 -mb-px px-4 py-2", touchTarget, focusRingInset],
134
192
  // Active styling is the ONLY styled state — unselected
135
193
  // segments stay transparent on purpose so they don't
136
194
  // compete visually with the selection.
137
195
  active
138
196
  ? variant === "row"
139
- ? "border border-transparent bg-[var(--accent-surface)] text-[var(--accent-text)] font-semibold"
140
- : "border-[var(--accent-text)] text-[var(--accent-text)] font-semibold"
197
+ ? "border border-transparent bg-[var(--accent-surface)] text-[var(--accent-text)]"
198
+ : "border-[var(--accent-text)] text-[var(--accent-text)]"
141
199
  : variant === "row"
142
200
  ? "border border-transparent text-muted-foreground hover:text-foreground"
143
201
  : "border-transparent text-muted-foreground hover:text-foreground",
@@ -162,3 +162,18 @@ export const Overview: Story = {
162
162
  </div>
163
163
  ),
164
164
  }
165
+
166
+ /** A value longer than its trigger ends in an ellipsis before the chevron. */
167
+ export const LongValue: Story = {
168
+ render: () => (
169
+ <Select defaultValue="concierge">
170
+ <SelectTrigger className="w-48">
171
+ <SelectValue />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ <SelectItem value="concierge">Luna — ReCenter Guest Concierge</SelectItem>
175
+ <SelectItem value="practitioner">Luna — ReCenter Wellness &amp; Practitioner Desk</SelectItem>
176
+ </SelectContent>
177
+ </Select>
178
+ ),
179
+ }
@@ -22,10 +22,12 @@ const SelectTrigger = React.forwardRef<
22
22
  <SelectPrimitive.Trigger
23
23
  ref={ref}
24
24
  className={cn(
25
- "flex w-full items-center justify-between whitespace-nowrap border py-1 shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
25
+ "flex w-full items-center justify-between gap-2 whitespace-nowrap border py-1 shadow-sm placeholder:text-[var(--text-dim)] disabled:cursor-not-allowed disabled:opacity-50",
26
26
  fieldPresentation,
27
27
  fieldTouchText,
28
- "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:text-left",
28
+ // A long value ends in an ellipsis before the chevron: line-clamp cannot
29
+ // ellipsize a nowrap line, so it clipped mid-glyph under the icon.
30
+ "data-[placeholder]:text-[var(--text-dim)] [&>span]:min-w-0 [&>span]:truncate [&>span]:text-left",
29
31
  // The designed open state: accent border and surface while the menu is up.
30
32
  "data-[state=open]:border-primary data-[state=open]:bg-[var(--accent-surface-strong)] [&[data-state=open]>svg]:rotate-180 [&>svg]:transition-transform",
31
33
  size === "compact" || size === "touch" ? cn(fieldSizes.md, controlSizes[size]) : fieldSizes[size === "default" ? "md" : size],