@streamsai/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -2
  3. package/dist/components/ui/agent-avatar.d.ts +50 -0
  4. package/dist/components/ui/agent-avatar.js +38 -0
  5. package/dist/components/ui/alert-dialog.d.ts +12 -0
  6. package/dist/components/ui/alert-dialog.js +37 -0
  7. package/dist/components/ui/alert.d.ts +10 -0
  8. package/dist/components/ui/alert.js +28 -0
  9. package/dist/components/ui/assets/loader.json.js +1 -0
  10. package/dist/components/ui/avatar-frame.d.ts +11 -0
  11. package/dist/components/ui/avatar-frame.js +20 -0
  12. package/dist/components/ui/avatar.d.ts +23 -0
  13. package/dist/components/ui/avatar.js +54 -0
  14. package/dist/components/ui/badge.d.ts +7 -0
  15. package/dist/components/ui/badge.js +36 -0
  16. package/dist/components/ui/bar-list.d.ts +40 -0
  17. package/dist/components/ui/bar-list.js +37 -0
  18. package/dist/components/ui/breadcrumb.d.ts +8 -0
  19. package/dist/components/ui/breadcrumb.js +21 -0
  20. package/dist/components/ui/button-group.d.ts +10 -0
  21. package/dist/components/ui/button-group.js +41 -0
  22. package/dist/components/ui/button.d.ts +9 -0
  23. package/dist/components/ui/button.js +39 -0
  24. package/dist/components/ui/card.d.ts +9 -0
  25. package/dist/components/ui/card.js +25 -0
  26. package/dist/components/ui/checkbox.d.ts +3 -0
  27. package/dist/components/ui/checkbox.js +16 -0
  28. package/dist/components/ui/chip.d.ts +13 -0
  29. package/dist/components/ui/chip.js +22 -0
  30. package/dist/components/ui/clickable.d.ts +27 -0
  31. package/dist/components/ui/clickable.js +54 -0
  32. package/dist/components/ui/code-block.d.ts +68 -0
  33. package/dist/components/ui/code-block.js +112 -0
  34. package/dist/components/ui/code-view.d.ts +21 -0
  35. package/dist/components/ui/code-view.js +20 -0
  36. package/dist/components/ui/collapsible.d.ts +6 -0
  37. package/dist/components/ui/collapsible.js +18 -0
  38. package/dist/components/ui/combobox.d.ts +14 -0
  39. package/dist/components/ui/combobox.js +37 -0
  40. package/dist/components/ui/command.d.ts +15 -0
  41. package/dist/components/ui/command.js +34 -0
  42. package/dist/components/ui/context-menu.d.ts +21 -0
  43. package/dist/components/ui/context-menu.js +60 -0
  44. package/dist/components/ui/counter.d.ts +11 -0
  45. package/dist/components/ui/counter.js +20 -0
  46. package/dist/components/ui/dialog.d.ts +17 -0
  47. package/dist/components/ui/dialog.js +41 -0
  48. package/dist/components/ui/disabled-reason.d.ts +22 -0
  49. package/dist/components/ui/disabled-reason.js +16 -0
  50. package/dist/components/ui/dropdown-menu.d.ts +33 -0
  51. package/dist/components/ui/dropdown-menu.js +58 -0
  52. package/dist/components/ui/editable-text.d.ts +95 -0
  53. package/dist/components/ui/editable-text.js +191 -0
  54. package/dist/components/ui/heading.d.ts +24 -0
  55. package/dist/components/ui/heading.js +92 -0
  56. package/dist/components/ui/hover-card.d.ts +9 -0
  57. package/dist/components/ui/hover-card.js +16 -0
  58. package/dist/components/ui/icon-button.d.ts +23 -0
  59. package/dist/components/ui/icon-button.js +32 -0
  60. package/dist/components/ui/input.d.ts +3 -0
  61. package/dist/components/ui/input.js +8 -0
  62. package/dist/components/ui/kbd.d.ts +12 -0
  63. package/dist/components/ui/kbd.js +12 -0
  64. package/dist/components/ui/label.d.ts +3 -0
  65. package/dist/components/ui/label.js +7 -0
  66. package/dist/components/ui/loader.d.ts +19 -0
  67. package/dist/components/ui/loader.js +157 -0
  68. package/dist/components/ui/message-toolbar.d.ts +20 -0
  69. package/dist/components/ui/message-toolbar.js +17 -0
  70. package/dist/components/ui/modal.d.ts +30 -0
  71. package/dist/components/ui/modal.js +41 -0
  72. package/dist/components/ui/nav-item.d.ts +25 -0
  73. package/dist/components/ui/nav-item.js +66 -0
  74. package/dist/components/ui/panel-section.d.ts +22 -0
  75. package/dist/components/ui/panel-section.js +26 -0
  76. package/dist/components/ui/popover.d.ts +14 -0
  77. package/dist/components/ui/popover.js +30 -0
  78. package/dist/components/ui/progress.d.ts +5 -0
  79. package/dist/components/ui/progress.js +16 -0
  80. package/dist/components/ui/provider-badge.d.ts +22 -0
  81. package/dist/components/ui/provider-badge.js +38 -0
  82. package/dist/components/ui/radio-group.d.ts +5 -0
  83. package/dist/components/ui/radio-group.js +21 -0
  84. package/dist/components/ui/reply-summary.d.ts +16 -0
  85. package/dist/components/ui/reply-summary.js +8 -0
  86. package/dist/components/ui/resizable.d.ts +8 -0
  87. package/dist/components/ui/resizable.js +15 -0
  88. package/dist/components/ui/rich-text-editor.d.ts +447 -0
  89. package/dist/components/ui/rich-text-editor.js +2419 -0
  90. package/dist/components/ui/running-indicator.d.ts +17 -0
  91. package/dist/components/ui/running-indicator.js +34 -0
  92. package/dist/components/ui/sandboxed-frame.d.ts +29 -0
  93. package/dist/components/ui/sandboxed-frame.js +77 -0
  94. package/dist/components/ui/scroll-area.d.ts +14 -0
  95. package/dist/components/ui/scroll-area.js +17 -0
  96. package/dist/components/ui/section-header.d.ts +12 -0
  97. package/dist/components/ui/section-header.js +12 -0
  98. package/dist/components/ui/select.d.ts +15 -0
  99. package/dist/components/ui/select.js +40 -0
  100. package/dist/components/ui/separator.d.ts +3 -0
  101. package/dist/components/ui/separator.js +13 -0
  102. package/dist/components/ui/sheet.d.ts +23 -0
  103. package/dist/components/ui/sheet.js +48 -0
  104. package/dist/components/ui/shimmer-text.d.ts +26 -0
  105. package/dist/components/ui/shimmer-text.js +32 -0
  106. package/dist/components/ui/sidebar.d.ts +63 -0
  107. package/dist/components/ui/sidebar.js +283 -0
  108. package/dist/components/ui/skeleton.d.ts +14 -0
  109. package/dist/components/ui/skeleton.js +12 -0
  110. package/dist/components/ui/sonner.d.ts +4 -0
  111. package/dist/components/ui/sonner.js +14 -0
  112. package/dist/components/ui/spark-bars.d.ts +29 -0
  113. package/dist/components/ui/spark-bars.js +24 -0
  114. package/dist/components/ui/spinner.d.ts +16 -0
  115. package/dist/components/ui/spinner.js +26 -0
  116. package/dist/components/ui/stacked-bar.d.ts +37 -0
  117. package/dist/components/ui/stacked-bar.js +56 -0
  118. package/dist/components/ui/status-dot.d.ts +16 -0
  119. package/dist/components/ui/status-dot.js +32 -0
  120. package/dist/components/ui/switch.d.ts +5 -0
  121. package/dist/components/ui/switch.js +13 -0
  122. package/dist/components/ui/tabs.d.ts +14 -0
  123. package/dist/components/ui/tabs.js +55 -0
  124. package/dist/components/ui/text.d.ts +27 -0
  125. package/dist/components/ui/text.js +100 -0
  126. package/dist/components/ui/textarea.d.ts +3 -0
  127. package/dist/components/ui/textarea.js +7 -0
  128. package/dist/components/ui/theme-toggle.d.ts +21 -0
  129. package/dist/components/ui/theme-toggle.js +17 -0
  130. package/dist/components/ui/toggle-group.d.ts +6 -0
  131. package/dist/components/ui/toggle-group.js +12 -0
  132. package/dist/components/ui/tooltip.d.ts +11 -0
  133. package/dist/components/ui/tooltip.js +21 -0
  134. package/dist/components/ui/trace.d.ts +24 -0
  135. package/dist/components/ui/trace.js +57 -0
  136. package/dist/components/ui/trend-line.d.ts +93 -0
  137. package/dist/components/ui/trend-line.js +215 -0
  138. package/dist/components/ui/truncate.d.ts +30 -0
  139. package/dist/components/ui/truncate.js +54 -0
  140. package/dist/components/ui/viewer-shell.d.ts +55 -0
  141. package/dist/components/ui/viewer-shell.js +15 -0
  142. package/dist/components/ui/virtual-list.d.ts +91 -0
  143. package/dist/components/ui/virtual-list.js +406 -0
  144. package/dist/components/ui/visually-hidden.d.ts +15 -0
  145. package/dist/components/ui/visually-hidden.js +19 -0
  146. package/dist/direction/index.d.ts +19 -0
  147. package/dist/direction/index.js +32 -0
  148. package/dist/hooks/use-code-highlighting.d.ts +22 -0
  149. package/dist/hooks/use-code-highlighting.js +47 -0
  150. package/dist/hooks/use-is-truncated.d.ts +7 -0
  151. package/dist/hooks/use-is-truncated.js +23 -0
  152. package/dist/hooks/use-mobile.d.ts +2 -0
  153. package/dist/hooks/use-mobile.js +15 -0
  154. package/dist/icons/custom.d.ts +29 -0
  155. package/dist/icons/custom.js +48 -0
  156. package/dist/icons/index.d.ts +49 -0
  157. package/dist/icons/index.js +50 -0
  158. package/dist/icons/provider-logos.d.ts +17 -0
  159. package/dist/icons/provider-logos.js +28 -0
  160. package/dist/icons/providers.d.ts +9 -0
  161. package/dist/icons/providers.js +51 -0
  162. package/dist/index.d.ts +102 -0
  163. package/dist/index.js +124 -0
  164. package/dist/lib/code-highlighter.d.ts +9 -0
  165. package/dist/lib/code-highlighter.js +65 -0
  166. package/dist/lib/code-language.d.ts +13 -0
  167. package/dist/lib/code-language.js +103 -0
  168. package/dist/lib/code-tokens.d.ts +9 -0
  169. package/dist/lib/code-tokens.js +27 -0
  170. package/dist/lib/copy-text.d.ts +6 -0
  171. package/dist/lib/copy-text.js +46 -0
  172. package/dist/lib/home-tile.d.ts +12 -0
  173. package/dist/lib/home-tile.js +45 -0
  174. package/dist/lib/reveal-mask.d.ts +9 -0
  175. package/dist/lib/reveal-mask.js +16 -0
  176. package/dist/lib/text-direction.d.ts +28 -0
  177. package/dist/lib/text-direction.js +119 -0
  178. package/dist/lib/utils.d.ts +11 -0
  179. package/dist/lib/utils.js +30 -0
  180. package/dist/motion/components.d.ts +184 -0
  181. package/dist/motion/components.js +211 -0
  182. package/dist/motion/config.d.ts +849 -0
  183. package/dist/motion/config.js +429 -0
  184. package/dist/motion/index.d.ts +8 -0
  185. package/dist/motion/index.js +8 -0
  186. package/dist/motion/modal-presence.d.ts +5 -0
  187. package/dist/motion/modal-presence.js +27 -0
  188. package/dist/motion/popup.d.ts +77 -0
  189. package/dist/motion/popup.js +103 -0
  190. package/dist/platform/index.d.ts +29 -0
  191. package/dist/platform/index.js +63 -0
  192. package/dist/styles/base.css +319 -0
  193. package/dist/styles/fonts/Figtree-Variable.woff2 +0 -0
  194. package/dist/styles/fonts/OFL.txt +93 -0
  195. package/dist/styles/fonts/Poppins-Medium.woff2 +0 -0
  196. package/dist/styles/fonts/Poppins-Regular.woff2 +0 -0
  197. package/dist/styles/fonts/Poppins-SemiBold.woff2 +0 -0
  198. package/dist/styles/fonts.css +28 -0
  199. package/dist/styles/globals.css +8 -0
  200. package/dist/styles/mcp-host.css +76 -0
  201. package/dist/styles/tokens.css +387 -0
  202. package/dist/styles/tokens.d.mts +57 -0
  203. package/dist/styles/tokens.mjs +59 -0
  204. package/dist/theme/index.d.ts +28 -0
  205. package/dist/theme/index.js +65 -0
  206. package/package.json +136 -3
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @streamsai/ui — public barrel.
3
+ *
4
+ * Every component in src/components/ui/ must appear here exactly once.
5
+ * The lint:stories gate (tooling/scripts/lint-story-coverage.ts) enforces this.
6
+ *
7
+ * Rules:
8
+ * - Type-only members use the inline `type` modifier so this file stays a single
9
+ * import per component and verbatimModuleSyntax + consistent-type-imports are happy.
10
+ * - Icons are re-exported as the `Icons` namespace.
11
+ * - Platform provider and hook live outside components/ui/ so they may have
12
+ * separate export lines without conflicting with rule 3.
13
+ * - prettier-ignore is used on lines that exceed the 100-char print width but must
14
+ * stay on one line for the story-coverage gate (which matches by line prefix).
15
+ */
16
+ export { ThemeProvider, useTheme, THEME_STORAGE_KEY, THEME_INIT_SCRIPT } from './theme/index.js';
17
+ export { DirectionProvider, useDirection, useTextDirection } from './direction/index.js';
18
+ // ── Icons ────────────────────────────────────────────────────────────────────
19
+ export * as Icons from './icons/index.js';
20
+ export { PlatformProvider, usePlatform } from './platform/index.js';
21
+ // ── Motion (motion.dev; tune in motion/config.ts) ─────────────────────────────
22
+ // prettier-ignore
23
+ export { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion, useTransform } from './motion/index.js';
24
+ // prettier-ignore
25
+ export { duration, ease, spring, distance, scale, stagger, transitions, press, presets, loops, runningIndicator, loopAnimate, loopTransition, popup, sheet, slide, resize, reflow, edgeReveal, stepSwap, staggerDelay } from './motion/index.js';
26
+ // prettier-ignore
27
+ export { MotionProvider, disableMotion, enableMotion, enablePresenceExits, disablePresenceExits, PageTransition, Collapse, AutoHeight, usePrefersReducedMotion, useAnyModalOpen, Swap, swapVariants, StaggerItem } from './motion/index.js';
28
+ // ── Components ───────────────────────────────────────────────────────────────
29
+ export { AgentAvatar, } from './components/ui/agent-avatar.js';
30
+ export { AGENT_AVATAR_SIZE, AGENT_AVATAR_RADIUS, AGENT_AVATAR_PX, } from './components/ui/avatar-frame.js';
31
+ export { Alert, AlertTitle, AlertDescription, AlertAction } from './components/ui/alert.js';
32
+ // prettier-ignore
33
+ export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge } from './components/ui/avatar.js';
34
+ export { ProviderBadge, PROVIDER_BADGE_MARKS, providerBadgeMarkOf, } from './components/ui/provider-badge.js';
35
+ export { Badge, badgeVariants } from './components/ui/badge.js';
36
+ // prettier-ignore
37
+ export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants } from './components/ui/button-group.js';
38
+ export { Button, buttonVariants } from './components/ui/button.js';
39
+ export { cn } from './lib/utils.js';
40
+ export { HOME_TILE_COUNT, homeTileIndex } from './lib/home-tile.js';
41
+ export { detectTextDirection, hasStrongCharacter, isNeutralToken } from './lib/text-direction.js';
42
+ export { Checkbox } from './components/ui/checkbox.js';
43
+ export { Clickable, clickableVariants } from './components/ui/clickable.js';
44
+ // prettier-ignore
45
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './components/ui/dialog.js';
46
+ // prettier-ignore
47
+ export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent } from './components/ui/dropdown-menu.js';
48
+ // prettier-ignore
49
+ export { EditableText, editableTextVariants } from './components/ui/editable-text.js';
50
+ // prettier-ignore
51
+ export { Heading, headingVariants } from './components/ui/heading.js';
52
+ export { IconButton, iconButtonVariants } from './components/ui/icon-button.js';
53
+ export { Input } from './components/ui/input.js';
54
+ export { Label } from './components/ui/label.js';
55
+ // prettier-ignore
56
+ export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from './components/ui/popover.js';
57
+ // prettier-ignore
58
+ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue } from './components/ui/select.js';
59
+ export { Separator } from './components/ui/separator.js';
60
+ export { Skeleton } from './components/ui/skeleton.js';
61
+ export { ShimmerText } from './components/ui/shimmer-text.js';
62
+ export { Spinner } from './components/ui/spinner.js';
63
+ export { Loader, } from './components/ui/loader.js';
64
+ export { Switch } from './components/ui/switch.js';
65
+ export { Text, textVariants } from './components/ui/text.js';
66
+ export { Truncate } from './components/ui/truncate.js';
67
+ export { Textarea } from './components/ui/textarea.js';
68
+ export { DisabledReason } from './components/ui/disabled-reason.js';
69
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './components/ui/tooltip.js';
70
+ export { VisuallyHidden, visuallyHiddenVariants } from './components/ui/visually-hidden.js';
71
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './components/ui/tabs.js';
72
+ // prettier-ignore
73
+ export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from './components/ui/sheet.js';
74
+ // prettier-ignore
75
+ export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar } from './components/ui/sidebar.js';
76
+ export { Toaster, toast } from './components/ui/sonner.js';
77
+ // prettier-ignore
78
+ export { Combobox, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxGroupLabel, ComboboxInput, ComboboxItem, ComboboxList } from './components/ui/combobox.js';
79
+ export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut, CommandDialog, } from './components/ui/command.js';
80
+ export { ScrollArea, ScrollBar } from './components/ui/scroll-area.js';
81
+ export { Kbd, KbdGroup } from './components/ui/kbd.js';
82
+ // prettier-ignore
83
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter } from './components/ui/card.js';
84
+ // prettier-ignore
85
+ export { Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter } from './components/ui/modal.js';
86
+ // prettier-ignore
87
+ export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from './components/ui/alert-dialog.js';
88
+ export { RadioGroup, RadioGroupItem } from './components/ui/radio-group.js';
89
+ export { Progress } from './components/ui/progress.js';
90
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent } from './components/ui/collapsible.js';
91
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable.js';
92
+ export { HoverCard, HoverCardTrigger, HoverCardContent } from './components/ui/hover-card.js';
93
+ // prettier-ignore
94
+ export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent } from './components/ui/context-menu.js';
95
+ export { ToggleGroup, ToggleGroupItem } from './components/ui/toggle-group.js';
96
+ // prettier-ignore
97
+ export { RichTextEditor, INSERT_CYCLE_TRIGGER_COMMAND, INSERT_CUSTOM_EMOJI_COMMAND, MENTION_PAGE_SIZE, EMOJI_TYPEAHEAD_LIMIT, STREAM_TYPEAHEAD_LIMIT, isAllMentionCandidate } from './components/ui/rich-text-editor.js';
98
+ export { VirtualList, } from './components/ui/virtual-list.js';
99
+ export { TrendLine, TrendLineSkeleton, } from './components/ui/trend-line.js';
100
+ export { BarList, BarListSkeleton, } from './components/ui/bar-list.js';
101
+ export { StackedBar, StackedBarSkeleton, stackedBarWidths, } from './components/ui/stacked-bar.js';
102
+ export { SparkBars } from './components/ui/spark-bars.js';
103
+ export { RunningIndicator } from './components/ui/running-indicator.js';
104
+ export { StatusDot, statusDotVariants } from './components/ui/status-dot.js';
105
+ export { Counter, counterVariants } from './components/ui/counter.js';
106
+ export { Chip, chipVariants } from './components/ui/chip.js';
107
+ export { SectionHeader } from './components/ui/section-header.js';
108
+ export { NavItem } from './components/ui/nav-item.js';
109
+ export { ReplySummary } from './components/ui/reply-summary.js';
110
+ export { MessageToolbar, MessageToolbarButton } from './components/ui/message-toolbar.js';
111
+ export { PanelSection } from './components/ui/panel-section.js';
112
+ // prettier-ignore
113
+ export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from './components/ui/breadcrumb.js';
114
+ export { SandboxedFrame, sandboxFor, } from './components/ui/sandboxed-frame.js';
115
+ export { CodeView } from './components/ui/code-view.js';
116
+ // prettier-ignore
117
+ export { CodeBlock, CodeBlockHeader, CodeBlockTitle, CodeBlockActions, CodeBlockCopyButton, CodeBlockContent, CodeBlockLine, CodeBlockLines, CodeBlockFooter } from './components/ui/code-block.js';
118
+ // prettier-ignore
119
+ export { Trace, TraceItem, TraceLine, TraceIcon, TraceStatusIcon, TraceTitle, TraceDescription, TraceCollapsible, TraceCollapsibleTrigger, TraceCollapsibleContent } from './components/ui/trace.js';
120
+ export { useCodeHighlighting } from './hooks/use-code-highlighting.js';
121
+ export { codeLanguageForFence, codeLanguageForFileName, } from './lib/code-language.js';
122
+ export { copyText } from './lib/copy-text.js';
123
+ export { ViewerShell, } from './components/ui/viewer-shell.js';
124
+ export { ThemeToggle } from './components/ui/theme-toggle.js';
@@ -0,0 +1,9 @@
1
+ import type { CodeLanguage } from './code-language.js';
2
+ export type CodeToken = {
3
+ content: string;
4
+ color?: string;
5
+ fontStyle?: number;
6
+ };
7
+ export type CodeLines = CodeToken[][];
8
+ /** Tokenises `code` line by line. Rejects if the highlighter or the grammar cannot load. */
9
+ export declare const highlightCode: (code: string, lang: CodeLanguage) => Promise<CodeLines>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The Shiki highlighter, loaded on demand. Nothing outside this file imports `shiki`, and this
3
+ * file is only ever reached through a dynamic `import()`, so the core, the regex engine and every
4
+ * grammar land in their own chunks and the main bundle does not grow.
5
+ *
6
+ * Uses the JavaScript regex engine (no Oniguruma wasm) and a CSS-variables theme: token colours
7
+ * are `var(--code-token-*)` references that `tokens.css` maps to the design tokens for light and
8
+ * dark, so switching theme never re-highlights.
9
+ */
10
+ import { createHighlighterCore, createCssVariablesTheme } from 'shiki/core';
11
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
12
+ const THEME_NAME = 'streams-code';
13
+ // One static import per grammar so the bundler emits one chunk each. A grammar that embeds others
14
+ // (php -> html/css/js/sql, scss -> css) pulls them in through its own default export.
15
+ const GRAMMARS = {
16
+ typescript: () => import('shiki/langs/typescript.mjs'),
17
+ tsx: () => import('shiki/langs/tsx.mjs'),
18
+ javascript: () => import('shiki/langs/javascript.mjs'),
19
+ jsx: () => import('shiki/langs/jsx.mjs'),
20
+ json: () => import('shiki/langs/json.mjs'),
21
+ jsonl: () => import('shiki/langs/jsonl.mjs'),
22
+ yaml: () => import('shiki/langs/yaml.mjs'),
23
+ python: () => import('shiki/langs/python.mjs'),
24
+ sql: () => import('shiki/langs/sql.mjs'),
25
+ go: () => import('shiki/langs/go.mjs'),
26
+ ruby: () => import('shiki/langs/ruby.mjs'),
27
+ java: () => import('shiki/langs/java.mjs'),
28
+ kotlin: () => import('shiki/langs/kotlin.mjs'),
29
+ swift: () => import('shiki/langs/swift.mjs'),
30
+ rust: () => import('shiki/langs/rust.mjs'),
31
+ c: () => import('shiki/langs/c.mjs'),
32
+ cpp: () => import('shiki/langs/cpp.mjs'),
33
+ csharp: () => import('shiki/langs/csharp.mjs'),
34
+ php: () => import('shiki/langs/php.mjs'),
35
+ css: () => import('shiki/langs/css.mjs'),
36
+ scss: () => import('shiki/langs/scss.mjs'),
37
+ xml: () => import('shiki/langs/xml.mjs'),
38
+ toml: () => import('shiki/langs/toml.mjs'),
39
+ ini: () => import('shiki/langs/ini.mjs'),
40
+ diff: () => import('shiki/langs/diff.mjs'),
41
+ shellscript: () => import('shiki/langs/shellscript.mjs'),
42
+ };
43
+ let highlighter;
44
+ const getHighlighter = () => {
45
+ highlighter ??= createHighlighterCore({
46
+ themes: [createCssVariablesTheme({ name: THEME_NAME, variablePrefix: '--code-' })],
47
+ langs: [],
48
+ // Forgiving: a grammar pattern the JS engine cannot translate matches nothing instead of throwing.
49
+ engine: createJavaScriptRegexEngine({ forgiving: true }),
50
+ }).catch((error) => {
51
+ highlighter = undefined; // let a later open retry
52
+ throw error;
53
+ });
54
+ return highlighter;
55
+ };
56
+ /** Tokenises `code` line by line. Rejects if the highlighter or the grammar cannot load. */
57
+ export const highlightCode = async (code, lang) => {
58
+ const core = await getHighlighter();
59
+ if (!core.getLoadedLanguages().includes(lang)) {
60
+ const grammar = await GRAMMARS[lang]();
61
+ await core.loadLanguage(grammar.default);
62
+ }
63
+ const { tokens } = core.codeToTokens(code, { lang, theme: THEME_NAME });
64
+ return tokens.map((line) => line.map(({ content, color, fontStyle }) => ({ content, color, fontStyle })));
65
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * File name -> highlight language, with no highlighter import so it stays in the main bundle.
3
+ * An unknown extension (txt, log, conf, cfg, none) maps to `null` and the viewer never loads
4
+ * the highlighter for it.
5
+ */
6
+ export type CodeLanguage = 'typescript' | 'tsx' | 'javascript' | 'jsx' | 'json' | 'jsonl' | 'yaml' | 'python' | 'sql' | 'go' | 'ruby' | 'java' | 'kotlin' | 'swift' | 'rust' | 'c' | 'cpp' | 'csharp' | 'php' | 'css' | 'scss' | 'xml' | 'toml' | 'ini' | 'diff' | 'shellscript';
7
+ export declare const codeLanguageForFileName: (fileName: string | undefined) => CodeLanguage | null;
8
+ /**
9
+ * Markdown fence label (```ts) -> highlight language. Only an allowlist resolves: an unsupported
10
+ * or hostile label returns `null`, so it can never pick a grammar import path. Callers keep the
11
+ * raw label for the visible header.
12
+ */
13
+ export declare const codeLanguageForFence: (label: string | undefined) => CodeLanguage | null;
@@ -0,0 +1,103 @@
1
+ const EXTENSIONS = {
2
+ ts: 'typescript',
3
+ mts: 'typescript',
4
+ cts: 'typescript',
5
+ tsx: 'tsx',
6
+ js: 'javascript',
7
+ mjs: 'javascript',
8
+ cjs: 'javascript',
9
+ jsx: 'jsx',
10
+ json: 'json',
11
+ jsonl: 'jsonl',
12
+ ndjson: 'jsonl',
13
+ yaml: 'yaml',
14
+ yml: 'yaml',
15
+ py: 'python',
16
+ sql: 'sql',
17
+ go: 'go',
18
+ rb: 'ruby',
19
+ java: 'java',
20
+ kt: 'kotlin',
21
+ kts: 'kotlin',
22
+ swift: 'swift',
23
+ rs: 'rust',
24
+ c: 'c',
25
+ h: 'c',
26
+ cpp: 'cpp',
27
+ cc: 'cpp',
28
+ cxx: 'cpp',
29
+ hpp: 'cpp',
30
+ hh: 'cpp',
31
+ hxx: 'cpp',
32
+ cs: 'csharp',
33
+ php: 'php',
34
+ css: 'css',
35
+ scss: 'scss',
36
+ xml: 'xml',
37
+ toml: 'toml',
38
+ ini: 'ini',
39
+ diff: 'diff',
40
+ patch: 'diff',
41
+ sh: 'shellscript',
42
+ bash: 'shellscript',
43
+ zsh: 'shellscript',
44
+ };
45
+ export const codeLanguageForFileName = (fileName) => {
46
+ if (!fileName)
47
+ return null;
48
+ const dot = fileName.lastIndexOf('.');
49
+ if (dot < 0)
50
+ return null;
51
+ // Own-property check: "constructor" or "__proto__" after a dot must not resolve to a prototype member.
52
+ const ext = fileName.slice(dot + 1).toLowerCase();
53
+ return Object.hasOwn(EXTENSIONS, ext) ? EXTENSIONS[ext] : null;
54
+ };
55
+ const FENCE_LABELS = {
56
+ ...Object.fromEntries([
57
+ 'typescript',
58
+ 'tsx',
59
+ 'javascript',
60
+ 'jsx',
61
+ 'json',
62
+ 'jsonl',
63
+ 'yaml',
64
+ 'python',
65
+ 'sql',
66
+ 'go',
67
+ 'ruby',
68
+ 'java',
69
+ 'kotlin',
70
+ 'swift',
71
+ 'rust',
72
+ 'c',
73
+ 'cpp',
74
+ 'csharp',
75
+ 'php',
76
+ 'css',
77
+ 'scss',
78
+ 'xml',
79
+ 'toml',
80
+ 'ini',
81
+ 'diff',
82
+ 'shellscript',
83
+ ].map((name) => [name, name])),
84
+ ...EXTENSIONS,
85
+ golang: 'go',
86
+ 'c++': 'cpp',
87
+ 'c#': 'csharp',
88
+ python3: 'python',
89
+ node: 'javascript',
90
+ shell: 'shellscript',
91
+ console: 'shellscript',
92
+ };
93
+ /**
94
+ * Markdown fence label (```ts) -> highlight language. Only an allowlist resolves: an unsupported
95
+ * or hostile label returns `null`, so it can never pick a grammar import path. Callers keep the
96
+ * raw label for the visible header.
97
+ */
98
+ export const codeLanguageForFence = (label) => {
99
+ const key = label?.trim().split(/\s+/)[0]?.toLowerCase();
100
+ if (!key)
101
+ return null;
102
+ return Object.hasOwn(FENCE_LABELS, key) ? FENCE_LABELS[key] : null;
103
+ };
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { CodeToken } from './code-highlighter.js';
3
+ /** One line of highlighter tokens as `<span>` text nodes. Token content is never markup. */
4
+ export declare const renderCodeTokens: (line: CodeToken[]) => ReactNode;
5
+ /**
6
+ * The lines to draw for `code`: split on newline, minus the one empty line a trailing newline
7
+ * would add. Display only; copy always uses the original string.
8
+ */
9
+ export declare const codeDisplayLines: (code: string) => string[];
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Fragment } from 'react';
3
+ const FONT_ITALIC = 1;
4
+ const FONT_BOLD = 2;
5
+ const FONT_UNDERLINE = 4;
6
+ const styleOf = (color, fontStyle) => {
7
+ if (!color && !fontStyle)
8
+ return undefined;
9
+ return {
10
+ color,
11
+ fontStyle: fontStyle && fontStyle & FONT_ITALIC ? 'italic' : undefined,
12
+ fontWeight: fontStyle && fontStyle & FONT_BOLD ? 700 : undefined,
13
+ textDecoration: fontStyle && fontStyle & FONT_UNDERLINE ? 'underline' : undefined,
14
+ };
15
+ };
16
+ /** One line of highlighter tokens as `<span>` text nodes. Token content is never markup. */
17
+ export const renderCodeTokens = (line) => (_jsx(Fragment, { children: line.map((token, index) => (_jsx("span", { style: styleOf(token.color, token.fontStyle), children: token.content }, index))) }));
18
+ /**
19
+ * The lines to draw for `code`: split on newline, minus the one empty line a trailing newline
20
+ * would add. Display only; copy always uses the original string.
21
+ */
22
+ export const codeDisplayLines = (code) => {
23
+ const lines = code.split('\n');
24
+ if (lines.length > 1 && lines[lines.length - 1] === '')
25
+ lines.pop();
26
+ return lines;
27
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Writes `text` to the clipboard exactly as given. Resolves true only when the browser
3
+ * reports success: the async Clipboard API first, then a hidden-textarea selection copy for
4
+ * webviews where the API is missing or rejects. Never reads the clipboard.
5
+ */
6
+ export declare const copyText: (text: string) => Promise<boolean>;
@@ -0,0 +1,46 @@
1
+ const copyWithSelection = (text) => {
2
+ if (typeof document === 'undefined' || typeof document.execCommand !== 'function')
3
+ return false;
4
+ const field = document.createElement('textarea');
5
+ field.value = text;
6
+ field.setAttribute('readonly', '');
7
+ field.setAttribute('aria-hidden', 'true');
8
+ field.style.position = 'fixed';
9
+ field.style.top = '0';
10
+ field.style.opacity = '0';
11
+ const selection = document.getSelection();
12
+ const saved = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
13
+ document.body.appendChild(field);
14
+ let copied = false;
15
+ try {
16
+ field.select();
17
+ field.setSelectionRange(0, text.length);
18
+ copied = document.execCommand('copy');
19
+ }
20
+ catch {
21
+ copied = false;
22
+ }
23
+ field.remove();
24
+ if (saved && selection) {
25
+ selection.removeAllRanges();
26
+ selection.addRange(saved);
27
+ }
28
+ return copied;
29
+ };
30
+ /**
31
+ * Writes `text` to the clipboard exactly as given. Resolves true only when the browser
32
+ * reports success: the async Clipboard API first, then a hidden-textarea selection copy for
33
+ * webviews where the API is missing or rejects. Never reads the clipboard.
34
+ */
35
+ export const copyText = async (text) => {
36
+ try {
37
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
38
+ await navigator.clipboard.writeText(text);
39
+ return true;
40
+ }
41
+ }
42
+ catch {
43
+ // fall through to the selection copy
44
+ }
45
+ return copyWithSelection(text);
46
+ };
@@ -0,0 +1,12 @@
1
+ /** Number of stream tile colours (`--home-tile-1` … `--home-tile-4`). Append-only, see tokens.mjs. */
2
+ export declare const HOME_TILE_COUNT = 4;
3
+ /**
4
+ * Which tile colour (1-based) a stream gets. Rendezvous (highest-random-weight) hashing: every
5
+ * colour scores the stream id independently and the highest score wins. Appending colour N+1
6
+ * therefore only moves the streams that now score highest on it (about 1/(N+1) of them), where
7
+ * `hash % N` would recolour almost everything. Never reorder or insert colours, only append.
8
+ * iOS computes the same function (`HomeTileColor`); both test suites check the same fixed ids,
9
+ * so the two clients never disagree.
10
+ */
11
+ export declare const pickHomeTile: (streamId: string, count: number) => number;
12
+ export declare const homeTileIndex: (streamId: string) => number;
@@ -0,0 +1,45 @@
1
+ /** Number of stream tile colours (`--home-tile-1` … `--home-tile-4`). Append-only, see tokens.mjs. */
2
+ export const HOME_TILE_COUNT = 4;
3
+ const FNV_OFFSET = 0x811c9dc5;
4
+ const FNV_PRIME = 0x01000193;
5
+ const INDEX_SALT = 0x9e3779b1;
6
+ const fnv1a = (text) => {
7
+ let hash = FNV_OFFSET;
8
+ for (const byte of new TextEncoder().encode(text)) {
9
+ hash ^= byte;
10
+ hash = Math.imul(hash, FNV_PRIME) >>> 0;
11
+ }
12
+ return hash;
13
+ };
14
+ // murmur3 finaliser: spreads the bits so every colour gets an independent score.
15
+ const mix = (value) => {
16
+ let h = value >>> 0;
17
+ h ^= h >>> 16;
18
+ h = Math.imul(h, 0x85ebca6b) >>> 0;
19
+ h ^= h >>> 13;
20
+ h = Math.imul(h, 0xc2b2ae35) >>> 0;
21
+ h ^= h >>> 16;
22
+ return h >>> 0;
23
+ };
24
+ /**
25
+ * Which tile colour (1-based) a stream gets. Rendezvous (highest-random-weight) hashing: every
26
+ * colour scores the stream id independently and the highest score wins. Appending colour N+1
27
+ * therefore only moves the streams that now score highest on it (about 1/(N+1) of them), where
28
+ * `hash % N` would recolour almost everything. Never reorder or insert colours, only append.
29
+ * iOS computes the same function (`HomeTileColor`); both test suites check the same fixed ids,
30
+ * so the two clients never disagree.
31
+ */
32
+ export const pickHomeTile = (streamId, count) => {
33
+ const base = fnv1a(streamId.toLowerCase());
34
+ let best = 1;
35
+ let bestScore = -1;
36
+ for (let index = 1; index <= count; index += 1) {
37
+ const score = mix((base ^ Math.imul(index, INDEX_SALT)) >>> 0);
38
+ if (score > bestScore) {
39
+ best = index;
40
+ bestScore = score;
41
+ }
42
+ }
43
+ return best;
44
+ };
45
+ export const homeTileIndex = (streamId) => pickHomeTile(streamId, HOME_TILE_COUNT);
@@ -0,0 +1,9 @@
1
+ /** Width of the soft edge a clipped one-line label fades out over, instead of an ellipsis. */
2
+ export declare const REVEAL_FADE_PX = 24;
3
+ /**
4
+ * `mask-image` for a one-line label clipped by `hiddenPx` and slid `offsetPx` toward its end
5
+ * (`NavItem`'s hover reveal). The leading edge fades in as text leaves it; the trailing fade melts
6
+ * away as the last letters arrive, so a fully revealed title ends crisp. `none` when nothing is
7
+ * clipped. `rtl` puts the start on the right.
8
+ */
9
+ export declare function revealMask(offsetPx: number, hiddenPx: number, rtl?: boolean): string;
@@ -0,0 +1,16 @@
1
+ /** Width of the soft edge a clipped one-line label fades out over, instead of an ellipsis. */
2
+ export const REVEAL_FADE_PX = 24;
3
+ /**
4
+ * `mask-image` for a one-line label clipped by `hiddenPx` and slid `offsetPx` toward its end
5
+ * (`NavItem`'s hover reveal). The leading edge fades in as text leaves it; the trailing fade melts
6
+ * away as the last letters arrive, so a fully revealed title ends crisp. `none` when nothing is
7
+ * clipped. `rtl` puts the start on the right.
8
+ */
9
+ export function revealMask(offsetPx, hiddenPx, rtl = false) {
10
+ const moved = Math.abs(offsetPx);
11
+ const lead = Math.min(REVEAL_FADE_PX, moved);
12
+ const tail = Math.min(REVEAL_FADE_PX, Math.max(0, hiddenPx - moved));
13
+ if (lead === 0 && tail === 0)
14
+ return 'none';
15
+ return `linear-gradient(to ${rtl ? 'left' : 'right'}, transparent, #000 ${lead}px, #000 calc(100% - ${tail}px), transparent)`;
16
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Mixed-script text direction — faithful port of HAC #1045
3
+ * (`packages/chat-ui-components/src/utils/textDirection.ts`).
4
+ *
5
+ * Word-votes Hebrew vs English *prose*; product labels, acronyms, camelCase and
6
+ * URL-shaped tokens are neutral so e.g. `"monday יצר…"` stays RTL. Ordinary
7
+ * lowercase English words (`hello`, `please`) **do** vote LTR — only technical
8
+ * shapes are excluded. Falls back to UAX #9 first-strong + 70% dominant-script.
9
+ * BOM (U+FEFF) never counts as RTL.
10
+ */
11
+ export type TextDirection = 'ltr' | 'rtl';
12
+ /**
13
+ * Technical / product-label runs must not tip the prose vote.
14
+ * Ordinary lowercase English (`hello`, `please`) is **not** neutral — only
15
+ * acronyms, camelCase, and tokens with technical punctuation.
16
+ */
17
+ export declare const isNeutralToken: (token: string) => boolean;
18
+ /** Whether `text` contains any character that can decide a direction. */
19
+ export declare const hasStrongCharacter: (text: string) => boolean;
20
+ /**
21
+ * Resolve the base direction of a run of text.
22
+ *
23
+ * Hebrew and English prose words vote first. Product labels, acronyms and
24
+ * code-shaped tokens are neutral. A prose tie uses its first non-technical
25
+ * word; content with no recognized prose falls back to UAX #9 first-strong
26
+ * with a dominant-script override.
27
+ */
28
+ export declare const detectTextDirection: (text: string) => TextDirection;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Mixed-script text direction — faithful port of HAC #1045
3
+ * (`packages/chat-ui-components/src/utils/textDirection.ts`).
4
+ *
5
+ * Word-votes Hebrew vs English *prose*; product labels, acronyms, camelCase and
6
+ * URL-shaped tokens are neutral so e.g. `"monday יצר…"` stays RTL. Ordinary
7
+ * lowercase English words (`hello`, `please`) **do** vote LTR — only technical
8
+ * shapes are excluded. Falls back to UAX #9 first-strong + 70% dominant-script.
9
+ * BOM (U+FEFF) never counts as RTL.
10
+ */
11
+ /**
12
+ * Hebrew and its presentation forms — the RTL script chat users write in.
13
+ * Other RTL blocks are listed so switching from `dir="auto"` does not drop
14
+ * support those scripts already had.
15
+ */
16
+ const RTL_LETTER = /[\u0590-\u05FF\uFB1D-\uFB4F\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFE]/;
17
+ /**
18
+ * Strong LTR letters. Enumerated (not `\p{L}`) to stay compatible with older
19
+ * targets; Latin / Greek / Cyrillic / CJK cover what chat sees.
20
+ */
21
+ const LTR_LETTER = /[A-Za-z\u00C0-\u024F\u0370-\u03FF\u0400-\u04FF\u3040-\u30FF\u4E00-\u9FFF]/;
22
+ /** Hebrew words are the prose signal this chat is tuned to recognize. */
23
+ const HEBREW_WORD = /^[\u0590-\u05FF\uFB1D-\uFB4F]+$/;
24
+ /**
25
+ * A broad Latin token captures product names, prose words, URLs, commands and
26
+ * identifiers. Wider than a dictionary word so technical text can be excluded.
27
+ */
28
+ const LATIN_TOKEN = /^[A-Za-z][A-Za-z0-9_./:@?&=+#%~-]*$/;
29
+ /**
30
+ * Technical / product-label runs must not tip the prose vote.
31
+ * Ordinary lowercase English (`hello`, `please`) is **not** neutral — only
32
+ * acronyms, camelCase, and tokens with technical punctuation.
33
+ */
34
+ export const isNeutralToken = (token) => {
35
+ if (!token)
36
+ return true;
37
+ if (/^[A-Z0-9]{2,}$/.test(token))
38
+ return true;
39
+ return /[_./:@?&=+#%~-]/.test(token) || /[a-z][A-Z]/.test(token);
40
+ };
41
+ /**
42
+ * Count Hebrew and ordinary English *words*, excluding identifiers / labels.
43
+ * Character counts over-weight a short English product name; word voting mirrors
44
+ * what people perceive as prose.
45
+ */
46
+ const proseDirection = (text) => {
47
+ let rtlWords = 0;
48
+ let ltrWords = 0;
49
+ let firstProseDirection = null;
50
+ const tokens = text.match(/[\u0590-\u05FF\uFB1D-\uFB4F]+|[A-Za-z][A-Za-z0-9_./:@?&=+#%~-]*/g) ?? [];
51
+ for (const token of tokens) {
52
+ if (HEBREW_WORD.test(token)) {
53
+ rtlWords += 1;
54
+ if (firstProseDirection === null)
55
+ firstProseDirection = 'rtl';
56
+ }
57
+ else if (LATIN_TOKEN.test(token) && !isNeutralToken(token)) {
58
+ ltrWords += 1;
59
+ if (firstProseDirection === null)
60
+ firstProseDirection = 'ltr';
61
+ }
62
+ }
63
+ if (rtlWords === 0 && ltrWords === 0)
64
+ return null;
65
+ if (rtlWords === ltrWords)
66
+ return firstProseDirection;
67
+ return rtlWords > ltrWords ? 'rtl' : 'ltr';
68
+ };
69
+ const strengthOf = (character) => {
70
+ if (RTL_LETTER.test(character))
71
+ return 'rtl';
72
+ if (LTR_LETTER.test(character))
73
+ return 'ltr';
74
+ return null;
75
+ };
76
+ /** Whether `text` contains any character that can decide a direction. */
77
+ export const hasStrongCharacter = (text) => {
78
+ for (const character of text) {
79
+ if (strengthOf(character) !== null)
80
+ return true;
81
+ }
82
+ return false;
83
+ };
84
+ const DOMINANT_SCRIPT_SHARE = 0.7;
85
+ /**
86
+ * Resolve the base direction of a run of text.
87
+ *
88
+ * Hebrew and English prose words vote first. Product labels, acronyms and
89
+ * code-shaped tokens are neutral. A prose tie uses its first non-technical
90
+ * word; content with no recognized prose falls back to UAX #9 first-strong
91
+ * with a dominant-script override.
92
+ */
93
+ export const detectTextDirection = (text) => {
94
+ const prose = proseDirection(text);
95
+ if (prose !== null)
96
+ return prose;
97
+ let rtlCount = 0;
98
+ let ltrCount = 0;
99
+ let firstStrong = null;
100
+ for (const character of text) {
101
+ const strength = strengthOf(character);
102
+ if (strength === null)
103
+ continue;
104
+ if (firstStrong === null)
105
+ firstStrong = strength;
106
+ if (strength === 'rtl')
107
+ rtlCount += 1;
108
+ else
109
+ ltrCount += 1;
110
+ }
111
+ if (firstStrong === null)
112
+ return 'ltr';
113
+ const total = rtlCount + ltrCount;
114
+ if (rtlCount / total >= DOMINANT_SCRIPT_SHARE)
115
+ return 'rtl';
116
+ if (ltrCount / total >= DOMINANT_SCRIPT_SHARE)
117
+ return 'ltr';
118
+ return firstStrong;
119
+ };