@mova-ui/react 0.4.1 → 0.4.4

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 (478) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +477 -829
  3. package/dist/styles.css +116 -1
  4. package/dist/ui/ai/ai-context-meter.d.ts +2 -1
  5. package/dist/ui/ai/ai-sources.d.ts +2 -1
  6. package/dist/ui/ai/ai-task-list.d.ts +2 -1
  7. package/dist/ui/ai/ai-tool-call.d.ts +2 -1
  8. package/package.json +1 -1
  9. package/registry/1.json +1 -1
  10. package/registry/10.json +1 -1
  11. package/registry/11.json +1 -1
  12. package/registry/12.json +1 -1
  13. package/registry/13.json +1 -1
  14. package/registry/14.json +1 -1
  15. package/registry/15.json +1 -1
  16. package/registry/16.json +1 -1
  17. package/registry/17.json +1 -1
  18. package/registry/18.json +1 -1
  19. package/registry/19.json +1 -1
  20. package/registry/2.json +1 -1
  21. package/registry/20.json +1 -1
  22. package/registry/21.json +1 -1
  23. package/registry/22.json +1 -1
  24. package/registry/23.json +1 -1
  25. package/registry/24.json +1 -1
  26. package/registry/25.json +1 -1
  27. package/registry/26.json +1 -1
  28. package/registry/27.json +1 -1
  29. package/registry/28.json +1 -1
  30. package/registry/29.json +1 -1
  31. package/registry/3.json +1 -1
  32. package/registry/30.json +1 -1
  33. package/registry/31.json +1 -1
  34. package/registry/32.json +1 -1
  35. package/registry/33.json +1 -1
  36. package/registry/34.json +1 -1
  37. package/registry/35.json +1 -1
  38. package/registry/36.json +1 -1
  39. package/registry/37.json +1 -1
  40. package/registry/4.json +1 -1
  41. package/registry/5.json +1 -1
  42. package/registry/6.json +1 -1
  43. package/registry/7.json +1 -1
  44. package/registry/8.json +1 -1
  45. package/registry/9.json +1 -1
  46. package/registry/ab-segmented-tabs.json +1 -1
  47. package/registry/accordion-blind.json +1 -1
  48. package/registry/accordion-loader.json +1 -1
  49. package/registry/ai-approval-card.json +2 -2
  50. package/registry/ai-code-block.json +2 -2
  51. package/registry/ai-context-meter.json +2 -2
  52. package/registry/ai-fine-tune-card.json +2 -2
  53. package/registry/ai-prompt-bar.json +2 -2
  54. package/registry/ai-sidebar-nav.json +2 -2
  55. package/registry/ai-sources.json +2 -2
  56. package/registry/ai-suggestions.json +1 -1
  57. package/registry/ai-task-list.json +2 -2
  58. package/registry/ai-tool-call.json +2 -2
  59. package/registry/analyzing-image.json +1 -1
  60. package/registry/animated-sidebar.json +1 -1
  61. package/registry/animated-tags.json +1 -1
  62. package/registry/app-icon-load.json +1 -1
  63. package/registry/apple-breathe.json +1 -1
  64. package/registry/apple-equalizer.json +1 -1
  65. package/registry/apple-icon-morph.json +1 -1
  66. package/registry/apple-pulse-dots.json +1 -1
  67. package/registry/apple-scale-pulse.json +1 -1
  68. package/registry/apple-sound-wave.json +1 -1
  69. package/registry/apple-text-reveal.json +1 -1
  70. package/registry/apple-unlock.json +1 -1
  71. package/registry/arc-tracer.json +1 -1
  72. package/registry/bar-cascade.json +1 -1
  73. package/registry/bar-sweep.json +1 -1
  74. package/registry/blur-text.json +1 -1
  75. package/registry/blur-up-char.json +1 -1
  76. package/registry/blur-up-word.json +1 -1
  77. package/registry/bobbing-dots.json +1 -1
  78. package/registry/bookmark-corner-peel.json +1 -1
  79. package/registry/bookmark-save-pill.json +1 -1
  80. package/registry/bookmark-toggle.json +1 -1
  81. package/registry/border-trail-loading.json +1 -1
  82. package/registry/bounce-dots.json +1 -1
  83. package/registry/bounce-in-char.json +1 -1
  84. package/registry/bouncing-bars.json +1 -1
  85. package/registry/bouncing-dots.json +1 -1
  86. package/registry/bouncing-lines.json +1 -1
  87. package/registry/bouncing-square.json +1 -1
  88. package/registry/breathe-ring.json +1 -1
  89. package/registry/breathing-glow.json +1 -1
  90. package/registry/breathing-square.json +1 -1
  91. package/registry/btn-1.json +1 -1
  92. package/registry/btn-10.json +1 -1
  93. package/registry/btn-11.json +1 -1
  94. package/registry/btn-12.json +1 -1
  95. package/registry/btn-13.json +1 -1
  96. package/registry/btn-14.json +1 -1
  97. package/registry/btn-15.json +1 -1
  98. package/registry/btn-16.json +1 -1
  99. package/registry/btn-17.json +1 -1
  100. package/registry/btn-18.json +1 -1
  101. package/registry/btn-19.json +1 -1
  102. package/registry/btn-2.json +1 -1
  103. package/registry/btn-20.json +1 -1
  104. package/registry/btn-21.json +1 -1
  105. package/registry/btn-22.json +1 -1
  106. package/registry/btn-23.json +1 -1
  107. package/registry/btn-24.json +1 -1
  108. package/registry/btn-25.json +1 -1
  109. package/registry/btn-26.json +1 -1
  110. package/registry/btn-27.json +1 -1
  111. package/registry/btn-28.json +1 -1
  112. package/registry/btn-29.json +1 -1
  113. package/registry/btn-3.json +1 -1
  114. package/registry/btn-30.json +1 -1
  115. package/registry/btn-31.json +1 -1
  116. package/registry/btn-32.json +1 -1
  117. package/registry/btn-33.json +1 -1
  118. package/registry/btn-34.json +1 -1
  119. package/registry/btn-35.json +1 -1
  120. package/registry/btn-36.json +1 -1
  121. package/registry/btn-37.json +1 -1
  122. package/registry/btn-4.json +1 -1
  123. package/registry/btn-5.json +1 -1
  124. package/registry/btn-6.json +1 -1
  125. package/registry/btn-7.json +1 -1
  126. package/registry/btn-8.json +1 -1
  127. package/registry/btn-9.json +1 -1
  128. package/registry/button-slide-arrow.json +1 -1
  129. package/registry/cacheable-bandwidth-cost.json +1 -1
  130. package/registry/card-arc-5.json +1 -1
  131. package/registry/card-arc-7.json +1 -1
  132. package/registry/card-carousel-mono.json +1 -1
  133. package/registry/card-carousel.json +1 -1
  134. package/registry/card-cascade-stagger.json +1 -1
  135. package/registry/card-corner-fan.json +1 -1
  136. package/registry/card-cover-flow-mono.json +1 -1
  137. package/registry/card-cover-flow.json +1 -1
  138. package/registry/card-deck-cascade.json +1 -1
  139. package/registry/card-glance-preview.json +1 -1
  140. package/registry/card-hover.json +1 -1
  141. package/registry/card-linear-spread.json +1 -1
  142. package/registry/card-long-arc-5.json +1 -1
  143. package/registry/card-scatter-spread.json +1 -1
  144. package/registry/card-stack-peel.json +1 -1
  145. package/registry/card-stamp-arc.json +1 -1
  146. package/registry/card-time-machine-mono.json +1 -1
  147. package/registry/card-time-machine.json +1 -1
  148. package/registry/card-wheel-fan.json +1 -1
  149. package/registry/category-select.json +1 -1
  150. package/registry/character-stagger.json +1 -1
  151. package/registry/checkbox-draw.json +1 -1
  152. package/registry/checkmark-draw.json +1 -1
  153. package/registry/circle-toggle.json +1 -1
  154. package/registry/circuit-trace-draw.json +1 -1
  155. package/registry/circular-bars.json +1 -1
  156. package/registry/circular-create-button.json +1 -1
  157. package/registry/classic-spinner.json +1 -1
  158. package/registry/classic-toggle.json +1 -1
  159. package/registry/clock-spinner.json +1 -1
  160. package/registry/comet-spinner.json +1 -1
  161. package/registry/compass-needle-deflect.json +1 -1
  162. package/registry/concentric-pulse.json +1 -1
  163. package/registry/concentric-ring.json +1 -1
  164. package/registry/concentric-squares.json +1 -1
  165. package/registry/context-menu.json +1 -1
  166. package/registry/conveyor-loop.json +1 -1
  167. package/registry/credit-score-barcode-meter.json +1 -1
  168. package/registry/cross-spinner.json +1 -1
  169. package/registry/cube-flip-spring.json +1 -1
  170. package/registry/cursor-trail.json +1 -1
  171. package/registry/dash-ring.json +1 -1
  172. package/registry/dashed-spiral.json +1 -1
  173. package/registry/date-position-selector.json +1 -1
  174. package/registry/dia-text-reveal.json +1 -1
  175. package/registry/diamond-grid.json +1 -1
  176. package/registry/diamond-rotate-spring.json +1 -1
  177. package/registry/dislike-toggle.json +1 -1
  178. package/registry/dither-book.json +1 -1
  179. package/registry/domino-cascade.json +1 -1
  180. package/registry/dot-spinner.json +1 -1
  181. package/registry/dots-ring.json +1 -1
  182. package/registry/double-bounce-toggle.json +1 -1
  183. package/registry/double-ring.json +1 -1
  184. package/registry/download-icons.json +1 -1
  185. package/registry/drop-dot.json +1 -1
  186. package/registry/drop-in-char.json +1 -1
  187. package/registry/droplet-squish.json +1 -1
  188. package/registry/dual-arc.json +1 -1
  189. package/registry/dual-magnet-dipole.json +1 -1
  190. package/registry/dynamic-island.json +1 -1
  191. package/registry/elastic-bars.json +1 -1
  192. package/registry/elastic-square.json +1 -1
  193. package/registry/elastic-tag-snap.json +1 -1
  194. package/registry/error-shake.json +1 -1
  195. package/registry/expanding-cross.json +1 -1
  196. package/registry/expanding-icon-tabs.json +1 -1
  197. package/registry/face-id-scan.json +1 -1
  198. package/registry/fade-arc.json +1 -1
  199. package/registry/fade-dots.json +1 -1
  200. package/registry/fade-down.json +1 -1
  201. package/registry/fade-in-char.json +1 -1
  202. package/registry/fade-in-text.json +1 -1
  203. package/registry/fade-in-word.json +1 -1
  204. package/registry/fade-in.json +1 -1
  205. package/registry/fade-up.json +1 -1
  206. package/registry/file-upload-dropzone.json +1 -1
  207. package/registry/filter-tag-pill.json +1 -1
  208. package/registry/flip-square.json +1 -1
  209. package/registry/flip-x-char.json +1 -1
  210. package/registry/flip-y-char.json +1 -1
  211. package/registry/float-char.json +1 -1
  212. package/registry/float-word.json +1 -1
  213. package/registry/floating-diamonds.json +1 -1
  214. package/registry/floating-label-input.json +1 -1
  215. package/registry/fluid-bars.json +1 -1
  216. package/registry/fluid-diamond.json +1 -1
  217. package/registry/fluid-dot-orbit.json +1 -1
  218. package/registry/fluid-skeleton.json +1 -1
  219. package/registry/focus-blur.json +1 -1
  220. package/registry/form-submit-button.json +1 -1
  221. package/registry/french-doors-3d.json +1 -1
  222. package/registry/gear-tooth-step.json +1 -1
  223. package/registry/gears.json +1 -1
  224. package/registry/gelatin-wobble.json +1 -1
  225. package/registry/geometric-iris-shutter.json +1 -1
  226. package/registry/glassmorphic-card.json +1 -1
  227. package/registry/glow-button.json +1 -1
  228. package/registry/glow-text.json +1 -1
  229. package/registry/gooey-popover.json +1 -1
  230. package/registry/gradient-arc.json +1 -1
  231. package/registry/gradient-shimmer.json +1 -1
  232. package/registry/grid-dots.json +1 -1
  233. package/registry/haptic-ring.json +1 -1
  234. package/registry/heartbeat.json +1 -1
  235. package/registry/hex-lattice-draw.json +1 -1
  236. package/registry/hexagon-spinner.json +1 -1
  237. package/registry/hourglass.json +1 -1
  238. package/registry/hover-lift-char.json +1 -1
  239. package/registry/hover-lift-word.json +1 -1
  240. package/registry/hover-link-card.json +1 -1
  241. package/registry/hover-scale-char.json +1 -1
  242. package/registry/hover-scale-word.json +1 -1
  243. package/registry/inertia-skid-stop.json +1 -1
  244. package/registry/infinity-path.json +1 -1
  245. package/registry/input-focus-glow.json +1 -1
  246. package/registry/inset-action-button.json +1 -1
  247. package/registry/inset-button.json +1 -1
  248. package/registry/intersecting-rings.json +1 -1
  249. package/registry/ios-spinner.json +1 -1
  250. package/registry/kinetic-metronome-tick.json +1 -1
  251. package/registry/kinetic-tension-capsule.json +1 -1
  252. package/registry/light-dark-morph.json +1 -1
  253. package/registry/like-toggle.json +1 -1
  254. package/registry/line-spinner.json +1 -1
  255. package/registry/liquid-dots.json +1 -1
  256. package/registry/mac-terminal.json +1 -1
  257. package/registry/magnetic-button.json +1 -1
  258. package/registry/magnetic-disks.json +1 -1
  259. package/registry/magnetic-dots.json +1 -1
  260. package/registry/magnetic-slide-btn.json +1 -1
  261. package/registry/manifest.json +656 -650
  262. package/registry/matrix-grid-loader.json +1 -1
  263. package/registry/minimal-triangle.json +1 -1
  264. package/registry/modular-tile-snap.json +1 -1
  265. package/registry/mono-activity-blue.json +1 -1
  266. package/registry/mono-activity-green.json +1 -1
  267. package/registry/mono-activity-purple.json +1 -1
  268. package/registry/mono-heatmap.json +1 -1
  269. package/registry/mono-rounded-area.json +1 -1
  270. package/registry/mono-rounded-bar.json +1 -1
  271. package/registry/mono-rounded-bubble.json +1 -1
  272. package/registry/mono-rounded-bullet.json +1 -1
  273. package/registry/mono-rounded-candlestick.json +1 -1
  274. package/registry/mono-rounded-composed.json +1 -1
  275. package/registry/mono-rounded-donut.json +1 -1
  276. package/registry/mono-rounded-funnel.json +1 -1
  277. package/registry/mono-rounded-gauge-arc.json +1 -1
  278. package/registry/mono-rounded-heatmap.json +1 -1
  279. package/registry/mono-rounded-kpi.json +1 -1
  280. package/registry/mono-rounded-line.json +1 -1
  281. package/registry/mono-rounded-meter.json +1 -1
  282. package/registry/mono-rounded-polar.json +1 -1
  283. package/registry/mono-rounded-pyramid.json +1 -1
  284. package/registry/mono-rounded-radar.json +1 -1
  285. package/registry/mono-rounded-radial-gauge.json +1 -1
  286. package/registry/mono-rounded-radial-group.json +1 -1
  287. package/registry/mono-rounded-range.json +1 -1
  288. package/registry/mono-rounded-sankey.json +1 -1
  289. package/registry/mono-rounded-scatter.json +1 -1
  290. package/registry/mono-rounded-sparkline.json +1 -1
  291. package/registry/mono-rounded-stacked-bar.json +1 -1
  292. package/registry/mono-rounded-step.json +1 -1
  293. package/registry/mono-rounded-stream.json +1 -1
  294. package/registry/mono-rounded-treemap.json +1 -1
  295. package/registry/mono-rounded-waterfall.json +1 -1
  296. package/registry/mono-stock.json +1 -1
  297. package/registry/morph-action-pill.json +1 -1
  298. package/registry/morph-dot-ring.json +1 -1
  299. package/registry/morph-loader.json +1 -1
  300. package/registry/morph-toggle.json +1 -1
  301. package/registry/morphing-bars.json +1 -1
  302. package/registry/morphing-infinity.json +1 -1
  303. package/registry/morphing-ring.json +1 -1
  304. package/registry/morphing-shape.json +1 -1
  305. package/registry/mouse-follow.json +1 -1
  306. package/registry/multi-select-chips.json +1 -1
  307. package/registry/multi-tab-bar.json +1 -1
  308. package/registry/neon-sign-draw.json +1 -1
  309. package/registry/nested-orbital-gimbal.json +1 -1
  310. package/registry/network-telemetry-matrix.json +1 -1
  311. package/registry/newtons-cradle.json +1 -1
  312. package/registry/noise-decibel-level.json +1 -1
  313. package/registry/obsidian-liquid-wave.json +1 -1
  314. package/registry/offset-rings.json +1 -1
  315. package/registry/orbiting-circles.json +1 -1
  316. package/registry/orbiting-dot.json +1 -1
  317. package/registry/origami-envelope-unfold.json +1 -1
  318. package/registry/origami-shape.json +1 -1
  319. package/registry/otp-input.json +1 -1
  320. package/registry/overview-bar-scrubber-card.json +1 -1
  321. package/registry/page-transition.json +1 -1
  322. package/registry/page-turn-curl.json +1 -1
  323. package/registry/password-toggle.json +1 -1
  324. package/registry/pendulum-bubble-level.json +1 -1
  325. package/registry/pendulum.json +1 -1
  326. package/registry/perspective-flip-stage.json +1 -1
  327. package/registry/perspective-flip.json +1 -1
  328. package/registry/perspective-layout-switcher.json +1 -1
  329. package/registry/physics-dock.json +1 -1
  330. package/registry/pill-tabs.json +1 -1
  331. package/registry/plus-minus-toggle.json +1 -1
  332. package/registry/presets.json +1 -1
  333. package/registry/prism-block-stack.json +1 -1
  334. package/registry/progress-indicator-piano.json +1 -1
  335. package/registry/progress-indicator.json +1 -1
  336. package/registry/progress-stepper.json +1 -1
  337. package/registry/pulsating-dots.json +1 -1
  338. package/registry/pulse-char.json +1 -1
  339. package/registry/pulse-dot.json +1 -1
  340. package/registry/pulse-dots.json +1 -1
  341. package/registry/pulse-square.json +1 -1
  342. package/registry/pulse-word.json +1 -1
  343. package/registry/pulse.json +1 -1
  344. package/registry/pumping-heart.json +1 -1
  345. package/registry/pyramid-block-build.json +1 -1
  346. package/registry/radar-sweep.json +1 -1
  347. package/registry/radial-iris-mask.json +1 -1
  348. package/registry/radial-spinner.json +1 -1
  349. package/registry/radio-scale.json +1 -1
  350. package/registry/range-slider.json +1 -1
  351. package/registry/receipt-tape-print.json +1 -1
  352. package/registry/rectangle-toggle.json +1 -1
  353. package/registry/repost-toggle.json +1 -1
  354. package/registry/reveal-text.json +1 -1
  355. package/registry/ribbon-banner-slide.json +1 -1
  356. package/registry/ring-sweep.json +1 -1
  357. package/registry/ripple-effect.json +1 -1
  358. package/registry/rise-up-word.json +1 -1
  359. package/registry/roller-blind-drop.json +1 -1
  360. package/registry/rolling-tumble.json +1 -1
  361. package/registry/rotate-in-char.json +1 -1
  362. package/registry/rotating-cross.json +1 -1
  363. package/registry/rotating-louvers.json +1 -1
  364. package/registry/rotating-triangle.json +1 -1
  365. package/registry/rudder-issue-board.json +2 -2
  366. package/registry/sales-analytics-dual-bars.json +1 -1
  367. package/registry/sales-overview-radial-dashboard.json +1 -1
  368. package/registry/sales-target-segmented-arc.json +1 -1
  369. package/registry/scale-in-char.json +1 -1
  370. package/registry/scale-in-text.json +1 -1
  371. package/registry/scale-in-word.json +1 -1
  372. package/registry/scale-in.json +1 -1
  373. package/registry/scramble-text.json +1 -1
  374. package/registry/scroll-canvas-unroll.json +1 -1
  375. package/registry/scroll-reveal.json +1 -1
  376. package/registry/search-expand.json +1 -1
  377. package/registry/segmented-arc-meter.json +1 -1
  378. package/registry/segmented-link-stretch.json +1 -1
  379. package/registry/segmented-step-bar.json +1 -1
  380. package/registry/segmented-stepper-dots.json +1 -1
  381. package/registry/select-dropdown.json +1 -1
  382. package/registry/server-performance-step-bars.json +1 -1
  383. package/registry/shape-shift-grid.json +1 -1
  384. package/registry/shimmer-line.json +1 -1
  385. package/registry/shimmer-text.json +1 -1
  386. package/registry/shutter-slide.json +1 -1
  387. package/registry/shutter-step-slices.json +1 -1
  388. package/registry/siri-wave.json +1 -1
  389. package/registry/skeleton-loader.json +1 -1
  390. package/registry/skeleton.json +1 -1
  391. package/registry/skew-x-char.json +1 -1
  392. package/registry/slide-down-char.json +1 -1
  393. package/registry/slide-down-word.json +1 -1
  394. package/registry/slide-left-char.json +1 -1
  395. package/registry/slide-left.json +1 -1
  396. package/registry/slide-right-char.json +1 -1
  397. package/registry/slide-right.json +1 -1
  398. package/registry/slide-up-char.json +1 -1
  399. package/registry/slide-up-text.json +1 -1
  400. package/registry/slide-up-word.json +1 -1
  401. package/registry/sliding-bars.json +1 -1
  402. package/registry/slinky-coil.json +1 -1
  403. package/registry/smart-card-dispenser.json +1 -1
  404. package/registry/smooth-dot-shift.json +1 -1
  405. package/registry/smooth-ring.json +1 -1
  406. package/registry/smooth-rounded-square.json +1 -1
  407. package/registry/solid-switch.json +1 -1
  408. package/registry/spatial-door-portal.json +1 -1
  409. package/registry/spinning-squares.json +1 -1
  410. package/registry/spiral-spinner.json +1 -1
  411. package/registry/split-gate-reveal.json +1 -1
  412. package/registry/spotlight.json +1 -1
  413. package/registry/spring-bars.json +1 -1
  414. package/registry/spring-dot-matrix.json +1 -1
  415. package/registry/spring-hexagon.json +1 -1
  416. package/registry/spring-ring-expand.json +1 -1
  417. package/registry/spring-text-pop.json +1 -1
  418. package/registry/spring-text.json +1 -1
  419. package/registry/square-accordion.json +1 -1
  420. package/registry/square-grid.json +1 -1
  421. package/registry/square-snake.json +1 -1
  422. package/registry/square-spinner.json +1 -1
  423. package/registry/squash-sphere.json +1 -1
  424. package/registry/stacked-bar-pulse.json +1 -1
  425. package/registry/stagger-text.json +1 -1
  426. package/registry/staggered-glass-curtain.json +1 -1
  427. package/registry/sticky-note-peel.json +1 -1
  428. package/registry/sticky-reveal.json +1 -1
  429. package/registry/stretch-x-char.json +1 -1
  430. package/registry/stretch-y-char.json +1 -1
  431. package/registry/stroke-waveform.json +1 -1
  432. package/registry/submenu-flyout.json +1 -1
  433. package/registry/success-check.json +1 -1
  434. package/registry/sudden-brake.json +1 -1
  435. package/registry/swapping-dots.json +1 -1
  436. package/registry/swing-word.json +1 -1
  437. package/registry/swirling-spinner.json +1 -1
  438. package/registry/symmetric-wave.json +1 -1
  439. package/registry/terminal-loader.json +1 -1
  440. package/registry/text-blink.json +1 -1
  441. package/registry/text-dots.json +1 -1
  442. package/registry/text-morph.json +1 -1
  443. package/registry/text-reveal.json +1 -1
  444. package/registry/text-shimmer-wave.json +1 -1
  445. package/registry/text-shimmer.json +1 -1
  446. package/registry/textarea-auto-grow.json +1 -1
  447. package/registry/theme-toggle.json +1 -1
  448. package/registry/tilt-card.json +1 -1
  449. package/registry/timer-preparation-segmented.json +1 -1
  450. package/registry/tracking-in-text.json +1 -1
  451. package/registry/tracking-out-text.json +1 -1
  452. package/registry/trailing-dots.json +1 -1
  453. package/registry/triple-dot-spinner.json +1 -1
  454. package/registry/twin-orbit.json +1 -1
  455. package/registry/typewriter-text.json +1 -1
  456. package/registry/typing-indicator.json +1 -1
  457. package/registry/typing.json +1 -1
  458. package/registry/use-mouse-position.json +1 -1
  459. package/registry/use-reduced-motion.json +1 -1
  460. package/registry/use-scroll-progress.json +1 -1
  461. package/registry/use-stagger.json +1 -1
  462. package/registry/use-web-haptics.json +1 -1
  463. package/registry/users-growth-pill-progress.json +1 -1
  464. package/registry/utils.json +1 -1
  465. package/registry/vertical-wheel-counter.json +1 -1
  466. package/registry/views-hourly-wave-chart.json +1 -1
  467. package/registry/wandering-cube.json +1 -1
  468. package/registry/wandering-eyes.json +1 -1
  469. package/registry/watch-spinner.json +1 -1
  470. package/registry/wave-dots.json +1 -1
  471. package/registry/wave-physics-loader.json +1 -1
  472. package/registry/wave-reveal.json +1 -1
  473. package/registry/waveform-loader.json +1 -1
  474. package/registry/word-reveal.json +1 -1
  475. package/registry/zig-zag-pulse.json +1 -1
  476. package/registry/zoom-in-text.json +1 -1
  477. package/registry/zoom-in.json +1 -1
  478. package/registry/zoom-out-text.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-code-block",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.4",
