@tangle-network/ui 11.51.20 → 11.51.22

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.
@@ -25,7 +25,6 @@ const namedModes = [
25
25
  light: "agents-light"
26
26
  }
27
27
  ];
28
- let preference;
29
28
  function isTheme(value) {
30
29
  return value === "light" || value === "dark" || value === "system";
31
30
  }
@@ -33,88 +32,129 @@ function getSystemTheme() {
33
32
  if (typeof window === "undefined" || !window.matchMedia) return "dark";
34
33
  return window.matchMedia(systemQuery).matches ? "dark" : "light";
35
34
  }
36
- function getServerTheme() {
37
- return "system";
38
- }
39
- function getServerMode() {
40
- return "dark";
41
- }
42
- function readInitialTheme() {
43
- try {
44
- const stored = window.localStorage.getItem("theme");
45
- if (isTheme(stored)) return stored;
46
- } catch {}
47
- const root = document.documentElement;
48
- const named = root.getAttribute("data-theme");
49
- if (named === "light" || named === "dark") return named;
50
- for (const pair of namedModes) {
51
- if (named === pair.light) return "light";
52
- if (named === pair.dark) return "dark";
53
- }
54
- if (root.classList.contains("light")) return "light";
55
- if (root.classList.contains("dark")) return "dark";
56
- const legacy = root.getAttribute("data-sandbox-theme");
57
- if (legacy === "vault" || legacy === "dawn") return "light";
58
- return "system";
59
- }
60
- function getTheme() {
61
- if (typeof window === "undefined") return "system";
62
- return preference ??= readInitialTheme();
63
- }
64
- function applyMode(mode) {
65
- const root = document.documentElement;
66
- root.classList.toggle("dark", mode === "dark");
67
- root.classList.toggle("light", mode === "light");
68
- const named = root.getAttribute("data-theme");
69
- if (named === "dark" || named === "light") root.setAttribute("data-theme", mode);
70
- else {
71
- const pair = namedModes.find((pair) => named === pair.dark || named === pair.light);
72
- if (pair) root.setAttribute("data-theme", pair[mode]);
73
- }
74
- }
75
- function setTheme(next) {
76
- if (!isTheme(next) || typeof window === "undefined") return;
77
- preference = next;
78
- try {
79
- if (next === "system") window.localStorage.removeItem("theme");
80
- else window.localStorage.setItem("theme", next);
81
- } catch {}
82
- applyMode(next === "system" ? getSystemTheme() : next);
83
- window.dispatchEvent(new Event(preferenceEvent));
84
- }
85
- function subscribeTheme(notify) {
86
- const onStorage = (event) => {
87
- if (event.key !== "theme" && event.key !== null) return;
88
- preference = isTheme(event.newValue) ? event.newValue : "system";
89
- notify();
90
- };
91
- window.addEventListener(preferenceEvent, notify);
92
- window.addEventListener("storage", onStorage);
93
- return () => {
94
- window.removeEventListener(preferenceEvent, notify);
95
- window.removeEventListener("storage", onStorage);
96
- };
97
- }
98
35
  function subscribeSystem(notify) {
99
36
  if (!window.matchMedia) return () => {};
100
37
  const media = window.matchMedia(systemQuery);
101
38
  media.addEventListener("change", notify);
102
39
  return () => media.removeEventListener("change", notify);
103
40
  }
