@butlerw/vellum 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 (446) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/__fixtures__/responses/code-generation.json +42 -0
  4. package/__fixtures__/responses/error-response.json +20 -0
  5. package/__fixtures__/responses/hello-world.json +32 -0
  6. package/dist/auth-6MCXESOH.js +26 -0
  7. package/dist/chunk-SECXJGWA.js +597 -0
  8. package/dist/index.js +34023 -0
  9. package/package.json +67 -0
  10. package/src/__tests__/commands.e2e.test.ts +728 -0
  11. package/src/__tests__/credentials.test.ts +713 -0
  12. package/src/__tests__/mode-e2e.test.ts +391 -0
  13. package/src/__tests__/tui-integration.test.tsx +1271 -0
  14. package/src/agents/__tests__/task-persistence.test.ts +235 -0
  15. package/src/agents/commands/delegate.ts +240 -0
  16. package/src/agents/commands/index.ts +10 -0
  17. package/src/agents/commands/resume.ts +335 -0
  18. package/src/agents/index.ts +29 -0
  19. package/src/agents/task-persistence.ts +272 -0
  20. package/src/agents/task-resumption.ts +242 -0
  21. package/src/app.tsx +4737 -0
  22. package/src/commands/__tests__/.gitkeep +1 -0
  23. package/src/commands/__tests__/agents.test.ts +606 -0
  24. package/src/commands/__tests__/auth.test.ts +626 -0
  25. package/src/commands/__tests__/autocomplete.test.ts +683 -0
  26. package/src/commands/__tests__/batch.test.ts +287 -0
  27. package/src/commands/__tests__/chain-pipe-parser.test.ts +654 -0
  28. package/src/commands/__tests__/completion.test.ts +238 -0
  29. package/src/commands/__tests__/core.test.ts +363 -0
  30. package/src/commands/__tests__/executor.test.ts +496 -0
  31. package/src/commands/__tests__/exit-codes.test.ts +220 -0
  32. package/src/commands/__tests__/init.test.ts +243 -0
  33. package/src/commands/__tests__/language.test.ts +353 -0
  34. package/src/commands/__tests__/mode-cli.test.ts +667 -0
  35. package/src/commands/__tests__/model.test.ts +277 -0
  36. package/src/commands/__tests__/parser.test.ts +493 -0
  37. package/src/commands/__tests__/performance.bench.ts +380 -0
  38. package/src/commands/__tests__/registry.test.ts +534 -0
  39. package/src/commands/__tests__/resume.test.ts +449 -0
  40. package/src/commands/__tests__/security.test.ts +845 -0
  41. package/src/commands/__tests__/stream-json.test.ts +372 -0
  42. package/src/commands/__tests__/user-commands.test.ts +597 -0
  43. package/src/commands/adapters.ts +267 -0
  44. package/src/commands/agent.ts +395 -0
  45. package/src/commands/agents/generate.ts +506 -0
  46. package/src/commands/agents/index.ts +272 -0
  47. package/src/commands/agents/show.ts +271 -0
  48. package/src/commands/agents/validate.ts +387 -0
  49. package/src/commands/auth.ts +883 -0
  50. package/src/commands/autocomplete.ts +480 -0
  51. package/src/commands/batch/command.ts +388 -0
  52. package/src/commands/batch/executor.ts +361 -0
  53. package/src/commands/batch/index.ts +12 -0
  54. package/src/commands/commit.ts +235 -0
  55. package/src/commands/completion/index.ts +371 -0
  56. package/src/commands/condense.ts +191 -0
  57. package/src/commands/config.ts +344 -0
  58. package/src/commands/context-provider.ts +173 -0
  59. package/src/commands/copy.ts +329 -0
  60. package/src/commands/core/clear.ts +38 -0
  61. package/src/commands/core/exit.ts +43 -0
  62. package/src/commands/core/help.ts +354 -0
  63. package/src/commands/core/index.ts +15 -0
  64. package/src/commands/cost.ts +179 -0
  65. package/src/commands/credentials.tsx +618 -0
  66. package/src/commands/custom-agents/__tests__/custom-agents.test.ts +709 -0
  67. package/src/commands/custom-agents/create.ts +377 -0
  68. package/src/commands/custom-agents/export.ts +135 -0
  69. package/src/commands/custom-agents/import.ts +199 -0
  70. package/src/commands/custom-agents/index.ts +372 -0
  71. package/src/commands/custom-agents/info.ts +318 -0
  72. package/src/commands/custom-agents/list.ts +267 -0
  73. package/src/commands/custom-agents/validate.ts +388 -0
  74. package/src/commands/diff-mode.ts +241 -0
  75. package/src/commands/env.ts +53 -0
  76. package/src/commands/executor.ts +579 -0
  77. package/src/commands/exit-codes.ts +202 -0
  78. package/src/commands/index.ts +701 -0
  79. package/src/commands/init/index.ts +15 -0
  80. package/src/commands/init/prompts.ts +366 -0
  81. package/src/commands/init/templates/commands-readme.md +80 -0
  82. package/src/commands/init/templates/example-command.md +79 -0
  83. package/src/commands/init/templates/example-skill.md +168 -0
  84. package/src/commands/init/templates/example-workflow.md +101 -0
  85. package/src/commands/init/templates/prompts-readme.md +52 -0
  86. package/src/commands/init/templates/rules-readme.md +63 -0
  87. package/src/commands/init/templates/skills-readme.md +83 -0
  88. package/src/commands/init/templates/workflows-readme.md +94 -0
  89. package/src/commands/init.ts +391 -0
  90. package/src/commands/install.ts +90 -0
  91. package/src/commands/language.ts +191 -0
  92. package/src/commands/loaders/.gitkeep +1 -0
  93. package/src/commands/lsp.ts +199 -0
  94. package/src/commands/markdown-commands.ts +253 -0
  95. package/src/commands/mcp.ts +588 -0
  96. package/src/commands/memory/export.ts +341 -0
  97. package/src/commands/memory/index.ts +148 -0
  98. package/src/commands/memory/list.ts +261 -0
  99. package/src/commands/memory/search.ts +346 -0
  100. package/src/commands/memory/utils.ts +15 -0
  101. package/src/commands/metrics.ts +75 -0
  102. package/src/commands/migrate/index.ts +16 -0
  103. package/src/commands/migrate/prompts.ts +477 -0
  104. package/src/commands/mode.ts +331 -0
  105. package/src/commands/model.ts +298 -0
  106. package/src/commands/onboard.ts +205 -0
  107. package/src/commands/open.ts +169 -0
  108. package/src/commands/output/stream-json.ts +373 -0
  109. package/src/commands/parser/chain-parser.ts +370 -0
  110. package/src/commands/parser/index.ts +29 -0
  111. package/src/commands/parser/pipe-parser.ts +480 -0
  112. package/src/commands/parser.ts +588 -0
  113. package/src/commands/persistence.ts +355 -0
  114. package/src/commands/progress.ts +18 -0
  115. package/src/commands/prompt/index.ts +17 -0
  116. package/src/commands/prompt/validate.ts +621 -0
  117. package/src/commands/prompt-priority.ts +401 -0
  118. package/src/commands/registry.ts +374 -0
  119. package/src/commands/sandbox/index.ts +131 -0
  120. package/src/commands/security/index.ts +21 -0
  121. package/src/commands/security/input-sanitizer.ts +168 -0
  122. package/src/commands/security/permission-checker.ts +456 -0
  123. package/src/commands/security/sensitive-data.ts +350 -0
  124. package/src/commands/session/delete.ts +38 -0
  125. package/src/commands/session/export.ts +39 -0
  126. package/src/commands/session/index.ts +26 -0
  127. package/src/commands/session/list.ts +26 -0
  128. package/src/commands/session/resume.ts +562 -0
  129. package/src/commands/session/search.ts +434 -0
  130. package/src/commands/session/show.ts +26 -0
  131. package/src/commands/settings.ts +368 -0
  132. package/src/commands/setup.ts +23 -0
  133. package/src/commands/shell/index.ts +16 -0
  134. package/src/commands/shell/setup.ts +422 -0
  135. package/src/commands/shell-init.ts +50 -0
  136. package/src/commands/shell-integration/index.ts +194 -0
  137. package/src/commands/skill.ts +1220 -0
  138. package/src/commands/spec.ts +558 -0
  139. package/src/commands/status.ts +246 -0
  140. package/src/commands/theme.ts +211 -0
  141. package/src/commands/think.ts +551 -0
  142. package/src/commands/trust.ts +211 -0
  143. package/src/commands/tutorial.ts +522 -0
  144. package/src/commands/types.ts +512 -0
  145. package/src/commands/update.ts +274 -0
  146. package/src/commands/usage.ts +213 -0
  147. package/src/commands/user-commands.ts +630 -0
  148. package/src/commands/utils.ts +142 -0
  149. package/src/commands/vim.ts +152 -0
  150. package/src/commands/workflow.ts +257 -0
  151. package/src/components/header.tsx +25 -0
  152. package/src/components/input.tsx +25 -0
  153. package/src/components/message-list.tsx +32 -0
  154. package/src/components/status-bar.tsx +23 -0
  155. package/src/index.tsx +614 -0
  156. package/src/onboarding/__tests__/tutorial.test.ts +740 -0
  157. package/src/onboarding/index.ts +69 -0
  158. package/src/onboarding/tips/index.ts +9 -0
  159. package/src/onboarding/tips/tip-engine.ts +459 -0
  160. package/src/onboarding/tutorial/index.ts +88 -0
  161. package/src/onboarding/tutorial/lessons/basics.ts +151 -0
  162. package/src/onboarding/tutorial/lessons/index.ts +151 -0
  163. package/src/onboarding/tutorial/lessons/modes.ts +230 -0
  164. package/src/onboarding/tutorial/lessons/tools.ts +172 -0
  165. package/src/onboarding/tutorial/progress-tracker.ts +350 -0
  166. package/src/onboarding/tutorial/storage.ts +249 -0
  167. package/src/onboarding/tutorial/tutorial-system.ts +462 -0
  168. package/src/onboarding/tutorial/types.ts +310 -0
  169. package/src/orchestrator-singleton.ts +129 -0
  170. package/src/shutdown.ts +33 -0
  171. package/src/test/e2e/assertions.ts +267 -0
  172. package/src/test/e2e/fixtures.ts +204 -0
  173. package/src/test/e2e/harness.ts +575 -0
  174. package/src/test/e2e/index.ts +57 -0
  175. package/src/test/e2e/types.ts +228 -0
  176. package/src/test/fixtures/__tests__/fake-response-loader.test.ts +314 -0
  177. package/src/test/fixtures/fake-response-loader.ts +314 -0
  178. package/src/test/fixtures/index.ts +20 -0
  179. package/src/tui/__tests__/mcp-panel.test.tsx +82 -0
  180. package/src/tui/__tests__/mcp-wiring.test.tsx +78 -0
  181. package/src/tui/__tests__/mode-components.test.tsx +395 -0
  182. package/src/tui/__tests__/permission-ask-flow.test.tsx +138 -0
  183. package/src/tui/__tests__/sidebar-panel-data.test.tsx +148 -0
  184. package/src/tui/__tests__/tools-panel-hotkeys.test.tsx +41 -0
  185. package/src/tui/adapters/agent-adapter.ts +1008 -0
  186. package/src/tui/adapters/index.ts +48 -0
  187. package/src/tui/adapters/message-adapter.ts +315 -0
  188. package/src/tui/adapters/persistence-bridge.ts +331 -0
  189. package/src/tui/adapters/session-adapter.ts +419 -0
  190. package/src/tui/buffered-stdout.ts +223 -0
  191. package/src/tui/components/AgentProgress.tsx +424 -0
  192. package/src/tui/components/Banner/AsciiArt.ts +160 -0
  193. package/src/tui/components/Banner/Banner.tsx +355 -0
  194. package/src/tui/components/Banner/ShimmerContext.tsx +131 -0
  195. package/src/tui/components/Banner/ShimmerText.tsx +193 -0
  196. package/src/tui/components/Banner/TypeWriterGradient.tsx +321 -0
  197. package/src/tui/components/Banner/index.ts +61 -0
  198. package/src/tui/components/Banner/useShimmer.ts +241 -0
  199. package/src/tui/components/ChatView.tsx +11 -0
  200. package/src/tui/components/Checkpoint/CheckpointDiffView.tsx +371 -0
  201. package/src/tui/components/Checkpoint/SnapshotCheckpointPanel.tsx +440 -0
  202. package/src/tui/components/Checkpoint/index.ts +19 -0
  203. package/src/tui/components/CostDisplay.tsx +226 -0
  204. package/src/tui/components/InitErrorBanner.tsx +122 -0
  205. package/src/tui/components/Input/Autocomplete.tsx +603 -0
  206. package/src/tui/components/Input/EnhancedCommandInput.tsx +471 -0
  207. package/src/tui/components/Input/HighlightedText.tsx +236 -0
  208. package/src/tui/components/Input/MentionAutocomplete.tsx +375 -0
  209. package/src/tui/components/Input/TextInput.tsx +1002 -0
  210. package/src/tui/components/Input/__tests__/Autocomplete.test.tsx +374 -0
  211. package/src/tui/components/Input/__tests__/TextInput.test.tsx +241 -0
  212. package/src/tui/components/Input/__tests__/highlight.test.ts +219 -0
  213. package/src/tui/components/Input/__tests__/slash-command-utils.test.ts +104 -0
  214. package/src/tui/components/Input/highlight.ts +362 -0
  215. package/src/tui/components/Input/index.ts +36 -0
  216. package/src/tui/components/Input/slash-command-utils.ts +135 -0
  217. package/src/tui/components/Layout.tsx +432 -0
  218. package/src/tui/components/McpPanel.tsx +137 -0
  219. package/src/tui/components/MemoryPanel.tsx +448 -0
  220. package/src/tui/components/Messages/CodeBlock.tsx +527 -0
  221. package/src/tui/components/Messages/DiffView.tsx +679 -0
  222. package/src/tui/components/Messages/ImageReference.tsx +89 -0
  223. package/src/tui/components/Messages/MarkdownBlock.tsx +228 -0
  224. package/src/tui/components/Messages/MarkdownRenderer.tsx +498 -0
  225. package/src/tui/components/Messages/MessageBubble.tsx +270 -0
  226. package/src/tui/components/Messages/MessageList.tsx +1719 -0
  227. package/src/tui/components/Messages/StreamingText.tsx +216 -0
  228. package/src/tui/components/Messages/ThinkingBlock.tsx +408 -0
  229. package/src/tui/components/Messages/ToolResultPreview.tsx +243 -0
  230. package/src/tui/components/Messages/__tests__/CodeBlock.test.tsx +296 -0
  231. package/src/tui/components/Messages/__tests__/DiffView.test.tsx +239 -0
  232. package/src/tui/components/Messages/__tests__/MarkdownRenderer.test.tsx +303 -0
  233. package/src/tui/components/Messages/__tests__/MessageBubble.test.tsx +268 -0
  234. package/src/tui/components/Messages/__tests__/MessageList.test.tsx +324 -0
  235. package/src/tui/components/Messages/__tests__/StreamingText.test.tsx +215 -0
  236. package/src/tui/components/Messages/index.ts +25 -0
  237. package/src/tui/components/ModeIndicator.tsx +177 -0
  238. package/src/tui/components/ModeSelector.tsx +216 -0
  239. package/src/tui/components/ModelSelector.tsx +339 -0
  240. package/src/tui/components/OnboardingWizard.tsx +670 -0
  241. package/src/tui/components/PhaseProgressIndicator.tsx +270 -0
  242. package/src/tui/components/RateLimitIndicator.tsx +82 -0
  243. package/src/tui/components/ScreenReaderLayout.tsx +295 -0
  244. package/src/tui/components/SettingsPanel.tsx +643 -0
  245. package/src/tui/components/Sidebar/SystemStatusPanel.tsx +284 -0
  246. package/src/tui/components/Sidebar/index.ts +9 -0
  247. package/src/tui/components/Status/ModelStatusBar.tsx +270 -0
  248. package/src/tui/components/Status/index.ts +12 -0
  249. package/src/tui/components/StatusBar/AgentModeIndicator.tsx +257 -0
  250. package/src/tui/components/StatusBar/ContextProgress.tsx +167 -0
  251. package/src/tui/components/StatusBar/FileChangesIndicator.tsx +62 -0
  252. package/src/tui/components/StatusBar/GitIndicator.tsx +89 -0
  253. package/src/tui/components/StatusBar/HeaderBar.tsx +126 -0
  254. package/src/tui/components/StatusBar/ModelIndicator.tsx +157 -0
  255. package/src/tui/components/StatusBar/PersistenceStatusIndicator.tsx +210 -0
  256. package/src/tui/components/StatusBar/ResilienceIndicator.tsx +106 -0
  257. package/src/tui/components/StatusBar/SandboxIndicator.tsx +167 -0
  258. package/src/tui/components/StatusBar/StatusBar.tsx +368 -0
  259. package/src/tui/components/StatusBar/ThinkingModeIndicator.tsx +170 -0
  260. package/src/tui/components/StatusBar/TokenBreakdown.tsx +246 -0
  261. package/src/tui/components/StatusBar/TokenCounter.tsx +135 -0
  262. package/src/tui/components/StatusBar/TrustModeIndicator.tsx +130 -0
  263. package/src/tui/components/StatusBar/WorkspaceIndicator.tsx +86 -0
  264. package/src/tui/components/StatusBar/__tests__/AgentModeIndicator.test.tsx +193 -0
  265. package/src/tui/components/StatusBar/__tests__/StatusBar.test.tsx +729 -0
  266. package/src/tui/components/StatusBar/index.ts +60 -0
  267. package/src/tui/components/TipBanner.tsx +115 -0
  268. package/src/tui/components/TodoItem.tsx +208 -0
  269. package/src/tui/components/TodoPanel.tsx +455 -0
  270. package/src/tui/components/Tools/ApprovalQueue.tsx +407 -0
  271. package/src/tui/components/Tools/OptionSelector.tsx +160 -0
  272. package/src/tui/components/Tools/PermissionDialog.tsx +286 -0
  273. package/src/tui/components/Tools/ToolParams.tsx +483 -0
  274. package/src/tui/components/Tools/ToolsPanel.tsx +178 -0
  275. package/src/tui/components/Tools/__tests__/PermissionDialog.test.tsx +510 -0
  276. package/src/tui/components/Tools/__tests__/ToolParams.test.tsx +432 -0
  277. package/src/tui/components/Tools/index.ts +21 -0
  278. package/src/tui/components/TrustPrompt.tsx +279 -0
  279. package/src/tui/components/UpdateBanner.tsx +166 -0
  280. package/src/tui/components/VimModeIndicator.tsx +112 -0
  281. package/src/tui/components/backtrack/BacktrackControls.tsx +402 -0
  282. package/src/tui/components/backtrack/index.ts +13 -0
  283. package/src/tui/components/common/AutoApprovalStatus.tsx +251 -0
  284. package/src/tui/components/common/CostWarning.tsx +294 -0
  285. package/src/tui/components/common/DynamicShortcutHints.tsx +209 -0
  286. package/src/tui/components/common/EnhancedLoadingIndicator.tsx +305 -0
  287. package/src/tui/components/common/ErrorBoundary.tsx +140 -0
  288. package/src/tui/components/common/GradientText.tsx +224 -0
  289. package/src/tui/components/common/HotkeyHelpModal.tsx +193 -0
  290. package/src/tui/components/common/HotkeyHints.tsx +70 -0
  291. package/src/tui/components/common/MaxSizedBox.tsx +354 -0
  292. package/src/tui/components/common/NewMessagesBadge.tsx +65 -0
  293. package/src/tui/components/common/ProtectedFileLegend.tsx +89 -0
  294. package/src/tui/components/common/ScrollIndicator.tsx +160 -0
  295. package/src/tui/components/common/Spinner.tsx +342 -0
  296. package/src/tui/components/common/StreamingIndicator.tsx +316 -0
  297. package/src/tui/components/common/VirtualizedList/VirtualizedList.tsx +428 -0
  298. package/src/tui/components/common/VirtualizedList/hooks/index.ts +19 -0
  299. package/src/tui/components/common/VirtualizedList/hooks/useBatchedScroll.ts +64 -0
  300. package/src/tui/components/common/VirtualizedList/hooks/useScrollAnchor.ts +290 -0
  301. package/src/tui/components/common/VirtualizedList/hooks/useVirtualization.ts +340 -0
  302. package/src/tui/components/common/VirtualizedList/index.ts +30 -0
  303. package/src/tui/components/common/VirtualizedList/types.ts +107 -0
  304. package/src/tui/components/common/__tests__/NewMessagesBadge.test.tsx +74 -0
  305. package/src/tui/components/common/__tests__/ScrollIndicator.test.tsx +193 -0
  306. package/src/tui/components/common/index.ts +110 -0
  307. package/src/tui/components/index.ts +79 -0
  308. package/src/tui/components/session/CheckpointPanel.tsx +323 -0
  309. package/src/tui/components/session/RollbackDialog.tsx +169 -0
  310. package/src/tui/components/session/SessionItem.tsx +136 -0
  311. package/src/tui/components/session/SessionListPanel.tsx +252 -0
  312. package/src/tui/components/session/SessionPicker.tsx +449 -0
  313. package/src/tui/components/session/SessionPreview.tsx +240 -0
  314. package/src/tui/components/session/__tests__/session.test.tsx +408 -0
  315. package/src/tui/components/session/index.ts +28 -0
  316. package/src/tui/components/session/types.ts +116 -0
  317. package/src/tui/components/theme/__tests__/tokens.test.ts +471 -0
  318. package/src/tui/components/theme/index.ts +227 -0
  319. package/src/tui/components/theme/tokens.ts +484 -0
  320. package/src/tui/config/defaults.ts +134 -0
  321. package/src/tui/config/index.ts +17 -0
  322. package/src/tui/context/AnimationContext.tsx +284 -0
  323. package/src/tui/context/AppContext.tsx +349 -0
  324. package/src/tui/context/BracketedPasteContext.tsx +372 -0
  325. package/src/tui/context/LspContext.tsx +192 -0
  326. package/src/tui/context/McpContext.tsx +325 -0
  327. package/src/tui/context/MessagesContext.tsx +870 -0
  328. package/src/tui/context/OverflowContext.tsx +213 -0
  329. package/src/tui/context/RateLimitContext.tsx +108 -0
  330. package/src/tui/context/ResilienceContext.tsx +275 -0
  331. package/src/tui/context/RootProvider.tsx +136 -0
  332. package/src/tui/context/ScrollContext.tsx +331 -0
  333. package/src/tui/context/ToolsContext.tsx +702 -0
  334. package/src/tui/context/__tests__/BracketedPasteContext.test.tsx +416 -0
  335. package/src/tui/context/index.ts +140 -0
  336. package/src/tui/enterprise-integration.ts +282 -0
  337. package/src/tui/hooks/__tests__/useBacktrack.test.tsx +138 -0
  338. package/src/tui/hooks/__tests__/useBracketedPaste.test.tsx +222 -0
  339. package/src/tui/hooks/__tests__/useCopyMode.test.tsx +336 -0
  340. package/src/tui/hooks/__tests__/useHotkeys.ctrl-input.test.tsx +96 -0
  341. package/src/tui/hooks/__tests__/useHotkeys.test.tsx +454 -0
  342. package/src/tui/hooks/__tests__/useInputHistory.test.tsx +660 -0
  343. package/src/tui/hooks/__tests__/useLineBuffer.test.ts +295 -0
  344. package/src/tui/hooks/__tests__/useModeController.test.ts +137 -0
  345. package/src/tui/hooks/__tests__/useModeShortcuts.test.tsx +142 -0
  346. package/src/tui/hooks/__tests__/useScrollController.test.ts +464 -0
  347. package/src/tui/hooks/__tests__/useVim.test.tsx +531 -0
  348. package/src/tui/hooks/index.ts +252 -0
  349. package/src/tui/hooks/useAgentLoop.ts +712 -0
  350. package/src/tui/hooks/useAlternateBuffer.ts +398 -0
  351. package/src/tui/hooks/useAnimatedScrollbar.ts +241 -0
  352. package/src/tui/hooks/useBacktrack.ts +443 -0
  353. package/src/tui/hooks/useBracketedPaste.ts +104 -0
  354. package/src/tui/hooks/useCollapsible.ts +240 -0
  355. package/src/tui/hooks/useCopyMode.ts +382 -0
  356. package/src/tui/hooks/useCostSummary.ts +75 -0
  357. package/src/tui/hooks/useDesktopNotification.ts +414 -0
  358. package/src/tui/hooks/useDiffMode.ts +44 -0
  359. package/src/tui/hooks/useFileChangeStats.ts +110 -0
  360. package/src/tui/hooks/useFileSuggestions.ts +284 -0
  361. package/src/tui/hooks/useFlickerDetector.ts +250 -0
  362. package/src/tui/hooks/useGitStatus.ts +200 -0
  363. package/src/tui/hooks/useHotkeys.ts +579 -0
  364. package/src/tui/hooks/useImagePaste.ts +114 -0
  365. package/src/tui/hooks/useInputHighlight.ts +145 -0
  366. package/src/tui/hooks/useInputHistory.ts +246 -0
  367. package/src/tui/hooks/useKeyboardScroll.ts +209 -0
  368. package/src/tui/hooks/useLineBuffer.ts +356 -0
  369. package/src/tui/hooks/useMentionAutocomplete.ts +235 -0
  370. package/src/tui/hooks/useModeController.ts +167 -0
  371. package/src/tui/hooks/useModeShortcuts.ts +196 -0
  372. package/src/tui/hooks/usePermissionHandler.ts +146 -0
  373. package/src/tui/hooks/usePersistence.ts +480 -0
  374. package/src/tui/hooks/usePersistenceShortcuts.ts +225 -0
  375. package/src/tui/hooks/usePlaceholderRotation.ts +143 -0
  376. package/src/tui/hooks/useProviderStatus.ts +270 -0
  377. package/src/tui/hooks/useRateLimitStatus.ts +90 -0
  378. package/src/tui/hooks/useScreenReader.ts +315 -0
  379. package/src/tui/hooks/useScrollController.ts +450 -0
  380. package/src/tui/hooks/useScrollEventBatcher.ts +185 -0
  381. package/src/tui/hooks/useSidebarPanelData.ts +115 -0
  382. package/src/tui/hooks/useSmoothScroll.ts +202 -0
  383. package/src/tui/hooks/useSnapshots.ts +300 -0
  384. package/src/tui/hooks/useStateAndRef.ts +50 -0
  385. package/src/tui/hooks/useTerminalSize.ts +206 -0
  386. package/src/tui/hooks/useToolApprovalController.ts +91 -0
  387. package/src/tui/hooks/useVim.ts +334 -0
  388. package/src/tui/hooks/useWorkspace.ts +56 -0
  389. package/src/tui/i18n/__tests__/init.test.ts +278 -0
  390. package/src/tui/i18n/__tests__/language-config.test.ts +199 -0
  391. package/src/tui/i18n/__tests__/locale-detection.test.ts +250 -0
  392. package/src/tui/i18n/__tests__/settings-integration.test.ts +262 -0
  393. package/src/tui/i18n/index.ts +72 -0
  394. package/src/tui/i18n/init.ts +131 -0
  395. package/src/tui/i18n/language-config.ts +106 -0
  396. package/src/tui/i18n/locale-detection.ts +173 -0
  397. package/src/tui/i18n/settings-integration.ts +557 -0
  398. package/src/tui/i18n/tui-namespace.ts +538 -0
  399. package/src/tui/i18n/types.ts +312 -0
  400. package/src/tui/index.ts +43 -0
  401. package/src/tui/lsp-integration.ts +409 -0
  402. package/src/tui/metrics-integration.ts +366 -0
  403. package/src/tui/plugins.ts +383 -0
  404. package/src/tui/resilience.ts +342 -0
  405. package/src/tui/sandbox-integration.ts +317 -0
  406. package/src/tui/services/clipboard.ts +348 -0
  407. package/src/tui/services/fuzzy-search.ts +441 -0
  408. package/src/tui/services/index.ts +72 -0
  409. package/src/tui/services/markdown-renderer.ts +565 -0
  410. package/src/tui/services/open-external.ts +247 -0
  411. package/src/tui/services/syntax-highlighter.ts +483 -0
  412. package/src/tui/slash-commands.ts +12 -0
  413. package/src/tui/theme/index.ts +15 -0
  414. package/src/tui/theme/provider.tsx +206 -0
  415. package/src/tui/tip-integration.ts +300 -0
  416. package/src/tui/types/__tests__/ink-extended.test.ts +121 -0
  417. package/src/tui/types/ink-extended.ts +87 -0
  418. package/src/tui/utils/__tests__/bracketedPaste.test.ts +231 -0
  419. package/src/tui/utils/__tests__/heightEstimator.test.ts +157 -0
  420. package/src/tui/utils/__tests__/text-width.test.ts +158 -0
  421. package/src/tui/utils/__tests__/textSanitizer.test.ts +266 -0
  422. package/src/tui/utils/__tests__/ui-sizing.test.ts +169 -0
  423. package/src/tui/utils/bracketedPaste.ts +107 -0
  424. package/src/tui/utils/cursor-manager.ts +131 -0
  425. package/src/tui/utils/detectTerminal.ts +596 -0
  426. package/src/tui/utils/findLastSafeSplitPoint.ts +92 -0
  427. package/src/tui/utils/heightEstimator.ts +198 -0
  428. package/src/tui/utils/index.ts +91 -0
  429. package/src/tui/utils/isNarrowWidth.ts +52 -0
  430. package/src/tui/utils/stdoutGuard.ts +90 -0
  431. package/src/tui/utils/synchronized-update.ts +70 -0
  432. package/src/tui/utils/text-width.ts +225 -0
  433. package/src/tui/utils/textSanitizer.ts +225 -0
  434. package/src/tui/utils/textUtils.ts +114 -0
  435. package/src/tui/utils/ui-sizing.ts +192 -0
  436. package/src/tui-blessed/app.ts +160 -0
  437. package/src/tui-blessed/index.ts +2 -0
  438. package/src/tui-blessed/neo-blessed.d.ts +6 -0
  439. package/src/tui-blessed/test.ts +21 -0
  440. package/src/tui-blessed/types.ts +14 -0
  441. package/src/utils/icons.ts +130 -0
  442. package/src/utils/index.ts +33 -0
  443. package/src/utils/resume-hint.ts +86 -0
  444. package/src/version.ts +1 -0
  445. package/tsconfig.json +8 -0
  446. package/vitest.config.ts +35 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Banner Component
