@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 +16 -0
- package/dist/chat/agent-timeline.js +2 -1
- package/dist/chat/chat-message.js +12 -2
- package/dist/chat/transcript-text.d.ts +11 -0
- package/dist/chat/transcript-text.js +12 -0
- package/dist/chat/user-message.js +3 -2
- package/dist/chat.d.ts +2 -1
- package/dist/chat.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/control-presentation.js +6 -1
- package/dist/markdown/code-block.d.ts +0 -1
- package/dist/markdown/code-block.js +7 -1
- package/dist/primitives/segmented-control.d.ts +6 -0
- package/dist/primitives/segmented-control.js +59 -2
- package/dist/primitives/select.js +1 -1
- package/package.json +2 -2
- package/src/chat/agent-timeline.test.tsx +20 -0
- package/src/chat/agent-timeline.tsx +3 -3
- package/src/chat/chat-message.tsx +3 -2
- package/src/chat/index.ts +1 -0
- package/src/chat/message-author.test.tsx +7 -7
- package/src/chat/transcript-text.ts +10 -0
- package/src/chat/user-message.tsx +3 -2
- package/src/lib/control-presentation.ts +6 -0
- package/src/markdown/code-block.test.tsx +18 -0
- package/src/markdown/code-block.tsx +4 -1
- package/src/primitives/control-presentation.test.tsx +16 -0
- package/src/primitives/segmented-control.test.tsx +88 -1
- package/src/primitives/segmented-control.tsx +63 -5
- package/src/primitives/select.stories.tsx +15 -0
- package/src/primitives/select.tsx +4 -2
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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" && [
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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 {
|
|
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)]
|
|
140
|
-
: "border-[var(--accent-text)] text-[var(--accent-text)]
|
|
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 & 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
|
|
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
|
-
|
|
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],
|