104
- function useTheme() {
105
- const theme = useSyncExternalStore(subscribeTheme, getTheme, getServerTheme);
106
- const system = useSyncExternalStore(subscribeSystem, getSystemTheme, getServerMode);
107
- const resolvedTheme = theme === "system" ? system : theme;
108
- useEffect(() => {
109
- const selected = getTheme();
110
- applyMode(selected === "system" ? getSystemTheme() : selected);
111
- }, [resolvedTheme]);
41
+ /** Create once at module scope. Each product owns its persisted names, not a second theme engine. */
42
+ function createThemeController({ storageKey = "theme", defaultTheme = "system", storageValues, attribute } = {}) {
43
+ const values = {
44
+ light: "light",
45
+ dark: "dark",
46
+ system: null,
47
+ ...storageValues
48
+ };
49
+ const eventName = `${preferenceEvent}:${storageKey}`;
50
+ let preference;
51
+ let persisted;
52
+ const decode = (value) => Object.keys(values).find((theme) => value !== null && values[theme] === value) ?? (value === "system" && values.system === null ? "system" : defaultTheme);
53
+ const serverTheme = () => defaultTheme;
54
+ const serverMode = () => defaultTheme === "system" ? "dark" : defaultTheme;
55
+ function readInitialTheme() {
56
+ try {
57
+ const stored = window.localStorage.getItem(storageKey);
58
+ persisted = stored;
59
+ if (stored !== null && (Object.values(values).includes(stored) || stored === "system" && values.system === null)) return decode(stored);
60
+ } catch {}
61
+ if (attribute || defaultTheme !== "system") return defaultTheme;
62
+ const root = document.documentElement;
63
+ const named = root.getAttribute("data-theme");
64
+ if (named === "light" || named === "dark") return named;
65
+ for (const pair of namedModes) {
66
+ if (named === pair.light) return "light";
67
+ if (named === pair.dark) return "dark";
68
+ }
69
+ if (root.classList.contains("light")) return "light";
70
+ if (root.classList.contains("dark")) return "dark";
71
+ const legacy = root.getAttribute("data-sandbox-theme");
72
+ return legacy === "vault" || legacy === "dawn" ? "light" : defaultTheme;
73
+ }
74
+ function getTheme() {
75
+ if (typeof window === "undefined") return defaultTheme;
76
+ if (preference === void 0) return preference = readInitialTheme();
77
+ try {
78
+ const stored = window.localStorage.getItem(storageKey);
79
+ if (stored !== persisted) {
80
+ persisted = stored;
81
+ preference = decode(stored);
82
+ }
83
+ } catch {}
84
+ return preference;
85
+ }
86
+ /** Apply a temporary page mode without overwriting the user's preference. */
87
+ function applyTheme(theme = getTheme()) {
88
+ if (typeof document === "undefined") return;
89
+ const mode = theme === "system" ? getSystemTheme() : theme;
90
+ const root = document.documentElement;
91
+ root.classList.toggle("dark", mode === "dark");
92
+ root.classList.toggle("light", !attribute && mode === "light");
93
+ if (attribute) {
94
+ const value = attribute[mode];
95
+ if (value === void 0) root.removeAttribute(attribute.name);
96
+ else root.setAttribute(attribute.name, value);
97
+ } else {
98
+ const named = root.getAttribute("data-theme");
99
+ if (named === "dark" || named === "light") root.setAttribute("data-theme", mode);
100
+ else {
101
+ const pair = namedModes.find((pair) => named === pair.dark || named === pair.light);
102
+ if (pair) root.setAttribute("data-theme", pair[mode]);
103
+ }
104
+ }
105
+ }
106
+ function setTheme(next) {
107
+ if (!isTheme(next) || typeof window === "undefined") return;
108
+ preference = next;
109
+ try {
110
+ const value = values[next];
111
+ if (value === null) window.localStorage.removeItem(storageKey);
112
+ else window.localStorage.setItem(storageKey, value);
113
+ persisted = value;
114
+ } catch {}
115
+ applyTheme(next);
116
+ window.dispatchEvent(new CustomEvent(eventName, { detail: next }));
117
+ }
118
+ function subscribeTheme(notify) {
119
+ const onStorage = (event) => {
120
+ if (event.key !== storageKey && event.key !== null) return;
121
+ persisted = event.newValue;
122
+ preference = decode(event.newValue);
123
+ notify();
124
+ };
125
+ const onPreference = (event) => {
126
+ const next = event.detail;
127
+ if (isTheme(next)) preference = next;
128
+ notify();
129
+ };
130
+ window.addEventListener(eventName, onPreference);
131
+ window.addEventListener("storage", onStorage);
132
+ return () => {
133
+ window.removeEventListener(eventName, onPreference);
134
+ window.removeEventListener("storage", onStorage);
135
+ };
136
+ }
137
+ function useTheme() {
138
+ const theme = useSyncExternalStore(subscribeTheme, getTheme, serverTheme);
139
+ const system = useSyncExternalStore(subscribeSystem, getSystemTheme, serverMode);
140
+ const resolvedTheme = theme === "system" ? system : theme;
141
+ useEffect(() => {
142
+ applyTheme(getTheme());
143
+ }, [resolvedTheme]);
144
+ return {
145
+ theme,
146
+ setTheme,
147
+ resolvedTheme
148
+ };
149
+ }
112
150
  return {
113
- theme,
151
+ useTheme,
152
+ getTheme,
114
153
  setTheme,
115
- resolvedTheme
154
+ applyTheme
116
155
  };
117
156
  }
157
+ const { useTheme } = createThemeController();
118
158
  const iconClass = "h-4 w-4";
119
159
  function SunIcon() {
120
160
  return /* @__PURE__ */ jsxs("svg", {
@@ -158,4 +198,4 @@ function ThemeToggle() {
158
198
  children: resolvedTheme === "dark" ? /* @__PURE__ */ jsx(SunIcon, {}) : /* @__PURE__ */ jsx(MoonIcon, {})
159
199
  });
160
200
  }
161
- export { ThemeToggle, useTheme };
201
+ export { ThemeToggle, createThemeController, useTheme };
@@ -19,7 +19,7 @@ import { Badge, BadgeProps, badgeVariants } from "./primitives/badge.js";
19
19
  import { Avatar, AvatarFallback, AvatarImage, AvatarProps } from "./primitives/avatar.js";
20
20
  import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./primitives/dropdown-menu.js";
21
21
  import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from "./primitives/select.js";
22
- import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableProps, TableRow } from "./primitives/table.js";
22
+ import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeadProps, TableHeader, TableProps, TableRow } from "./primitives/table.js";
23
23
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/tabs.js";
24
24
  import { SegmentedControl, SegmentedControlOption, SegmentedControlProps } from "./primitives/segmented-control.js";