5
5
  "file": "ai-code-block.tsx",
6
6
  "sourcePath": "ui/ai/ai-code-block.tsx",
7
7
  "registryFile": "ai-code-block.json",
@@ -15,5 +15,5 @@
15
15
  "dependencies": [],
16
16
  "license": "MIT",
17
17
  "previewMode": "client",
18
- "content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party. */\n\nimport { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CODE BLOCK\n * A light editor panel with two versions (switch in the card):\n * · Code — a line-numbered listing\n * · Diff — a unified diff: old/new gutters, a green/red accent\n * bar and row tint, plus word-level add/del highlights.\n * Both share syntax coloring, insets, and wrapping behavior.\n * ───────────────────────────────────────────────────────── */\n\nconst FILE = \"churn.ts\";\n\nconst CODE_LINES = [\n \"export async function churnBatch() {\",\n ' const flavor = await getFlavor(\"pistachio\");',\n \" const base = await dairy.fetch({ flavor });\",\n ' await freezer.store(base, { temp: \"-16C\" });',\n \" if (!base.approved) return null;\",\n \" return base.gallons;\",\n \"}\",\n];\n\n/* A single run of code within a diff row; `change` tints it as an add/del. */\nexport type CodePiece = { text: string; change?: \"add\" | \"del\" };\n/* One row of a unified diff: old/new line numbers, its kind, and its pieces. */\nexport type DiffRow = {\n old: number | null;\n cur: number | null;\n type: \"ctx\" | \"add\" | \"del\";\n pieces: CodePiece[];\n};\n/* Prominent copy strings on the code block. */\nexport type CodeBlockLabels = { copy: string; copied: string };\n\n// Back-compat internal aliases for the local component signatures.\ntype Piece = CodePiece;\ntype Row = DiffRow;\n\nconst DIFF: Row[] = [\n { old: 1, cur: 1, type: \"ctx\", pieces: [{ text: \"export async function churnBatch() {\" }] },\n { old: 2, cur: 2, type: \"ctx\", pieces: [{ text: ' const flavor = await getFlavor(\"pistachio\");' }] },\n { old: 3, cur: 3, type: \"ctx\", pieces: [{ text: \" const base = await dairy.fetch({ flavor });\" }] },\n { old: 4, cur: null, type: \"del\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-14C\"', change: \"del\" }, { text: \" });\" }] },\n { old: null, cur: 4, type: \"add\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-16C\"', change: \"add\" }, { text: \" });\" }] },\n { old: null, cur: 5, type: \"add\", pieces: [{ text: \" if (!base.approved) return null;\" }] },\n { old: 5, cur: 6, type: \"ctx\", pieces: [{ text: \" return base.gallons;\" }] },\n { old: 6, cur: 7, type: \"ctx\", pieces: [{ text: \"}\" }] },\n];\n\nconst HATCH = \"repeating-linear-gradient(45deg, var(--red) 0, var(--red) 1.5px, transparent 1.5px, transparent 3px)\";\n\n/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */\nconst KEYWORDS = new Set([\"import\", \"from\", \"export\", \"default\", \"async\", \"function\", \"const\", \"let\", \"var\", \"await\", \"return\", \"if\", \"else\", \"for\", \"while\", \"new\", \"throw\", \"try\", \"catch\", \"null\", \"true\", \"false\", \"undefined\"]);\nconst TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`[^`]*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\\b|[A-Za-z_$][\\w$]*(?=\\s*\\())/g;\n\nfunction highlight(text: string): ReactNode[] {\n const nodes: ReactNode[] = [];\n let last = 0;\n let k = 0;\n for (const m of text.matchAll(TOKEN)) {\n const idx = m.index ?? 0;\n const t = m[0];\n if (idx > last) nodes.push(<span key={k++}>{text.slice(last, idx)}</span>);\n let color: string;\n let weight: number | undefined;\n if (/^[\"'`]/.test(t) || /^\\d/.test(t)) color = \"var(--orange)\"; // string / number\n else if (KEYWORDS.has(t)) color = \"var(--accent-ink)\"; // keyword / import / conditional\n else { color = \"var(--ink)\"; weight = 500; } // function call\n nodes.push(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);\n last = idx + t.length;\n }\n if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);\n return nodes;\n}\n\nfunction Pieces({ pieces }: { pieces: Piece[] }) {\n return (\n <>\n {pieces.map((p, i) => {\n if (p.change) {\n const add = p.change === \"add\";\n return (\n <span\n key={i}\n className=\"rounded-[3px]\"\n style={{\n background: `color-mix(in srgb, var(--${add ? \"green\" : \"red\"}) 18%, transparent)`,\n padding: \"0 2px\",\n margin: \"0 -1px\",\n boxDecorationBreak: \"clone\",\n WebkitBoxDecorationBreak: \"clone\",\n }}\n >\n {highlight(p.text)}\n </span>\n );\n }\n return <span key={i}>{highlight(p.text)}</span>;\n })}\n </>\n );\n}\n\nfunction FileIcon() {\n return (\n <svg aria-hidden width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0 text-[var(--ink-3)]\">\n <path d=\"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5\" />\n </svg>\n );\n}\n\nconst DEFAULT_LABELS: CodeBlockLabels = { copy: \"Copy\", copied: \"Copied\" };\n\nexport type CodeBlockProps = {\n /** Which view to render — \"Code\" (line-numbered listing) or \"Diff\". */\n variant?: string;\n /** The lines shown in the Code view. */\n lines?: string[];\n /** Raw text placed on the clipboard by Copy. Defaults to `lines` joined. */\n code?: string;\n /** The unified-diff rows shown in the Diff view. */\n diff?: DiffRow[];\n /** Filename shown in the header. */\n filename?: string;\n /** Prominent copy strings. */\n labels?: Partial<CodeBlockLabels>;\n /** Called with the copied text after a successful copy. */\n onCopy?: (text: string) => void;\n};\n\nfunction CodeBlock({\n variant,\n lines = CODE_LINES,\n code,\n diff = DIFF,\n filename = FILE,\n labels,\n onCopy,\n}: CodeBlockProps) {\n const [copied, setCopied] = useState(false);\n const [view, setView] = useState(\"Code\");\n const isDiff = (variant ?? view) === \"Diff\";\n const [copyError, setCopyError] = useState(false);\n const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => () => {if (copyTimer.current) clearTimeout(copyTimer.current);}, []);\n const text = { ...DEFAULT_LABELS, ...labels };\n const raw = code ?? (isDiff ? diff.map((row) => `${row.type === \"add\" ? \"+\" : row.type === \"del\" ? \"-\" : \" \"}${row.pieces.map((piece) => piece.text).join(\"\")}`).join(\"\\n\") : lines.join(\"\\n\"));\n\n const copy = useCallback(() => {\n setCopyError(false);\n if (!navigator.clipboard) {setCopyError(true); return;}\n navigator.clipboard.writeText(raw).then(() => {\n setCopied(true);\n onCopy?.(raw);\n if (copyTimer.current) clearTimeout(copyTimer.current);\n copyTimer.current = setTimeout(() => setCopied(false), 1500);\n }).catch(() => setCopyError(true));\n }, [raw, onCopy]);\n\n const added = diff.filter((r) => r.type === \"add\").length;\n const removed = diff.filter((r) => r.type === \"del\").length;\n\n return (\n <div className=\"w-full max-w-105 overflow-hidden rounded-[10px] bg-[var(--surface)] shadow-[var(--shadow-card)]\">\n {/* header — file · (diff stat | copy) */}\n <div className=\"flex min-h-11 flex-wrap items-center gap-2 border-b border-[var(--line)] px-4 text-[12.5px]\">\n <span className=\"inline-flex min-w-0 items-center gap-[7px]\">\n <FileIcon />\n <span className=\"truncate font-mono leading-none text-[var(--ink)]\">{filename}</span>\n </span>\n\n {variant === undefined && <div role=\"group\" aria-label=\"Code view\" className=\"ml-auto flex shrink-0 items-center rounded-[6px] bg-[var(--field)] p-0.5\">\n {[\"Code\", \"Diff\"].map((item) => <button key={item} type=\"button\" aria-pressed={view === item} onClick={() => {setView(item);setCopied(false);setCopyError(false);}} className={`rounded-[4px] px-1.5 py-1 text-[11px] leading-none ${view === item ? \"bg-[var(--surface)] text-[var(--ink)] shadow-[var(--shadow-btn)]\" : \"text-[var(--ink-3)]\"}`}>{item}</button>)}\n </div>}\n {isDiff && (\n <span className=\"ml-auto inline-flex items-center gap-2 font-mono text-[12px] leading-none tabular-nums\">\n <span className=\"text-[var(--green)]\">+{added}</span>\n <span className=\"text-[var(--red)]\">-{removed}</span>\n </span>\n )}\n <button\n type=\"button\"\n aria-label={isDiff ? \"Copy diff\" : \"Copy code\"}\n aria-live=\"polite\"\n onClick={copy}\n className={`-mr-1 ml-auto flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[12px]\n font-medium transition-colors duration-100 hover:bg-[var(--hover)]\n ${copied ? \"text-[var(--green)]\" : \"text-[var(--ink-3)] hover:text-[var(--ink)]\"}`}\n >\n {copied ? (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n ) : (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2.5\" /><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" /></svg>\n )}\n {copied ? text.copied : text.copy}\n </button>\n </div>\n {copyError && <div role=\"alert\" className=\"px-4 py-1 text-[11px] text-[var(--red)]\">Clipboard unavailable. Select the code to copy.</div>}\n\n {/* body — equal 12px inset on top / left / right; lines wrap */}\n <div className=\"py-3 font-mono text-[12.5px] leading-[1.65] text-[var(--ink-2)]\">\n {isDiff ? (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--line)]\" />\n {diff.map((r, i) => {\n const add = r.type === \"add\";\n const del = r.type === \"del\";\n // one gutter column: removals keep the old number, additions/context show the new one\n const num = del ? r.old : r.cur;\n return (\n <div\n key={i}\n className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start\n ${add ? \"bg-[var(--green-tint)]\" : del ? \"bg-[var(--red-tint)]\" : \"\"}`}\n >\n {(add || del) && (\n <span className=\"absolute inset-y-0 left-0 w-[3px]\" style={{ background: add ? \"var(--green)\" : HATCH }} />\n )}\n <span className={`select-none text-center text-[11px] ${add ? \"text-[var(--green)]\" : del ? \"text-[var(--red)]\" : \"text-[var(--ink-3)]\"}`}>{num ?? \"\"}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">\n <Pieces pieces={r.pieces} />\n </code>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--line)]\" />\n {lines.map((line, i) => (\n <div key={i} className=\"grid grid-cols-[20px_minmax(0,1fr)] items-start\">\n <span className=\"select-none text-center text-[11px] text-[var(--ink-3)]\">{i + 1}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">{highlight(line)}</code>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst scopedStyles = `.mova-ai-code-block{\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--radius-control:8px;\n width:100%;max-width:420px;min-width:0;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:-.01em;color:var(--ink);}\n.mova-ai-code-block[data-theme=\"dark\"]{\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n.mova-ai-code-block .primitive-card-pad{padding:12px} .mova-ai-code-block .primitive-card-bar{padding:10px 12px} .mova-ai-code-block .primitive-card-footer{padding:10px} .mova-ai-code-block .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-code-block :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--accent)!important;outline-offset:2px} .mova-ai-code-block button:not(:disabled){cursor:pointer} .mova-ai-code-block code{overflow-wrap:anywhere} .mova-ai-code-block *, .mova-ai-code-block *::before,.mova-ai-code-block *::after{box-sizing:border-box}\n@keyframes mova-ai-code-block-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-code-block-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-code-block-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-code-block *, .mova-ai-code-block *::before, .mova-ai-code-block *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AICodeBlock({theme = \"light\", ...props}: CodeBlockProps & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-code-block\" data-theme={theme}><style>{scopedStyles}</style><CodeBlock {...props} /></div>;\n}\n"
18
+ "content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party. */\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * CODE BLOCK\n * A light editor panel with two versions (switch in the card):\n * · Code — a line-numbered listing\n * · Diff — a unified diff: old/new gutters, a green/red accent\n * bar and row tint, plus word-level add/del highlights.\n * Both share syntax coloring, insets, and wrapping behavior.\n * ───────────────────────────────────────────────────────── */\n\nconst FILE = \"churn.ts\";\n\nconst CODE_LINES = [\n \"export async function churnBatch() {\",\n ' const flavor = await getFlavor(\"pistachio\");',\n \" const base = await dairy.fetch({ flavor });\",\n ' await freezer.store(base, { temp: \"-16C\" });',\n \" if (!base.approved) return null;\",\n \" return base.gallons;\",\n \"}\",\n];\n\n/* A single run of code within a diff row; `change` tints it as an add/del. */\nexport type CodePiece = { text: string; change?: \"add\" | \"del\" };\n/* One row of a unified diff: old/new line numbers, its kind, and its pieces. */\nexport type DiffRow = {\n old: number | null;\n cur: number | null;\n type: \"ctx\" | \"add\" | \"del\";\n pieces: CodePiece[];\n};\n/* Prominent copy strings on the code block. */\nexport type CodeBlockLabels = { copy: string; copied: string };\n\n// Back-compat internal aliases for the local component signatures.\ntype Piece = CodePiece;\ntype Row = DiffRow;\n\nconst DIFF: Row[] = [\n { old: 1, cur: 1, type: \"ctx\", pieces: [{ text: \"export async function churnBatch() {\" }] },\n { old: 2, cur: 2, type: \"ctx\", pieces: [{ text: ' const flavor = await getFlavor(\"pistachio\");' }] },\n { old: 3, cur: 3, type: \"ctx\", pieces: [{ text: \" const base = await dairy.fetch({ flavor });\" }] },\n { old: 4, cur: null, type: \"del\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-14C\"', change: \"del\" }, { text: \" });\" }] },\n { old: null, cur: 4, type: \"add\", pieces: [{ text: \" await freezer.store(base, { temp: \" }, { text: '\"-16C\"', change: \"add\" }, { text: \" });\" }] },\n { old: null, cur: 5, type: \"add\", pieces: [{ text: \" if (!base.approved) return null;\" }] },\n { old: 5, cur: 6, type: \"ctx\", pieces: [{ text: \" return base.gallons;\" }] },\n { old: 6, cur: 7, type: \"ctx\", pieces: [{ text: \"}\" }] },\n];\n\nconst HATCH = \"repeating-linear-gradient(45deg, var(--mova-ai-danger,#a33c3c) 0, var(--mova-ai-danger,#a33c3c) 1.5px, transparent 1.5px, transparent 3px)\";\n\n/* light syntax coloring — keywords/imports/conditionals, functions, strings & numbers */\nconst KEYWORDS = new Set([\"import\", \"from\", \"export\", \"default\", \"async\", \"function\", \"const\", \"let\", \"var\", \"await\", \"return\", \"if\", \"else\", \"for\", \"while\", \"new\", \"throw\", \"try\", \"catch\", \"null\", \"true\", \"false\", \"undefined\"]);\nconst TOKEN = /(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`[^`]*`|\\b\\d+(?:\\.\\d+)?\\b|\\b(?:import|from|export|default|async|function|const|let|var|await|return|if|else|for|while|new|throw|try|catch|null|true|false|undefined)\\b|[A-Za-z_$][\\w$]*(?=\\s*\\())/g;\n\nfunction highlight(text: string): ReactNode[] {\n const nodes: ReactNode[] = [];\n let last = 0;\n let k = 0;\n for (const m of text.matchAll(TOKEN)) {\n const idx = m.index ?? 0;\n const t = m[0];\n if (idx > last) nodes.push(<span key={k++}>{text.slice(last, idx)}</span>);\n let color: string;\n let weight: number | undefined;\n if (/^[\"'`]/.test(t) || /^\\d/.test(t)) color = \"var(--mova-ai-warning,#986517)\"; // string / number\n else if (KEYWORDS.has(t)) color = \"var(--mova-ai-accent-ink,#333)\"; // keyword / import / conditional\n else { color = \"var(--mova-ai-ink,#202020)\"; weight = 500; } // function call\n nodes.push(<span key={k++} style={{ color, fontWeight: weight }}>{t}</span>);\n last = idx + t.length;\n }\n if (last < text.length) nodes.push(<span key={k++}>{text.slice(last)}</span>);\n return nodes;\n}\n\nfunction Pieces({ pieces }: { pieces: Piece[] }) {\n return (\n <>\n {pieces.map((p, i) => {\n if (p.change) {\n const add = p.change === \"add\";\n return (\n <span\n key={i}\n className=\"rounded-[3px]\"\n style={{\n background: `color-mix(in srgb, var(--${add ? \"green\" : \"red\"}) 18%, transparent)`,\n padding: \"0 2px\",\n margin: \"0 -1px\",\n boxDecorationBreak: \"clone\",\n WebkitBoxDecorationBreak: \"clone\",\n }}\n >\n {highlight(p.text)}\n </span>\n );\n }\n return <span key={i}>{highlight(p.text)}</span>;\n })}\n </>\n );\n}\n\nfunction CodeContent({ isDiff, lines, diff }: { isDiff: boolean; lines: string[]; diff: Row[] }) {\n return isDiff ? (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--mova-ai-border,#dedede)]\" />\n {diff.map((r, i) => {\n const add = r.type === \"add\";\n const del = r.type === \"del\";\n // one gutter column: removals keep the old number, additions/context show the new one\n const num = del ? r.old : r.cur;\n return (\n <div\n key={i}\n className={`relative grid grid-cols-[20px_minmax(0,1fr)] items-start\n ${add ? \"bg-[var(--mova-ai-success-tint,#e8f3ec)]\" : del ? \"bg-[var(--mova-ai-danger-tint,#f7eaea)]\" : \"\"}`}\n >\n {(add || del) && (\n <span className=\"absolute inset-y-0 left-0 w-[3px]\" style={{ background: add ? \"var(--mova-ai-success,#28734d)\" : HATCH }} />\n )}\n <span className={`select-none text-center text-[11px] ${add ? \"text-[var(--mova-ai-success,#28734d)]\" : del ? \"text-[var(--mova-ai-danger,#a33c3c)]\" : \"text-[var(--mova-ai-ink-muted,#858585)]\"}`}>{num ?? \"\"}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">\n <Pieces pieces={r.pieces} />\n </code>\n </div>\n );\n })}\n </div>\n ) : (\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute inset-y-0 left-5 w-px bg-[var(--mova-ai-border,#dedede)]\" />\n {lines.map((line, i) => (\n <div key={i} className=\"grid grid-cols-[20px_minmax(0,1fr)] items-start\">\n <span className=\"select-none text-center text-[11px] text-[var(--mova-ai-ink-muted,#858585)]\">{i + 1}</span>\n <code className=\"pr-3 pl-1 break-words whitespace-pre-wrap\">{highlight(line)}</code>\n </div>\n ))}\n </div>\n );\n}\n\nfunction FileIcon() {\n return (\n <svg aria-hidden width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0 text-[var(--mova-ai-ink-muted,#858585)]\">\n <path d=\"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5\" />\n </svg>\n );\n}\n\nconst DEFAULT_LABELS: CodeBlockLabels = { copy: \"Copy\", copied: \"Copied\" };\n\nexport type CodeBlockProps = {\n /** Which view to render — \"Code\" (line-numbered listing) or \"Diff\". */\n variant?: string;\n /** The lines shown in the Code view. */\n lines?: string[];\n /** Raw text placed on the clipboard by Copy. Defaults to `lines` joined. */\n code?: string;\n /** The unified-diff rows shown in the Diff view. */\n diff?: DiffRow[];\n /** Filename shown in the header. */\n filename?: string;\n /** Prominent copy strings. */\n labels?: Partial<CodeBlockLabels>;\n /** Called with the copied text after a successful copy. */\n onCopy?: (text: string) => void;\n};\n\nfunction CodeBlock({\n variant,\n lines = CODE_LINES,\n code,\n diff = DIFF,\n filename = FILE,\n labels,\n onCopy,\n}: CodeBlockProps) {\n const [copied, setCopied] = useState(false);\n const [view, setView] = useState(\"Code\");\n const [hasSwitchedView, setHasSwitchedView] = useState(false);\n const isDiff = (variant ?? view) === \"Diff\";\n const [copyError, setCopyError] = useState(false);\n const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => () => {if (copyTimer.current) clearTimeout(copyTimer.current);}, []);\n const contentRef = useRef<HTMLDivElement>(null);\n const codeMeasureRef = useRef<HTMLDivElement>(null);\n const diffMeasureRef = useRef<HTMLDivElement>(null);\n const [reservedHeight, setReservedHeight] = useState(0);\n useLayoutEffect(() => {\n const content = contentRef.current;\n if (!content) return;\n let measuredWidth = -1;\n const measure = (force = false) => {\n const width = content.clientWidth;\n if (!force && width === measuredWidth) return;\n measuredWidth = width;\n const codeHeight = codeMeasureRef.current?.scrollHeight ?? 0;\n const diffHeight = diffMeasureRef.current?.scrollHeight ?? 0;\n setReservedHeight(Math.max(codeHeight, diffHeight));\n };\n measure();\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(() => measure(true));\n const handleResize = () => measure(true);\n observer?.observe(content);\n if (codeMeasureRef.current) observer?.observe(codeMeasureRef.current);\n if (diffMeasureRef.current) observer?.observe(diffMeasureRef.current);\n window.addEventListener(\"resize\", handleResize);\n return () => {\n observer?.disconnect();\n window.removeEventListener(\"resize\", handleResize);\n };\n }, [lines, diff]);\n const text = { ...DEFAULT_LABELS, ...labels };\n const raw = code ?? (isDiff ? diff.map((row) => `${row.type === \"add\" ? \"+\" : row.type === \"del\" ? \"-\" : \" \"}${row.pieces.map((piece) => piece.text).join(\"\")}`).join(\"\\n\") : lines.join(\"\\n\"));\n\n const copy = useCallback(() => {\n setCopyError(false);\n if (!navigator.clipboard) {setCopyError(true); return;}\n navigator.clipboard.writeText(raw).then(() => {\n setCopied(true);\n onCopy?.(raw);\n if (copyTimer.current) clearTimeout(copyTimer.current);\n copyTimer.current = setTimeout(() => setCopied(false), 1500);\n }).catch(() => setCopyError(true));\n }, [raw, onCopy]);\n\n const added = diff.filter((r) => r.type === \"add\").length;\n const removed = diff.filter((r) => r.type === \"del\").length;\n\n return (\n <div className=\"w-full max-w-105 overflow-hidden rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] text-[color:var(--mova-ai-ink,#202020)] shadow-[var(--mova-ai-shadow-card,none)]\">\n {/* header — file · (diff stat | copy) */}\n <div className=\"mova-ai-code-header grid min-h-11 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 border-b border-[var(--mova-ai-border,#dedede)] px-4 text-[12.5px]\">\n <span className=\"mova-ai-code-filename inline-flex min-w-0 items-center gap-[7px]\">\n <FileIcon />\n <span className=\"min-w-0 truncate font-mono leading-none text-[var(--mova-ai-ink,#202020)]\">{filename}</span>\n </span>\n\n {variant === undefined && <div role=\"group\" aria-label=\"Code view\" className=\"mova-ai-code-view-controls relative inline-grid shrink-0 grid-cols-2 items-center justify-self-center rounded-[6px] bg-[var(--mova-ai-field,#f6f6f6)] p-0.5\">\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-0.5 top-0.5 bottom-0.5 rounded-[4px] bg-[var(--mova-ai-surface,#fff)] shadow-[var(--mova-ai-shadow-btn,none)] transition-transform duration-200 ease-out motion-reduce:transition-none\"\n style={{ width: \"calc((100% - 4px) / 2)\", transform: view === \"Diff\" ? \"translateX(100%)\" : \"translateX(0)\" }}\n />\n {[\"Code\", \"Diff\"].map((item) => <button key={item} type=\"button\" aria-pressed={view === item} onClick={() => {setView(item);setHasSwitchedView(true);setCopied(false);setCopyError(false);}} className={`relative z-10 min-w-[34px] rounded-[4px] px-1.5 py-1 text-[11px] leading-none transition-colors duration-150 ${view === item ? \"text-[var(--mova-ai-ink,#202020)]\" : \"text-[var(--mova-ai-ink-muted,#858585)]\"}`}>{item}</button>)}\n </div>}\n <div className=\"mova-ai-code-actions col-start-3 flex min-w-0 flex-wrap items-center justify-end gap-2\">\n <span\n aria-hidden={!isDiff}\n className={`inline-flex shrink-0 items-center gap-2 font-mono text-[12px] leading-none tabular-nums ${isDiff ? \"visible\" : \"invisible\"}`}\n >\n <span className=\"text-[var(--mova-ai-success,#28734d)]\">+{added}</span>\n <span className=\"text-[var(--mova-ai-danger,#a33c3c)]\">-{removed}</span>\n </span>\n <button\n type=\"button\"\n aria-label={isDiff ? \"Copy diff\" : \"Copy code\"}\n aria-live=\"polite\"\n onClick={copy}\n className={`mova-ai-code-copy-button flex min-h-6 min-w-0 max-w-full items-center gap-1 rounded-[6px] px-1.5 text-[12px]\n font-medium transition-colors duration-100 hover:bg-[var(--mova-ai-hover,#f1f1f1)]\n ${copied ? \"text-[var(--mova-ai-success,#28734d)]\" : \"text-[var(--mova-ai-ink-muted,#858585)] hover:text-[var(--mova-ai-ink,#202020)]\"}`}\n >\n {copied ? (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n ) : (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2.5\" /><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" /></svg>\n )}\n <span className=\"min-w-0 break-words\">{copied ? text.copied : text.copy}</span>\n </button>\n </div>\n </div>\n {copyError && <div role=\"alert\" className=\"px-4 py-1 text-[11px] text-[var(--mova-ai-danger,#a33c3c)]\">Clipboard unavailable. Select the code to copy.</div>}\n\n {/* body — equal 12px inset on top / left / right; lines wrap */}\n <div className=\"py-3 font-mono text-[12.5px] leading-[1.65] text-[var(--mova-ai-ink-secondary,#666)]\">\n <div ref={contentRef} className=\"relative\" style={{ minHeight: reservedHeight || undefined }}>\n {/* Passive layout copies account for wrapping; the only visible content layer is the active view below. */}\n <div aria-hidden=\"true\" className=\"pointer-events-none invisible absolute inset-0 select-none\">\n <div ref={codeMeasureRef} className=\"absolute inset-x-0 top-0\">\n <CodeContent isDiff={false} lines={lines} diff={diff} />\n </div>\n <div ref={diffMeasureRef} className=\"absolute inset-x-0 top-0\">\n <CodeContent isDiff={true} lines={lines} diff={diff} />\n </div>\n </div>\n <div key={isDiff ? \"diff\" : \"code\"} className={hasSwitchedView ? \"mova-ai-code-view-enter\" : undefined}>\n <CodeContent isDiff={isDiff} lines={lines} diff={diff} />\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst scopedStyles = `.mova-ai-code-block{container-type:inline-size} .mova-ai-code-block .primitive-card-pad{padding:12px} .mova-ai-code-block .primitive-card-bar{padding:10px 12px} .mova-ai-code-block .primitive-card-footer{padding:10px} .mova-ai-code-block .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-code-block :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--mova-ai-accent,#454545)!important;outline-offset:2px} .mova-ai-code-block button:not(:disabled){cursor:pointer} .mova-ai-code-block code{overflow-wrap:anywhere} .mova-ai-code-block *, .mova-ai-code-block *::before,.mova-ai-code-block *::after{box-sizing:border-box}\n.mova-ai-code-block .mova-ai-code-view-enter{animation:mova-ai-code-block-view-enter 180ms cubic-bezier(.2,.7,.2,1) both}\n@keyframes mova-ai-code-block-view-enter{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-code-block-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-code-block-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-code-block-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@container (max-width:360px){.mova-ai-code-block .mova-ai-code-header{grid-template-columns:minmax(0,1fr) minmax(0,1fr);row-gap:8px;padding-top:8px;padding-bottom:8px}.mova-ai-code-block .mova-ai-code-filename{grid-column:1;grid-row:1}.mova-ai-code-block .mova-ai-code-actions{grid-column:2;grid-row:1;max-width:100%}.mova-ai-code-block .mova-ai-code-view-controls{grid-column:1/-1;grid-row:2}}\n@media(prefers-reduced-motion:reduce){.mova-ai-code-block *, .mova-ai-code-block *::before, .mova-ai-code-block *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AICodeBlock({theme = \"light\", ...props}: CodeBlockProps & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-theme mova-ai-code-block w-full min-w-0 max-w-105\" data-theme={theme}><style>{AI_THEME_STYLES}{scopedStyles}</style><CodeBlock {...props} /></div>;\n}\n"
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-context-meter",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.4",
5
5
  "file": "ai-context-meter.tsx",
6
6
  "sourcePath": "ui/ai/ai-context-meter.tsx",
7
7
  "registryFile": "ai-context-meter.json",
@@ -15,5 +15,5 @@
15
15
  ],
16
16
  "license": "MIT",
17
17
  "previewMode": "client",
18
- "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-context-meter (MIT). See ../patterns/smoothui.LICENSE.\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst VIEWBOX = 32;\nconst CENTER = VIEWBOX / 2;\nconst RADIUS = 13;\nconst STROKE_WIDTH = 3;\nconst DEFAULT_WARNING_AT = 0.8;\nconst DEFAULT_DANGER_AT = 0.95;\nconst WARNING_COLOR = \"oklch(78% 0.16 75)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AIContextBreakdownItem = { label: string; tokens: number };\n\nexport type AIContextMeterProps = {\n breakdown?: AIContextBreakdownItem[];\n className?: string;\n dangerAt?: number;\n limit: number;\n used: number;\n warningAt?: number;\n};\n\nconst formatTokens = (tokens: number): string => {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;\n if (tokens >= 10_000) return `${Math.round(tokens / 1000)}k`;\n if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;\n return String(tokens);\n};\n\nexport function AIContextMeter({\n breakdown,\n className = \"\",\n dangerAt = DEFAULT_DANGER_AT,\n limit,\n used,\n warningAt = DEFAULT_WARNING_AT,\n}: AIContextMeterProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(false);\n const fraction = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n const percent = Math.round(fraction * 100);\n const color = fraction >= dangerAt ? DANGER_COLOR : fraction >= warningAt ? WARNING_COLOR : \"currentColor\";\n const hasBreakdown = Boolean(breakdown?.length);\n\n return (\n <div className={`relative inline-block ${className}`}>\n <button\n aria-expanded={hasBreakdown ? isOpen : undefined}\n aria-label={`Context window ${percent}% used, ${formatTokens(used)} of ${formatTokens(limit)} tokens`}\n className=\"flex cursor-pointer items-center gap-1.5 rounded-lg px-1 py-0.5 text-muted-foreground text-xs transition-colors hover:text-foreground\"\n disabled={!hasBreakdown}\n onBlur={() => setIsOpen(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => hasBreakdown && setIsOpen(true)}\n onMouseEnter={() => hasBreakdown && setIsOpen(true)}\n onMouseLeave={() => setIsOpen(false)}\n type=\"button\"\n >\n <svg aria-hidden=\"true\" className=\"h-4 w-4 -rotate-90\" viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}>\n <circle cx={CENTER} cy={CENTER} fill=\"none\" r={RADIUS} stroke=\"currentColor\" strokeOpacity={0.2} strokeWidth={STROKE_WIDTH} />\n <motion.circle\n animate={{ stroke: color, strokeDasharray: `${fraction} ${1 - fraction}` }}\n cx={CENTER}\n cy={CENTER}\n fill=\"none\"\n pathLength={1}\n r={RADIUS}\n strokeLinecap=\"round\"\n strokeWidth={STROKE_WIDTH}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n />\n </svg>\n <span className=\"tabular-nums\">{formatTokens(used)}/{formatTokens(limit)}</span>\n </button>\n\n <AnimatePresence>\n {isOpen && hasBreakdown ? (\n <motion.div\n animate={{ opacity: 1, scale: 1, y: 0 }}\n className=\"absolute bottom-full left-0 z-50 mb-1.5 w-56 rounded-xl border border-border bg-background p-2.5 shadow-lg\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: 0.96, y: 4 }}\n initial={shouldReduceMotion ? { opacity: 1, scale: 1, y: 0 } : { opacity: 0, scale: 0.96, y: 4 }}\n style={{ transformOrigin: \"bottom left\" }}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: EASE_OUT }}\n >\n <ul className=\"list-none space-y-1\">\n {breakdown?.map((item) => (\n <li className=\"flex items-baseline justify-between gap-3 text-xs\" key={item.label}>\n <span className=\"truncate text-muted-foreground\">{item.label}</span>\n <span className=\"shrink-0 text-foreground tabular-nums\">{formatTokens(item.tokens)}</span>\n </li>\n ))}\n </ul>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AIContextMeter;\n"
18
+ "content": "\"use client\";\n\"use client\";\n\n// Adapted from SmoothUI's ai-context-meter (MIT). See ../patterns/smoothui.LICENSE.\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst VIEWBOX = 32;\nconst CENTER = VIEWBOX / 2;\nconst RADIUS = 13;\nconst STROKE_WIDTH = 3;\nconst DEFAULT_WARNING_AT = 0.8;\nconst DEFAULT_DANGER_AT = 0.95;\nconst WARNING_COLOR = \"var(--mova-ai-warning,#986517)\";\nconst DANGER_COLOR = \"var(--mova-ai-danger,#a33c3c)\";\n\nexport type AIContextBreakdownItem = { label: string; tokens: number };\n\nexport type AIContextMeterProps = {\n breakdown?: AIContextBreakdownItem[];\n className?: string;\n dangerAt?: number;\n limit: number;\n theme?: \"light\" | \"dark\";\n used: number;\n warningAt?: number;\n};\n\nconst formatTokens = (tokens: number): string => {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;\n if (tokens >= 10_000) return `${Math.round(tokens / 1000)}k`;\n if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;\n return String(tokens);\n};\n\nexport function AIContextMeter({\n breakdown,\n className = \"\",\n dangerAt = DEFAULT_DANGER_AT,\n limit,\n theme = \"light\",\n used,\n warningAt = DEFAULT_WARNING_AT,\n}: AIContextMeterProps) {\n const shouldReduceMotion = useReducedMotion();\n const [isOpen, setIsOpen] = useState(false);\n const fraction = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n const percent = Math.round(fraction * 100);\n const color = fraction >= dangerAt ? DANGER_COLOR : fraction >= warningAt ? WARNING_COLOR : \"currentColor\";\n const hasBreakdown = Boolean(breakdown?.length);\n\n return (\n <div className={`mova-ai-theme relative inline-block ${className}`} data-theme={theme}>\n <style>{AI_THEME_STYLES}</style>\n <button\n aria-expanded={hasBreakdown ? isOpen : undefined}\n aria-label={`Context window ${percent}% used, ${formatTokens(used)} of ${formatTokens(limit)} tokens`}\n className=\"flex cursor-pointer items-center gap-1.5 rounded-lg px-1 py-0.5 text-[color:var(--mova-ai-ink-secondary,#666)] text-xs transition-colors hover:text-[color:var(--mova-ai-ink,#202020)]\"\n disabled={!hasBreakdown}\n onBlur={() => setIsOpen(false)}\n onClick={() => setIsOpen((current) => !current)}\n onFocus={() => hasBreakdown && setIsOpen(true)}\n onMouseEnter={() => hasBreakdown && setIsOpen(true)}\n onMouseLeave={() => setIsOpen(false)}\n type=\"button\"\n >\n <svg aria-hidden=\"true\" className=\"h-4 w-4 -rotate-90\" viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}>\n <circle cx={CENTER} cy={CENTER} fill=\"none\" r={RADIUS} stroke=\"currentColor\" strokeOpacity={0.2} strokeWidth={STROKE_WIDTH} />\n <motion.circle\n animate={{ stroke: color, strokeDasharray: `${fraction} ${1 - fraction}` }}\n cx={CENTER}\n cy={CENTER}\n fill=\"none\"\n pathLength={1}\n r={RADIUS}\n strokeLinecap=\"round\"\n strokeWidth={STROKE_WIDTH}\n transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n />\n </svg>\n <span className=\"tabular-nums\">{formatTokens(used)}/{formatTokens(limit)}</span>\n </button>\n\n <AnimatePresence>\n {isOpen && hasBreakdown ? (\n <motion.div\n animate={{ opacity: 1, scale: 1, y: 0 }}\n className=\"absolute bottom-full left-1/2 z-50 mb-1.5 w-56 -translate-x-1/2 rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] p-2.5 text-[color:var(--mova-ai-ink,#202020)] shadow-lg\"\n exit={shouldReduceMotion ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: 0.96, y: 4 }}\n initial={shouldReduceMotion ? { opacity: 1, scale: 1, y: 0 } : { opacity: 0, scale: 0.96, y: 4 }}\n style={{ transformOrigin: \"bottom center\" }}\n transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.18, ease: EASE_OUT }}\n >\n <ul className=\"list-none space-y-1\">\n {breakdown?.map((item) => (\n <li className=\"flex items-baseline justify-between gap-3 text-xs\" key={item.label}>\n <span className=\"truncate text-[color:var(--mova-ai-ink-secondary,#666)]\">{item.label}</span>\n <span className=\"shrink-0 text-[color:var(--mova-ai-ink,#202020)] tabular-nums\">{formatTokens(item.tokens)}</span>\n </li>\n ))}\n </ul>\n </motion.div>\n ) : null}\n </AnimatePresence>\n </div>\n );\n}\n\nexport default AIContextMeter;\n"
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-fine-tune-card",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.4",
5
5
  "file": "ai-fine-tune-card.tsx",
6
6
  "sourcePath": "ui/ai/ai-fine-tune-card.tsx",
7
7
  "registryFile": "ai-fine-tune-card.json",
@@ -15,5 +15,5 @@
15
15
  "dependencies": [],
16
16
  "license": "MIT",
17
17
  "previewMode": "client",
18
- "content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party.\n * Original card geometry, interactions, and motion are preserved. */\nimport * as React from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * FINE-TUNE CARD — compact interactive inspector.\n * Number fields scrub: hover the label for an ↔ cursor and\n * drag to adjust, use ↑/↓ (⇧ for ×10), or type directly.\n * ───────────────────────────────────────────────────────── */\n\nfunction ScrubField({\n label,\n value,\n onChange,\n min,\n max,\n step = 1,\n suffix = \"\",\n active,\n}: {\n label: string;\n value: number;\n onChange: (v: number) => void;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n active?: boolean;\n}) {\n const drag = useRef<{ x: number; v: number } | null>(null);\n const [draft, setDraft] = useState(String(value));\n useEffect(() => setDraft(String(value)), [value]);\n const clamp = (v: number) => Math.min(max, Math.max(min, Number((Math.round(v / step) * step).toFixed(8))));\n\n const commitDraft = () => {\n const parsed = draft.trim() === \"\" ? NaN : Number(draft);\n if (!Number.isFinite(parsed)) { setDraft(String(value)); return; }\n const next = clamp(parsed);\n setDraft(String(next));\n if (next !== value) onChange(next);\n };\n\n return (\n <label\n className=\"flex h-6.5 min-w-0 items-center gap-1 rounded-[6px] py-1 pr-1 pl-0.5\n transition-[background-color,box-shadow] duration-200\"\n style={{\n background: active ? \"var(--accent-tint)\" : \"var(--field)\",\n boxShadow: active ? \"0 0 0 1px var(--accent)\" : \"none\",\n }}\n >\n {/* scrub handle */}\n <span\n role=\"slider\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n tabIndex={0}\n onPointerDown={(e) => {\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n drag.current = { x: e.clientX, v: value };\n }}\n onPointerMove={(e) => {\n if (!drag.current) return;\n onChange(clamp(drag.current.v + ((e.clientX - drag.current.x) / 2) * step));\n }}\n onPointerUp={() => (drag.current = null)}\n onPointerCancel={() => (drag.current = null)}\n onLostPointerCapture={() => (drag.current = null)}\n onKeyDown={(e) => {\n const mult = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n onChange(clamp(value + step * mult));\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n onChange(clamp(value - step * mult));\n }\n }}\n className=\"flex h-full shrink-0 cursor-ew-resize touch-none items-center rounded-[4px]\n px-0.5 text-[12px] text-[var(--ink-3)] select-none hover:text-[var(--ink-2)] focus-visible:text-[var(--accent-ink)]\n focus-visible:outline-none\"\n >\n {label}\n </span>\n <input\n inputMode=\"decimal\"\n value={draft}\n onChange={(event) => {\n // Keep incomplete numbers editable; validation happens on commit.\n if (/^-?\\d*\\.?\\d*$/.test(event.target.value)) setDraft(event.target.value);\n }}\n onBlur={commitDraft}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") { event.preventDefault(); commitDraft(); }\n if (event.key === \"Escape\") { event.preventDefault(); event.stopPropagation(); setDraft(String(value)); }\n }}\n aria-label={`${label} value`}\n className=\"min-w-0 flex-1 bg-transparent text-[12px] text-[var(--ink)] tabular-nums outline-none\"\n />\n {suffix && <span className=\"shrink-0 pr-0.5 text-[11.5px] text-[var(--ink-3)]\">{suffix}</span>}\n </label>\n );\n}\n\nconst SEGMENTS = [\"row\", \"col\", \"grid\"] as const;\n\nfunction SegmentIcon({ kind }: { kind: string }) {\n const dot = \"size-1.5 rounded-[2px] border-[1.2px] border-current\";\n if (kind === \"row\")\n return <span className=\"flex gap-0.5\">{[0, 1, 2].map((i) => <span key={i} className={dot} />)}</span>;\n if (kind === \"col\")\n return <span className=\"flex flex-col gap-0.5\">{[0, 1].map((i) => <span key={i} className={dot} />)}</span>;\n return (\n <span className=\"grid grid-cols-2 gap-0.5\">\n {[0, 1, 2, 3].map((i) => <span key={i} className={dot} />)}\n </span>\n );\n}\n\n/* A single scrub-able number property. `value` is the initial/default value. */\nexport type FineTuneField = {\n key: string;\n label: string;\n value: number;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n};\n/* Prominent copy strings on the card. */\nexport type FineTuneCardLabels = {\n title: string;\n layout: string;\n type: string;\n placeholder: string;\n adjust: string;\n edited: string;\n};\n/* The editable state emitted by `onChange`. */\nexport type FineTuneState = {\n segment: number;\n values: Record<string, number>;\n type: string;\n};\n\nconst FIELDS: FineTuneField[] = [\n { key: \"width\", label: \"W\", value: 324, min: 40, max: 999 },\n { key: \"height\", label: \"H\", value: 96, min: 24, max: 999 },\n { key: \"radius\", label: \"Radius\", value: 28, min: 0, max: 64 },\n { key: \"opacity\", label: \"Opacity\", value: 100, min: 0, max: 100, suffix: \"%\" },\n];\n\nconst OPTIONS = [\"Seasonal\", \"Classic\", \"Limited\"];\n\nconst DEFAULT_LABELS: FineTuneCardLabels = {\n title: \"Flavor card\",\n layout: \"Layout\",\n type: \"Type\",\n placeholder: \"Select type\",\n adjust: \"Adjust\",\n edited: \"Edited\",\n};\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const rows: T[][] = [];\n for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size));\n return rows;\n}\n\nexport type FineTuneCardProps = {\n /** Accepted for gallery/registry parity; not used by this card. */\n variant?: string;\n /** The scrub-able properties shown in the layout grid (rendered in pairs). */\n fields?: FineTuneField[];\n /** Options offered in the Type menu. */\n options?: string[];\n /** Prominent copy strings. */\n labels?: Partial<FineTuneCardLabels>;\n /** Called with the full editable state whenever the user edits it. */\n onChange?: (state: FineTuneState) => void;\n};\n\nfunction FineTuneCard({\n fields = FIELDS,\n options = OPTIONS,\n labels,\n onChange,\n}: FineTuneCardProps) {\n const text = { ...DEFAULT_LABELS, ...labels };\n const [seg, setSeg] = useState(0);\n const [values, setValues] = useState<Record<string, number>>(() =>\n Object.fromEntries(fields.map((f) => [f.key, f.value])),\n );\n const [menuOpen, setMenuOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n useEffect(() => {\n if (!menuOpen) return;\n const dismiss = (event: PointerEvent) => { if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false); };\n document.addEventListener(\"pointerdown\", dismiss);\n return () => document.removeEventListener(\"pointerdown\", dismiss);\n }, [menuOpen]);\n const [typeValue, setTypeValue] = useState(text.placeholder);\n\n const selectSeg = (i: number) => {\n setSeg(i);\n onChange?.({ segment: i, values, type: typeValue });\n };\n const setValue = (key: string, v: number) => {\n const next = { ...values, [key]: v };\n setValues(next);\n onChange?.({ segment: seg, values: next, type: typeValue });\n };\n const selectType = (value: string) => {\n setTypeValue(value);\n setMenuOpen(false);\n triggerRef.current?.focus();\n onChange?.({ segment: seg, values, type: value });\n };\n\n const changed = fields.some((f) => values[f.key] !== f.value);\n const done = seg !== 0 || changed || typeValue !== text.placeholder;\n\n return (\n <div className=\"relative w-full max-w-60 rounded-[10px] bg-[var(--surface)] shadow-[var(--shadow-raised)]\">\n {/* header */}\n <div className=\"primitive-card-bar flex items-center justify-between border-b border-[var(--line)]\">\n <span className=\"text-[13px] font-medium text-[var(--ink)]\">{text.title}</span>\n {done ? (\n <span\n className=\"flex items-center gap-1.5 text-[12px] font-medium text-[var(--green)]\"\n style={{ animation: \"mova-ai-fine-tune-card-pop-in 250ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M20 6L9 17l-5-5\" />\n </svg>\n {text.edited}\n </span>\n ) : (\n <span className=\"flex items-center gap-1.5\">\n <span className=\"flex size-4.5 items-center justify-center rounded-[5px] border border-[var(--accent)]/30 bg-[var(--accent-tint)]\">\n <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"var(--accent)\">\n <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n </svg>\n </span>\n <span\n className=\"bg-clip-text text-[12px] font-medium text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(90deg, var(--accent) 35%, var(--accent-ink) 50%, var(--accent) 65%)\",\n backgroundSize: \"200% 100%\",\n animation: \"mova-ai-fine-tune-card-shimmer-text 1.4s linear infinite\",\n }}\n >\n {text.adjust}\n </span>\n </span>\n )}\n </div>\n\n {/* layout section */}\n <div className=\"primitive-card-pad flex flex-col gap-2 border-b border-[var(--line)]\">\n <p className=\"text-[12.5px] font-medium text-[var(--ink)]\">{text.layout}</p>\n {/* segmented control: gray track, raised white thumb */}\n <div className=\"relative grid grid-cols-3 rounded-[8px] bg-[var(--field)] p-0.5\">\n <span\n aria-hidden\n className=\"absolute inset-y-0.5 rounded-[6px] bg-[var(--surface)] shadow-[var(--shadow-btn)] transition-transform duration-300\"\n style={{\n width: \"calc((100% - 4px) / 3)\",\n left: 2,\n transform: `translateX(${seg * 100}%)`,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n />\n {SEGMENTS.map((s, i) => (\n <button\n key={s}\n type=\"button\"\n aria-label={`${s} layout`}\n aria-pressed={i === seg}\n onClick={() => selectSeg(i)}\n className={`relative z-10 flex h-6 items-center justify-center transition-colors duration-200\n ${i === seg ? \"text-[var(--accent)]\" : \"text-[var(--ink-3)]\"}`}\n >\n <SegmentIcon kind={s} />\n </button>\n ))}\n </div>\n {chunk(fields, 2).map((pair, ri) => (\n <div key={ri} className=\"grid min-w-0 grid-cols-2 gap-2\">\n {pair.map((f) => (\n <ScrubField\n key={f.key}\n label={f.label}\n value={values[f.key]}\n onChange={(v) => setValue(f.key, v)}\n min={f.min}\n max={f.max}\n step={f.step}\n suffix={f.suffix}\n active={values[f.key] !== f.value}\n />\n ))}\n </div>\n ))}\n </div>\n\n {/* interaction section */}\n <div className=\"primitive-card-footer flex items-center justify-between\">\n <span className=\"text-[12px] text-[var(--ink-3)]\">{text.type}</span>\n <div ref={menuRef} className=\"relative -mr-0.5 w-30\" onBlur={(event) => {if (!event.currentTarget.contains(event.relatedTarget)) setMenuOpen(false);}} onKeyDown={(event) => {\n if (event.key === \"Escape\") {setMenuOpen(false); triggerRef.current?.focus();}\n if (menuOpen && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n event.preventDefault();\n const items = Array.from(menuRef.current?.querySelectorAll<HTMLButtonElement>(\"[role=menuitemradio]\") ?? []);\n const current = items.indexOf(document.activeElement as HTMLButtonElement);\n items[(current + (event.key === \"ArrowDown\" ? 1 : -1) + items.length) % items.length]?.focus();\n }\n }}>\n <button\n type=\"button\"\n ref={triggerRef}\n aria-haspopup=\"menu\"\n aria-label={text.type}\n aria-expanded={menuOpen}\n onClick={() => setMenuOpen((current) => !current)}\n className=\"flex h-6.5 w-full items-center justify-between rounded-[6px] bg-[var(--inset)] py-1 pr-1 pl-2\n shadow-[var(--shadow-hairline)] transition-shadow duration-200 focus-visible:outline-none\"\n style={{ boxShadow: menuOpen ? \"0 0 0 1px var(--accent)\" : undefined }}\n >\n <span className={`text-[12px] ${typeValue !== text.placeholder ? \"text-[var(--ink)]\" : \"text-[var(--ink-3)]\"}`}>\n {typeValue}\n </span>\n <svg\n width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className=\"transition-transform duration-200\"\n style={{ transform: menuOpen ? \"rotate(180deg)\" : \"rotate(0)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </button>\n\n {menuOpen && (\n <div\n role=\"menu\"\n aria-label={text.type}\n className=\"absolute right-0 bottom-8 z-10 w-30 rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{\n animation: \"mova-ai-fine-tune-card-pop-in 200ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"bottom right\",\n }}\n >\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[6px] bg-[var(--field)]\">\n {options.map((item) => (\n <button\n key={item}\n data-menu-row\n role=\"menuitemradio\"\n aria-checked={item === typeValue}\n type=\"button\"\n onClick={() => selectType(item)}\n className={`relative z-10 flex h-6.5 w-full items-center rounded-[6px] px-2 text-left text-[12.5px] text-[var(--ink)] ${\n item === typeValue ? \"bg-[var(--field)] group-hover/glide-menu:bg-transparent\" : \"\"\n }`}\n >\n {item}\n </button>\n ))}\n </GlideMenu>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--hover)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst scopedStyles = `.mova-ai-fine-tune-card{\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--radius-control:8px;\n width:100%;max-width:240px;min-width:0;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:14px;line-height:1.5;letter-spacing:-.01em;color:var(--ink);}\n.mova-ai-fine-tune-card[data-theme=\"dark\"]{\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n.mova-ai-fine-tune-card .primitive-card-pad{padding:12px} .mova-ai-fine-tune-card .primitive-card-bar{padding:10px 12px} .mova-ai-fine-tune-card .primitive-card-footer{padding:10px} .mova-ai-fine-tune-card .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-fine-tune-card :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--accent)!important;outline-offset:2px} .mova-ai-fine-tune-card button:not(:disabled){cursor:pointer} .mova-ai-fine-tune-card code{overflow-wrap:anywhere} .mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before,.mova-ai-fine-tune-card *::after{box-sizing:border-box}\n@keyframes mova-ai-fine-tune-card-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-fine-tune-card-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-fine-tune-card-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before, .mova-ai-fine-tune-card *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AIFineTuneCard({theme = \"light\", ...props}: React.ComponentProps<typeof FineTuneCard> & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-fine-tune-card\" data-theme={theme}><style>{scopedStyles}</style><FineTuneCard {...props} /></div>;\n}\n"
18
+ "content": "\"use client\";\n\"use client\";\n\n/** Adapted from Beautiful UI (https://github.com/slev12397/beautiful-ui).\n * Copyright (c) 2026 Shane Levine. MIT License; see LICENSE.third-party.\n * Original card geometry, interactions, and motion are preserved. */\nimport * as React from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * FINE-TUNE CARD — compact interactive inspector.\n * Number fields scrub: hover the label for an ↔ cursor and\n * drag to adjust, use ↑/↓ (⇧ for ×10), or type directly.\n * ───────────────────────────────────────────────────────── */\n\nfunction ScrubField({\n label,\n value,\n onChange,\n min,\n max,\n step = 1,\n suffix = \"\",\n active,\n}: {\n label: string;\n value: number;\n onChange: (v: number) => void;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n active?: boolean;\n}) {\n const drag = useRef<{ x: number; v: number } | null>(null);\n const [draft, setDraft] = useState(String(value));\n useEffect(() => setDraft(String(value)), [value]);\n const clamp = (v: number) => Math.min(max, Math.max(min, Number((Math.round(v / step) * step).toFixed(8))));\n\n const commitDraft = () => {\n const parsed = draft.trim() === \"\" ? NaN : Number(draft);\n if (!Number.isFinite(parsed)) { setDraft(String(value)); return; }\n const next = clamp(parsed);\n setDraft(String(next));\n if (next !== value) onChange(next);\n };\n\n return (\n <label\n className=\"flex h-6.5 min-w-0 items-center gap-1 rounded-[6px] py-1 pr-1 pl-0.5\n transition-[background-color,box-shadow] duration-200\"\n style={{\n background: active ? \"var(--mova-ai-accent-tint,#eee)\" : \"var(--mova-ai-field,#f6f6f6)\",\n boxShadow: active ? \"0 0 0 1px var(--mova-ai-accent,#454545)\" : \"none\",\n }}\n >\n {/* scrub handle */}\n <span\n role=\"slider\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n tabIndex={0}\n onPointerDown={(e) => {\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n drag.current = { x: e.clientX, v: value };\n }}\n onPointerMove={(e) => {\n if (!drag.current) return;\n onChange(clamp(drag.current.v + ((e.clientX - drag.current.x) / 2) * step));\n }}\n onPointerUp={() => (drag.current = null)}\n onPointerCancel={() => (drag.current = null)}\n onLostPointerCapture={() => (drag.current = null)}\n onKeyDown={(e) => {\n const mult = e.shiftKey ? 10 : 1;\n if (e.key === \"ArrowUp\" || e.key === \"ArrowRight\") {\n e.preventDefault();\n onChange(clamp(value + step * mult));\n } else if (e.key === \"ArrowDown\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n onChange(clamp(value - step * mult));\n }\n }}\n className=\"flex h-full shrink-0 cursor-ew-resize touch-none items-center rounded-[4px]\n px-0.5 text-[12px] text-[var(--mova-ai-ink-muted,#858585)] select-none hover:text-[var(--mova-ai-ink-secondary,#666)] focus-visible:text-[var(--mova-ai-accent-ink,#333)]\n focus-visible:outline-none\"\n >\n {label}\n </span>\n <input\n inputMode=\"decimal\"\n value={draft}\n onChange={(event) => {\n // Keep incomplete numbers editable; validation happens on commit.\n if (/^-?\\d*\\.?\\d*$/.test(event.target.value)) setDraft(event.target.value);\n }}\n onBlur={commitDraft}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") { event.preventDefault(); commitDraft(); }\n if (event.key === \"Escape\") { event.preventDefault(); event.stopPropagation(); setDraft(String(value)); }\n }}\n aria-label={`${label} value`}\n className=\"min-w-0 flex-1 bg-transparent text-[12px] text-[var(--mova-ai-ink,#202020)] tabular-nums outline-none\"\n />\n {suffix && <span className=\"shrink-0 pr-0.5 text-[11.5px] text-[var(--mova-ai-ink-muted,#858585)]\">{suffix}</span>}\n </label>\n );\n}\n\nconst SEGMENTS = [\"row\", \"col\", \"grid\"] as const;\n\nfunction SegmentIcon({ kind }: { kind: string }) {\n const dot = \"size-1.5 rounded-[2px] border-[1.2px] border-current\";\n if (kind === \"row\")\n return <span className=\"flex gap-0.5\">{[0, 1, 2].map((i) => <span key={i} className={dot} />)}</span>;\n if (kind === \"col\")\n return <span className=\"flex flex-col gap-0.5\">{[0, 1].map((i) => <span key={i} className={dot} />)}</span>;\n return (\n <span className=\"grid grid-cols-2 gap-0.5\">\n {[0, 1, 2, 3].map((i) => <span key={i} className={dot} />)}\n </span>\n );\n}\n\n/* A single scrub-able number property. `value` is the initial/default value. */\nexport type FineTuneField = {\n key: string;\n label: string;\n value: number;\n min: number;\n max: number;\n step?: number;\n suffix?: string;\n};\n/* Prominent copy strings on the card. */\nexport type FineTuneCardLabels = {\n title: string;\n layout: string;\n type: string;\n placeholder: string;\n adjust: string;\n edited: string;\n};\n/* The editable state emitted by `onChange`. */\nexport type FineTuneState = {\n segment: number;\n values: Record<string, number>;\n type: string;\n};\n\nconst FIELDS: FineTuneField[] = [\n { key: \"width\", label: \"W\", value: 324, min: 40, max: 999 },\n { key: \"height\", label: \"H\", value: 96, min: 24, max: 999 },\n { key: \"radius\", label: \"Radius\", value: 28, min: 0, max: 64 },\n { key: \"opacity\", label: \"Opacity\", value: 100, min: 0, max: 100, suffix: \"%\" },\n];\n\nconst OPTIONS = [\"Seasonal\", \"Classic\", \"Limited\"];\n\nconst DEFAULT_LABELS: FineTuneCardLabels = {\n title: \"Flavor card\",\n layout: \"Layout\",\n type: \"Type\",\n placeholder: \"Select type\",\n adjust: \"Adjust\",\n edited: \"Edited\",\n};\n\nfunction chunk<T>(items: T[], size: number): T[][] {\n const rows: T[][] = [];\n for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size));\n return rows;\n}\n\nexport type FineTuneCardProps = {\n /** Accepted for gallery/registry parity; not used by this card. */\n variant?: string;\n /** The scrub-able properties shown in the layout grid (rendered in pairs). */\n fields?: FineTuneField[];\n /** Options offered in the Type menu. */\n options?: string[];\n /** Prominent copy strings. */\n labels?: Partial<FineTuneCardLabels>;\n /** Called with the full editable state whenever the user edits it. */\n onChange?: (state: FineTuneState) => void;\n};\n\nfunction FineTuneCard({\n fields = FIELDS,\n options = OPTIONS,\n labels,\n onChange,\n}: FineTuneCardProps) {\n const text = { ...DEFAULT_LABELS, ...labels };\n const [seg, setSeg] = useState(0);\n const [values, setValues] = useState<Record<string, number>>(() =>\n Object.fromEntries(fields.map((f) => [f.key, f.value])),\n );\n const [menuOpen, setMenuOpen] = useState(false);\n const menuRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n useEffect(() => {\n if (!menuOpen) return;\n const dismiss = (event: PointerEvent) => { if (!menuRef.current?.contains(event.target as Node)) setMenuOpen(false); };\n document.addEventListener(\"pointerdown\", dismiss);\n return () => document.removeEventListener(\"pointerdown\", dismiss);\n }, [menuOpen]);\n const [typeValue, setTypeValue] = useState(text.placeholder);\n\n const selectSeg = (i: number) => {\n setSeg(i);\n onChange?.({ segment: i, values, type: typeValue });\n };\n const setValue = (key: string, v: number) => {\n const next = { ...values, [key]: v };\n setValues(next);\n onChange?.({ segment: seg, values: next, type: typeValue });\n };\n const selectType = (value: string) => {\n setTypeValue(value);\n setMenuOpen(false);\n triggerRef.current?.focus();\n onChange?.({ segment: seg, values, type: value });\n };\n\n const changed = fields.some((f) => values[f.key] !== f.value);\n const done = seg !== 0 || changed || typeValue !== text.placeholder;\n\n return (\n <div className=\"relative w-full max-w-60 rounded-lg border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] text-[color:var(--mova-ai-ink,#202020)] shadow-[var(--mova-ai-shadow-raised,none)]\">\n {/* header */}\n <div className=\"primitive-card-bar flex items-center justify-between border-b border-[var(--mova-ai-border,#dedede)]\">\n <span className=\"text-[13px] font-medium text-[var(--mova-ai-ink,#202020)]\">{text.title}</span>\n {done ? (\n <span\n className=\"flex items-center gap-1.5 text-[12px] font-medium text-[var(--mova-ai-success,#28734d)]\"\n style={{ animation: \"mova-ai-fine-tune-card-pop-in 250ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M20 6L9 17l-5-5\" />\n </svg>\n {text.edited}\n </span>\n ) : (\n <span className=\"flex items-center gap-1.5\">\n <span className=\"flex size-4.5 items-center justify-center rounded-[5px] border border-[var(--mova-ai-accent,#454545)]/30 bg-[var(--mova-ai-accent-tint,#eee)]\">\n <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"var(--mova-ai-accent,#454545)\">\n <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n </svg>\n </span>\n <span\n className=\"bg-clip-text text-[12px] font-medium text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(90deg, var(--mova-ai-accent,#454545) 35%, var(--mova-ai-accent-ink,#333) 50%, var(--mova-ai-accent,#454545) 65%)\",\n backgroundSize: \"200% 100%\",\n animation: \"mova-ai-fine-tune-card-shimmer-text 1.4s linear infinite\",\n }}\n >\n {text.adjust}\n </span>\n </span>\n )}\n </div>\n\n {/* layout section */}\n <div className=\"primitive-card-pad flex flex-col gap-2 border-b border-[var(--mova-ai-border,#dedede)]\">\n <p className=\"text-[12.5px] font-medium text-[var(--mova-ai-ink,#202020)]\">{text.layout}</p>\n {/* segmented control: gray track, raised white thumb */}\n <div className=\"relative grid grid-cols-3 rounded-[8px] bg-[var(--mova-ai-field,#f6f6f6)] p-0.5\">\n <span\n aria-hidden\n className=\"absolute inset-y-0.5 rounded-[6px] bg-[var(--mova-ai-surface,#fff)] shadow-[var(--mova-ai-shadow-btn,none)] transition-transform duration-300\"\n style={{\n width: \"calc((100% - 4px) / 3)\",\n left: 2,\n transform: `translateX(${seg * 100}%)`,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n />\n {SEGMENTS.map((s, i) => (\n <button\n key={s}\n type=\"button\"\n aria-label={`${s} layout`}\n aria-pressed={i === seg}\n onClick={() => selectSeg(i)}\n className={`relative z-10 flex h-6 items-center justify-center transition-colors duration-200\n ${i === seg ? \"text-[var(--mova-ai-accent,#454545)]\" : \"text-[var(--mova-ai-ink-muted,#858585)]\"}`}\n >\n <SegmentIcon kind={s} />\n </button>\n ))}\n </div>\n {chunk(fields, 2).map((pair, ri) => (\n <div key={ri} className=\"grid min-w-0 grid-cols-2 gap-2\">\n {pair.map((f) => (\n <ScrubField\n key={f.key}\n label={f.label}\n value={values[f.key]}\n onChange={(v) => setValue(f.key, v)}\n min={f.min}\n max={f.max}\n step={f.step}\n suffix={f.suffix}\n active={values[f.key] !== f.value}\n />\n ))}\n </div>\n ))}\n </div>\n\n {/* interaction section */}\n <div className=\"primitive-card-footer flex items-center justify-between\">\n <span className=\"text-[12px] text-[var(--mova-ai-ink-muted,#858585)]\">{text.type}</span>\n <div ref={menuRef} className=\"relative -mr-0.5 w-30\" onBlur={(event) => {if (!event.currentTarget.contains(event.relatedTarget)) setMenuOpen(false);}} onKeyDown={(event) => {\n if (event.key === \"Escape\") {setMenuOpen(false); triggerRef.current?.focus();}\n if (menuOpen && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n event.preventDefault();\n const items = Array.from(menuRef.current?.querySelectorAll<HTMLButtonElement>(\"[role=menuitemradio]\") ?? []);\n const current = items.indexOf(document.activeElement as HTMLButtonElement);\n items[(current + (event.key === \"ArrowDown\" ? 1 : -1) + items.length) % items.length]?.focus();\n }\n }}>\n <button\n type=\"button\"\n ref={triggerRef}\n aria-haspopup=\"menu\"\n aria-label={text.type}\n aria-expanded={menuOpen}\n onClick={() => setMenuOpen((current) => !current)}\n className=\"flex h-6.5 w-full items-center justify-between rounded-[6px] bg-[var(--mova-ai-inset,#f8f8f8)] py-1 pr-1 pl-2\n shadow-[var(--mova-ai-shadow-hairline,none)] transition-shadow duration-200 focus-visible:outline-none\"\n style={{ boxShadow: menuOpen ? \"0 0 0 1px var(--mova-ai-accent,#454545)\" : undefined }}\n >\n <span className={`text-[12px] ${typeValue !== text.placeholder ? \"text-[var(--mova-ai-ink,#202020)]\" : \"text-[var(--mova-ai-ink-muted,#858585)]\"}`}>\n {typeValue}\n </span>\n <svg\n width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--mova-ai-ink-muted,#858585)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className=\"transition-transform duration-200\"\n style={{ transform: menuOpen ? \"rotate(180deg)\" : \"rotate(0)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </button>\n\n {menuOpen && (\n <div\n role=\"menu\"\n aria-label={text.type}\n className=\"absolute right-0 bottom-8 z-10 w-30 rounded-[10px] bg-[var(--mova-ai-surface,#fff)] p-1 shadow-[var(--mova-ai-shadow-raised,none)]\"\n style={{\n animation: \"mova-ai-fine-tune-card-pop-in 200ms cubic-bezier(0.23,1,0.32,1) both\",\n transformOrigin: \"bottom right\",\n }}\n >\n <GlideMenu className=\"flex flex-col gap-px\" highlightClassName=\"inset-x-0 rounded-[6px] bg-[var(--mova-ai-field,#f6f6f6)]\">\n {options.map((item) => (\n <button\n key={item}\n data-menu-row\n role=\"menuitemradio\"\n aria-checked={item === typeValue}\n type=\"button\"\n onClick={() => selectType(item)}\n className={`relative z-10 flex h-6.5 w-full items-center rounded-[6px] px-2 text-left text-[12.5px] text-[var(--mova-ai-ink,#202020)] ${\n item === typeValue ? \"bg-[var(--mova-ai-field,#f6f6f6)] group-hover/glide-menu:bg-transparent\" : \"\"\n }`}\n >\n {item}\n </button>\n ))}\n </GlideMenu>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\ntype GlideMenuProps = {\n children: ReactNode;\n className?: string;\n highlightClassName?: string;\n rowSelector?: string;\n};\n\n/** A single hover layer that glides between interactive menu rows. */\nfunction GlideMenu({\n children,\n className = \"\",\n highlightClassName = \"inset-x-0 rounded-[8px] bg-[var(--mova-ai-hover,#f1f1f1)]\",\n rowSelector = \"[data-menu-row]\",\n}: GlideMenuProps) {\n const ref = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState<{ top: number; height: number } | null>(null);\n const [visible, setVisible] = useState(false);\n\n const moveTo = (target: EventTarget | null) => {\n const container = ref.current;\n if (!(target instanceof Element) || !container) return;\n const row = target.closest(rowSelector);\n if (!(row instanceof HTMLElement) || !container.contains(row)) return;\n const containerRect = container.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n setBox({ top: rowRect.top - containerRect.top, height: rowRect.height });\n setVisible(true);\n };\n\n return (\n <div\n ref={ref}\n onMouseOver={(event) => moveTo(event.target)}\n onMouseLeave={() => setVisible(false)}\n onFocusCapture={(event) => moveTo(event.target)}\n onBlurCapture={(event) => {\n if (!ref.current?.contains(event.relatedTarget as Node | null)) setVisible(false);\n }}\n className={`group/glide-menu relative ${className}`}\n >\n <span\n aria-hidden\n className={`pointer-events-none absolute ${highlightClassName}`}\n style={{\n top: box?.top ?? 0,\n height: box?.height ?? 0,\n opacity: box && visible ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {children}\n </div>\n );\n}\n\n/* Smooth shadow values: shadow-plugin, Copyright (c) Florian Kiem, MIT. */\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst scopedStyles = `.mova-ai-fine-tune-card .primitive-card-pad{padding:12px} .mova-ai-fine-tune-card .primitive-card-bar{padding:10px 12px} .mova-ai-fine-tune-card .primitive-card-footer{padding:10px} .mova-ai-fine-tune-card .primitive-icon-button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;border-radius:8px}\n.mova-ai-fine-tune-card :is(button,input,[tabindex]):focus-visible{outline:2px solid var(--mova-ai-accent,#454545)!important;outline-offset:2px} .mova-ai-fine-tune-card button:not(:disabled){cursor:pointer} .mova-ai-fine-tune-card code{overflow-wrap:anywhere} .mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before,.mova-ai-fine-tune-card *::after{box-sizing:border-box}\n@keyframes mova-ai-fine-tune-card-pop-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n@keyframes mova-ai-fine-tune-card-fade-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n@keyframes mova-ai-fine-tune-card-shimmer-text{from{background-position:150% center}to{background-position:-50% center}}\n@media(prefers-reduced-motion:reduce){.mova-ai-fine-tune-card *, .mova-ai-fine-tune-card *::before, .mova-ai-fine-tune-card *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}`;\n\nexport function AIFineTuneCard({theme = \"light\", ...props}: React.ComponentProps<typeof FineTuneCard> & {theme?: \"light\" | \"dark\"} = {}) {\n return <div className=\"mova-ai-theme mova-ai-fine-tune-card\" data-theme={theme}><style>{AI_THEME_STYLES}{scopedStyles}</style><FineTuneCard {...props} /></div>;\n}\n"
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "ai-prompt-bar",
3
3
  "tier": "free",
4
- "version": "0.4.1",
4
+ "version": "0.4.4",
5
5
  "file": "ai-prompt-bar.tsx",
6
6
  "sourcePath": "ui/ai/ai-prompt-bar.tsx",
7
7
  "registryFile": "ai-prompt-bar.json",
@@ -13,5 +13,5 @@
13
13
  ],
14
14
  "license": "MIT",
15
15
  "previewMode": "client",
16
- "content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Original composer geometry, controls, glide motion, and Glimm sweep are retained.\n\nimport { useEffect, useLayoutEffect, useRef, useState, useId } from \"react\";\nimport type { Palette, ShaderController, SweepHandle } from \"glimm\";\n\ntype SpeechRecognizer = {\n lang: string; interimResults: boolean;\n onresult: ((event: { results: ArrayLike<{ 0: { transcript: string } }> }) => void) | null;\n onend: (() => void) | null; onerror: (() => void) | null;\n start(): void; stop(): void; abort(): void;\n};\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst COMPONENT_STYLES = `[data-mova-prompt] {--shadow-xs: 0 0 4px 0 rgba(0, 0, 0, 0.04);--shadow-sm: 0 18px 47px 0 rgba(0, 0, 0, 0.03), 0 7.5px 19px 0 rgba(0, 0, 0, 0.02),\n 0 4px 10.5px 0 rgba(0, 0, 0, 0.02), 0 2.3px 5.8px 0 rgba(0, 0, 0, 0.01),\n 0 1.2px 3.1px 0 rgba(0, 0, 0, 0.01), 0 0.5px 1.3px 0 rgba(0, 0, 0, 0.01);--shadow-md: 0 17.54px 23.39px 0 rgba(0, 0, 0, 0.04), 0 9.4px 12.5px 0 rgba(0, 0, 0, 0.03),\n 0 5.25px 7px 0 rgba(0, 0, 0, 0.02), 0 2.79px 3.72px -2px rgba(0, 0, 0, 0.01),\n 0 1.16px 1.5px 0 rgba(0, 0, 0, 0.01);--shadow-lg: 0 25px 50px 0 rgba(0, 0, 0, 0.05), 0 12px 24px 0 rgba(0, 0, 0, 0.04),\n 0 6px 12px 0 rgba(0, 0, 0, 0.03), 0 3px 6px 0 rgba(0, 0, 0, 0.02),\n 0 1.5px 3px 0 rgba(0, 0, 0, 0.02); font-family:Inter,ui-sans-serif,system-ui,sans-serif; font-size:14px;line-height:1.5;letter-spacing:-0.01em;color:var(--ink);\n /* surfaces — cool, blue-tinted neutrals */\n --page: oklch(0.985 0.001 286.376);\n --canvas: oklch(0.961 0.002 247.84);\n --surface: oklch(1 0 0);\n --inset: oklch(0.979 0.002 247.839);\n --hover: oklch(0.97 0.002 247.839);\n --hover-2: oklch(0.933 0.003 247.86);\n\n /* ink ramp */\n --ink: oklch(0.247 0.006 258.361);\n --ink-2: oklch(0.506 0.01 264.477);\n --ink-3: oklch(0.695 0.009 264.505);\n\n /* borders — solid and crisp, not alpha */\n --line: oklch(0.946 0.003 264.542);\n --line-strong: oklch(0.912 0.005 258.326);\n --line-soft: oklch(0.966 0.002 264.542);\n --grid-line: color-mix(in srgb, var(--line) 78%, transparent);\n --field: oklch(0.961 0.001 286.375);\n --stripe: oklch(0.405 0 0 / 0.075);\n --stripe-bg: oklch(0.97 0 0);\n\n /* accent */\n --accent: oklch(0.626 0.205 254.947);\n --accent-ink: oklch(0.556 0.187 255.617);\n --accent-tint: oklch(0.96 0.019 252.878);\n\n /* semantic */\n --green: oklch(0.603 0.155 150.883);\n --green-tint: oklch(0.958 0.017 159.118);\n --orange: oklch(0.689 0.179 49.902);\n --orange-tint: oklch(0.964 0.021 67.581);\n --red: oklch(0.621 0.192 23.042);\n --red-tint: oklch(0.956 0.017 17.462);\n\n /* chart overlays — neutral system colors with enough contrast on either theme */\n --tooltip-bg: oklch(0.272 0.008 264.435);\n --tooltip-fg: oklch(0.976 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.356 0.007 264.474);\n\n /* shadows — our solid hairline ring + shadow-plugin's smooth layered stacks\n * (--shadow-xs…lg come from shadow-plugin/unprefixed) so every surface reads\n * consistently across the app. */\n --shadow-hairline: 0 0 0 1px var(--line);\n --shadow-btn: 0 0 0 1px var(--line-strong), var(--shadow-xs);\n --shadow-card: 0 0 0 1px var(--line), var(--shadow-sm);\n --shadow-raised: 0 0 0 1px var(--line), var(--shadow-md);\n --shadow-overlay: 0 0 0 1px var(--line), var(--shadow-lg);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);}\n[data-mova-prompt][data-theme=\"dark\"] {\n --page: oklch(0.209 0.004 264.477);\n --canvas: oklch(0.231 0.004 264.487);\n --surface: oklch(0.26 0.006 271.191);\n --inset: oklch(0.243 0.004 264.492);\n --hover: oklch(0.289 0.006 271.22);\n --hover-2: oklch(0.318 0.007 274.747);\n\n --ink: oklch(0.964 0.002 247.839);\n --ink-2: oklch(0.731 0.008 260.731);\n --ink-3: oklch(0.541 0.01 264.484);\n\n --line: oklch(0.308 0.006 258.354);\n --line-strong: oklch(0.356 0.007 264.474);\n --line-soft: oklch(0.278 0.006 258.354);\n --field: oklch(0.293 0.006 271.223);\n --stripe: oklch(1 0 0 / 0.055);\n --stripe-bg: oklch(0.226 0.004 264.485);\n\n --accent: oklch(0.68 0.173 253.301);\n --accent-ink: oklch(0.788 0.113 248.33);\n --accent-tint: oklch(0.68 0.173 253.301 / 0.16);\n\n --green: oklch(0.705 0.154 153.814);\n --green-tint: oklch(0.705 0.154 153.814 / 0.14);\n --orange: oklch(0.746 0.156 55.642);\n --orange-tint: oklch(0.746 0.156 55.642 / 0.14);\n --red: oklch(0.666 0.18 21.433);\n --red-tint: oklch(0.666 0.18 21.433 / 0.14);\n\n --tooltip-bg: oklch(0.182 0.004 264.459);\n --tooltip-fg: oklch(0.964 0.002 247.839);\n --tooltip-muted: oklch(0.731 0.008 260.731);\n --tooltip-border: oklch(0.308 0.006 258.354);\n\n --shadow-hairline: 0 0 0 1px var(--line);\n /* Elevation rings go to a subtle *light* hairline in dark mode: the ring\n * paints over the surface behind the element, so a low-alpha white edge\n * dissolves into the shadow as one continuous stroke (per smooth-shadow-ring),\n * graduated by how far the surface floats. */\n --shadow-btn: 0 0 0 1px oklch(1 0 0 / 0.1), 0 1px 2px oklch(0 0 0 / 0.3);\n --shadow-card:\n 0 0 0 1px oklch(1 0 0 / 0.11), 0 1px 2px oklch(0 0 0 / 0.2),\n 0 2px 6px oklch(0 0 0 / 0.2);\n --shadow-raised:\n 0 0 0 1px oklch(1 0 0 / 0.13), 0 2px 10px oklch(0 0 0 / 0.22);\n --shadow-overlay:\n 0 0 0 1px oklch(1 0 0 / 0.15), 0 8px 28px oklch(0 0 0 / 0.34);\n --shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);}\n[data-mova-prompt] :is(button,input,textarea):focus-visible { outline:2px solid var(--accent);outline-offset:2px; }\n[data-mova-prompt] button { cursor:pointer; } [data-mova-prompt] button:disabled { cursor:default; }\n\n@keyframes mova-prompt-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-prompt-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-prompt], [data-mova-prompt] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\n/* The built-in \"prism\" palette is only cyan→indigo→magenta, so a sweep\n * reads as blue/purple. Build a true full-spectrum rainbow instead. */\ntype GlimmRuntime = typeof import(\"glimm\");\n\n\n/* ─────────────────────────────────────────────────────────\n * PROMPT BAR\n * A composer with real controls: attach, @ data sources,\n * / commands, a model picker, dictation, and send.\n * Type @ or / to open the menus; ↑↓ + Enter to pick.\n * Variants: Rounded (card radius) · Pill (full radius).\n * ───────────────────────────────────────────────────────── */\n\nfunction Icon({ children, size = 15, strokeWidth = 1.8 }: { children: React.ReactNode; size?: number; strokeWidth?: number }) {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n {children}\n </svg>\n );\n}\n\nconst GLYPHS: Record<string, React.ReactNode> = {\n clip: <path d=\"m21.4 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\" />,\n chart: <path d=\"M4 20V10M10 20V4M16 20v-7M22 20H2\" />,\n layers: <g><path d=\"M12 2 2 7l10 5 10-5-10-5z\" /><path d=\"M2 17l10 5 10-5M2 12l10 5 10-5\" /></g>,\n globe: <g><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></g>,\n};\n\n/* real product marks, inline so the file stays self-contained */\nconst BRANDS: Record<string, React.ReactNode> = {\n figma: (\n <svg width=\"11\" height=\"16\" viewBox=\"0 0 38 57\" aria-hidden=\"true\">\n <path d=\"M9.5 57A9.5 9.5 0 0 0 19 47.5V38H9.5a9.5 9.5 0 0 0 0 19z\" fill=\"#0ACF83\" />\n <path d=\"M0 28.5A9.5 9.5 0 0 1 9.5 19H19v19H9.5A9.5 9.5 0 0 1 0 28.5z\" fill=\"#A259FF\" />\n <path d=\"M0 9.5A9.5 9.5 0 0 1 9.5 0H19v19H9.5A9.5 9.5 0 0 1 0 9.5z\" fill=\"#F24E1E\" />\n <path d=\"M19 0h9.5a9.5 9.5 0 1 1 0 19H19V0z\" fill=\"#FF7262\" />\n <path d=\"M38 28.5a9.5 9.5 0 1 1-19 0 9.5 9.5 0 0 1 19 0z\" fill=\"#1ABCFE\" />\n </svg>\n ),\n slack: (\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 127 127\" aria-hidden=\"true\">\n <path d=\"M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z\" fill=\"#E01E5A\" />\n <path d=\"M47 27.2c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.7 39.7.8 47 .8c7.3 0 13.2 5.9 13.2 13.2v13.2H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.3.7 54.4.7 47.1c0-7.3 5.9-13.2 13.2-13.2H47z\" fill=\"#36C5F0\" />\n <path d=\"M99.9 47.1c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V47.1zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.9C66.9 6.6 72.8.7 80.1.7c7.3 0 13.2 5.9 13.2 13.2v33.2z\" fill=\"#2EB67D\" />\n <path d=\"M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z\" fill=\"#ECB22E\" />\n </svg>\n ),\n gmail: (\n <svg width=\"15\" height=\"12\" viewBox=\"0 0 256 193\" aria-hidden=\"true\">\n <path d=\"M58.182 192.05V93.14L27.507 65.077 0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455h40.727Z\" fill=\"#4285F4\" />\n <path d=\"M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837-27.026 25.798v98.91Z\" fill=\"#34A853\" />\n <path d=\"m58.182 93.14-4.174-38.647 4.174-36.989L128 69.868l69.818-52.364 4.669 34.992-4.669 40.644L128 145.504 58.182 93.14Z\" fill=\"#EA4335\" />\n <path d=\"M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945l-16.292 12.218Z\" fill=\"#FBBC04\" />\n <path d=\"m0 49.504 26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23v23.273Z\" fill=\"#C5221F\" />\n </svg>\n ),\n};\n\ntype Source = {\n key: string;\n name: string;\n desc: string;\n glyph?: string;\n brand?: string;\n attach?: boolean;\n connect?: boolean;\n};\n\nconst SOURCES: Source[] = [\n { key: \"attach\", name: \"Add photos & files\", desc: \"Upload from your computer\", glyph: \"clip\", attach: true },\n { key: \"scoop\", name: \"Scoop Data\", desc: \"Sales & churn metrics\", glyph: \"chart\" },\n { key: \"flavors\", name: \"Flavor records\", desc: \"26 makers, tags, links\", glyph: \"layers\" },\n { key: \"web\", name: \"Web search\", desc: \"Real-time news and info\", glyph: \"globe\" },\n { key: \"figma\", name: \"Figma\", desc: \"Design-to-code workflows\", brand: \"figma\" },\n { key: \"slack\", name: \"Slack\", desc: \"Read and manage Slack\", brand: \"slack\" },\n { key: \"gmail\", name: \"Gmail\", desc: \"Read and manage Gmail\", brand: \"gmail\", connect: true },\n];\n\nconst COMMANDS = [\n { key: \"compare\", name: \"/compare\", desc: \"Flavor vs. last summer\" },\n { key: \"churn-plan\", name: \"/churn-plan\", desc: \"Draft a churn schedule\" },\n { key: \"restock\", name: \"/restock\", desc: \"Build a reorder list\" },\n { key: \"draft-email\", name: \"/draft-email\", desc: \"Write a supplier email\" },\n { key: \"summarize\", name: \"/summarize\", desc: \"Digest the thread so far\" },\n];\n\nconst MODELS = [\n { key: \"sprinkles-5\", name: \"Sprinkles 5\", tag: \"Flagship\" },\n { key: \"vanilla-1\", name: \"Vanilla 1\", tag: \"Basic\" },\n { key: \"freezer-burn\", name: \"Freezer Burn 0.4\", tag: \"Stale\" },\n];\n\n\n\n/* self-running demo: walk the @ menu, then the / menu, and repeat.\n * Any pointer or key interaction hands control to the user. */\nconst AUTO_STEPS: {\n draft: string;\n active?: number;\n connect?: boolean;\n modelOpen?: boolean;\n model?: string;\n hold: number;\n}[] = [\n { draft: \"\", connect: false, model: \"vanilla-1\", hold: 1100 },\n { draft: \"@\", active: 0, hold: 900 },\n { draft: \"@\", active: 1, hold: 620 },\n { draft: \"@\", active: 4, hold: 620 },\n { draft: \"@\", active: 6, hold: 700 },\n { draft: \"@\", active: 6, connect: true, hold: 1000 },\n { draft: \"\", hold: 700 },\n { draft: \"/\", active: 0, hold: 900 },\n { draft: \"/\", active: 1, hold: 620 },\n { draft: \"/\", active: 3, hold: 1000 },\n { draft: \"\", hold: 800 },\n // open the model picker and upgrade to the flagship → rainbow sweep\n { draft: \"\", modelOpen: true, hold: 1200 },\n { draft: \"\", model: \"sprinkles-5\", hold: 2400 },\n { draft: \"\", hold: 900 },\n];\n\n/* the last @word or /word being typed, if any */\nfunction parseToken(draft: string): { kind: \"at\" | \"slash\"; query: string; start: number } | null {\n const match = /(^|\\s)([@/])([\\w-]*)$/.exec(draft);\n if (!match) return null;\n return {\n kind: match[2] === \"@\" ? \"at\" : \"slash\",\n query: match[3].toLowerCase(),\n start: match.index + match[1].length,\n };\n}\n\nexport function AIPromptBar({\n theme = \"dark\",\n className = \"\",\n variant = \"Rounded\",\n demo = true,\n tall = false,\n placeholder,\n onSend,\n}: {\n theme?: \"dark\" | \"light\";\n className?: string;\n variant?: string;\n /** the self-running walkthrough; turn off when embedding in a real surface */\n demo?: boolean;\n /** hero sizing: a multi-line input with controls on their own row */\n tall?: boolean;\n placeholder?: string;\n /** The first argument preserves the original text-only callback API. */\n onSend?: (text: string, submission: {\n files: File[];\n sources: Array<{ key: string; name: string; desc: string }>;\n model: { key: string; name: string; tag: string };\n }) => void;\n}) {\n const sourceMenuId = useId();\n const modelMenuId = useId();\n const [feedback, setFeedback] = useState(\"\");\n const fileRef = useRef<HTMLInputElement>(null);\n const recognitionRef = useRef<SpeechRecognizer | null>(null);\n const sweepRef = useRef<SweepHandle | null>(null);\n const glimmRuntimeRef = useRef<{ api: GlimmRuntime; rainbow: Palette } | null>(null);\n const pendingSweepRef = useRef(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const pill = variant === \"Pill\";\n const [draft, setDraft] = useState(\"\");\n const [dismissed, setDismissed] = useState(false);\n const [plusOpen, setPlusOpen] = useState(false);\n const [modelOpen, setModelOpen] = useState(false);\n const [model, setModel] = useState(MODELS[1]);\n const [attachments, setAttachments] = useState<File[]>([]);\n const [selectedSources, setSelectedSources] = useState<Source[]>([]);\n const [connected, setConnected] = useState(false);\n const [active, setActive] = useState(0);\n const [listening, setListening] = useState(false);\n const [auto, setAuto] = useState(demo);\n const [autoStep, setAutoStep] = useState(0);\n const [expanded, setExpanded] = useState(false);\n const wide = expanded || tall;\n const [rowBox, setRowBox] = useState<{ top: number; height: number } | null>(null);\n const [engaged, setEngaged] = useState(false);\n const [modelBox, setModelBox] = useState<{ top: number; height: number } | null>(null);\n const [modelHovered, setModelHovered] = useState<number | null>(null);\n const [modelMenuLeft, setModelMenuLeft] = useState(0);\n const [modelMenuBottom, setModelMenuBottom] = useState(0);\n const composerAnchorRef = useRef<HTMLDivElement>(null);\n const controlsRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const measureRef = useRef<HTMLSpanElement>(null);\n const modelRef = useRef<HTMLButtonElement>(null);\n const modelMenuRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const modelRowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const glimmRef = useRef<HTMLCanvasElement>(null);\n const shaderRef = useRef<ShaderController | null>(null);\n const sweepingRef = useRef(false);\n\n /* hand control to the user: stop the demo loop, and when they aim at\n * the input itself, clear the demo's leftover draft for a clean start */\n const takeOver = (event: { target: EventTarget | null }) => {\n setAuto(false);\n if (auto && event.target === inputRef.current) setDraft(\"\");\n };\n\n const token = dismissed ? null : parseToken(draft);\n const menu: \"at\" | \"slash\" | null = plusOpen ? \"at\" : token?.kind ?? null;\n const query = plusOpen ? \"\" : token?.query ?? \"\";\n\n const rows: { key: string; name: string; desc: string }[] =\n menu === \"at\"\n ? SOURCES.filter((s) => s.name.toLowerCase().includes(query))\n : menu === \"slash\"\n ? COMMANDS.filter((c) => c.name.slice(1).startsWith(query))\n : [];\n\n useEffect(() => {\n setActive(0);\n setEngaged(false);\n }, [menu, query]);\n\n /* a single highlight glides to the active row instead of each row\n * toggling its own background — matches the gliding pill in the nav */\n useLayoutEffect(() => {\n const target = rowRefs.current[active];\n if (target) setRowBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [menu, query, active, connected, rows.length]);\n\n /* same gliding highlight in the model menu — floats to the hovered\n * row, falling back to the currently-selected model */\n const modelIndex = MODELS.findIndex((m) => m.key === model.key);\n useLayoutEffect(() => {\n if (!modelOpen) return;\n const target = modelRowRefs.current[modelHovered ?? modelIndex];\n if (target) setModelBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [modelOpen, modelHovered, modelIndex]);\n\n /* The menu is outside the clipped composer. Reposition it when the\n * preview resizes as well as when it opens. */\n useLayoutEffect(() => {\n if (!modelOpen || !composerAnchorRef.current || !modelRef.current) return;\n const reposition = () => {\n const anchorRect = composerAnchorRef.current?.getBoundingClientRect();\n const triggerRect = modelRef.current?.getBoundingClientRect();\n if (!anchorRect || !triggerRect) return;\n const menuWidth = modelMenuRef.current?.offsetWidth ?? 208;\n setModelMenuLeft(Math.max(0, Math.min(triggerRect.left - anchorRect.left, anchorRect.width - menuWidth)));\n setModelMenuBottom(anchorRect.bottom - triggerRect.top + 8);\n };\n reposition();\n const observer = new ResizeObserver(reposition);\n observer.observe(composerAnchorRef.current);\n window.addEventListener(\"resize\", reposition);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"resize\", reposition);\n };\n }, [modelOpen, wide, model.name]);\n\n useEffect(() => {\n if (!modelOpen) setModelHovered(null);\n }, [modelOpen]);\n\n /* Build the shader with a pinned hue phase. createShader seeds its\n * internal hueShift from Math.random(), which made the sweep a different\n * colour on every reload — pin it so the rainbow is identical each time. */\n const makeShader = () => {\n const canvas = glimmRef.current;\n const runtime = glimmRuntimeRef.current;\n if (!runtime || !canvas || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return null;\n const random = Math.random;\n Math.random = () => 0;\n try {\n return runtime.api.createShader({\n canvas,\n palette: runtime.rainbow,\n direction: \"ltr\",\n bandTight: 10,\n swellAmount: 0.85,\n });\n } catch {\n return null;\n } finally {\n Math.random = random;\n }\n };\n\n const stopSweep = () => {\n pendingSweepRef.current = false;\n sweepingRef.current = false;\n sweepRef.current?.cancel();\n sweepRef.current = null;\n shaderRef.current?.destroy();\n shaderRef.current = null;\n };\n\n /* Glimm shader lives inside the composer, invisible at rest. Selecting\n * the flagship model fires a one-shot rainbow sweep across the interior. */\n useEffect(() => {\n let disposed = false;\n const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const motionChanged = () => { if (motionQuery.matches) stopSweep(); };\n motionQuery.addEventListener(\"change\", motionChanged);\n // Glimm is ESM-only: keep module evaluation safe for CJS and SSR consumers.\n void import(\"glimm\").then((api) => {\n if (disposed) return;\n const { ACCENTS, accentChain } = api;\n glimmRuntimeRef.current = {\n api,\n rainbow: accentChain([ACCENTS.red, ACCENTS.orange, ACCENTS.yellow, ACCENTS.green, ACCENTS.cyan, ACCENTS.blue, ACCENTS.purple]),\n };\n if (pendingSweepRef.current) celebrate();\n }).catch(() => {\n // The composer remains functional if the optional visual cannot load.\n if (!disposed) pendingSweepRef.current = false;\n });\n return () => {\n disposed = true;\n motionQuery.removeEventListener(\"change\", motionChanged);\n stopSweep();\n glimmRuntimeRef.current = null;\n recognitionRef.current?.abort();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const celebrate = () => {\n if (sweepingRef.current) return;\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const runtime = glimmRuntimeRef.current;\n if (!runtime) { pendingSweepRef.current = true; return; }\n pendingSweepRef.current = false;\n // Recreate the shader per sweep so uTime restarts at 0 — the hue phase\n // (which drifts with time) is then identical on every trigger.\n shaderRef.current?.destroy();\n const shader = makeShader();\n shaderRef.current = shader;\n if (!shader) return;\n sweepingRef.current = true;\n const sweep = runtime.api.playSweep(shader, {\n palette: runtime.rainbow,\n direction: \"ltr\",\n sweepMs: 570,\n outroMs: 80,\n peakAlpha: 1.3,\n bandTight: 10,\n brightness: 1.4,\n swellAmount: 1,\n waveSpeed: 1.8,\n easing: \"easeOutExpo\",\n });\n sweepRef.current = sweep;\n sweep.done.finally(() => {\n if (sweepRef.current === sweep) {\n sweepingRef.current = false;\n sweepRef.current = null;\n shader.destroy();\n if (shaderRef.current === shader) shaderRef.current = null;\n }\n });\n };\n\n const selectModel = (next: (typeof MODELS)[number]) => {\n setModel(next);\n setModelOpen(false);\n if (next.key === \"sprinkles-5\") celebrate();\n else stopSweep();\n };\n\n /* autoplay: apply the current step, then advance after its hold */\n useEffect(() => {\n if (!auto || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const step = AUTO_STEPS[autoStep % AUTO_STEPS.length];\n setDraft(step.draft);\n if (step.active !== undefined) setActive(step.active);\n if (step.connect !== undefined) setConnected(step.connect);\n if (step.modelOpen !== undefined) setModelOpen(step.modelOpen);\n if (step.model) {\n const next = MODELS.find((m) => m.key === step.model);\n if (next) selectModel(next);\n }\n const t = setTimeout(() => setAutoStep((s) => s + 1), step.hold);\n return () => clearTimeout(t);\n }, [auto, autoStep]);\n\n const toggleDictation = () => {\n if (listening) { recognitionRef.current?.stop(); return; }\n const speechWindow = window as Window & { SpeechRecognition?: new () => SpeechRecognizer; webkitSpeechRecognition?: new () => SpeechRecognizer };\n const Recognition = speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;\n if (!Recognition) { setFeedback(\"Dictation is unavailable in this browser. You can type your message instead.\"); return; }\n const recognition = new Recognition();\n recognitionRef.current = recognition;\n recognition.lang = navigator.language;\n recognition.interimResults = false;\n recognition.onresult = (event) => {\n const text = Array.from(event.results).map((result) => result[0].transcript).join(\" \");\n setDraft((current) => `${current.trimEnd()} ${text}`.trimStart());\n setFeedback(\"Dictation added to your message.\");\n };\n recognition.onend = () => { setListening(false); inputRef.current?.focus(); };\n recognition.onerror = () => { setListening(false); setFeedback(\"Dictation could not start. Check microphone access or type your message.\"); };\n try { recognition.start(); setListening(true); setFeedback(\"Listening through your microphone…\"); }\n catch { setListening(false); setFeedback(\"Dictation is unavailable right now.\"); }\n };\n\n /* Move wrapped text above the controls, then grow to a compact maximum. */\n useLayoutEffect(() => {\n const input = inputRef.current;\n const controls = controlsRef.current;\n const measure = measureRef.current;\n const modelButton = modelRef.current;\n if (!input || !controls || !measure || !modelButton) return;\n\n const fixedControlsWidth = 28 * 3 + modelButton.offsetWidth;\n const inlineGaps = 4 * 4;\n const inlineInputWidth = controls.clientWidth - fixedControlsWidth - inlineGaps;\n const needsFullWidth = draft.includes(\"\\n\") || measure.offsetWidth + 8 > inlineInputWidth;\n if (needsFullWidth !== expanded) {\n setExpanded(needsFullWidth);\n }\n\n const minHeight = 28;\n const maxHeight = 100;\n input.style.height = \"0px\";\n const contentHeight = input.scrollHeight;\n input.style.height = `${Math.min(Math.max(contentHeight, minHeight), maxHeight)}px`;\n input.style.overflowY = contentHeight > maxHeight ? \"auto\" : \"hidden\";\n }, [draft, expanded]);\n\n /* clicking anywhere outside the composer closes the open menus */\n useEffect(() => {\n if (!modelOpen && !plusOpen) return;\n const close = (event: PointerEvent) => {\n if (!rootRef.current?.contains(event.target as Node)) {\n setDismissed(true);\n setModelOpen(false);\n setPlusOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [modelOpen, plusOpen]);\n\n const closeMenus = () => {\n setPlusOpen(false);\n setModelOpen(false);\n };\n\n const pick = (row: { key: string; name: string }) => {\n const source = SOURCES.find((s) => s.key === row.key);\n if (source?.attach) {\n fileRef.current?.click();\n if (token) setDraft(draft.slice(0, token.start));\n } else if (menu === \"at\") {\n if (source) setSelectedSources((current) => current.some((item) => item.key === source.key) ? current : [...current, source]);\n if (source?.connect) { setConnected(true); setFeedback(`${source.name} selected for this demo. Connect your account in your app to use live data.`); }\n setDraft(`${token ? draft.slice(0, token.start) : draft}@${row.name} `);\n } else {\n setDraft(`${token ? draft.slice(0, token.start) : draft}${row.name} `);\n }\n setPlusOpen(false);\n setDismissed(false);\n inputRef.current?.focus();\n };\n\n const canSend = draft.trim().length > 0 || attachments.length > 0;\n const send = () => {\n if (!canSend) return;\n onSend?.(draft.trim(), {\n files: [...attachments],\n sources: selectedSources.filter((source) => draft.includes(`@${source.name}`)).map(({ key, name, desc }) => ({ key, name, desc })),\n model: { ...model },\n });\n setFeedback(`Sent${attachments.length ? ` with ${attachments.length} attachment${attachments.length === 1 ? \"\" : \"s\"}` : \"\"}: ${draft.trim() || attachments.map((file) => file.name).join(\", \")}`);\n setDraft(\"\");\n setAttachments([]);\n setSelectedSources([]);\n closeMenus();\n };\n\n return (\n <div\n ref={rootRef}\n data-mova-prompt\n data-theme={theme}\n data-promptbar\n className={`${demo ? \"flex min-h-[384px] w-full max-w-[420px] flex-col justify-end pb-8\" : \"w-full\"} ${className}`}\n onPointerDownCapture={takeOver}\n onKeyDownCapture={(event) => { takeOver(event); if (event.key === \"Escape\") { event.stopPropagation(); setDismissed(true); closeMenus(); inputRef.current?.focus(); } }}\n >\n <style>{COMPONENT_STYLES}</style>\n <input ref={fileRef} type=\"file\" multiple className=\"hidden\" aria-label=\"Attach files\" onChange={(event) => { const files = Array.from(event.target.files ?? []); setAttachments((current) => [...current, ...files]); setFeedback(`${files.length} file${files.length === 1 ? \"\" : \"s\"} attached.`); event.target.value = \"\"; inputRef.current?.focus(); }} />\n {/* composer is the anchor — menus grow up from its top edge */}\n <div ref={composerAnchorRef} className=\"relative\">\n {/* ── @ / slash menu ─────────────────────────────── */}\n {menu && (\n <div\n id={sourceMenuId}\n role=\"listbox\" aria-label=\"Sources and commands\"\n onMouseLeave={() => setEngaged(false)}\n className=\"absolute inset-x-0 bottom-full z-10 mb-2 max-h-[min(320px,60dvh)] overflow-y-auto rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{ animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom center\" }}\n >\n {/* single gliding highlight — appears once a row is hovered */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--hover)]\"\n style={{\n top: rowBox?.top ?? 0,\n height: rowBox?.height ?? 0,\n opacity: rowBox && engaged && rows.length > 0 ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {rows.map((row, i) => {\n const source = menu === \"at\" ? SOURCES.find((s) => s.key === row.key) : undefined;\n return (\n <button\n key={row.key}\n id={`${sourceMenuId}-${i}`} role=\"option\" aria-selected={i === active}\n type=\"button\"\n ref={(el) => {\n rowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => {\n setActive(i);\n setEngaged(true);\n }}\n onFocus={() => { setActive(i); setEngaged(true); }}\n onClick={() => pick(row)}\n className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[6px] px-2 text-left\"\n >\n {source && (\n <span className=\"flex size-5.5 shrink-0 items-center justify-center text-[var(--ink-2)]\">\n {source.brand ? BRANDS[source.brand] : <Icon size={15}>{GLYPHS[source.glyph ?? \"clip\"]}</Icon>}\n </span>\n )}\n <span className=\"shrink-0 text-[12.5px] font-medium text-[var(--ink)]\">\n {row.name}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[12px] text-[var(--ink-3)]\">{row.desc}</span>\n {source?.connect && (\n <span\n className={`shrink-0 text-[12px] font-medium transition-colors duration-100 ${\n connected ? \"text-[var(--green)]\" : \"text-[var(--accent-ink)] hover:underline\"\n }`}\n >\n {connected ? \"Connected\" : \"Connect\"}\n </span>\n )}\n </button>\n );\n })}\n {rows.length === 0 && (\n <div className=\"flex h-9 items-center px-2 text-[12px] text-[var(--ink-3)]\">\n No matches for “{query}”\n </div>\n )}\n <div className=\"mt-1 border-t border-[var(--line)] px-2 pt-1.5 pb-1 text-[11px] text-[var(--ink-3)]\">\n {menu === \"at\" ? \"Type to search sources & files\" : \"Type to search commands\"}\n </div>\n </div>\n )}\n\n {/* ── model menu ─────────────────────────────────── */}\n {modelOpen && (\n <div\n ref={modelMenuRef}\n id={modelMenuId}\n role=\"group\" aria-label=\"Available models\"\n onKeyDown={(event) => { if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const current = modelRowRefs.current.indexOf(document.activeElement as HTMLButtonElement); modelRowRefs.current[(current + (event.key === \"ArrowDown\" ? 1 : MODELS.length - 1)) % MODELS.length]?.focus(); } }}\n onMouseLeave={() => setModelHovered(null)}\n className=\"absolute z-10 w-52 max-w-full rounded-[10px] bg-[var(--surface)] p-1 shadow-[var(--shadow-raised)]\"\n style={{ left: modelMenuLeft, bottom: modelMenuBottom, animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom left\" }}\n >\n {/* single gliding highlight — floats to the hovered / selected row */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--hover)]\"\n style={{\n top: modelBox?.top ?? 0,\n height: modelBox?.height ?? 0,\n opacity: modelBox && modelHovered !== null ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {MODELS.map((m, i) => (\n <button\n key={m.key}\n type=\"button\"\n ref={(el) => {\n modelRowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onFocus={() => setModelHovered(i)}\n onMouseEnter={() => setModelHovered(i)}\n onClick={() => {\n selectModel(m);\n inputRef.current?.focus();\n }}\n className=\"relative z-10 flex h-7.5 w-full items-center gap-2 rounded-[6px] px-2 text-left\"\n >\n <span className=\"min-w-0 flex-1 truncate text-[12.5px] font-medium text-[var(--ink)]\">{m.name}</span>\n <span className=\"shrink-0 text-[11px] text-[var(--ink-3)]\">{m.tag}</span>\n <span className={`shrink-0 text-[var(--ink)] ${m.key === model.key ? \"\" : \"invisible\"}`}>\n <Icon size={13} strokeWidth={2.5}><path d=\"M20 6L9 17l-5-5\" /></Icon>\n </span>\n </button>\n ))}\n </div>\n )}\n\n {/* ── composer ───────────────────────────────────── */}\n <div\n className={`relative isolate flex flex-col overflow-hidden border border-[var(--line)] bg-[var(--surface)] shadow-[var(--shadow-card)] transition-[border-color,border-radius] duration-150 focus-within:border-[var(--line-strong)] ${\n tall ? \"gap-2.5 p-3.5\" : \"gap-1.5 p-1.5\"\n } ${\n pill ? (attachments.length > 0 || wide ? \"rounded-[24px]\" : \"rounded-full\") : tall ? \"rounded-[22px]\" : \"rounded-[14px]\"\n }`}\n >\n {/* rainbow glimm sweep — plays across the interior on model change.\n explicit w/h: a <canvas> is a replaced element and won't stretch\n to inset-0 alone, which feeds back into the shader's ResizeObserver. */}\n <canvas\n ref={glimmRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 -z-10 h-full w-full\"\n style={{ borderRadius: \"inherit\" }}\n />\n <span\n ref={measureRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute invisible whitespace-pre text-[13px] leading-[18px]\"\n >\n {draft}\n </span>\n\n {attachments.length > 0 && (\n <div className={`flex flex-wrap gap-1.5 pt-0.5 ${pill ? \"px-1\" : \"px-0.5\"}`}>\n {attachments.map((file, i) => (\n <span\n key={`${file.name}-${i}`}\n className={`flex h-6.5 items-center gap-1.5 bg-[var(--field)] py-1 pr-1 pl-1.5 text-[11.5px] text-[var(--ink-2)] shadow-[var(--shadow-hairline)] ${\n pill ? \"rounded-full\" : \"rounded-[6px]\"\n }`}\n style={{ animation: \"mova-prompt-pop 200ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <Icon size={12}><g><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></g></Icon>\n <span className=\"max-w-36 truncate\">{file.name}</span>\n <button\n type=\"button\"\n aria-label={`Remove ${file.name}`}\n onClick={() => setAttachments((current) => current.filter((_, j) => j !== i))}\n className={`-my-1 flex size-6 items-center justify-center text-[var(--ink-3)] transition-colors duration-100 hover:bg-[color-mix(in_srgb,var(--line)_70%,transparent)] hover:text-[var(--ink)] ${\n pill ? \"rounded-full\" : \"rounded-[5px]\"\n }`}\n >\n <Icon size={10} strokeWidth={2.5}><path d=\"M18 6L6 18M6 6l12 12\" /></Icon>\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div\n ref={controlsRef}\n className={`grid items-end gap-x-1 gap-y-1.5 ${\n wide\n ? \"grid-cols-[28px_auto_minmax(0,1fr)_28px_28px]\"\n : \"grid-cols-[28px_minmax(0,1fr)_auto_28px_28px]\"\n }`}\n >\n <button\n type=\"button\"\n aria-label=\"Add attachments and sources\"\n aria-expanded={plusOpen}\n onClick={() => {\n setModelOpen(false);\n setPlusOpen((current) => !current);\n inputRef.current?.focus();\n }}\n className={`flex size-7 shrink-0 items-center justify-center justify-self-start text-[var(--ink-3)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--hover)] hover:text-[var(--ink)] active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${plusOpen ? \"bg-[var(--hover)] text-[var(--ink)]\" : \"\"} ${wide ? \"col-start-1 row-start-2\" : \"col-start-1 row-start-1\"}`}\n >\n <Icon size={16} strokeWidth={2}><path d=\"M12 5v14M5 12h14\" /></Icon>\n </button>\n\n <textarea\n ref={inputRef}\n rows={1}\n value={draft}\n onChange={(event) => {\n setDraft(event.target.value);\n setDismissed(false);\n setPlusOpen(false);\n }}\n onKeyDown={(event) => {\n if (event.nativeEvent.isComposing) return;\n if (menu && rows.length > 0) {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n setEngaged(true);\n setActive((current) => (current + (event.key === \"ArrowDown\" ? 1 : rows.length - 1)) % rows.length);\n return;\n }\n if ((event.key === \"Enter\" && !event.shiftKey) || event.key === \"Tab\") {\n event.preventDefault();\n pick(rows[active]);\n return;\n }\n }\n if (event.key === \"Escape\") {\n setDismissed(true);\n closeMenus();\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault();\n send();\n }\n }}\n placeholder={listening ? \"Listening…\" : placeholder ?? \"Write a message…\"}\n aria-label=\"Prompt\"\n aria-expanded={!!menu}\n aria-controls={menu ? sourceMenuId : undefined}\n aria-activedescendant={menu && rows[active] ? `${sourceMenuId}-${active}` : undefined}\n className={`${tall ? \"min-h-[68px] px-2 py-2 text-[14px] leading-5\" : \"min-h-7 px-1 py-[5px] text-[13px] leading-[18px]\"} min-w-0 w-full resize-none bg-transparent text-[var(--ink)] outline-none [overflow-wrap:anywhere] placeholder:text-[var(--ink-3)] ${\n wide ? \"col-span-full col-start-1 row-start-1\" : \"col-start-2 row-start-1\"\n }`}\n />\n\n {/* model picker */}\n <button\n ref={modelRef}\n type=\"button\"\n aria-expanded={modelOpen}\n aria-label=\"Choose model\"\n aria-controls={modelMenuId}\n onKeyDown={(event) => { if (event.key === \"ArrowDown\") { event.preventDefault(); setPlusOpen(false); setDismissed(true); setModelOpen(true); requestAnimationFrame(() => modelRowRefs.current[modelIndex]?.focus()); } }}\n onClick={() => {\n setPlusOpen(false);\n setDismissed(true);\n setModelOpen((current) => !current);\n }}\n className={`flex h-7 shrink-0 items-center gap-1 px-1.5 text-[12px] font-medium text-[var(--ink-2)] transition-colors duration-150 hover:bg-[var(--hover)] hover:text-[var(--ink)] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-2 row-start-2 justify-self-start\" : \"col-start-3 row-start-1\"}`}\n >\n {model.name}\n <span className=\"text-[var(--ink-3)]\">\n <Icon size={11} strokeWidth={2.4}><path d=\"M6 9l6 6 6-6\" /></Icon>\n </span>\n </button>\n\n {/* dictation */}\n <button\n type=\"button\"\n aria-label={listening ? \"Stop dictation\" : \"Start dictation\"}\n aria-pressed={listening}\n onClick={toggleDictation}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-150 active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${listening ? \"bg-[var(--accent-tint)] text-[var(--accent-ink)]\" : \"text-[var(--ink-3)] hover:bg-[var(--hover)] hover:text-[var(--ink)]\"} ${wide ? \"col-start-4 row-start-2\" : \"col-start-4 row-start-1\"}`}\n >\n {listening ? (\n <span className=\"flex h-3.5 items-center gap-[2.5px]\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"w-[2.5px] rounded-full bg-current\"\n style={{ height: \"100%\", animation: `mova-prompt-eq 900ms ease-in-out ${i * 150}ms infinite` }}\n />\n ))}\n </span>\n ) : (\n <Icon size={15} strokeWidth={2}><g><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z\" /><path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v3\" /></g></Icon>\n )}\n </button>\n\n {/* send — tactile square (round in the pill variant) */}\n <button\n type=\"button\"\n aria-label=\"Send\"\n disabled={!canSend}\n onClick={send}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-200 enabled:active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-5 row-start-2\" : \"col-start-5 row-start-1\"}`}\n style={{\n background: canSend ? \"var(--ink)\" : \"var(--line-strong)\",\n color: canSend ? \"var(--surface)\" : \"var(--ink-2)\",\n }}\n >\n <Icon size={16} strokeWidth={2.4}><path d=\"M12 19V5M5 12l7-7 7 7\" /></Icon>\n </button>\n </div>\n </div>\n </div>\n <p role=\"status\" className=\"mt-2 min-h-4 break-words text-[11px] text-[var(--ink-3)]\">{feedback}</p>\n </div>\n );\n}\n\nexport default AIPromptBar;\n"
16
+ "content": "\"use client\";\n\"use client\";\n\n// SPDX-License-Identifier: MIT\n// Adapted from Beautiful UI, Copyright (c) 2026 Shane Levine.\n// Original composer geometry, controls, glide motion, and Glimm sweep are retained.\n\nimport { useEffect, useLayoutEffect, useRef, useState, useId } from \"react\";\nimport type { Palette, ShaderController, SweepHandle } from \"glimm\";\n\ntype SpeechRecognizer = {\n lang: string; interimResults: boolean;\n onresult: ((event: { results: ArrayLike<{ 0: { transcript: string } }> }) => void) | null;\n onend: (() => void) | null; onerror: (() => void) | null;\n start(): void; stop(): void; abort(): void;\n};\n/* Shadow tokens from shadow-plugin 2.1.1.\nMIT License\n\nCopyright (c) 2026 Florian Kiem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\nconst AI_THEME_STYLES = \"/* Shared public tokens for the standalone AI component family. */\\n.mova-ai-theme {\\n color-scheme: light;\\n color: var(--mova-ai-ink, #202020);\\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\n font-size: 13px;\\n line-height: 1.45;\\n letter-spacing: -0.01em;\\n\\n --mova-ai-page: #f7f7f7;\\n --mova-ai-canvas: #f4f4f4;\\n --mova-ai-surface: #ffffff;\\n --mova-ai-inset: #f8f8f8;\\n --mova-ai-hover: #f1f1f1;\\n --mova-ai-hover-strong: #e9e9e9;\\n\\n --mova-ai-ink: #202020;\\n --mova-ai-ink-secondary: #666666;\\n --mova-ai-ink-muted: #858585;\\n\\n --mova-ai-border: #dedede;\\n --mova-ai-border-soft: #ededed;\\n --mova-ai-border-strong: #c8c8c8;\\n --mova-ai-field: #f6f6f6;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(32 35 38 / 7%);\\n --mova-ai-stripe-bg: #242424;\\n\\n --mova-ai-accent: #454545;\\n --mova-ai-accent-ink: #333333;\\n --mova-ai-accent-foreground: #ffffff;\\n --mova-ai-accent-tint: #eeeeee;\\n --mova-ai-success: #28734d;\\n --mova-ai-success-tint: #e8f3ec;\\n --mova-ai-warning: #986517;\\n --mova-ai-warning-tint: #f8f0df;\\n --mova-ai-danger: #a33c3c;\\n --mova-ai-danger-tint: #f7eaea;\\n --mova-ai-info: #496b89;\\n --mova-ai-info-tint: #eaf0f5;\\n --mova-ai-neutral-status: #69737d;\\n --mova-ai-status-on: #ffffff;\\n\\n --mova-ai-tooltip-bg: #292929;\\n --mova-ai-tooltip-fg: #f6f6f6;\\n --mova-ai-tooltip-muted: #c3c3c3;\\n --mova-ai-tooltip-border: #484848;\\n\\n --mova-ai-radius-panel: 8px;\\n --mova-ai-radius-control: 8px;\\n --mova-ai-shadow-xs: 0 1px 2px rgb(22 28 33 / 5%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(22 28 33 / 7%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(22 28 33 / 11%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(22 28 33 / 15%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(22 28 33 / 12%);\\n}\\n\\n.mova-ai-theme[data-theme=\\\"dark\\\"] {\\n color-scheme: dark;\\n --mova-ai-page: #181818;\\n --mova-ai-canvas: #202020;\\n --mova-ai-surface: #272727;\\n --mova-ai-inset: #1d1d1d;\\n --mova-ai-hover: #2d2d2d;\\n --mova-ai-hover-strong: #393939;\\n\\n --mova-ai-ink: #eeeeee;\\n --mova-ai-ink-secondary: #b5b5b5;\\n --mova-ai-ink-muted: #929292;\\n\\n --mova-ai-border: #3b3b3b;\\n --mova-ai-border-soft: #303030;\\n --mova-ai-border-strong: #505050;\\n --mova-ai-field: #2c2c2c;\\n --mova-ai-grid: color-mix(in srgb, var(--mova-ai-border) 78%, transparent);\\n --mova-ai-stripe: rgb(255 255 255 / 6%);\\n --mova-ai-stripe-bg: #202528;\\n\\n --mova-ai-accent: #c2c2c2;\\n --mova-ai-accent-ink: #d5d5d5;\\n --mova-ai-accent-foreground: #202020;\\n --mova-ai-accent-tint: #353535;\\n --mova-ai-success: #88c7a0;\\n --mova-ai-success-tint: #293b30;\\n --mova-ai-warning: #d7b66f;\\n --mova-ai-warning-tint: #3c3525;\\n --mova-ai-danger: #e5928e;\\n --mova-ai-danger-tint: #3c2b2b;\\n --mova-ai-info: #9bbbd6;\\n --mova-ai-info-tint: #293541;\\n --mova-ai-neutral-status: #a0a0a0;\\n --mova-ai-status-on: #181818;\\n\\n --mova-ai-tooltip-bg: #121212;\\n --mova-ai-tooltip-fg: #eeeeee;\\n --mova-ai-tooltip-muted: #b5b5b5;\\n --mova-ai-tooltip-border: #3b3b3b;\\n\\n --mova-ai-shadow-xs: 0 1px 2px rgb(0 0 0 / 22%);\\n --mova-ai-shadow-sm: 0 2px 8px rgb(0 0 0 / 25%);\\n --mova-ai-shadow-md: 0 8px 20px rgb(0 0 0 / 30%);\\n --mova-ai-shadow-lg: 0 14px 32px rgb(0 0 0 / 38%);\\n --mova-ai-shadow-hairline: 0 0 0 1px var(--mova-ai-border);\\n --mova-ai-shadow-btn: 0 0 0 1px var(--mova-ai-border-strong), var(--mova-ai-shadow-xs);\\n --mova-ai-shadow-card: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-sm);\\n --mova-ai-shadow-raised: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-md);\\n --mova-ai-shadow-overlay: 0 0 0 1px var(--mova-ai-border), var(--mova-ai-shadow-lg);\\n --mova-ai-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 42%);\\n}\\n\";\nconst COMPONENT_STYLES = `[data-mova-prompt] button:focus-visible { outline:2px solid var(--mova-ai-accent,#454545);outline-offset:2px; }\n[data-mova-prompt] :is(input,textarea):focus-visible { outline:none; }\n[data-mova-prompt] .prompt-composer:focus-within { outline:2px solid var(--mova-ai-accent,#454545);outline-offset:2px; }\n[data-mova-prompt] button { cursor:pointer; } [data-mova-prompt] button:disabled { cursor:default; }\n\n@keyframes mova-prompt-pop {from {opacity:0;transform:scale(.95)} to {opacity:1;transform:scale(1)}}\n@keyframes mova-prompt-eq {0%,100% {transform:scaleY(.35)} 50% {transform:scaleY(1)}}\n@media(prefers-reduced-motion:reduce) { [data-mova-prompt], [data-mova-prompt] * {animation:none!important;transition:none!important;scroll-behavior:auto!important;} }\n`;\n\n/* The built-in \"prism\" palette is only cyan→indigo→magenta, so a sweep\n * reads as blue/purple. Build a true full-spectrum rainbow instead. */\ntype GlimmRuntime = typeof import(\"glimm\");\n\n\n/* ─────────────────────────────────────────────────────────\n * PROMPT BAR\n * A composer with real controls: attach, @ data sources,\n * / commands, a model picker, dictation, and send.\n * Type @ or / to open the menus; ↑↓ + Enter to pick.\n * Variants: Rounded (card radius) · Pill (full radius).\n * ───────────────────────────────────────────────────────── */\n\nfunction Icon({ children, size = 15, strokeWidth = 1.8 }: { children: React.ReactNode; size?: number; strokeWidth?: number }) {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n {children}\n </svg>\n );\n}\n\nconst GLYPHS: Record<string, React.ReactNode> = {\n clip: <path d=\"m21.4 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\" />,\n chart: <path d=\"M4 20V10M10 20V4M16 20v-7M22 20H2\" />,\n layers: <g><path d=\"M12 2 2 7l10 5 10-5-10-5z\" /><path d=\"M2 17l10 5 10-5M2 12l10 5 10-5\" /></g>,\n globe: <g><circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></g>,\n};\n\n/* real product marks, inline so the file stays self-contained */\nconst BRANDS: Record<string, React.ReactNode> = {\n figma: (\n <svg width=\"11\" height=\"16\" viewBox=\"0 0 38 57\" aria-hidden=\"true\">\n <path d=\"M9.5 57A9.5 9.5 0 0 0 19 47.5V38H9.5a9.5 9.5 0 0 0 0 19z\" fill=\"#0ACF83\" />\n <path d=\"M0 28.5A9.5 9.5 0 0 1 9.5 19H19v19H9.5A9.5 9.5 0 0 1 0 28.5z\" fill=\"#A259FF\" />\n <path d=\"M0 9.5A9.5 9.5 0 0 1 9.5 0H19v19H9.5A9.5 9.5 0 0 1 0 9.5z\" fill=\"#F24E1E\" />\n <path d=\"M19 0h9.5a9.5 9.5 0 1 1 0 19H19V0z\" fill=\"#FF7262\" />\n <path d=\"M38 28.5a9.5 9.5 0 1 1-19 0 9.5 9.5 0 0 1 19 0z\" fill=\"#1ABCFE\" />\n </svg>\n ),\n slack: (\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 127 127\" aria-hidden=\"true\">\n <path d=\"M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z\" fill=\"#E01E5A\" />\n <path d=\"M47 27.2c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.7 39.7.8 47 .8c7.3 0 13.2 5.9 13.2 13.2v13.2H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.3.7 54.4.7 47.1c0-7.3 5.9-13.2 13.2-13.2H47z\" fill=\"#36C5F0\" />\n <path d=\"M99.9 47.1c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V47.1zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.9C66.9 6.6 72.8.7 80.1.7c7.3 0 13.2 5.9 13.2 13.2v33.2z\" fill=\"#2EB67D\" />\n <path d=\"M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z\" fill=\"#ECB22E\" />\n </svg>\n ),\n gmail: (\n <svg width=\"15\" height=\"12\" viewBox=\"0 0 256 193\" aria-hidden=\"true\">\n <path d=\"M58.182 192.05V93.14L27.507 65.077 0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455h40.727Z\" fill=\"#4285F4\" />\n <path d=\"M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837-27.026 25.798v98.91Z\" fill=\"#34A853\" />\n <path d=\"m58.182 93.14-4.174-38.647 4.174-36.989L128 69.868l69.818-52.364 4.669 34.992-4.669 40.644L128 145.504 58.182 93.14Z\" fill=\"#EA4335\" />\n <path d=\"M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945l-16.292 12.218Z\" fill=\"#FBBC04\" />\n <path d=\"m0 49.504 26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23v23.273Z\" fill=\"#C5221F\" />\n </svg>\n ),\n};\n\ntype Source = {\n key: string;\n name: string;\n desc: string;\n glyph?: string;\n brand?: string;\n attach?: boolean;\n connect?: boolean;\n};\n\nconst SOURCES: Source[] = [\n { key: \"attach\", name: \"Add photos & files\", desc: \"Upload from your computer\", glyph: \"clip\", attach: true },\n { key: \"scoop\", name: \"Scoop Data\", desc: \"Sales & churn metrics\", glyph: \"chart\" },\n { key: \"flavors\", name: \"Flavor records\", desc: \"26 makers, tags, links\", glyph: \"layers\" },\n { key: \"web\", name: \"Web search\", desc: \"Real-time news and info\", glyph: \"globe\" },\n { key: \"figma\", name: \"Figma\", desc: \"Design-to-code workflows\", brand: \"figma\" },\n { key: \"slack\", name: \"Slack\", desc: \"Read and manage Slack\", brand: \"slack\" },\n { key: \"gmail\", name: \"Gmail\", desc: \"Read and manage Gmail\", brand: \"gmail\", connect: true },\n];\n\nconst COMMANDS = [\n { key: \"compare\", name: \"/compare\", desc: \"Flavor vs. last summer\" },\n { key: \"churn-plan\", name: \"/churn-plan\", desc: \"Draft a churn schedule\" },\n { key: \"restock\", name: \"/restock\", desc: \"Build a reorder list\" },\n { key: \"draft-email\", name: \"/draft-email\", desc: \"Write a supplier email\" },\n { key: \"summarize\", name: \"/summarize\", desc: \"Digest the thread so far\" },\n];\n\nconst MODELS = [\n { key: \"sprinkles-5\", name: \"Sprinkles 5\", tag: \"Flagship\" },\n { key: \"vanilla-1\", name: \"Vanilla 1\", tag: \"Basic\" },\n { key: \"freezer-burn\", name: \"Freezer Burn 0.4\", tag: \"Stale\" },\n];\n\n\n\n/* self-running demo: walk the @ menu, then the / menu, and repeat.\n * Any pointer or key interaction hands control to the user. */\nconst AUTO_STEPS: {\n draft: string;\n active?: number;\n connect?: boolean;\n modelOpen?: boolean;\n model?: string;\n hold: number;\n}[] = [\n { draft: \"\", connect: false, model: \"vanilla-1\", hold: 1100 },\n { draft: \"@\", active: 0, hold: 900 },\n { draft: \"@\", active: 1, hold: 620 },\n { draft: \"@\", active: 4, hold: 620 },\n { draft: \"@\", active: 6, hold: 700 },\n { draft: \"@\", active: 6, connect: true, hold: 1000 },\n { draft: \"\", hold: 700 },\n { draft: \"/\", active: 0, hold: 900 },\n { draft: \"/\", active: 1, hold: 620 },\n { draft: \"/\", active: 3, hold: 1000 },\n { draft: \"\", hold: 800 },\n // open the model picker and upgrade to the flagship → rainbow sweep\n { draft: \"\", modelOpen: true, hold: 1200 },\n { draft: \"\", model: \"sprinkles-5\", hold: 2400 },\n { draft: \"\", hold: 900 },\n];\n\n/* the last @word or /word being typed, if any */\nfunction parseToken(draft: string): { kind: \"at\" | \"slash\"; query: string; start: number } | null {\n const match = /(^|\\s)([@/])([\\w-]*)$/.exec(draft);\n if (!match) return null;\n return {\n kind: match[2] === \"@\" ? \"at\" : \"slash\",\n query: match[3].toLowerCase(),\n start: match.index + match[1].length,\n };\n}\n\nexport function AIPromptBar({\n theme = \"dark\",\n className = \"\",\n variant = \"Rounded\",\n demo = true,\n tall = false,\n placeholder,\n onSend,\n}: {\n theme?: \"dark\" | \"light\";\n className?: string;\n variant?: string;\n /** the self-running walkthrough; turn off when embedding in a real surface */\n demo?: boolean;\n /** hero sizing: a multi-line input with controls on their own row */\n tall?: boolean;\n placeholder?: string;\n /** The first argument preserves the original text-only callback API. */\n onSend?: (text: string, submission: {\n files: File[];\n sources: Array<{ key: string; name: string; desc: string }>;\n model: { key: string; name: string; tag: string };\n }) => void;\n}) {\n const sourceMenuId = useId();\n const modelMenuId = useId();\n const [feedback, setFeedback] = useState(\"\");\n const fileRef = useRef<HTMLInputElement>(null);\n const recognitionRef = useRef<SpeechRecognizer | null>(null);\n const sweepRef = useRef<SweepHandle | null>(null);\n const glimmRuntimeRef = useRef<{ api: GlimmRuntime; rainbow: Palette } | null>(null);\n const pendingSweepRef = useRef(false);\n const rootRef = useRef<HTMLDivElement>(null);\n const pill = variant === \"Pill\";\n const [draft, setDraft] = useState(\"\");\n const [dismissed, setDismissed] = useState(false);\n const [plusOpen, setPlusOpen] = useState(false);\n const [modelOpen, setModelOpen] = useState(false);\n const [model, setModel] = useState(MODELS[1]);\n const [attachments, setAttachments] = useState<File[]>([]);\n const [selectedSources, setSelectedSources] = useState<Source[]>([]);\n const [connected, setConnected] = useState(false);\n const [active, setActive] = useState(0);\n const [listening, setListening] = useState(false);\n const [auto, setAuto] = useState(demo);\n const [autoStep, setAutoStep] = useState(0);\n const [expanded, setExpanded] = useState(false);\n const wide = expanded || tall;\n const [rowBox, setRowBox] = useState<{ top: number; height: number } | null>(null);\n const [engaged, setEngaged] = useState(false);\n const [modelBox, setModelBox] = useState<{ top: number; height: number } | null>(null);\n const [modelHovered, setModelHovered] = useState<number | null>(null);\n const [modelMenuLeft, setModelMenuLeft] = useState(0);\n const [modelMenuBottom, setModelMenuBottom] = useState(0);\n const composerAnchorRef = useRef<HTMLDivElement>(null);\n const controlsRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const measureRef = useRef<HTMLSpanElement>(null);\n const modelRef = useRef<HTMLButtonElement>(null);\n const modelMenuRef = useRef<HTMLDivElement>(null);\n const rowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const modelRowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const glimmRef = useRef<HTMLCanvasElement>(null);\n const shaderRef = useRef<ShaderController | null>(null);\n const sweepingRef = useRef(false);\n\n /* hand control to the user: stop the demo loop, and when they aim at\n * the input itself, clear the demo's leftover draft for a clean start */\n const takeOver = (event: { target: EventTarget | null }) => {\n setAuto(false);\n if (auto && event.target === inputRef.current) setDraft(\"\");\n };\n\n const token = dismissed ? null : parseToken(draft);\n const menu: \"at\" | \"slash\" | null = plusOpen ? \"at\" : token?.kind ?? null;\n const query = plusOpen ? \"\" : token?.query ?? \"\";\n\n const rows: { key: string; name: string; desc: string }[] =\n menu === \"at\"\n ? SOURCES.filter((s) => s.name.toLowerCase().includes(query))\n : menu === \"slash\"\n ? COMMANDS.filter((c) => c.name.slice(1).startsWith(query))\n : [];\n\n useEffect(() => {\n setActive(0);\n setEngaged(false);\n }, [menu, query]);\n\n /* a single highlight glides to the active row instead of each row\n * toggling its own background — matches the gliding pill in the nav */\n useLayoutEffect(() => {\n const target = rowRefs.current[active];\n if (target) setRowBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [menu, query, active, connected, rows.length]);\n\n /* same gliding highlight in the model menu — floats to the hovered\n * row, falling back to the currently-selected model */\n const modelIndex = MODELS.findIndex((m) => m.key === model.key);\n useLayoutEffect(() => {\n if (!modelOpen) return;\n const target = modelRowRefs.current[modelHovered ?? modelIndex];\n if (target) setModelBox({ top: target.offsetTop, height: target.offsetHeight });\n }, [modelOpen, modelHovered, modelIndex]);\n\n /* The menu is outside the clipped composer. Reposition it when the\n * preview resizes as well as when it opens. */\n useLayoutEffect(() => {\n if (!modelOpen || !composerAnchorRef.current || !modelRef.current) return;\n const reposition = () => {\n const anchorRect = composerAnchorRef.current?.getBoundingClientRect();\n const triggerRect = modelRef.current?.getBoundingClientRect();\n if (!anchorRect || !triggerRect) return;\n const menuWidth = modelMenuRef.current?.offsetWidth ?? 208;\n setModelMenuLeft(Math.max(0, Math.min(triggerRect.left - anchorRect.left, anchorRect.width - menuWidth)));\n setModelMenuBottom(anchorRect.bottom - triggerRect.top + 8);\n };\n reposition();\n const observer = new ResizeObserver(reposition);\n observer.observe(composerAnchorRef.current);\n window.addEventListener(\"resize\", reposition);\n return () => {\n observer.disconnect();\n window.removeEventListener(\"resize\", reposition);\n };\n }, [modelOpen, wide, model.name]);\n\n useEffect(() => {\n if (!modelOpen) setModelHovered(null);\n }, [modelOpen]);\n\n /* Build the shader with a pinned hue phase. createShader seeds its\n * internal hueShift from Math.random(), which made the sweep a different\n * colour on every reload — pin it so the rainbow is identical each time. */\n const makeShader = () => {\n const canvas = glimmRef.current;\n const runtime = glimmRuntimeRef.current;\n if (!runtime || !canvas || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return null;\n const random = Math.random;\n Math.random = () => 0;\n try {\n return runtime.api.createShader({\n canvas,\n palette: runtime.rainbow,\n direction: \"ltr\",\n bandTight: 10,\n swellAmount: 0.85,\n });\n } catch {\n return null;\n } finally {\n Math.random = random;\n }\n };\n\n const stopSweep = () => {\n pendingSweepRef.current = false;\n sweepingRef.current = false;\n sweepRef.current?.cancel();\n sweepRef.current = null;\n shaderRef.current?.destroy();\n shaderRef.current = null;\n };\n\n /* Glimm shader lives inside the composer, invisible at rest. Selecting\n * the flagship model fires a one-shot rainbow sweep across the interior. */\n useEffect(() => {\n let disposed = false;\n const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const motionChanged = () => { if (motionQuery.matches) stopSweep(); };\n motionQuery.addEventListener(\"change\", motionChanged);\n // Glimm is ESM-only: keep module evaluation safe for CJS and SSR consumers.\n void import(\"glimm\").then((api) => {\n if (disposed) return;\n const { ACCENTS, accentChain } = api;\n glimmRuntimeRef.current = {\n api,\n rainbow: accentChain([ACCENTS.red, ACCENTS.orange, ACCENTS.yellow, ACCENTS.green, ACCENTS.cyan, ACCENTS.blue, ACCENTS.purple]),\n };\n if (pendingSweepRef.current) celebrate();\n }).catch(() => {\n // The composer remains functional if the optional visual cannot load.\n if (!disposed) pendingSweepRef.current = false;\n });\n return () => {\n disposed = true;\n motionQuery.removeEventListener(\"change\", motionChanged);\n stopSweep();\n glimmRuntimeRef.current = null;\n recognitionRef.current?.abort();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const celebrate = () => {\n if (sweepingRef.current) return;\n if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const runtime = glimmRuntimeRef.current;\n if (!runtime) { pendingSweepRef.current = true; return; }\n pendingSweepRef.current = false;\n // Recreate the shader per sweep so uTime restarts at 0 — the hue phase\n // (which drifts with time) is then identical on every trigger.\n shaderRef.current?.destroy();\n const shader = makeShader();\n shaderRef.current = shader;\n if (!shader) return;\n sweepingRef.current = true;\n const sweep = runtime.api.playSweep(shader, {\n palette: runtime.rainbow,\n direction: \"ltr\",\n sweepMs: 570,\n outroMs: 80,\n peakAlpha: 1.3,\n bandTight: 10,\n brightness: 1.4,\n swellAmount: 1,\n waveSpeed: 1.8,\n easing: \"easeOutExpo\",\n });\n sweepRef.current = sweep;\n sweep.done.finally(() => {\n if (sweepRef.current === sweep) {\n sweepingRef.current = false;\n sweepRef.current = null;\n shader.destroy();\n if (shaderRef.current === shader) shaderRef.current = null;\n }\n });\n };\n\n const selectModel = (next: (typeof MODELS)[number]) => {\n setModel(next);\n setModelOpen(false);\n if (next.key === \"sprinkles-5\") celebrate();\n else stopSweep();\n };\n\n /* autoplay: apply the current step, then advance after its hold */\n useEffect(() => {\n if (!auto || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n const step = AUTO_STEPS[autoStep % AUTO_STEPS.length];\n setDraft(step.draft);\n if (step.active !== undefined) setActive(step.active);\n if (step.connect !== undefined) setConnected(step.connect);\n if (step.modelOpen !== undefined) setModelOpen(step.modelOpen);\n if (step.model) {\n const next = MODELS.find((m) => m.key === step.model);\n if (next) selectModel(next);\n }\n const t = setTimeout(() => setAutoStep((s) => s + 1), step.hold);\n return () => clearTimeout(t);\n }, [auto, autoStep]);\n\n const toggleDictation = () => {\n if (listening) { recognitionRef.current?.stop(); return; }\n const speechWindow = window as Window & { SpeechRecognition?: new () => SpeechRecognizer; webkitSpeechRecognition?: new () => SpeechRecognizer };\n const Recognition = speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;\n if (!Recognition) { setFeedback(\"Dictation is unavailable in this browser. You can type your message instead.\"); return; }\n const recognition = new Recognition();\n recognitionRef.current = recognition;\n recognition.lang = navigator.language;\n recognition.interimResults = false;\n recognition.onresult = (event) => {\n const text = Array.from(event.results).map((result) => result[0].transcript).join(\" \");\n setDraft((current) => `${current.trimEnd()} ${text}`.trimStart());\n setFeedback(\"Dictation added to your message.\");\n };\n recognition.onend = () => { setListening(false); inputRef.current?.focus(); };\n recognition.onerror = () => { setListening(false); setFeedback(\"Dictation could not start. Check microphone access or type your message.\"); };\n try { recognition.start(); setListening(true); setFeedback(\"Listening through your microphone…\"); }\n catch { setListening(false); setFeedback(\"Dictation is unavailable right now.\"); }\n };\n\n /* Move wrapped text above the controls, then grow to a compact maximum. */\n useLayoutEffect(() => {\n const input = inputRef.current;\n const controls = controlsRef.current;\n const measure = measureRef.current;\n const modelButton = modelRef.current;\n if (!input || !controls || !measure || !modelButton) return;\n\n const fixedControlsWidth = 28 * 3 + modelButton.offsetWidth;\n const inlineGaps = 4 * 4;\n const inlineInputWidth = controls.clientWidth - fixedControlsWidth - inlineGaps;\n const needsFullWidth = draft.includes(\"\\n\") || measure.offsetWidth + 8 > inlineInputWidth;\n if (needsFullWidth !== expanded) {\n setExpanded(needsFullWidth);\n }\n\n const minHeight = 28;\n const maxHeight = 100;\n input.style.height = \"0px\";\n const contentHeight = input.scrollHeight;\n input.style.height = `${Math.min(Math.max(contentHeight, minHeight), maxHeight)}px`;\n input.style.overflowY = contentHeight > maxHeight ? \"auto\" : \"hidden\";\n }, [draft, expanded]);\n\n /* clicking anywhere outside the composer closes the open menus */\n useEffect(() => {\n if (!modelOpen && !plusOpen) return;\n const close = (event: PointerEvent) => {\n if (!rootRef.current?.contains(event.target as Node)) {\n setDismissed(true);\n setModelOpen(false);\n setPlusOpen(false);\n }\n };\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [modelOpen, plusOpen]);\n\n const closeMenus = () => {\n setPlusOpen(false);\n setModelOpen(false);\n };\n\n const pick = (row: { key: string; name: string }) => {\n const source = SOURCES.find((s) => s.key === row.key);\n if (source?.attach) {\n fileRef.current?.click();\n if (token) setDraft(draft.slice(0, token.start));\n } else if (menu === \"at\") {\n if (source) setSelectedSources((current) => current.some((item) => item.key === source.key) ? current : [...current, source]);\n if (source?.connect) { setConnected(true); setFeedback(`${source.name} selected for this demo. Connect your account in your app to use live data.`); }\n setDraft(`${token ? draft.slice(0, token.start) : draft}@${row.name} `);\n } else {\n setDraft(`${token ? draft.slice(0, token.start) : draft}${row.name} `);\n }\n setPlusOpen(false);\n setDismissed(false);\n inputRef.current?.focus();\n };\n\n const canSend = draft.trim().length > 0 || attachments.length > 0;\n const send = () => {\n if (!canSend) return;\n onSend?.(draft.trim(), {\n files: [...attachments],\n sources: selectedSources.filter((source) => draft.includes(`@${source.name}`)).map(({ key, name, desc }) => ({ key, name, desc })),\n model: { ...model },\n });\n setFeedback(`Sent${attachments.length ? ` with ${attachments.length} attachment${attachments.length === 1 ? \"\" : \"s\"}` : \"\"}: ${draft.trim() || attachments.map((file) => file.name).join(\", \")}`);\n setDraft(\"\");\n setAttachments([]);\n setSelectedSources([]);\n closeMenus();\n };\n\n return (\n <div\n ref={rootRef}\n data-mova-prompt\n data-theme={theme}\n data-promptbar\n className={`mova-ai-theme ${demo ? \"flex min-h-[384px] w-full max-w-[420px] flex-col justify-end pb-8\" : \"w-full\"} ${className}`}\n onPointerDownCapture={takeOver}\n onKeyDownCapture={(event) => { takeOver(event); if (event.key === \"Escape\") { event.stopPropagation(); setDismissed(true); closeMenus(); inputRef.current?.focus(); } }}\n >\n <style>{AI_THEME_STYLES}{COMPONENT_STYLES}</style>\n <input ref={fileRef} type=\"file\" multiple className=\"hidden\" aria-label=\"Attach files\" onChange={(event) => { const files = Array.from(event.target.files ?? []); setAttachments((current) => [...current, ...files]); setFeedback(`${files.length} file${files.length === 1 ? \"\" : \"s\"} attached.`); event.target.value = \"\"; inputRef.current?.focus(); }} />\n {/* composer is the anchor — menus grow up from its top edge */}\n <div ref={composerAnchorRef} className=\"relative\">\n {/* ── @ / slash menu ─────────────────────────────── */}\n {menu && (\n <div\n id={sourceMenuId}\n role=\"listbox\" aria-label=\"Sources and commands\"\n onMouseLeave={() => setEngaged(false)}\n className=\"absolute inset-x-0 bottom-full z-10 mb-2 max-h-[min(320px,60dvh)] overflow-y-auto rounded-[10px] bg-[var(--mova-ai-surface,#fff)] p-1 shadow-[var(--mova-ai-shadow-raised,none)]\"\n style={{ animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom center\" }}\n >\n {/* single gliding highlight — appears once a row is hovered */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--mova-ai-hover,#f1f1f1)]\"\n style={{\n top: rowBox?.top ?? 0,\n height: rowBox?.height ?? 0,\n opacity: rowBox && engaged && rows.length > 0 ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {rows.map((row, i) => {\n const source = menu === \"at\" ? SOURCES.find((s) => s.key === row.key) : undefined;\n return (\n <button\n key={row.key}\n id={`${sourceMenuId}-${i}`} role=\"option\" aria-selected={i === active}\n type=\"button\"\n ref={(el) => {\n rowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => {\n setActive(i);\n setEngaged(true);\n }}\n onFocus={() => { setActive(i); setEngaged(true); }}\n onClick={() => pick(row)}\n className=\"relative z-10 flex h-9 w-full items-center gap-2.5 rounded-[6px] px-2 text-left\"\n >\n {source && (\n <span className=\"flex size-5.5 shrink-0 items-center justify-center text-[var(--mova-ai-ink-secondary,#666)]\">\n {source.brand ? BRANDS[source.brand] : <Icon size={15}>{GLYPHS[source.glyph ?? \"clip\"]}</Icon>}\n </span>\n )}\n <span className=\"shrink-0 text-[12.5px] font-medium text-[var(--mova-ai-ink,#202020)]\">\n {row.name}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-[12px] text-[var(--mova-ai-ink-muted,#858585)]\">{row.desc}</span>\n {source?.connect && (\n <span\n className={`shrink-0 text-[12px] font-medium transition-colors duration-100 ${\n connected ? \"text-[var(--mova-ai-success,#28734d)]\" : \"text-[var(--mova-ai-accent-ink,#333)] hover:underline\"\n }`}\n >\n {connected ? \"Connected\" : \"Connect\"}\n </span>\n )}\n </button>\n );\n })}\n {rows.length === 0 && (\n <div className=\"flex h-9 items-center px-2 text-[12px] text-[var(--mova-ai-ink-muted,#858585)]\">\n No matches for “{query}”\n </div>\n )}\n <div className=\"mt-1 border-t border-[var(--mova-ai-border,#dedede)] px-2 pt-1.5 pb-1 text-[11px] text-[var(--mova-ai-ink-muted,#858585)]\">\n {menu === \"at\" ? \"Type to search sources & files\" : \"Type to search commands\"}\n </div>\n </div>\n )}\n\n {/* ── model menu ─────────────────────────────────── */}\n {modelOpen && (\n <div\n ref={modelMenuRef}\n id={modelMenuId}\n role=\"group\" aria-label=\"Available models\"\n onKeyDown={(event) => { if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") { event.preventDefault(); const current = modelRowRefs.current.indexOf(document.activeElement as HTMLButtonElement); modelRowRefs.current[(current + (event.key === \"ArrowDown\" ? 1 : MODELS.length - 1)) % MODELS.length]?.focus(); } }}\n onMouseLeave={() => setModelHovered(null)}\n className=\"absolute z-10 w-52 max-w-full rounded-[10px] bg-[var(--mova-ai-surface,#fff)] p-1 shadow-[var(--mova-ai-shadow-raised,none)]\"\n style={{ left: modelMenuLeft, bottom: modelMenuBottom, animation: \"mova-prompt-pop 180ms cubic-bezier(0.23,1,0.32,1) both\", transformOrigin: \"bottom left\" }}\n >\n {/* single gliding highlight — floats to the hovered / selected row */}\n <span\n aria-hidden\n className=\"pointer-events-none absolute inset-x-1 rounded-[6px] bg-[var(--mova-ai-hover,#f1f1f1)]\"\n style={{\n top: modelBox?.top ?? 0,\n height: modelBox?.height ?? 0,\n opacity: modelBox && modelHovered !== null ? 1 : 0,\n transition:\n \"top 220ms cubic-bezier(0.23,1,0.32,1), height 220ms cubic-bezier(0.23,1,0.32,1), opacity 150ms ease\",\n }}\n />\n {MODELS.map((m, i) => (\n <button\n key={m.key}\n type=\"button\"\n ref={(el) => {\n modelRowRefs.current[i] = el;\n }}\n onMouseDown={(event) => event.preventDefault()}\n onFocus={() => setModelHovered(i)}\n onMouseEnter={() => setModelHovered(i)}\n onClick={() => {\n selectModel(m);\n inputRef.current?.focus();\n }}\n className=\"relative z-10 flex h-7.5 w-full items-center gap-2 rounded-[6px] px-2 text-left\"\n >\n <span className=\"min-w-0 flex-1 truncate text-[12.5px] font-medium text-[var(--mova-ai-ink,#202020)]\">{m.name}</span>\n <span className=\"shrink-0 text-[11px] text-[var(--mova-ai-ink-muted,#858585)]\">{m.tag}</span>\n <span className={`shrink-0 text-[var(--mova-ai-ink,#202020)] ${m.key === model.key ? \"\" : \"invisible\"}`}>\n <Icon size={13} strokeWidth={2.5}><path d=\"M20 6L9 17l-5-5\" /></Icon>\n </span>\n </button>\n ))}\n </div>\n )}\n\n {/* ── composer ───────────────────────────────────── */}\n <div\n className={`prompt-composer relative isolate flex flex-col overflow-hidden border border-[var(--mova-ai-border,#dedede)] bg-[var(--mova-ai-surface,#fff)] shadow-[var(--mova-ai-shadow-card,none)] transition-[border-color,border-radius] duration-150 focus-within:border-[var(--mova-ai-border-strong,#c8c8c8)] ${\n tall ? \"gap-2.5 p-3.5\" : \"gap-1.5 p-1.5\"\n } ${\n pill ? (attachments.length > 0 || wide ? \"rounded-[24px]\" : \"rounded-full\") : tall ? \"rounded-[22px]\" : \"rounded-[14px]\"\n }`}\n >\n {/* rainbow glimm sweep — plays across the interior on model change.\n explicit w/h: a <canvas> is a replaced element and won't stretch\n to inset-0 alone, which feeds back into the shader's ResizeObserver. */}\n <canvas\n ref={glimmRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-0 -z-10 h-full w-full\"\n style={{ borderRadius: \"inherit\" }}\n />\n <span\n ref={measureRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute invisible whitespace-pre text-[13px] leading-[18px]\"\n >\n {draft}\n </span>\n\n {attachments.length > 0 && (\n <div className={`flex flex-wrap gap-1.5 pt-0.5 ${pill ? \"px-1\" : \"px-0.5\"}`}>\n {attachments.map((file, i) => (\n <span\n key={`${file.name}-${i}`}\n className={`flex h-6.5 items-center gap-1.5 bg-[var(--mova-ai-field,#f6f6f6)] py-1 pr-1 pl-1.5 text-[11.5px] text-[var(--mova-ai-ink-secondary,#666)] shadow-[var(--mova-ai-shadow-hairline,none)] ${\n pill ? \"rounded-full\" : \"rounded-[6px]\"\n }`}\n style={{ animation: \"mova-prompt-pop 200ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <Icon size={12}><g><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /></g></Icon>\n <span className=\"max-w-36 truncate\">{file.name}</span>\n <button\n type=\"button\"\n aria-label={`Remove ${file.name}`}\n onClick={() => setAttachments((current) => current.filter((_, j) => j !== i))}\n className={`-my-1 flex size-6 items-center justify-center text-[var(--mova-ai-ink-muted,#858585)] transition-colors duration-100 hover:bg-[color-mix(in_srgb,var(--mova-ai-border,#dedede)_70%,transparent)] hover:text-[var(--mova-ai-ink,#202020)] ${\n pill ? \"rounded-full\" : \"rounded-[5px]\"\n }`}\n >\n <Icon size={10} strokeWidth={2.5}><path d=\"M18 6L6 18M6 6l12 12\" /></Icon>\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div\n ref={controlsRef}\n className={`grid items-end gap-x-1 gap-y-1.5 ${\n wide\n ? \"grid-cols-[28px_auto_minmax(0,1fr)_28px_28px]\"\n : \"grid-cols-[28px_minmax(0,1fr)_auto_28px_28px]\"\n }`}\n >\n <button\n type=\"button\"\n aria-label=\"Add attachments and sources\"\n aria-expanded={plusOpen}\n onClick={() => {\n setModelOpen(false);\n setPlusOpen((current) => !current);\n inputRef.current?.focus();\n }}\n className={`flex size-7 shrink-0 items-center justify-center justify-self-start text-[var(--mova-ai-ink-muted,#858585)] transition-[background-color,color,transform] duration-150 hover:bg-[var(--mova-ai-hover,#f1f1f1)] hover:text-[var(--mova-ai-ink,#202020)] active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${plusOpen ? \"bg-[var(--mova-ai-hover,#f1f1f1)] text-[var(--mova-ai-ink,#202020)]\" : \"\"} ${wide ? \"col-start-1 row-start-2\" : \"col-start-1 row-start-1\"}`}\n >\n <Icon size={16} strokeWidth={2}><path d=\"M12 5v14M5 12h14\" /></Icon>\n </button>\n\n <textarea\n ref={inputRef}\n rows={1}\n value={draft}\n onChange={(event) => {\n setDraft(event.target.value);\n setDismissed(false);\n setPlusOpen(false);\n }}\n onKeyDown={(event) => {\n if (event.nativeEvent.isComposing) return;\n if (menu && rows.length > 0) {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n setEngaged(true);\n setActive((current) => (current + (event.key === \"ArrowDown\" ? 1 : rows.length - 1)) % rows.length);\n return;\n }\n if ((event.key === \"Enter\" && !event.shiftKey) || event.key === \"Tab\") {\n event.preventDefault();\n pick(rows[active]);\n return;\n }\n }\n if (event.key === \"Escape\") {\n setDismissed(true);\n closeMenus();\n return;\n }\n if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n event.preventDefault();\n send();\n }\n }}\n placeholder={listening ? \"Listening…\" : placeholder ?? \"Write a message…\"}\n aria-label=\"Prompt\"\n aria-expanded={!!menu}\n aria-controls={menu ? sourceMenuId : undefined}\n aria-activedescendant={menu && rows[active] ? `${sourceMenuId}-${active}` : undefined}\n className={`${tall ? \"min-h-[68px] px-2 py-2 text-[14px] leading-5\" : \"min-h-7 px-1 py-[5px] text-[13px] leading-[18px]\"} min-w-0 w-full resize-none bg-transparent text-[var(--mova-ai-ink,#202020)] outline-none [overflow-wrap:anywhere] placeholder:text-[var(--mova-ai-ink-muted,#858585)] ${\n wide ? \"col-span-full col-start-1 row-start-1\" : \"col-start-2 row-start-1\"\n }`}\n />\n\n {/* model picker */}\n <button\n ref={modelRef}\n type=\"button\"\n aria-expanded={modelOpen}\n aria-label=\"Choose model\"\n aria-controls={modelMenuId}\n onKeyDown={(event) => { if (event.key === \"ArrowDown\") { event.preventDefault(); setPlusOpen(false); setDismissed(true); setModelOpen(true); requestAnimationFrame(() => modelRowRefs.current[modelIndex]?.focus()); } }}\n onClick={() => {\n setPlusOpen(false);\n setDismissed(true);\n setModelOpen((current) => !current);\n }}\n className={`flex h-7 shrink-0 items-center gap-1 px-1.5 text-[12px] font-medium text-[var(--mova-ai-ink-secondary,#666)] transition-colors duration-150 hover:bg-[var(--mova-ai-hover,#f1f1f1)] hover:text-[var(--mova-ai-ink,#202020)] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-2 row-start-2 justify-self-start\" : \"col-start-3 row-start-1\"}`}\n >\n {model.name}\n <span className=\"text-[var(--mova-ai-ink-muted,#858585)]\">\n <Icon size={11} strokeWidth={2.4}><path d=\"M6 9l6 6 6-6\" /></Icon>\n </span>\n </button>\n\n {/* dictation */}\n <button\n type=\"button\"\n aria-label={listening ? \"Stop dictation\" : \"Start dictation\"}\n aria-pressed={listening}\n onClick={toggleDictation}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-150 active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${listening ? \"bg-[var(--mova-ai-accent-tint,#eee)] text-[var(--mova-ai-accent-ink,#333)]\" : \"text-[var(--mova-ai-ink-muted,#858585)] hover:bg-[var(--mova-ai-hover,#f1f1f1)] hover:text-[var(--mova-ai-ink,#202020)]\"} ${wide ? \"col-start-4 row-start-2\" : \"col-start-4 row-start-1\"}`}\n >\n {listening ? (\n <span className=\"flex h-3.5 items-center gap-[2.5px]\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"w-[2.5px] rounded-full bg-current\"\n style={{ height: \"100%\", animation: `mova-prompt-eq 900ms ease-in-out ${i * 150}ms infinite` }}\n />\n ))}\n </span>\n ) : (\n <Icon size={15} strokeWidth={2}><g><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z\" /><path d=\"M19 10v2a7 7 0 0 1-14 0v-2M12 19v3\" /></g></Icon>\n )}\n </button>\n\n {/* send — tactile square (round in the pill variant) */}\n <button\n type=\"button\"\n aria-label=\"Send\"\n disabled={!canSend}\n onClick={send}\n className={`flex size-7 shrink-0 items-center justify-center transition-[background-color,color,transform] duration-200 enabled:active:scale-[0.94] ${\n pill ? \"rounded-full\" : \"rounded-[8px]\"\n } ${wide ? \"col-start-5 row-start-2\" : \"col-start-5 row-start-1\"}`}\n style={{\n background: canSend ? \"var(--mova-ai-ink,#202020)\" : \"var(--mova-ai-border-strong,#c8c8c8)\",\n color: canSend ? \"var(--mova-ai-surface,#fff)\" : \"var(--mova-ai-ink-secondary,#666)\",\n }}\n >\n <Icon size={16} strokeWidth={2.4}><path d=\"M12 19V5M5 12l7-7 7 7\" /></Icon>\n </button>\n </div>\n </div>\n </div>\n <p role=\"status\" className=\"mt-2 min-h-4 break-words text-[11px] text-[var(--mova-ai-ink-muted,#858585)]\">{feedback}</p>\n </div>\n );\n}\n\nexport default AIPromptBar;\n"
17
17
  }