3
+ *
4
+ * Main ASCII art banner with gradient colors and shimmer animation.
5
+ * Features ancient parchment/scroll styling for the Vellum brand.
6
+ *
7
+ * @module tui/components/Banner/Banner
8
+ */
9
+
10
+ import { Box, Text, useStdout } from "ink";
11
+ import Gradient from "ink-gradient";
12
+ import type React from "react";
13
+ import { useEffect, useMemo, useRef, useState } from "react";
14
+ import { useTheme } from "../../theme/index.js";
15
+ import { selectAsciiArt } from "./AsciiArt.js";
16
+ import { interpolateColor } from "./ShimmerText.js";
17
+ import { TypeWriterGradient } from "./TypeWriterGradient.js";
18
+ import { useShimmer } from "./useShimmer.js";
19
+
20
+ // Note: useShimmer & interpolateColor still used by HeaderBanner
21
+
22
+ // =============================================================================
23
+ // Types
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Props for the Banner component.
28
+ */
29
+ export interface BannerProps {
30
+ /** Custom ASCII art override (uses responsive selection by default) */
31
+ readonly customArt?: string;
32
+ /** Whether to show version text */
33
+ readonly showVersion?: boolean;
34
+ /** Version string to display */
35
+ readonly version?: string;
36
+ /** Whether shimmer animation is enabled (default: true) */
37
+ readonly animated?: boolean;
38
+ /** Shimmer cycle duration in milliseconds (default: 3000) */
39
+ readonly cycleDuration?: number;
40
+ /** Shimmer update interval in milliseconds (default: 100) */
41
+ readonly updateInterval?: number;
42
+ /** Callback when banner fade-out completes */
43
+ readonly onComplete?: () => void;
44
+ /** Duration to display before fading (ms, default: 2000) */
45
+ readonly displayDuration?: number;
46
+ /** Whether to auto-hide after displayDuration */
47
+ readonly autoHide?: boolean;
48
+ /** Number of animation cycles before stopping (default: infinite) */
49
+ readonly cycles?: number;
50
+ /** Whether to show typewriter effect on startup (default: true) */
51
+ readonly typewriter?: boolean;
52
+ /** Typewriter speed: lines/sec for line mode, chars/sec for char mode (default: 50 lines/sec) */
53
+ readonly typewriterSpeed?: number;
54
+ /** Typewriter mode: 'line' reveals whole lines, 'char' reveals characters (default: 'line') */
55
+ readonly typewriterMode?: "line" | "char";
56
+ }
57
+
58
+ // =============================================================================
59
+ // Constants
60
+ // =============================================================================
61
+
62
+ /** Interpolated steps per gradient segment for smoother shimmer shifts. */
63
+ const GRADIENT_STEPS_PER_SEGMENT = 12;
64
+
65
+ /**
66
+ * Build gradient steps from theme brand colors.
67
+ * Used for shimmer animation in HeaderBanner.
68
+ */
69
+ function buildGradientSteps(colors: readonly string[], stepsPerSegment: number): string[] {
70
+ if (colors.length === 0) return [];
71
+ if (colors.length === 1) return [colors[0] ?? "#000000"];
72
+
73
+ const steps: string[] = [];
74
+
75
+ for (let i = 0; i < colors.length - 1; i += 1) {
76
+ const start = colors[i] ?? "#000000";
77
+ const end = colors[i + 1] ?? start;
78
+ for (let step = 0; step < stepsPerSegment; step += 1) {
79
+ const t = step / stepsPerSegment;
80
+ steps.push(interpolateColor(start, end, t));
81
+ }
82
+ }
83
+
84
+ steps.push(colors[colors.length - 1] ?? "#000000");
85
+ return steps;
86
+ }
87
+
88
+ /**
89
+ * Get parchment gradient colors from theme brand.
90
+ * Returns a mutable array for compatibility with ink-gradient.
91
+ */
92
+ function getParchmentGradient(brand: {
93
+ accent: string;
94
+ mid: string;
95
+ secondary: string;
96
+ primary: string;
97
+ highlight: string;
98
+ light: string;
99
+ }): string[] {
100
+ return [
101
+ brand.accent, // Saddle Brown
102
+ brand.mid, // Sienna
103
+ brand.secondary, // Peru
104
+ brand.primary, // Goldenrod
105
+ brand.highlight, // Gold
106
+ brand.light, // Lemon Chiffon
107
+ ];
108
+ }
109
+
110
+ // =============================================================================
111
+ // Sub-Components
112
+ // =============================================================================
113
+
114
+ /**
115
+ * Static version display.
116
+ */
117
+ interface VersionDisplayProps {
118
+ readonly version: string;
119
+ readonly brand: {
120
+ accent: string;
121
+ secondary: string;
122
+ primary: string;
123
+ };
124
+ }
125
+
126
+ function VersionDisplay({ version, brand }: VersionDisplayProps): React.JSX.Element {
127
+ return (
128
+ <Box marginTop={1} justifyContent="center">
129
+ <Text color={brand.accent}>v{version}</Text>
130
+ <Text color={brand.secondary}> | </Text>
131
+ <Text color={brand.primary} italic>
132
+ AI-Powered Coding Assistant
133
+ </Text>
134
+ </Box>
135
+ );
136
+ }
137
+
138
+ // =============================================================================
139
+ // Main Component
140
+ // =============================================================================
141
+
142
+ /**
143
+ * Banner displays the Vellum ASCII art logo with gradient and shimmer effects.
144
+ *
145
+ * Features:
146
+ * - Responsive ASCII art selection based on terminal width
147
+ * - Smooth gradient colors (brown to gold parchment theme)
148
+ * - Animated shimmer sweep effect
149
+ * - Optional version display
150
+ * - Auto-hide capability with callback
151
+ *
152
+ * @example
153
+ * ```tsx
154
+ * // Basic usage
155
+ * <Banner />
156
+ *
157
+ * // With version and auto-hide
158
+ * <Banner
159
+ * showVersion
160
+ * version="1.0.0"
161
+ * autoHide
162
+ * displayDuration={3000}
163
+ * onComplete={() => setShowBanner(false)}
164
+ * />
165
+ * ```
166
+ */
167
+ export function Banner({
168
+ customArt,
169
+ showVersion = false,
170
+ version = "0.1.0",
171
+ // Note: animated, cycleDuration, updateInterval, cycles kept for API compat
172
+ // but shimmer effect is removed - typing goes directly to static gradient
173
+ onComplete,
174
+ displayDuration = 2000,
175
+ autoHide = false,
176
+ typewriter = true,
177
+ typewriterSpeed,
178
+ typewriterMode = "char",
179
+ }: BannerProps): React.JSX.Element | null {
180
+ const { stdout } = useStdout();
181
+ const { theme } = useTheme();
182
+ const [visible, setVisible] = useState(true);
183
+ const [opacity, setOpacity] = useState(1);
184
+ const [typingComplete, setTypingComplete] = useState(!typewriter);
185
+
186
+ // Refs for nested timer cleanup
187
+ const step1Ref = useRef<ReturnType<typeof setTimeout> | null>(null);
188
+ const step2Ref = useRef<ReturnType<typeof setTimeout> | null>(null);
189
+
190
+ // Get gradient colors from theme
191
+ const parchmentGradient = useMemo(() => getParchmentGradient(theme.brand), [theme.brand]);
192
+
193
+ // Get terminal dimensions for responsive art selection
194
+ const terminalWidth = stdout?.columns ?? 80;
195
+
196
+ // Select appropriate ASCII art
197
+ const asciiArt = useMemo(() => {
198
+ return customArt ?? selectAsciiArt(terminalWidth);
199
+ }, [customArt, terminalWidth]);
200
+
201
+ // Auto-calculate typewriter speed if not provided
202
+ // Target: typing completes at 80% of displayDuration for comfortable viewing
203
+ const autoTypewriterSpeed = useMemo(() => {
204
+ if (typewriterSpeed !== undefined) return typewriterSpeed;
205
+ const charCount = asciiArt.length;
206
+ const typingTimeMs = displayDuration * 0.8; // 80% of display time
207
+ const typingTimeSec = typingTimeMs / 1000;
208
+ // Calculate chars/sec, minimum 500 to avoid being too slow
209
+ return Math.max(500, Math.ceil(charCount / typingTimeSec));
210
+ }, [typewriterSpeed, asciiArt.length, displayDuration]);
211
+
212
+ // Auto-hide timer with smooth transition
213
+ useEffect(() => {
214
+ if (!autoHide) return;
215
+
216
+ // Use simple timeout - start hiding 300ms before end for quicker transition
217
+ const hideTimer = setTimeout(() => {
218
+ // Quick fade: 0.7 -> 0 in 300ms total
219
+ setOpacity(0.7);
220
+
221
+ step1Ref.current = setTimeout(() => {
222
+ setOpacity(0.3);
223
+ }, 100);
224
+
225
+ step2Ref.current = setTimeout(() => {
226
+ setOpacity(0);
227
+ setVisible(false);
228
+ onComplete?.();
229
+ }, 200);
230
+ }, displayDuration - 300);
231
+
232
+ return () => {
233
+ clearTimeout(hideTimer);
234
+ if (step1Ref.current) clearTimeout(step1Ref.current);
235
+ if (step2Ref.current) clearTimeout(step2Ref.current);
236
+ };
237
+ }, [autoHide, displayDuration, onComplete]);
238
+
239
+ if (!visible) return null;
240
+
241
+ // Determine which phase we're in: typing or static gradient (no shimmer)
242
+ const isTypingPhase = typewriter && !typingComplete;
243
+
244
+ return (
245
+ <Box
246
+ flexDirection="column"
247
+ alignItems="center"
248
+ justifyContent="center"
249
+ paddingX={1}
250
+ paddingY={1}
251
+ >
252
+ {isTypingPhase ? (
253
+ <TypeWriterGradient
254
+ text={asciiArt}
255
+ speed={autoTypewriterSpeed}
256
+ colors={parchmentGradient}
257
+ showCursor
258
+ mode={typewriterMode}
259
+ onComplete={() => setTypingComplete(true)}
260
+ />
261
+ ) : (
262
+ <Gradient colors={parchmentGradient}>{asciiArt}</Gradient>
263
+ )}
264
+
265
+ {showVersion && version ? <VersionDisplay version={version} brand={theme.brand} /> : null}
266
+
267
+ {/* Loading indicator - hide when fading out */}
268
+ {opacity >= 0.5 && (
269
+ <Box marginTop={1}>
270
+ <Text color={theme.brand.accent}>Initializing...</Text>
271
+ </Box>
272
+ )}
273
+ </Box>
274
+ );
275
+ }
276
+
277
+ /**
278
+ * Compact banner for narrow terminals or inline use.
279
+ */
280
+ export function CompactBanner(): React.JSX.Element {
281
+ const { theme } = useTheme();
282
+ const parchmentGradient = useMemo(() => getParchmentGradient(theme.brand), [theme.brand]);
283
+ return (
284
+ <Box>
285
+ <Gradient colors={parchmentGradient}>{"◇ VELLUM ◇"}</Gradient>
286
+ </Box>
287
+ );
288
+ }
289
+
290
+ /**
291
+ * Minimal text-only banner.
292
+ */
293
+ export function MinimalBanner(): React.JSX.Element {
294
+ const { theme } = useTheme();
295
+ return (
296
+ <Text color={theme.brand.primary} bold>
297
+ VELLUM
298
+ </Text>
299
+ );
300
+ }
301
+
302
+ // =============================================================================
303
+ // Header Banner (Compact CLI Header)
304
+ // =============================================================================
305
+
306
+ /**
307
+ * Props for the HeaderBanner component.
308
+ */
309
+ export interface HeaderBannerProps {
310
+ /** Whether shimmer animation is enabled (default: true) */
311
+ readonly animated?: boolean;
312
+ }
313
+
314
+ /**
315
+ * Compact header banner with animated gradient for use at CLI top.
316
+ * Displays a single-line parchment-styled logo with continuous shimmer.
317
+ */
318
+ export function HeaderBanner({ animated = true }: HeaderBannerProps): React.JSX.Element {
319
+ const { theme } = useTheme();
320
+ const { position } = useShimmer({
321
+ cycleDuration: 3000,
322
+ enabled: animated,
323
+ });
324
+
325
+ // Get gradient colors from theme
326
+ const parchmentGradient = useMemo(() => getParchmentGradient(theme.brand), [theme.brand]);
327
+
328
+ // Build gradient steps for shimmer animation
329
+ const gradientSteps = useMemo(
330
+ () => buildGradientSteps(parchmentGradient, GRADIENT_STEPS_PER_SEGMENT),
331
+ [parchmentGradient]
332
+ );
333
+
334
+ // Calculate discrete color shift to reduce re-computation
335
+ const colorShift = Math.floor(position * gradientSteps.length) % gradientSteps.length;
336
+
337
+ // Shift gradient colors based on discrete colorShift value
338
+ const shiftedColors = useMemo(() => {
339
+ if (colorShift === 0) return gradientSteps;
340
+ return [...gradientSteps.slice(colorShift), ...gradientSteps.slice(0, colorShift)];
341
+ }, [colorShift, gradientSteps]);
342
+
343
+ // Compact ASCII art for header (single line with decorations)
344
+ const headerArt = "═══╣ ◊ ╠═══ V E L L U M ═══╣ ◊ ╠═══";
345
+
346
+ return (
347
+ <Box justifyContent="center" width="100%">
348
+ {animated ? (
349
+ <Gradient colors={shiftedColors}>{headerArt}</Gradient>
350
+ ) : (
351
+ <Gradient colors={parchmentGradient}>{headerArt}</Gradient>
352
+ )}
353
+ </Box>
354
+ );
355
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ShimmerContext - Shared shimmer state provider
3
+ *
4
+ * Provides a single shared shimmer timer for all consumers,
5
+ * eliminating multiple independent timers that can cause animation flickering.
6
+ *
7
+ * @module tui/components/Banner/ShimmerContext
8
+ */
9
+
10
+ import { createContext, type ReactNode, useContext } from "react";
11
+ import { type ShimmerState, useShimmer } from "./useShimmer.js";
12
+
13
+ // =============================================================================
14
+ // Types
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Context value extends ShimmerState for shared shimmer state
19
+ */
20
+ type ShimmerContextValue = ShimmerState;
21
+
22
+ // =============================================================================
23
+ // Context
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Default shimmer context value (inactive state)
28
+ */
29
+ const defaultShimmerValue: ShimmerContextValue = {
30
+ position: 0,
31
+ intensity: 0,
32
+ cycleCount: 0,
33
+ isComplete: false,
34
+ isActive: false,
35
+ pause: () => {},
36
+ resume: () => {},
37
+ };
38
+
39
+ const ShimmerContext = createContext<ShimmerContextValue>(defaultShimmerValue);
40
+
41
+ // =============================================================================
42
+ // Provider
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Props for the ShimmerProvider component.
47
+ */
48
+ export interface ShimmerProviderProps {
49
+ /** Child components that will share the shimmer state */
50
+ readonly children: ReactNode;
51
+ /** Whether shimmer animation is enabled (default: true) */
52
+ readonly enabled?: boolean;
53
+ /** Maximum number of cycles before stopping (undefined = infinite) */
54
+ readonly maxCycles?: number;
55
+ /** Duration of one complete shimmer cycle in milliseconds (default: 3000) */
56
+ readonly cycleDuration?: number;
57
+ /** Update interval in milliseconds (default: 100 for smoother motion) */
58
+ readonly updateInterval?: number;
59
+ /** Callback when max cycles completed */
60
+ readonly onComplete?: () => void;
61
+ }
62
+
63
+ /**
64
+ * ShimmerProvider creates a single shared shimmer timer for all child components.
65
+ *
66
+ * Use this to wrap multiple shimmer-consuming components to ensure they all
67
+ * animate in sync and share a single timer instead of each running their own.
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * <ShimmerProvider enabled maxCycles={3} cycleDuration={3000}>
72
+ * <Banner />
73
+ * <HeaderBanner />
74
+ * <Header />
75
+ * </ShimmerProvider>
76
+ * ```
77
+ */
78
+ export function ShimmerProvider({
79
+ children,
80
+ enabled = true,
81
+ maxCycles,
82
+ cycleDuration = 3000,
83
+ updateInterval = 100,
84
+ onComplete,
85
+ }: ShimmerProviderProps): React.JSX.Element {
86
+ const shimmer = useShimmer({
87
+ enabled,
88
+ maxCycles,
89
+ cycleDuration,
90
+ updateInterval,
91
+ onComplete,
92
+ });
93
+
94
+ return <ShimmerContext.Provider value={shimmer}>{children}</ShimmerContext.Provider>;
95
+ }
96
+
97
+ // =============================================================================
98
+ // Hook
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Hook to access shared shimmer state from ShimmerProvider.
103
+ *
104
+ * If used outside a ShimmerProvider, returns default inactive state.
105
+ * For standalone shimmer (not sharing timer), use useShimmer() directly instead.
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * function AnimatedComponent() {
110
+ * const { position, intensity, isComplete } = useSharedShimmer();
111
+ * // Use position (0-1) and intensity (0-1) for animation
112
+ * }
113
+ * ```
114
+ */
115
+ export function useSharedShimmer(): ShimmerContextValue {
116
+ return useContext(ShimmerContext);
117
+ }
118
+
119
+ /**
120
+ * Hook to check if component is inside a ShimmerProvider.
121
+ * Useful for components that want to optionally use shared state.
122
+ */
123
+ export function useIsInShimmerProvider(): boolean {
124
+ const context = useContext(ShimmerContext);
125
+ // Check if we have the actual shimmer functions (not default no-ops)
126
+ return (
127
+ context.isActive !== defaultShimmerValue.isActive ||
128
+ context.position !== defaultShimmerValue.position ||
129
+ context.cycleCount !== defaultShimmerValue.cycleCount
130
+ );
131
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * ShimmerText Component for Banner
3
+ *
4
+ * Renders text with a flowing shimmer/glow effect.
5
+ * Uses the useShimmer hook for animation state.
6
+ *
7
+ * @module tui/components/Banner/ShimmerText
8
+ */
9
+
10
+ import { Text } from "ink";
11
+ import React, { useMemo } from "react";
12
+ import { calculateShimmerIntensity, type ShimmerConfig, useShimmer } from "./useShimmer.js";
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ /**
19
+ * Props for the BannerShimmerText component.
20
+ */
21
+ export interface BannerShimmerTextProps {
22
+ /** Text content to display with shimmer effect */
23
+ readonly children: string;
24
+ /** Base color (default: '#8B4513' - saddle brown) */
25
+ readonly baseColor?: string;
26
+ /** Highlight color for shimmer peak (default: '#FFD700' - gold) */
27
+ readonly highlightColor?: string;
28
+ /** Shimmer animation configuration */
29
+ readonly shimmerConfig?: ShimmerConfig;
30
+ /** Whether shimmer is enabled (default: true) */
31
+ readonly enabled?: boolean;
32
+ /** Width of the shimmer effect (0-1, default: 0.15) */
33
+ readonly shimmerWidth?: number;
34
+ /** Whether text should be bold */
35
+ readonly bold?: boolean;
36
+ }
37
+
38
+ // =============================================================================
39
+ // Color Interpolation
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Parse hex color to RGB components.
44
+ */
45
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
46
+ const cleaned = hex.replace("#", "");
47
+ const bigint = parseInt(cleaned, 16);
48
+ return {
49
+ r: (bigint >> 16) & 255,
50
+ g: (bigint >> 8) & 255,
51
+ b: bigint & 255,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Convert RGB to hex string.
57
+ */
58
+ function rgbToHex(r: number, g: number, b: number): string {
59
+ const toHex = (n: number) => Math.round(n).toString(16).padStart(2, "0");
60
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
61
+ }
62
+
63
+ /**
64
+ * Interpolate between two colors based on intensity (0-1).
65
+ */
66
+ export function interpolateColor(
67
+ baseColor: string,
68
+ highlightColor: string,
69
+ intensity: number
70
+ ): string {
71
+ const base = hexToRgb(baseColor);
72
+ const highlight = hexToRgb(highlightColor);
73
+
74
+ const r = base.r + (highlight.r - base.r) * intensity;
75
+ const g = base.g + (highlight.g - base.g) * intensity;
76
+ const b = base.b + (highlight.b - base.b) * intensity;
77
+
78
+ return rgbToHex(r, g, b);
79
+ }
80
+
81
+ // =============================================================================
82
+ // Character-Level Shimmer Component
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Single character with shimmer color applied.
87
+ * Memoized to prevent unnecessary re-renders.
88
+ */
89
+ interface ShimmerCharProps {
90
+ readonly char: string;
91
+ readonly color: string;
92
+ readonly bold?: boolean;
93
+ }
94
+
95
+ const ShimmerChar = React.memo(function ShimmerChar({
96
+ char,
97
+ color,
98
+ bold,
99
+ }: ShimmerCharProps): React.JSX.Element {
100
+ return (
101
+ <Text color={color} bold={bold}>
102
+ {char}
103
+ </Text>
104
+ );
105
+ });
106
+
107
+ // =============================================================================
108
+ // Main Component
109
+ // =============================================================================
110
+
111
+ /**
112
+ * BannerShimmerText renders text with a flowing shimmer effect.
113
+ *
114
+ * The shimmer sweeps from left to right, creating a glow effect
115
+ * that transitions from the base color to the highlight color.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * <BannerShimmerText
120
+ * baseColor="#8B4513"
121
+ * highlightColor="#FFD700"
122
+ * >
123
+ * VELLUM
124
+ * </BannerShimmerText>
125
+ * ```
126
+ */
127
+ export function BannerShimmerText({
128
+ children,
129
+ baseColor = "#8B4513",
130
+ highlightColor = "#FFD700",
131
+ shimmerConfig,
132
+ enabled = true,
133
+ shimmerWidth = 0.15,
134
+ bold = false,
135
+ }: BannerShimmerTextProps): React.JSX.Element {
136
+ const { position } = useShimmer({
137
+ ...shimmerConfig,
138
+ enabled,
139
+ });
140
+
141
+ // Calculate color for each character based on shimmer position
142
+ const coloredChars = useMemo(() => {
143
+ const chars = children.split("");
144
+ const totalChars = chars.length;
145
+
146
+ return chars.map((char, index) => {
147
+ // Skip whitespace - render as-is
148
+ if (char === " " || char === "\n" || char === "\t") {
149
+ return { char, color: baseColor, isWhitespace: true };
150
+ }
151
+
152
+ const intensity = calculateShimmerIntensity(index, totalChars, position, shimmerWidth);
153
+
154
+ const color = interpolateColor(baseColor, highlightColor, intensity);
155
+ return { char, color, isWhitespace: false };
156
+ });
157
+ }, [children, position, baseColor, highlightColor, shimmerWidth]);
158
+
159
+ return (
160
+ <Text>
161
+ {coloredChars.map((item, index) => (
162
+ <ShimmerChar
163
+ key={`${index}-${item.char}`}
164
+ char={item.char}
165
+ color={item.isWhitespace ? baseColor : item.color}
166
+ bold={bold}
167
+ />
168
+ ))}
169
+ </Text>
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Multi-line shimmer text that processes each line.
175
+ */
176
+ export interface MultiLineShimmerProps extends Omit<BannerShimmerTextProps, "children"> {
177
+ /** Lines of text to render */
178
+ readonly lines: string[];
179
+ }
180
+
181
+ export function MultiLineShimmer({ lines, ...props }: MultiLineShimmerProps): React.JSX.Element {
182
+ return (
183
+ <>
184
+ {lines.map((line, index) => (
185
+ // Using index as key is acceptable here since lines array is static and order never changes
186
+ // biome-ignore lint/suspicious/noArrayIndexKey: lines are static banner content that never reorders
187
+ <BannerShimmerText key={index} {...props}>
188
+ {line}
189
+ </BannerShimmerText>
190
+ ))}
191
+ </>
192
+ );
193
+ }