25
25
  import { Progress } from "./primitives/progress.js";
@@ -41,7 +41,7 @@ import { UploadFile, UploadProgress, UploadProgressProps } from "./primitives/up
41
41
  import { SidebarDropZone, SidebarDropZoneProps } from "./primitives/sidebar-drop-zone.js";
42
42
  import { CodeBlock, CodeBlockProps, CopyButton } from "./markdown/code-block.js";
43
43
  import { InlineCode, InlineCodeProps } from "./primitives/code-block.js";
44
- import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
44
+ import { ThemeControllerOptions, ThemeToggle, createThemeController, useTheme } from "./primitives/theme-toggle.js";
45
45
  import { Heading, HeadingProps, HeadingVariant } from "./primitives/heading.js";
46
46
  import { PageShell, PageShellProps } from "./primitives/page-shell.js";
47
47
  import { Field, FieldProps, fieldMessageId } from "./primitives/field.js";
@@ -63,4 +63,4 @@ import { ChartReferenceLine, MetricChart, MetricChartProps } from "./primitives/
63
63
  import { FilterField, FilterFieldProps, Toolbar, ToolbarProps } from "./primitives/toolbar.js";
64
64
  import { ExpandablePanel, ExpandablePanelProps } from "./primitives/expandable-panel.js";
65
65
  import { Logo, LogoProps, TangleKnot } from "@tangle-network/brand";
66
- export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandablePanel, type ExpandablePanelProps, Field, type FieldProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, badgeVariants, buttonVariants, fieldMessageId, formatCalendarDate, initialsOf, localToday, normalizeSeries, parseCalendarDate, periodDelta, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, toneFor, useTheme, useToast };
66
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandablePanel, type ExpandablePanelProps, Field, type FieldProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, type TableHeadProps, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, type ThemeControllerOptions, ThemeToggle, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, badgeVariants, buttonVariants, createThemeController, fieldMessageId, formatCalendarDate, initialsOf, localToday, normalizeSeries, parseCalendarDate, periodDelta, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, toneFor, useTheme, useToast };
@@ -40,7 +40,7 @@ import { SidebarDropZone } from "./primitives/sidebar-drop-zone.js";
40
40
  import { ArtifactPane } from "./primitives/artifact-pane.js";
41
41
  import { CodeBlock, CopyButton } from "./markdown/code-block.js";
42
42
  import { InlineCode } from "./primitives/code-block.js";
43
- import { ThemeToggle, useTheme } from "./primitives/theme-toggle.js";
43
+ import { ThemeToggle, createThemeController, useTheme } from "./primitives/theme-toggle.js";
44
44
  import { PageShell } from "./primitives/page-shell.js";
45
45
  import { Field, fieldMessageId } from "./primitives/field.js";
46
46
  import { StatusPill } from "./primitives/status-pill.js";
@@ -63,4 +63,4 @@ import { MetricChart } from "./primitives/metric-chart.js";
63
63
  import { FilterField, Toolbar } from "./primitives/toolbar.js";
64
64
  import { ExpandablePanel } from "./primitives/expandable-panel.js";
65
65
  import { Logo, TangleKnot } from "@tangle-network/brand";
