@plannotator/ui 0.22.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 (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+
3
+ interface SparklesIconProps {
4
+ className?: string;
5
+ animated?: boolean;
6
+ }
7
+
8
+ /**
9
+ * Three-star sparkle icon used for Ask AI entry points.
10
+ * Uses currentColor for fill so it inherits from parent text color.
11
+ */
12
+ export const SparklesIcon: React.FC<SparklesIconProps> = ({ className = 'w-3 h-3', animated = false }) => (
13
+ <svg className={`${className} flex-shrink-0`} viewBox="0 0 128 128" fill="none">
14
+ {animated && (
15
+ <style>{`
16
+ @keyframes sparkle-idle {
17
+ 0%, 100% { opacity: 0.5; }
18
+ 50% { opacity: 1; }
19
+ }
20
+ `}</style>
21
+ )}
22
+ <g style={animated ? { animation: 'sparkle-idle 2s infinite ease-in-out', animationDelay: '0.35s' } : undefined}>
23
+ <path
24
+ d="M121.59 60.83l-13.93-4.49c-8.91-2.94-14.13-10.15-16.58-19.21L84.95 7.27c-.16-.59-.55-1.38-1.75-1.38c-1.01 0-1.59.79-1.75 1.38l-6.13 29.87c-2.46 9.06-7.67 16.27-16.58 19.21l-13.93 4.49c-1.97.64-2 3.42-.04 4.09l14.03 4.83c8.88 2.95 14.06 10.15 16.52 19.17l6.14 29.53c.16.59.49 1.65 1.75 1.65c1.33 0 1.59-1.06 1.75-1.65l6.14-29.53c2.46-9.03 7.64-16.23 16.52-19.17l14.03-4.83c1.94-.68 1.91-3.46-.06-4.1z"
25
+ fill="currentColor"
26
+ />
27
+ <path
28
+ d="M122.91 62.08c-.22-.55-.65-1.03-1.32-1.25l-13.93-4.49c-8.91-2.94-14.13-10.15-16.58-19.21L84.95 7.27c-.09-.34-.41-.96-.78-1.14l1.98 29.97c1.47 13.68 2.73 20.12 13.65 22c9.38 1.62 20.23 3.48 23.11 3.98z"
29
+ fill="currentColor" opacity={0.7}
30
+ />
31
+ </g>
32
+ <g style={animated ? { animation: 'sparkle-idle 2s infinite ease-in-out', animationDelay: '0.15s' } : undefined}>
33
+ <path
34
+ d="M41.81 86.81c-8.33-2.75-9.09-5.85-10.49-11.08l-3.49-12.24c-.21-.79-2.27-.79-2.49 0L22.97 74.8c-1.41 5.21-4.41 9.35-9.53 11.04l-8.16 3.54c-1.13.37-1.15 1.97-.02 2.35l8.22 2.91c5.1 1.69 8.08 5.83 9.5 11.02l2.37 10.82c.22.79 2.27.79 2.48 0l2.78-10.77c1.41-5.22 3.57-9.37 10.5-11.07l7.72-2.91c1.13-.39 1.12-1.99-.02-2.36l-7-2.56z"
35
+ fill="currentColor"
36
+ />
37
+ </g>
38
+ <g style={animated ? { animation: 'sparkle-idle 2s infinite ease-in-out', animationDelay: '0.5s' } : undefined}>
39
+ <path
40
+ d="M59.74 28.14c.56-.19.54-.99-.03-1.15l-7.72-2.08a4.77 4.77 0 0 1-3.34-3.3L45.61 9.06c-.15-.61-1.02-.61-1.17.01l-2.86 12.5a4.734 4.734 0 0 1-3.4 3.37l-7.67 1.99c-.57.15-.61.95-.05 1.15l8.09 2.8c1.45.5 2.57 1.68 3.01 3.15l2.89 11.59c.15.6 1.01.61 1.16 0l2.99-11.63a4.773 4.773 0 0 1 3.04-3.13l8.1-2.72z"
41
+ fill="currentColor" opacity={0.5}
42
+ />
43
+ </g>
44
+ </svg>
45
+ );
@@ -0,0 +1,260 @@
1
+ /**
2
+ * StickyHeaderLane — compact "ghost" header that pins as the user scrolls
3
+ * past the AnnotationToolstrip.
4
+ *
5
+ * At rest (top of doc): invisible, non-interactive. The original toolstrip
6
+ * and badge cluster on the card remain the visible source of truth.
7
+ *
8
+ * Layout is driven by two ResizeObserver measurements — the sticky
9
+ * wrapper's actual width AND the Viewer action button cluster's actual
10
+ * width — so the bar fits exactly into the space between its left edge
11
+ * and the buttons, with no fixed pixel reserves.
12
+ *
13
+ * availableForBar = wrapperWidth - LEFT_OFFSET - actionsWidth - GAP
14
+ *
15
+ * Three states based on availableForBar:
16
+ * wide (>= WIDE_BAR_WIDTH): shared lane, toolstrip with active labels.
17
+ * tight (>= MIN_BAR_WIDTH): shared lane, toolstrip icon-only — gives
18
+ * the bar another ~160px of room before
19
+ * forcing a layout change.
20
+ * narrow (< MIN_BAR_WIDTH): stacked below the action buttons on its
21
+ * own full-width row, icon-only toolstrip.
22
+ *
23
+ * Composes <AnnotationToolstrip compact /> + <DocBadges layout="row" />.
24
+ * No state is duplicated — all props are passed through from App.tsx.
25
+ */
26
+
27
+ import React, { useEffect, useRef, useState } from 'react';
28
+ import { AnnotationToolstrip } from './AnnotationToolstrip';
29
+ import { DocBadges } from './DocBadges';
30
+ import { useScrollViewport } from '../hooks/useScrollViewport';
31
+ import type { EditorMode, InputMethod } from '../types';
32
+ import type { PlanDiffStats } from '../utils/planDiffEngine';
33
+
34
+ // Snap a measured pixel width to a 16px grid. ResizeObserver fires every
35
+ // frame during a drag; without quantization the sticky bar would
36
+ // re-render on every pixel. Hoisted to module scope so the effects
37
+ // (which use [] deps) can't accidentally close over a stale instance.
38
+ // Floor (not round) so wrapper undershoots and actions overshoots — both
39
+ // errors push toward a more cautious layout, avoiding a one-bucket overlap
40
+ // flash right at the 300/460 thresholds during a slow drag.
41
+ const snap = (n: number) => Math.floor(n / 16) * 16;
42
+
43
+ // Layout geometry — static tuning constants, hoisted alongside `snap`.
44
+ // LEFT_OFFSET: matches the bar's `md:left-5` (20px).
45
+ // GAP: minimum breathing room between the bar's right edge and the
46
+ // action button cluster's left edge when they share a lane.
47
+ // Two-stage shared-lane shrinkage, mirroring the right side:
48
+ // WIDE_BAR_WIDTH: full toolstrip (active labels Pinpoint/Markup ~300px)
49
+ // + badges (~140px) on a single line.
50
+ // MIN_BAR_WIDTH: icon-only toolstrip (~140px) + badges (~140px).
51
+ // Below MIN, even the icon-only bar can't fit beside the (likely also
52
+ // icon-only) action cluster — stack as the final fallback.
53
+ const LEFT_OFFSET = 20;
54
+ const GAP = 16;
55
+ const WIDE_BAR_WIDTH = 460;
56
+ const MIN_BAR_WIDTH = 300;
57
+
58
+ interface StickyHeaderLaneProps {
59
+ // Toolstrip state
60
+ inputMethod: InputMethod;
61
+ onInputMethodChange: (method: InputMethod) => void;
62
+ mode: EditorMode;
63
+ onModeChange: (mode: EditorMode) => void;
64
+ taterMode?: boolean;
65
+
66
+ // Badge state
67
+ repoInfo?: { display: string; branch?: string } | null;
68
+ planDiffStats?: PlanDiffStats | null;
69
+ isPlanDiffActive?: boolean;
70
+ hasPreviousVersion?: boolean;
71
+ onPlanDiffToggle?: () => void;
72
+ archiveInfo?: { status: 'approved' | 'denied' | 'unknown'; timestamp: string; title: string } | null;
73
+
74
+ // Layout
75
+ maxWidth?: number | null;
76
+
77
+ // Re-query token for the [data-sticky-actions] ResizeObserver. When the
78
+ // Viewer remounts (e.g., toggling a linked doc), its `data-sticky-actions`
79
+ // node is replaced — but StickyHeaderLane itself does NOT remount, so
80
+ // its observer would otherwise stay attached to the now-detached old
81
+ // node and freeze `actionsWidth`. Pass a string that changes whenever
82
+ // Viewer remounts and the effect re-runs against the fresh DOM.
83
+ remountToken?: string;
84
+ }
85
+
86
+ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
87
+ inputMethod,
88
+ onInputMethodChange,
89
+ mode,
90
+ onModeChange,
91
+ taterMode,
92
+ repoInfo,
93
+ planDiffStats,
94
+ isPlanDiffActive,
95
+ hasPreviousVersion,
96
+ onPlanDiffToggle,
97
+ archiveInfo,
98
+ maxWidth,
99
+ remountToken,
100
+ }) => {
101
+ const sentinelRef = useRef<HTMLDivElement>(null);
102
+ const wrapperRef = useRef<HTMLDivElement>(null);
103
+ const [isStuck, setIsStuck] = useState(false);
104
+ const [wrapperWidth, setWrapperWidth] = useState(0);
105
+ const [actionsWidth, setActionsWidth] = useState(0);
106
+ const scrollViewport = useScrollViewport();
107
+
108
+ // Space available for the bar in the shared lane = wrapper width, minus
109
+ // the bar's left offset, minus the action buttons' measured width, minus
110
+ // the breathing gap.
111
+ const availableForBar = wrapperWidth - LEFT_OFFSET - actionsWidth - GAP;
112
+
113
+ // Narrow = not enough room in the shared lane for even the icon-only
114
+ // bar. Falls back to a stacked row below the action buttons.
115
+ // actionsWidth=0 before measurement is treated as "don't know yet"
116
+ // (not narrow), so we don't flash the wrong layout on first paint.
117
+ const measured = wrapperWidth > 0 && actionsWidth > 0;
118
+ const isNarrow = measured && availableForBar < MIN_BAR_WIDTH;
119
+ // Tight = shared lane still fits, but only if the toolstrip drops its
120
+ // active labels and goes icon-only. Lets us stay horizontally aligned
121
+ // for an extra ~160px of width before stacking.
122
+ const isToolstripIconOnly =
123
+ measured && !isNarrow && availableForBar < WIDE_BAR_WIDTH;
124
+
125
+ useEffect(() => {
126
+ if (!wrapperRef.current) return;
127
+ const ro = new ResizeObserver(([entry]) => {
128
+ const next = snap(entry.contentRect.width);
129
+ setWrapperWidth((prev) => (prev === next ? prev : next));
130
+ });
131
+ ro.observe(wrapperRef.current);
132
+ return () => ro.disconnect();
133
+ }, []);
134
+
135
+ // Measure the Viewer's action button cluster so the right reserve
136
+ // reflects its REAL width (which varies with viewport — short vs full
137
+ // labels — and with button count). No more guessing at ~310/360/400px.
138
+ // The action button div is tagged with `data-sticky-actions` in
139
+ // Viewer.tsx. It's a sibling in the DOM by the time effects fire.
140
+ // Re-runs when `remountToken` changes so we re-query after Viewer
141
+ // unmounts/remounts (e.g., linked-doc toggle) instead of leaving the
142
+ // observer attached to a detached node.
143
+ useEffect(() => {
144
+ // Reset to the unmeasured state so the bar falls back to the safe
145
+ // "no maxWidth cap" path for the one frame between Viewer remounting
146
+ // and the new observer firing its first callback.
147
+ setActionsWidth(0);
148
+ const el = document.querySelector<HTMLElement>('[data-sticky-actions]');
149
+ if (!el) return;
150
+ const ro = new ResizeObserver(([entry]) => {
151
+ const next = snap(entry.contentRect.width);
152
+ setActionsWidth((prev) => (prev === next ? prev : next));
153
+ });
154
+ ro.observe(el);
155
+ return () => ro.disconnect();
156
+ }, [remountToken]);
157
+
158
+ // IntersectionObserver-on-sentinel pattern (mirrors Viewer.tsx:257-267).
159
+ // Sentinel sits inline at the top of the column. The 80px positive top
160
+ // rootMargin grows the effective viewport upward so the sentinel is
161
+ // considered "visible" for an extra ~80px of scroll — delaying the bar's
162
+ // appearance until the real toolstrip has actually scrolled past. Without
163
+ // this, the sentinel fires the moment scrolling begins and the ghost bar
164
+ // doubles up with the still-visible toolstrip. Root is the OverlayScrollArea
165
+ // viewport from context, NOT <main> (which doesn't actually scroll).
166
+ useEffect(() => {
167
+ if (!sentinelRef.current || !scrollViewport) return;
168
+ const observer = new IntersectionObserver(
169
+ ([entry]) => setIsStuck(!entry.isIntersecting),
170
+ { root: scrollViewport, rootMargin: '80px 0px 0px 0px', threshold: 0 }
171
+ );
172
+ observer.observe(sentinelRef.current);
173
+ return () => observer.disconnect();
174
+ }, [scrollViewport]);
175
+
176
+ return (
177
+ <>
178
+ {/* Sentinel — zero-size, rendered in normal flow at the top of the
179
+ column. When it scrolls out of the OverlayScrollArea viewport,
180
+ the sticky bar fades in. */}
181
+ <div ref={sentinelRef} aria-hidden="true" className="h-0 w-0" />
182
+
183
+ {/* Sticky wrapper — zero-height so it never pushes content down. The
184
+ visible bar is positioned absolutely relative to this wrapper.
185
+ The Viewer's outer wrapper uses z-50, so the sticky lane must
186
+ sit above that to paint over the card.
187
+
188
+ Narrow: the bar pins at top-[52px] / md:top-[60px] on its OWN
189
+ full-width row BELOW the card's sticky action buttons. Stacked
190
+ horizontal lanes, no horizontal collision possible.
191
+
192
+ Wide / tight: the bar shares the top-3 lane with the action
193
+ buttons (single horizontal header). */}
194
+ <div
195
+ ref={wrapperRef}
196
+ data-sticky-header-lane="true"
197
+ className={`sticky z-[60] w-full self-center pointer-events-none ${
198
+ isNarrow ? 'top-[52px] md:top-[60px]' : 'top-3'
199
+ }`}
200
+ style={maxWidth == null ? { height: 0 } : { maxWidth, height: 0 }}
201
+ >
202
+ {/* Responsive bar.
203
+
204
+ `inline-flex flex-wrap` + a measured `max-width` cap (set inline
205
+ below) lets the bar wrap badges to a second row if the toolstrip
206
+ + badges can't fit on one line. The max-width is computed from
207
+ the real measured action button width — no fixed reserves.
208
+
209
+ `flex-shrink-0` on the toolstrip wrapper is a defensive measure:
210
+ if a long branch name pushes the badges, this stops the toolstrip
211
+ from being squeezed below its natural width. `overflow-hidden`
212
+ is the final safety net so any overflow clips inside the chrome
213
+ rather than leaking out.
214
+
215
+ `inert` removes the bar from the tab order when not stuck. */}
216
+ <div
217
+ inert={!isStuck || undefined}
218
+ className={`absolute left-3 md:left-5 top-0 inline-flex flex-wrap items-center gap-x-3 gap-y-1 min-w-0 overflow-hidden rounded-lg py-1 md:py-1.5 bg-card/95 backdrop-blur-sm shadow-sm border border-border/30 motion-reduce:transform-none ${
219
+ isStuck
220
+ ? 'opacity-100 translate-y-0 pointer-events-auto'
221
+ : 'opacity-0 -translate-y-1 pointer-events-none'
222
+ }`}
223
+ style={{
224
+ paddingLeft: 12,
225
+ paddingRight: 12,
226
+ maxWidth: isNarrow
227
+ ? 'calc(100% - 24px)'
228
+ : availableForBar > 0
229
+ ? availableForBar
230
+ : undefined,
231
+ transition:
232
+ 'opacity 180ms cubic-bezier(0.2, 0, 0, 1), transform 180ms cubic-bezier(0.2, 0, 0, 1)',
233
+ willChange: 'opacity, transform',
234
+ }}
235
+ >
236
+ <div className="flex-shrink-0">
237
+ <AnnotationToolstrip
238
+ inputMethod={inputMethod}
239
+ onInputMethodChange={onInputMethodChange}
240
+ mode={mode}
241
+ onModeChange={onModeChange}
242
+ taterMode={taterMode}
243
+ compact
244
+ iconOnly={isNarrow || isToolstripIconOnly}
245
+ />
246
+ </div>
247
+ <DocBadges
248
+ layout="row"
249
+ repoInfo={repoInfo}
250
+ planDiffStats={planDiffStats}
251
+ isPlanDiffActive={isPlanDiffActive}
252
+ hasPreviousVersion={hasPreviousVersion}
253
+ onPlanDiffToggle={onPlanDiffToggle}
254
+ archiveInfo={archiveInfo}
255
+ />
256
+ </div>
257
+ </div>
258
+ </>
259
+ );
260
+ };
@@ -0,0 +1,157 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import type { Block, Annotation } from '../types';
3
+ import {
4
+ buildTocHierarchy,
5
+ getAnnotationCountBySection,
6
+ type TocItem,
7
+ } from '../utils/annotationHelpers';
8
+ import { useScrollViewport } from '../hooks/useScrollViewport';
9
+
10
+ interface TableOfContentsProps {
11
+ blocks: Block[];
12
+ annotations: Annotation[];
13
+ activeId: string | null;
14
+ onNavigate: (blockId: string) => void;
15
+ className?: string;
16
+ style?: React.CSSProperties;
17
+ linkedDocFilepath?: string | null;
18
+ onLinkedDocBack?: () => void;
19
+ backLabel?: string;
20
+ }
21
+
22
+ // The prototype's TOC is a FLAT list — heading depth is conveyed by indentation
23
+ // + tonal de-emphasis, not an expand/collapse chevron tree. Flatten the built
24
+ // hierarchy (depth-first = document order) into a single ordered list.
25
+ function flattenToc(items: TocItem[]): TocItem[] {
26
+ const out: TocItem[] = [];
27
+ const walk = (list: TocItem[]) => {
28
+ for (const it of list) {
29
+ out.push(it);
30
+ if (it.children.length) walk(it.children);
31
+ }
32
+ };
33
+ walk(items);
34
+ return out;
35
+ }
36
+
37
+ // Indentation + tonal de-emphasis by heading level (prototype style):
38
+ // H1 flush + near-full strength, H2/H3 indented and dimmed to muted. Active row
39
+ // is a soft neutral surface tint (not a loud primary fill).
40
+ function itemClasses(level: number, isActive: boolean): string {
41
+ const indent = level <= 1 ? '' : level === 2 ? 'ml-3' : 'ml-6';
42
+ const tone = isActive
43
+ ? 'bg-surface-1 text-foreground'
44
+ : level <= 1
45
+ ? 'text-foreground/80 hover:bg-surface-1/70'
46
+ : 'text-muted-foreground hover:bg-surface-1/70';
47
+ return `${indent} ${tone}`;
48
+ }
49
+
50
+ export function TableOfContents({
51
+ blocks,
52
+ annotations,
53
+ activeId,
54
+ onNavigate,
55
+ className = '',
56
+ style,
57
+ linkedDocFilepath,
58
+ onLinkedDocBack,
59
+ backLabel,
60
+ }: TableOfContentsProps) {
61
+ // Annotation count per section (kept — production feature).
62
+ const annotationCounts = useMemo(
63
+ () => getAnnotationCountBySection(blocks, annotations),
64
+ [blocks, annotations]
65
+ );
66
+
67
+ // Build the hierarchy (filters to heading levels ≤ 3 and attaches counts),
68
+ // then flatten to a plain list.
69
+ const tocItems = useMemo(
70
+ () => flattenToc(buildTocHierarchy(blocks, annotationCounts)),
71
+ [blocks, annotationCounts]
72
+ );
73
+
74
+ // The real scroll element is the OverlayScrollArea viewport, not <main>.
75
+ const scrollViewport = useScrollViewport();
76
+
77
+ // Smooth scroll-to-heading, accounting for the sticky header.
78
+ const handleNavigate = useCallback(
79
+ (blockId: string) => {
80
+ onNavigate(blockId);
81
+ const target = (scrollViewport ?? document).querySelector(`[data-block-id="${blockId}"]`);
82
+ if (target && scrollViewport) {
83
+ const scrollContainer = scrollViewport;
84
+ const headerOffset = 80; // sticky header (h-12) + breathing room
85
+ const containerRect = scrollContainer.getBoundingClientRect();
86
+ const targetRect = target.getBoundingClientRect();
87
+ const offsetPosition =
88
+ scrollContainer.scrollTop + (targetRect.top - containerRect.top) - headerOffset;
89
+ scrollContainer.scrollTo({ top: offsetPosition, behavior: 'smooth' });
90
+ }
91
+ },
92
+ [onNavigate, scrollViewport]
93
+ );
94
+
95
+ if (tocItems.length === 0) {
96
+ return null;
97
+ }
98
+
99
+ return (
100
+ <nav
101
+ // Use ?? not || — an explicit empty string from a caller means "I'm
102
+ // managing my own container styling" (e.g. SidebarContainer wrapping
103
+ // us in an OverlayScrollArea), which should NOT trigger the default.
104
+ className={className ?? 'bg-card/50 backdrop-blur-sm border-r border-border overflow-y-auto'}
105
+ aria-label="Table of contents"
106
+ style={style}
107
+ >
108
+ <div className="p-1.5">
109
+ {linkedDocFilepath && (
110
+ <div className="mb-2 px-0.5 pb-1.5 border-b border-border/50">
111
+ <div className="flex items-center justify-between">
112
+ <span className="text-[10px] font-medium text-primary/80">Viewing</span>
113
+ {onLinkedDocBack && (
114
+ <button
115
+ onClick={onLinkedDocBack}
116
+ className="flex items-center gap-0.5 text-[10px] font-medium text-primary hover:text-primary/80 transition-colors"
117
+ >
118
+ <svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
119
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
120
+ </svg>
121
+ Back to {backLabel || 'plan'}
122
+ </button>
123
+ )}
124
+ </div>
125
+ <p className="text-[11px] text-foreground/70 truncate mt-0.5" title={linkedDocFilepath}>
126
+ {linkedDocFilepath.split('/').pop()}
127
+ </p>
128
+ </div>
129
+ )}
130
+ <div className="flex flex-col gap-0.5">
131
+ {tocItems.map((item) => {
132
+ const isActive = item.id === activeId;
133
+ return (
134
+ <button
135
+ key={item.id}
136
+ type="button"
137
+ onClick={() => handleNavigate(item.id)}
138
+ aria-current={isActive ? 'location' : undefined}
139
+ className={`flex items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-[11px] font-medium leading-snug transition-colors ${itemClasses(
140
+ item.level,
141
+ isActive
142
+ )}`}
143
+ >
144
+ <span className="line-clamp-2">{item.content}</span>
145
+ {item.annotationCount > 0 && (
146
+ <span className="ml-1 flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-primary/10 px-1 font-mono text-[9px] text-primary">
147
+ {item.annotationCount}
148
+ </span>
149
+ )}
150
+ </button>
151
+ );
152
+ })}
153
+ </div>
154
+ </div>
155
+ </nav>
156
+ );
157
+ }
@@ -0,0 +1,34 @@
1
+ /// <reference path="../globals.d.ts" />
2
+ import React from 'react';
3
+ import spriteSheet from '../sprite_package_pulluphang/sprite.png';
4
+
5
+ // Sprite specs: 96x96 native, 24 frames, 3 seconds
6
+ const NATIVE_SIZE = 96;
7
+ const DISPLAY_SIZE = 56;
8
+ const FRAMES = 24;
9
+ const SCALE = DISPLAY_SIZE / NATIVE_SIZE;
10
+ const TOTAL_WIDTH = NATIVE_SIZE * FRAMES * SCALE;
11
+
12
+ export const TaterSpritePullup: React.FC = () => {
13
+ return (
14
+ <div
15
+ className="absolute pointer-events-none hidden md:block -z-10"
16
+ style={{
17
+ bottom: -49, // Hangs off bottom of dialog
18
+ left: 12,
19
+ width: DISPLAY_SIZE,
20
+ height: DISPLAY_SIZE,
21
+ backgroundImage: `url(${spriteSheet})`,
22
+ backgroundSize: `${TOTAL_WIDTH}px ${DISPLAY_SIZE}px`,
23
+ animation: 'tater-pullup 3.5s steps(24) infinite',
24
+ imageRendering: 'pixelated',
25
+ }}
26
+ >
27
+ <style>{`
28
+ @keyframes tater-pullup {
29
+ to { background-position: -${TOTAL_WIDTH}px 0; }
30
+ }
31
+ `}</style>
32
+ </div>
33
+ );
34
+ };
@@ -0,0 +1,54 @@
1
+ /// <reference path="../globals.d.ts" />
2
+ import React from 'react';
3
+ import spriteSheet from '../sprite_package_additional/sprite.png';
4
+
5
+ // Sprite: 176x96 native, 24 frames, 5 second animation cycle
6
+ // Running across screen should sync with animation speed
7
+ const NATIVE_WIDTH = 176;
8
+ const NATIVE_HEIGHT = 96;
9
+ const DISPLAY_HEIGHT = 64;
10
+ const SCALE = DISPLAY_HEIGHT / NATIVE_HEIGHT;
11
+ const DISPLAY_WIDTH = NATIVE_WIDTH * SCALE; // ~117px
12
+ const FRAMES = 24;
13
+ const FRAME_DURATION = 5; // seconds for full run cycle
14
+ const TOTAL_SPRITE_WIDTH = NATIVE_WIDTH * FRAMES * SCALE;
15
+
16
+ // Screen traverse: character runs across in ~12 seconds
17
+ // This gives ~2.4 run cycles per screen width, feels natural
18
+ const SCREEN_TRAVERSE_TIME = 18;
19
+
20
+ export const TaterSpriteRunning: React.FC = () => {
21
+ return (
22
+ <div
23
+ className="fixed pointer-events-none hidden md:block"
24
+ style={{
25
+ bottom: 0,
26
+ right: -DISPLAY_WIDTH, // Start off-screen right
27
+ width: DISPLAY_WIDTH,
28
+ height: DISPLAY_HEIGHT,
29
+ zIndex: 5, // Above sidebars (z-auto) but below plan document (z-10)
30
+ backgroundImage: `url(${spriteSheet})`,
31
+ backgroundSize: `${TOTAL_SPRITE_WIDTH}px ${DISPLAY_HEIGHT}px`,
32
+ backgroundPosition: 'left center',
33
+ imageRendering: 'pixelated',
34
+ animation: `tater-run-sprite ${FRAME_DURATION}s steps(${FRAMES}) infinite, tater-run-across ${SCREEN_TRAVERSE_TIME}s linear infinite`,
35
+ }}
36
+ >
37
+ <style>{`
38
+ @keyframes tater-run-sprite {
39
+ to {
40
+ background-position: -${TOTAL_SPRITE_WIDTH}px 0;
41
+ }
42
+ }
43
+ @keyframes tater-run-across {
44
+ from {
45
+ right: -${DISPLAY_WIDTH}px;
46
+ }
47
+ to {
48
+ right: 100vw;
49
+ }
50
+ }
51
+ `}</style>
52
+ </div>
53
+ );
54
+ };
@@ -0,0 +1,37 @@
1
+ /// <reference path="../globals.d.ts" />
2
+ import React from 'react';
3
+ import spriteSheet from '../sprite_package_new/sprite.png';
4
+
5
+ // Sprite: 96x96 native, 12 frames, scaled to 64x64 for display
6
+ const NATIVE_SIZE = 96;
7
+ const DISPLAY_SIZE = 64;
8
+ const FRAMES = 12;
9
+ const SCALE = DISPLAY_SIZE / NATIVE_SIZE;
10
+ const TOTAL_WIDTH = NATIVE_SIZE * FRAMES * SCALE; // 768px at 64px scale
11
+
12
+ export const TaterSpriteSitting: React.FC = () => {
13
+ return (
14
+ <div
15
+ className="hidden md:block absolute pointer-events-none z-10"
16
+ style={{
17
+ top: -40,
18
+ right: -4,
19
+ width: DISPLAY_SIZE,
20
+ height: DISPLAY_SIZE,
21
+ backgroundImage: `url(${spriteSheet})`,
22
+ backgroundSize: `${TOTAL_WIDTH}px ${DISPLAY_SIZE}px`,
23
+ backgroundPosition: 'left center',
24
+ animation: 'tater-sit 3s steps(12) infinite',
25
+ imageRendering: 'pixelated',
26
+ }}
27
+ >
28
+ <style>{`
29
+ @keyframes tater-sit {
30
+ to {
31
+ background-position: -${TOTAL_WIDTH}px 0;
32
+ }
33
+ }
34
+ `}</style>
35
+ </div>
36
+ );
37
+ };
@@ -0,0 +1,57 @@
1
+ import React, { useMemo, type JSX } from 'react';
2
+ import { motion } from 'motion/react';
3
+
4
+ export interface TextShimmerProps {
5
+ children: string;
6
+ as?: React.ElementType;
7
+ className?: string;
8
+ duration?: number;
9
+ spread?: number;
10
+ }
11
+
12
+ function TextShimmerComponent({
13
+ children,
14
+ as: Component = 'span',
15
+ className,
16
+ duration = 2,
17
+ spread = 2,
18
+ }: TextShimmerProps) {
19
+ const MotionComponent = motion.create(
20
+ Component as keyof JSX.IntrinsicElements
21
+ );
22
+
23
+ const dynamicSpread = useMemo(() => {
24
+ return children.length * spread;
25
+ }, [children, spread]);
26
+
27
+ return (
28
+ <MotionComponent
29
+ className={[
30
+ 'relative inline-block bg-[length:250%_100%,auto] bg-clip-text',
31
+ 'text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]',
32
+ '[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
33
+ 'dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
34
+ className,
35
+ ]
36
+ .filter(Boolean)
37
+ .join(' ')}
38
+ initial={{ backgroundPosition: '100% center' }}
39
+ animate={{ backgroundPosition: '0% center' }}
40
+ transition={{
41
+ repeat: Infinity,
42
+ duration,
43
+ ease: 'linear',
44
+ }}
45
+ style={
46
+ {
47
+ '--spread': `${dynamicSpread}px`,
48
+ backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
49
+ } as React.CSSProperties
50
+ }
51
+ >
52
+ {children}
53
+ </MotionComponent>
54
+ );
55
+ }
56
+
57
+ export const TextShimmer = React.memo(TextShimmerComponent);