66
- export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ComplianceBadge, 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, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandablePanel, Field, FilterField, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, Input, Label, Logo, Metric, MetricChart, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, TimeSelect, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, Wizard, WizardStep, badgeVariants, buttonVariants, fieldMessageId, formatCalendarDate, initialsOf, localToday, normalizeSeries, parseCalendarDate, periodDelta, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, toneFor, useTheme, useToast };
66
+ export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ComplianceBadge, 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, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandablePanel, Field, FilterField, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, Input, Label, Logo, Metric, MetricChart, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, TimeSelect, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, Wizard, WizardStep, badgeVariants, buttonVariants, createThemeController, fieldMessageId, formatCalendarDate, initialsOf, localToday, normalizeSeries, parseCalendarDate, periodDelta, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, toneFor, useTheme, useToast };
@@ -0,0 +1,2 @@
1
+ /** Resolve a scoped CSS time, including calc(); absent tokens remain unknown. */
2
+ export declare function tokenMilliseconds(token: string, element?: Element | null): number | undefined;
@@ -0,0 +1,17 @@
1
+ // src/utils/token-milliseconds.ts
2
+ /** Resolve a scoped CSS time, including calc(); absent tokens remain unknown. */
3
+ function tokenMilliseconds(token, element) {
4
+ const scope = element ?? (typeof document === "undefined" ? void 0 : document.documentElement), view = scope?.ownerDocument.defaultView;
5
+ if (!scope || !view || !view.getComputedStyle(scope).getPropertyValue(token).trim()) return void 0;
6
+ const probe = scope.ownerDocument.createElement("i");
7
+ probe.hidden = true;
8
+ scope.append(probe);
9
+ try {
10
+ probe.style.animationDuration = `var(${token})`;
11
+ const time = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(view.getComputedStyle(probe).animationDuration);
12
+ return time ? Number(time[1]) * (time[2] === "s" ? 1e3 : 1) : void 0;
13
+ } finally {
14
+ probe.remove();
15
+ }
16
+ }
17
+ export { tokenMilliseconds };
package/dist/utils.d.ts CHANGED
@@ -2,7 +2,8 @@ import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, i
2
2
  import { copyText } from "./utils/copy-text.js";
3
3
  import { formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatDuration as formatSpanDuration, formatUtcTime, plainTextFromMarkdown, truncateText } from "./utils/format.js";
4
4
  import { timeAgo } from "./utils/time-ago.js";
5
+ import { tokenMilliseconds } from "./utils/token-milliseconds.js";
5
6
  import { TOOL_CATEGORY_ICONS, getToolCategory, getToolDisplayMetadata, getToolErrorText } from "./utils/tool-display.js";
6
7
  import { cn } from "./lib/utils.js";
7
8
  import { focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset } from "./lib/focus.js";
8
- export { TOOL_CATEGORY_ICONS, cn, copyText, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatSpanDuration as formatDuration, formatSpanDuration, formatSpanDuration as formatUptime, formatUtcTime, getToolCategory, getToolDisplayMetadata, getToolErrorText, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, plainTextFromMarkdown, timeAgo, truncateText };
9
+ export { TOOL_CATEGORY_ICONS, cn, copyText, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatSpanDuration as formatDuration, formatSpanDuration, formatSpanDuration as formatUptime, formatUtcTime, getToolCategory, getToolDisplayMetadata, getToolErrorText, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, plainTextFromMarkdown, timeAgo, tokenMilliseconds, truncateText };
package/dist/utils.js CHANGED
@@ -2,7 +2,8 @@ import { ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, i
2
2
  import { focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset } from "./lib/focus.js";
3
3
  import { cn } from "./lib/utils.js";
4
4
  import { formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatDuration as formatSpanDuration, formatUtcTime, plainTextFromMarkdown, truncateText } from "./utils/format.js";
5
+ import { tokenMilliseconds } from "./utils/token-milliseconds.js";
5
6
  import { TOOL_CATEGORY_ICONS, getToolCategory, getToolDisplayMetadata, getToolErrorText } from "./utils/tool-display.js";
6
7
  import { copyText } from "./utils/copy-text.js";
7
8
  import { timeAgo } from "./utils/time-ago.js";
8
- export { TOOL_CATEGORY_ICONS, cn, copyText, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatSpanDuration as formatDuration, formatSpanDuration, formatSpanDuration as formatUptime, formatUtcTime, getToolCategory, getToolDisplayMetadata, getToolErrorText, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, plainTextFromMarkdown, timeAgo, truncateText };
9
+ export { TOOL_CATEGORY_ICONS, cn, copyText, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatAxisDuration, formatBytes, formatClock, formatCompact, formatCost, formatCount, formatSpanDuration as formatDuration, formatSpanDuration, formatSpanDuration as formatUptime, formatUtcTime, getToolCategory, getToolDisplayMetadata, getToolErrorText, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, plainTextFromMarkdown, timeAgo, tokenMilliseconds, truncateText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.51.20",
3
+ "version": "11.51.22",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -216,7 +216,7 @@
216
216
  "peerDependencies": {
217
217
  "@hocuspocus/provider": ">=2.15.0 <5.0.0",
218
218
  "@nanostores/react": ">=0.8.4 <3.0.0",
219
- "@tangle-network/brand": "^1.31.8",
219
+ "@tangle-network/brand": "^1.31.11",
220
220
  "@tiptap/core": ">=3.20.4 <4.0.0",
221
221
  "@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
222
222
  "@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
@@ -9,6 +9,6 @@ export * from "./use-realtime-session";
9
9
  export * from "./use-sdk-session";
10
10
  export * from "./use-sse-stream";
11
11
 
12
- export { useEventStream, type EventStreamHandlers, type EventStreamSource, type EventStreamOpener } from "./use-event-stream";
12
+ export { openNativeStream, useEventStream, type EventStreamHandlers, type EventStreamSource, type EventStreamOpener } from "./use-event-stream";
13
13
  export { useFollowTail } from "./use-follow-tail";
14
14
  export { usePhone, PHONE_MEDIA } from "./use-phone";
@@ -15,7 +15,6 @@ export interface EventStreamHandlers {
15
15
  onLiveChange?: (live: boolean) => void
16
16
  }
17
17
 
18
- /** Internal policy preserves the buffered hook's existing bounded exponential retries. */
19
18
  type NativePolicy = EventStreamHandlers & { terminalEvent?: string; onConnecting?: (attempt: number) => void; retryDelay?: (attempt: number) => number | undefined }
20
19
 
21
20
  /** The sole native EventSource lifecycle, shared by buffered and producer-owned streams. */
@@ -42,7 +41,7 @@ export function openNativeStream(url: string | (() => string), handlers: NativeP
42
41
  connection.close()
43
42
  if (stopped || source !== connection) return
44
43
  live(false)
45
- const delay = handlers.retryDelay ? handlers.retryDelay(attempt++) : 1_000
44
+ const delay = handlers.retryDelay ? handlers.retryDelay(attempt++) : [1_000, 2_000, 5_000, 10_000][Math.min(attempt++, 3)]
46
45
  if (delay !== undefined) retry = setTimeout(open, delay)
47
46
  }
48
47
  }
@@ -1,3 +1,4 @@
1
+ import { tokenMilliseconds } from "../utils/token-milliseconds";
1
2
  import { tabsPresentation } from "../presentation";
2
3
  import type { HighlighterCore, LanguageRegistration, ThemedToken } from "shiki/core";
3
4
  // Leave variables unresolved: the browser resolves them on each token span,
@@ -171,17 +172,8 @@ const COPY_FEEDBACK_MS=2000;
171
172
  export interface InteractionBudgets { feedback?:number; pending?:number; longWork?:number; mutation?:number; }
172
173
  /** Resolve scoped Brand times, including calc(), without duplicating token values. */
173
174
  export function interactionBudgets(scope:HTMLElement):InteractionBudgets {
174
- const view=scope.ownerDocument.defaultView;if(!view)return {};
175
- const probe=scope.ownerDocument.createElement("i");probe.hidden=true;scope.append(probe);
176
- const read=(role:string)=>{
177
- const name=`--interaction-${role}`;
178
- if(!view.getComputedStyle(scope).getPropertyValue(name).trim())return undefined;
179
- probe.style.animationDuration=`var(${name})`;
180
- const time=/^(\d+(?:\.\d+)?)(ms|s)$/.exec(view.getComputedStyle(probe).animationDuration);
181
- return time?Number(time[1])*(time[2]==="s"?1000:1):undefined;
182
- };
183
- try{return {feedback:read("feedback-budget"),pending:read("pending-delay"),longWork:read("long-work-delay"),mutation:read("mutation-budget")};}
184
- finally{probe.remove();}
175
+ const read=(role:string)=>tokenMilliseconds(`--interaction-${role}`,scope);
176
+ return scope.ownerDocument.defaultView?{feedback:read("feedback-budget"),pending:read("pending-delay"),longWork:read("long-work-delay"),mutation:read("mutation-budget")} : {};
185
177
  }
186
178
  export interface CopyFeedback {
187
179
  copied:boolean;status:string;pending:boolean;loading?:boolean;longRunning?:boolean;elapsedMs?:number;
@@ -2,8 +2,9 @@ import { cn } from "../lib/utils";
2
2
  import * as React from "react";
3
3
  import { Slot } from "@radix-ui/react-slot";
4
4
  import { Popover, PopoverAnchor, PopoverContent } from "./popover";
5
+ import { tokenMilliseconds } from "../utils/token-milliseconds";
5
6
 
6
- const HoverIntent = React.createContext({ enter: (_target: Element) => {}, leave: () => {}, restore: () => {}, press: () => {} });
7
+ const HoverIntent = React.createContext({ content: { current: null as HTMLDivElement | null }, enter: (_target: Element) => {}, leave: (_event?: { relatedTarget: EventTarget | null }) => {}, restore: () => {}, press: () => {}, owns: (_target: EventTarget | null) => false });
7
8
  type HoverCardProps = React.ComponentProps<typeof Popover> & {
8
9
  /** Existing DOM trigger for delegated canvas or virtual-list previews. */
9
10
  anchor?: HTMLElement | { getBoundingClientRect: () => DOMRect; contextElement: HTMLElement } | null;
@@ -11,31 +12,29 @@ type HoverCardProps = React.ComponentProps<typeof Popover> & {
11
12
  closeDelay?: number;
12
13
  };
13
14
 
14
- /** One hover policy and Popover's keyboard/focus behavior; delays follow the trigger's fast token. */
15
15
  export function HoverCard({ anchor, open: controlled, defaultOpen = false, onOpenChange, openDelay, closeDelay, children, ...props }: HoverCardProps) {
16
16
  const element = anchor && ("contextElement" in anchor ? anchor.contextElement : anchor);
17
17
  const [own, setOwn] = React.useState({ anchor: element, open: defaultOpen });
18
- const intent = React.useRef<{ timer?: ReturnType<typeof setTimeout>; target?: Element; restoring?: boolean; pressed?: boolean }>({});
18
+ const intent = React.useRef<{ timer?: ReturnType<typeof setTimeout>; target?: Element; content: { current: HTMLDivElement | null }; restoring?: boolean; pressed?: boolean; onOpenChange?: HoverCardProps["onOpenChange"] }>({ content: { current: null } });
19
+ intent.current.onOpenChange = onOpenChange;
19
20
  if (element) intent.current.target = element;
20
- const change = React.useCallback((next: boolean) => { clearTimeout(intent.current.timer); if (controlled === undefined) setOwn({ anchor: element, open: next }); onOpenChange?.(next); }, [controlled, onOpenChange, element]);
21
- const delay = (target: Element | null | undefined) => {
22
- const value = getComputedStyle(target ?? document.documentElement).getPropertyValue("--duration-fast").trim();
23
- return (parseFloat(value) || 0) * (value.endsWith("ms") ? 1 : 1000);
24
- };
25
- const enter = React.useCallback((target: Element) => { if (intent.current.restoring || (intent.current.pressed && target.closest("[data-hover-card]"))) return; intent.current.pressed = false; if (!target.closest("[data-hover-card]")) intent.current.target = target; clearTimeout(intent.current.timer); intent.current.timer = setTimeout(() => change(true), openDelay ?? delay(target)); }, [change, openDelay]);
26
- const leave = React.useCallback(() => { clearTimeout(intent.current.timer); intent.current.timer = setTimeout(() => change(false), closeDelay ?? delay(element ?? intent.current.target)); }, [change, closeDelay, element]);
21
+ const change = React.useCallback((next: boolean) => { clearTimeout(intent.current.timer); if (controlled === undefined) setOwn(current => current.anchor === element && current.open === next ? current : { anchor: element, open: next }); intent.current.onOpenChange?.(next); }, [controlled, element]);
22
+ const owns = React.useCallback((target: EventTarget | null) => target instanceof Node && (!!(element ?? intent.current.target)?.contains(target) || !!intent.current.content.current?.contains(target)), [element]);
23
+ const enter = React.useCallback((target: Element) => { if (intent.current.restoring || (intent.current.pressed && target === intent.current.content.current)) return; intent.current.pressed = false; if (target !== intent.current.content.current) intent.current.target = target; clearTimeout(intent.current.timer); intent.current.timer = setTimeout(() => change(true), openDelay ?? tokenMilliseconds("--duration-fast", target)); }, [change, openDelay]);
24
+ const leave = React.useCallback((event?: { relatedTarget: EventTarget | null }) => { if (owns(event?.relatedTarget ?? null)) return; clearTimeout(intent.current.timer); intent.current.timer = setTimeout(() => { if (!owns(document.activeElement) || (!intent.current.content.current?.contains(document.activeElement) && !document.activeElement?.matches(":focus-visible"))) change(false); }, closeDelay ?? tokenMilliseconds("--duration-fast", element ?? intent.current.target)); }, [change, closeDelay, element, owns]);
27
25
  React.useEffect(() => {
28
- if (!element) return () => clearTimeout(intent.current.timer);
29
- const show = () => enter(element), blur = (event: FocusEvent) => { if (!(event.relatedTarget instanceof Element && event.relatedTarget.closest("[data-hover-card]"))) leave(); };
26
+ if (!element) { if (anchor === null) setOwn(current => current.open ? { anchor: element, open: false } : current); return () => clearTimeout(intent.current.timer); }
27
+ const show = () => enter(element), blur = (event: FocusEvent) => leave(event);
30
28
  element.addEventListener("pointerenter", show); element.addEventListener("pointerleave", leave); element.addEventListener("focusin", show); element.addEventListener("focusout", blur);
29
+ if (element.matches(":hover,:focus-within")) show();
31
30
  return () => { clearTimeout(intent.current.timer); element.removeEventListener("pointerenter", show); element.removeEventListener("pointerleave", leave); element.removeEventListener("focusin", show); element.removeEventListener("focusout", blur); };
32
- }, [anchor, element, enter, leave]);
31
+ }, [element, enter, leave]);
33
32
  const restore = () => {
34
33
  intent.current.restoring = true;
35
34
  if (document.activeElement === document.body && intent.current.target instanceof HTMLElement) intent.current.target.focus({ preventScroll: true });
36
35
  queueMicrotask(() => { intent.current.restoring = false; });
37
36
  };
38
- return <HoverIntent.Provider value={{ enter, leave, restore, press: () => { intent.current.pressed = true; change(false); } }}><Popover {...props} open={anchor === null ? false : controlled ?? (own.anchor === element && own.open)} onOpenChange={change}>
37
+ return <HoverIntent.Provider value={{ content: intent.current.content, enter, leave, restore, press: () => { intent.current.pressed = true; change(false); }, owns }}><Popover {...props} open={anchor === null ? false : controlled ?? (own.anchor === element && own.open)} onOpenChange={change}>
39
38
  {anchor && <PopoverAnchor virtualRef={{ current: anchor }} />}{children}
40
39
  </Popover></HoverIntent.Provider>;
41
40
  }
@@ -46,22 +45,23 @@ export const HoverCardTrigger = React.forwardRef<HTMLAnchorElement, React.Anchor
46
45
  const Trigger = asChild ? Slot : "a";
47
46
  return <PopoverAnchor asChild><Trigger ref={ref} tabIndex={0} className={cn("cursor-pointer underline-offset-4 hover:underline", className)}
48
47
  onPointerEnter={event => { onPointerEnter?.(event); if (!event.defaultPrevented && event.pointerType !== "touch") intent.enter(event.currentTarget); }}
49
- onPointerLeave={event => { onPointerLeave?.(event); if (!event.defaultPrevented) intent.leave(); }}
48
+ onPointerLeave={event => { onPointerLeave?.(event); if (!event.defaultPrevented) intent.leave(event); }}
50
49
  onFocus={event => { onFocus?.(event); if (!event.defaultPrevented && event.target.matches(":focus-visible")) intent.enter(event.currentTarget); }}
51
50
  onClickCapture={event => { onClickCapture?.(event); const action = event.target instanceof Element ? event.target.closest('a,button,input,select,textarea,[role="button"]') : null; if (!event.defaultPrevented && action && action !== event.currentTarget) intent.press(); }}
52
- onBlur={event => { onBlur?.(event); if (!event.defaultPrevented && !(event.relatedTarget instanceof Element && event.relatedTarget.closest("[data-hover-card]"))) intent.leave(); }} {...props} /></PopoverAnchor>;
51
+ onBlur={event => { onBlur?.(event); if (!event.defaultPrevented) intent.leave(event); }} {...props} /></PopoverAnchor>;
53
52
  },
54
53
  );
55
54
  HoverCardTrigger.displayName = "HoverCardTrigger";
56
55
 
57
56
  export const HoverCardContent = React.forwardRef<React.ElementRef<typeof PopoverContent>, React.ComponentPropsWithoutRef<typeof PopoverContent>>(
58
- ({ className, onPointerEnter, onPointerLeave, onOpenAutoFocus, onCloseAutoFocus, ...props }, ref) => {
57
+ ({ className, onPointerEnter, onPointerLeave, onOpenAutoFocus, onCloseAutoFocus, onFocusOutside, ...props }, ref) => {
59
58
  const intent = React.useContext(HoverIntent);
60
- return <PopoverContent ref={ref} data-hover-card className={cn("w-64", className)}
59
+ return <Slot ref={intent.content}><PopoverContent ref={ref} data-hover-card className={cn("w-64", className)}
61
60
  onOpenAutoFocus={event => { onOpenAutoFocus?.(event); event.preventDefault(); }}
62
61
  onCloseAutoFocus={event => { onCloseAutoFocus?.(event); if (!event.defaultPrevented) { event.preventDefault(); intent.restore(); } }}
62
+ onFocusOutside={event => { onFocusOutside?.(event); if (intent.owns(event.detail.originalEvent.target)) event.preventDefault(); }}
63
63
  onPointerEnter={event => { onPointerEnter?.(event); if (!event.defaultPrevented) intent.enter(event.currentTarget); }}
64
- onPointerLeave={event => { onPointerLeave?.(event); if (!event.defaultPrevented) intent.leave(); }} {...props} />;
64
+ onPointerLeave={event => { onPointerLeave?.(event); if (!event.defaultPrevented) intent.leave(event); }} {...props} /></Slot>;
65
65
  },
66
66
  );
67
67
  HoverCardContent.displayName = "HoverCardContent";
@@ -192,7 +192,7 @@ export type { ArtifactPaneProps } from "./artifact-pane";
192
192
  export { CodeBlock, CopyButton, InlineCode } from "./code-block";
193
193
  export type { CodeBlockProps, InlineCodeProps } from "./code-block";
194
194
 
195
- export { ThemeToggle, useTheme } from "./theme-toggle";
195
+ export { ThemeToggle, useTheme, createThemeController } from "./theme-toggle";
196
196
 
197
197
  export { Heading } from "./heading";
198
198
  export type { HeadingProps, HeadingVariant } from "./heading";
@@ -282,3 +282,6 @@ export type { ToolbarProps, FilterFieldProps } from "./toolbar";
282
282
 
283
283
  export { ExpandablePanel } from "./expandable-panel";
284
284
  export type { ExpandablePanelProps } from "./expandable-panel";
285
+
286
+ export type { ThemeControllerOptions } from "./theme-toggle";
287
+ export type { TableHeadProps } from "./table";
@@ -87,19 +87,38 @@ const TableRow = React.forwardRef<
87
87
  ));
88
88
  TableRow.displayName = "TableRow";
89
89
 
90
- const TableHead = React.forwardRef<
91
- HTMLTableCellElement,
92
- React.ThHTMLAttributes<HTMLTableCellElement>
93
- >(({ className, ...props }, ref) => (
94
- <th
95
- ref={ref}
96
- className={cn(
97
- tablePresentation.columnHeader,
98
- className,
99
- )}
100
- {...props}
101
- />
102
- ));
90
+ export interface TableHeadProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
91
+ /** Controlled sort affordance. The caller retains row order and sort policy. */
92
+ sort?: {
93
+ direction?: "asc" | "desc";
94
+ onSort: () => void;
95
+ label?: string;
96
+ buttonClassName?: string;
97
+ };
98
+ }
99
+
100
+ const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
101
+ ({ className, children, sort, ...props }, ref) => (
102
+ <th
103
+ ref={ref}
104
+ className={cn(tablePresentation.columnHeader, className)}
105
+ {...props}
106
+ aria-sort={sort ? (sort.direction === "asc" ? "ascending" : sort.direction === "desc" ? "descending" : "none") : props["aria-sort"]}
107
+ >
108
+ {sort ? (
109
+ <button
110
+ type="button"
111
+ onClick={sort.onSort}
112
+ aria-label={sort.label ?? (typeof children === "string" ? `Sort by ${children}` : undefined)}
113
+ className={cn("inline-flex cursor-pointer items-center gap-1 rounded-sm font-inherit text-inherit hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2", sort.buttonClassName)}
114
+ >
115
+ {children}
116
+ <span aria-hidden="true" className="text-xs">{sort.direction === "asc" ? "↑" : sort.direction === "desc" ? "↓" : "↕"}</span>
117
+ </button>
118
+ ) : children}
119
+ </th>
120
+ ),
121
+ );
103
122
  TableHead.displayName = "TableHead";
104
123
 
105
124
  const TableCell = React.forwardRef<