@heyramzi/ui 7.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/COMPONENTS.md +28 -17
  2. package/README.md +6 -2
  3. package/package.json +25 -15
  4. package/scripts/sync-shadcn.mjs +17 -13
  5. package/scripts/sync-transitions.mjs +11 -11
  6. package/src/agents/agent-avatar.svelte +67 -114
  7. package/src/agents/agent-cast.svelte +31 -7
  8. package/src/agents/demo.svelte +55 -8
  9. package/src/agents/helpers.ts +58 -0
  10. package/src/agents/page-state.ts +70 -0
  11. package/src/agents/types.ts +3 -1
  12. package/src/ai/ai-button/AiButton.svelte +86 -0
  13. package/src/ai/ai-button/index.ts +4 -0
  14. package/src/ai/confirmation/Confirmation.svelte +8 -8
  15. package/src/ai/confirmation/confirmation-context.svelte.ts +2 -2
  16. package/src/ai/generation-progress/GenerationProgress.svelte +354 -0
  17. package/src/ai/generation-progress/index.ts +7 -0
  18. package/src/ai/index.ts +16 -6
  19. package/src/ai/prompt-input/PromptInput.svelte +19 -1
  20. package/src/ai/prompt-input/PromptInputSubmit.svelte +9 -3
  21. package/src/ai/reasoning/Reasoning.svelte +11 -8
  22. package/src/ambient.d.ts +4 -0
  23. package/src/animations/README.md +14 -14
  24. package/src/animations/index.css +3 -14
  25. package/src/animations/upstream-coverage.test.ts +1 -1
  26. package/src/components/copy/clipboard.test.ts +206 -0
  27. package/src/components/copy/clipboard.ts +49 -0
  28. package/src/components/copy/copied.svelte.ts +39 -0
  29. package/src/components/copy/copy-button.svelte +175 -0
  30. package/src/components/copy/copy-button.test.ts +110 -0
  31. package/src/components/copy/index.ts +3 -0
  32. package/src/components/data-table/data-table.test.ts +134 -0
  33. package/src/components/data-table/data-table.ts +77 -0
  34. package/src/components/data-table/index.ts +20 -0
  35. package/src/components/data-table/infinite-scroll.svelte +69 -0
  36. package/src/components/data-table/pagination.svelte +62 -0
  37. package/src/components/data-table/row-arrow.svelte +28 -0
  38. package/src/components/data-table/row-context-menu.svelte +106 -0
  39. package/src/components/data-table/row-context-menu.test.ts +18 -0
  40. package/src/components/data-table/sortable-head.svelte +56 -0
  41. package/src/components/data-table/table-skeleton.svelte +83 -0
  42. package/src/components/hold-to-confirm/hold-to-confirm.svelte +232 -0
  43. package/src/components/hold-to-confirm/index.ts +1 -0
  44. package/src/components/icon/icon.svelte +23 -0
  45. package/src/components/icon/index.ts +1 -0
  46. package/src/components/language-flag/index.ts +1 -0
  47. package/src/components/language-flag/language-flag.svelte +60 -0
  48. package/src/components/list-page/detail-empty.svelte +50 -0
  49. package/src/components/list-page/detail-header.svelte +107 -0
  50. package/src/components/list-page/detail-panel.svelte +90 -0
  51. package/src/components/list-page/detail-row.svelte +36 -0
  52. package/src/components/list-page/detail-stack.svelte +33 -0
  53. package/src/components/list-page/empty-state.svelte +91 -0
  54. package/src/components/list-page/filter-item.svelte +49 -0
  55. package/src/components/list-page/filter-popover.svelte +88 -0
  56. package/src/components/list-page/filter-section.ts +12 -0
  57. package/src/components/list-page/grouped-list.svelte +116 -0
  58. package/src/components/list-page/index.ts +28 -0
  59. package/src/components/list-page/list-page-shell.svelte +195 -0
  60. package/src/components/list-page/list-table.ts +19 -0
  61. package/src/components/list-page/master-detail.svelte +206 -0
  62. package/src/components/list-page/master-detail.test.ts +37 -0
  63. package/src/components/list-page/notice-bar.svelte +68 -0
  64. package/src/components/list-page/prev-next.svelte +104 -0
  65. package/src/components/list-page/queue-rail.ts +25 -0
  66. package/src/components/list-page/rail-body.svelte +29 -0
  67. package/src/components/list-page/rail-empty.svelte +51 -0
  68. package/src/components/list-page/rail-head.svelte +49 -0
  69. package/src/components/list-page/rail-row.svelte +163 -0
  70. package/src/components/list-page/section-host.ts +24 -0
  71. package/src/components/list-page/section-label.svelte +39 -0
  72. package/src/components/list-page/step.test.ts +21 -0
  73. package/src/components/list-page/step.ts +10 -0
  74. package/src/components/list-page/toolbar-search.svelte +63 -0
  75. package/src/components/list-page/toolbar-trigger.ts +23 -0
  76. package/src/components/list-page/types.ts +1 -0
  77. package/src/components/locale-switch/index.ts +1 -0
  78. package/src/components/locale-switch/locale-switch.svelte +59 -0
  79. package/src/components/mark/index.ts +1 -0
  80. package/src/components/mark/mark.svelte +70 -0
  81. package/src/components/open-in/index.ts +6 -0
  82. package/src/components/open-in/open-in.svelte +106 -0
  83. package/src/components/picker/index.ts +4 -0
  84. package/src/components/picker/mark-picker.svelte +84 -0
  85. package/src/components/picker/picker.svelte +143 -0
  86. package/src/components/picker/trigger.ts +42 -0
  87. package/src/components/picker/types.ts +26 -0
  88. package/src/components/segmented/animated-tabs.svelte +175 -0
  89. package/src/components/segmented/collapse.test.ts +49 -0
  90. package/src/components/segmented/collapse.ts +39 -0
  91. package/src/components/segmented/index.ts +41 -0
  92. package/src/components/segmented/section-runs.svelte +79 -0
  93. package/src/components/segmented/segment.svelte +106 -0
  94. package/src/components/segmented/segmented-filter.svelte +125 -0
  95. package/src/components/segmented/segmented-nav.svelte +82 -0
  96. package/src/components/segmented/segmented-overflow.svelte +85 -0
  97. package/src/components/segmented/segmented-track.svelte +181 -0
  98. package/src/components/segmented/segmented.test.ts +20 -0
  99. package/src/components/segmented/segmented.ts +80 -0
  100. package/src/components/star-rating-input/index.ts +9 -0
  101. package/src/components/star-rating-input/star-rating-input.svelte +256 -0
  102. package/src/components/svg/index.ts +1 -0
  103. package/src/components/svg/svg.svelte +9 -0
  104. package/src/effects/border-beam/BorderBeam.svelte +1 -1
  105. package/src/effects/image-comparison/ImageComparison.css +42 -8
  106. package/src/effects/image-comparison/ImageComparison.svelte +138 -21
  107. package/src/effects/image-comparison/compare.test.ts +17 -0
  108. package/src/effects/image-comparison/compare.ts +5 -0
  109. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +338 -0
  110. package/src/effects/magic-card/MagicCard.css +47 -0
  111. package/src/effects/magic-card/MagicCard.svelte +57 -0
  112. package/src/effects/marquee/Marquee.css +27 -1
  113. package/src/effects/marquee/Marquee.svelte +67 -3
  114. package/src/effects/reveal/index.ts +2 -1
  115. package/src/effects/reveal/observe.test.ts +60 -0
  116. package/src/effects/reveal/observe.ts +33 -1
  117. package/src/effects/reveal/types.ts +7 -0
  118. package/src/effects/theme-toggle/ThemeToggle.css +199 -6
  119. package/src/effects/theme-toggle/ThemeToggle.svelte +51 -26
  120. package/src/gallery.test.ts +52 -0
  121. package/src/index.ts +5 -1
  122. package/src/lib/panel.ts +7 -1
  123. package/src/lib/theme-transition.ts +37 -11
  124. package/src/lib/tone.ts +48 -0
  125. package/src/shadcn/accordion/accordion-trigger.svelte +15 -6
  126. package/src/shadcn/alert/alert.svelte +3 -1
  127. package/src/shadcn/badge/badge.svelte +8 -7
  128. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +2 -2
  129. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +2 -2
  130. package/src/shadcn/calendar/calendar-month-select.svelte +2 -2
  131. package/src/shadcn/calendar/calendar-next-button.svelte +2 -2
  132. package/src/shadcn/calendar/calendar-prev-button.svelte +2 -2
  133. package/src/shadcn/calendar/calendar-year-select.svelte +2 -2
  134. package/src/shadcn/calendar/calendar.svelte +6 -0
  135. package/src/shadcn/code-block/code-block-copy-button.svelte +6 -55
  136. package/src/shadcn/command/index.ts +3 -0
  137. package/src/shadcn/field/index.ts +3 -0
  138. package/src/shadcn/field/simple-field.svelte +71 -0
  139. package/src/shadcn/field/simple-field.test.ts +47 -0
  140. package/src/shadcn/message-scroller/message-scroller-button.svelte +2 -2
  141. package/src/shadcn/radio-group/radio-group-item.svelte +3 -2
  142. package/src/shadcn/sheet/sheet-content.svelte +1 -0
  143. package/src/shadcn/sheet/sheet.css +48 -0
  144. package/src/shadcn/sonner/sonner.svelte +7 -10
  145. package/src/shadcn/toggle-group/toggle-group.svelte +10 -3
  146. package/src/shadcn/trace/trace-collapsible-trigger.svelte +2 -2
  147. package/src/ai/composer/Composer.css +0 -108
  148. package/src/ai/composer/Composer.svelte +0 -111
  149. package/src/ai/composer/index.ts +0 -10
  150. package/src/animations/accordion.css +0 -58
  151. package/src/animations/icon-swap.css +0 -43
  152. package/src/animations/notification-badge.css +0 -81
  153. package/src/animations/panel.css +0 -86
  154. package/src/animations/reasoning-stream.css +0 -49
  155. package/src/animations/skeleton-reveal.css +0 -97
  156. package/src/animations/streaming-text.css +0 -31
  157. package/src/animations/tabs-sliding.css +0 -83
  158. package/src/animations/texts-reveal.css +0 -70
  159. package/src/animations/tooltip.css +0 -72
  160. package/src/animations/view-transition-reveal.ts +0 -74
  161. package/src/lib/utils.ts +0 -45
  162. package/src/showcase/AiSection.svelte +0 -150
  163. package/src/showcase/AnimationsSection.svelte +0 -1658
  164. package/src/showcase/EffectsSection.svelte +0 -152
  165. package/src/showcase/ShadcnSection.svelte +0 -27
  166. package/src/showcase/ShowcaseImport.svelte +0 -126
  167. package/src/showcase/ShowcaseList.svelte +0 -132
  168. package/src/showcase/ShowcaseNav.svelte +0 -247
  169. package/src/showcase/ShowcaseSection.svelte +0 -112
  170. package/src/showcase/ShowcaseShell.svelte +0 -55
  171. package/src/showcase/ShowcaseStage.svelte +0 -342
  172. package/src/showcase/ShowcaseVariant.svelte +0 -109
  173. package/src/showcase/assets/image-reveal/art-1.jpg +0 -0
  174. package/src/showcase/assets/image-reveal/art-2.jpg +0 -0
  175. package/src/showcase/assets/image-reveal/art-3.jpg +0 -0
  176. package/src/showcase/check.svg +0 -1
  177. package/src/showcase/chevron-arms.svg +0 -2
  178. package/src/showcase/chevron-down.svg +0 -1
  179. package/src/showcase/copy.svg +0 -2
  180. package/src/showcase/demos/ai/confirmation.svelte +0 -59
  181. package/src/showcase/demos/ai/loading-state.svelte +0 -19
  182. package/src/showcase/demos/ai/prompt-input.svelte +0 -57
  183. package/src/showcase/demos/ai/reasoning.svelte +0 -19
  184. package/src/showcase/demos/ai/thinking-dots.svelte +0 -15
  185. package/src/showcase/demos/ai/voice-waveform.svelte +0 -24
  186. package/src/showcase/demos/effects/animated-beam.svelte +0 -50
  187. package/src/showcase/demos/effects/animated-group.svelte +0 -47
  188. package/src/showcase/demos/effects/border-beam-default.svelte +0 -20
  189. package/src/showcase/demos/effects/border-beam-line.svelte +0 -20
  190. package/src/showcase/demos/effects/border-beam-mono.svelte +0 -20
  191. package/src/showcase/demos/effects/border-beam-sunset.svelte +0 -20
  192. package/src/showcase/demos/effects/image-comparison.svelte +0 -25
  193. package/src/showcase/demos/effects/image-reveal.svelte +0 -49
  194. package/src/showcase/demos/effects/in-view.svelte +0 -41
  195. package/src/showcase/demos/effects/integrations-hub.svelte +0 -82
  196. package/src/showcase/demos/effects/magnetic.svelte +0 -31
  197. package/src/showcase/demos/effects/marquee.svelte +0 -33
  198. package/src/showcase/demos/effects/progressive-blur.svelte +0 -47
  199. package/src/showcase/demos/effects/spotlight.svelte +0 -40
  200. package/src/showcase/demos/effects/text-effect.svelte +0 -55
  201. package/src/showcase/demos/shadcn/accordion.svelte +0 -45
  202. package/src/showcase/demos/shadcn/alert-dialog.svelte +0 -23
  203. package/src/showcase/demos/shadcn/alert.svelte +0 -30
  204. package/src/showcase/demos/shadcn/attachment.svelte +0 -71
  205. package/src/showcase/demos/shadcn/avatar.svelte +0 -30
  206. package/src/showcase/demos/shadcn/badge.svelte +0 -26
  207. package/src/showcase/demos/shadcn/breadcrumb.svelte +0 -34
  208. package/src/showcase/demos/shadcn/bubble.svelte +0 -36
  209. package/src/showcase/demos/shadcn/button.svelte +0 -11
  210. package/src/showcase/demos/shadcn/calendar.svelte +0 -8
  211. package/src/showcase/demos/shadcn/card.svelte +0 -39
  212. package/src/showcase/demos/shadcn/checkbox.svelte +0 -39
  213. package/src/showcase/demos/shadcn/code-block.svelte +0 -80
  214. package/src/showcase/demos/shadcn/collapsible.svelte +0 -20
  215. package/src/showcase/demos/shadcn/command.svelte +0 -48
  216. package/src/showcase/demos/shadcn/context-menu.svelte +0 -54
  217. package/src/showcase/demos/shadcn/demo-harness.svelte +0 -12
  218. package/src/showcase/demos/shadcn/demos.test.ts +0 -20
  219. package/src/showcase/demos/shadcn/dialog.svelte +0 -38
  220. package/src/showcase/demos/shadcn/dropdown-menu.svelte +0 -59
  221. package/src/showcase/demos/shadcn/empty.svelte +0 -29
  222. package/src/showcase/demos/shadcn/field.svelte +0 -115
  223. package/src/showcase/demos/shadcn/hover-card.svelte +0 -32
  224. package/src/showcase/demos/shadcn/index.ts +0 -382
  225. package/src/showcase/demos/shadcn/input-otp.svelte +0 -19
  226. package/src/showcase/demos/shadcn/input.svelte +0 -5
  227. package/src/showcase/demos/shadcn/label.svelte +0 -11
  228. package/src/showcase/demos/shadcn/marker.svelte +0 -30
  229. package/src/showcase/demos/shadcn/message-scroller.svelte +0 -44
  230. package/src/showcase/demos/shadcn/message.svelte +0 -75
  231. package/src/showcase/demos/shadcn/popover.svelte +0 -36
  232. package/src/showcase/demos/shadcn/progress.svelte +0 -13
  233. package/src/showcase/demos/shadcn/radio-group.svelte +0 -19
  234. package/src/showcase/demos/shadcn/scroll-area.svelte +0 -18
  235. package/src/showcase/demos/shadcn/select.svelte +0 -29
  236. package/src/showcase/demos/shadcn/separator.svelte +0 -12
  237. package/src/showcase/demos/shadcn/sheet.svelte +0 -48
  238. package/src/showcase/demos/shadcn/skeleton.svelte +0 -11
  239. package/src/showcase/demos/shadcn/slider.svelte +0 -7
  240. package/src/showcase/demos/shadcn/sonner.svelte +0 -18
  241. package/src/showcase/demos/shadcn/spinner.svelte +0 -11
  242. package/src/showcase/demos/shadcn/switch.svelte +0 -9
  243. package/src/showcase/demos/shadcn/table.svelte +0 -76
  244. package/src/showcase/demos/shadcn/tabs.svelte +0 -62
  245. package/src/showcase/demos/shadcn/textarea.svelte +0 -5
  246. package/src/showcase/demos/shadcn/toggle-group.svelte +0 -18
  247. package/src/showcase/demos/shadcn/toggle.svelte +0 -14
  248. package/src/showcase/demos/shadcn/tooltip.svelte +0 -13
  249. package/src/showcase/demos/shadcn/trace.svelte +0 -51
  250. package/src/showcase/heart.svg +0 -1
  251. package/src/showcase/index.ts +0 -38
  252. package/src/showcase/kit.test.ts +0 -72
  253. package/src/showcase/kit.ts +0 -16
  254. package/src/showcase/plus.svg +0 -1
  255. package/src/showcase/showcase-shell.css +0 -91
  256. package/src/showcase/stacked-section.css +0 -15
  257. package/src/showcase/success-check.svg +0 -1
  258. package/src/showcase/types.ts +0 -29
  259. package/src/svg-raw.d.ts +0 -7
package/COMPONENTS.md CHANGED
@@ -23,12 +23,12 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
23
23
  - `checkbox` — Checkbox
24
24
  - `code-block` — CodeBlock. Highlight with `useHighlightedCode` from `@heyramzi/ui/shadcn/hooks/use-highlighted-code.svelte` (Shiki, ANSI too)
25
25
  - `collapsible` — Collapsible
26
- - `command` — Command
26
+ - `command` — Command, computeCommandScore (the scorer, for a custom `filter`)
27
27
  - `context-menu` — ContextMenu
28
28
  - `dialog` — Dialog
29
29
  - `dropdown-menu` — CheckboxGroup, CheckboxItem, Content, Portal, DropdownMenu, DropdownMenuCheckboxGroup
30
30
  - `empty` — Empty
31
- - `field` — Field
31
+ - `field` — Field and its parts, plus SimpleField: label, hint or error and the control, with the message linked by `aria-describedby`
32
32
  - `hover-card` — HoverCard
33
33
  - `input` — Input
34
34
  - `input-otp` — InputOtp
@@ -57,54 +57,66 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
57
57
  - `tooltip` — Tooltip
58
58
  - `trace` — Trace, an agent's steps: reasoning, tool calls and sources as status rows that can expand
59
59
 
60
+ ## Components (`src/components`)
61
+
62
+ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<name>`.
63
+
64
+ - `copy` — CopyButton, copyText, copyRichText, htmlToPlainText, Copied
65
+ - `data-table` — SortableHead, Pagination, InfiniteScroll, RowContextMenu, TableSkeleton, and the sort, page and filter helpers
66
+ - `hold-to-confirm` — HoldToConfirm, press and hold to confirm
67
+ - `icon` — Icon, a Phosphor component at a size
68
+ - `language-flag` — LanguageFlag
69
+ - `list-page` — ListPageShell, MasterDetail, GroupedList, RailRow and the rest of the master-detail page kit, NoticeBar, EmptyState, FilterPopover, setSectionHost
70
+ - `locale-switch` — LocaleSwitch, a language switch on SegmentedFilter
71
+ - `mark` — Mark, a logo or photo glyph with an initial fallback
72
+ - `open-in` — OpenIn, a link out to a tool that takes the tool's name, label and mark as a prop
73
+ - `picker` — Picker, MarkPicker, pickerTriggerClass, a command palette at popover scale
74
+ - `segmented` — AnimatedTabs (pill or underline), SegmentedFilter, SegmentedNav, SectionRuns, one bordered track with a sliding pill, a badge note and an overflow menu
75
+ - `star-rating-input` — StarRatingInput
76
+ - `svg` — Svg, an SVG file imported with `?raw`
77
+
60
78
  ## Effects (`src/effects`)
61
79
 
80
+ - `interactive-hover-button` — InteractiveHoverButton, a dot that grows into the fill
81
+
62
82
  - `animated-beam` — AnimatedBeam, AnimatedBeamProps, AnchorPosition
63
83
  - `border-beam` — BorderBeam
64
- - `image-comparison` — ImageComparison
84
+ - `image-comparison` — ImageComparison, a before/after wipe with a bindable position, side badges and a hint sweep
65
85
  - `image-reveal` — ImageReveal, ImageRevealProps, ImageRevealHandle
66
86
  - `magnetic` — magnetic
87
+ - `magic-card` — MagicCard. Props: `glow`, `radius`, `class`, `contentClass`; the glow colour is `--magic-card-glow` set on `class`.
67
88
  - `marquee` — Marquee
68
89
  - `progressive-blur` — ProgressiveBlur
69
90
  - `reveal` — InView, AnimatedGroup, TextEffect, observeInView, RevealPreset, RevealPer
70
91
  - `shader-gradient` — ShaderGradient, ShaderGradientProps, ShaderGradientUniforms
71
92
  - `spotlight` — Spotlight
72
- - `theme-toggle` — ThemeToggle, revealTheme. Props: `isDark`, `mode` (`light`, `dark`, `system`; swaps in the Monitor icon), `onToggle`, `variant` (`outline`, `ghost`, `inverted`; `inverted` boolean is an alias), `size`, `label`, `title`, `skipTransition`, `class`. `revealTheme(event, apply, { skip, duration, easing })` is the circular wipe on its own: `import { revealTheme } from "@heyramzi/ui"` (the `./lib/*` subpath points at `.js` and the source is `.ts`).
93
+ - `theme-toggle` — ThemeToggle, revealTheme. Props: `isDark`, `mode` (`light`, `dark`, `system`; swaps in the Monitor icon), `onToggle`, `variant` (`outline`, `ghost`, `inverted`, `slide`; `inverted` boolean is an alias; `slide` is a pill with a moving thumb, coloured by `--theme-toggle-thumb-light`, `--theme-toggle-thumb-dark`, `--theme-toggle-track`, `--theme-toggle-glow` and friends set on `class`), `size`, `label`, `title`, `skipTransition`, `class`. `revealTheme(event, apply, { skip, duration, easing })` is the circular wipe on its own: `import { revealTheme } from "@heyramzi/ui"` (the `./lib/*` subpath points at `.js` and the source is `.ts`).
73
94
 
74
95
  ## Animations (`src/animations`)
75
96
 
76
- - `accordion` — keyframe utilities
77
97
  - `avatar-group-hover` — keyframe utilities
78
98
  - `card-tilt` — keyframe utilities
79
99
  - `digit-pop-in` — keyframe utilities
80
100
  - `dropdown` — keyframe utilities
81
- - `icon-swap` — keyframe utilities
82
101
  - `input-clear-dissolve` — keyframe utilities
83
102
  - `input-shake` — keyframe utilities
84
103
  - `learn-more-hover` — keyframe utilities
85
104
  - `like-button` — keyframe utilities
86
105
  - `modal` — keyframe utilities
87
- - `notification-badge` — keyframe utilities
88
106
  - `page-side-by-side` — keyframe utilities
89
- - `panel` — keyframe utilities
90
107
  - `plus-menu-morph` — keyframe utilities
91
- - `reasoning-stream` — keyframe utilities
92
108
  - `resize` — keyframe utilities
93
109
  - `shimmer-text` — keyframe utilities
94
- - `skeleton-reveal` — keyframe utilities
95
110
  - `spinning-counter` — keyframe utilities
96
- - `streaming-text` — keyframe utilities
97
111
  - `success-check` — keyframe utilities
98
- - `tabs-sliding` — keyframe utilities
99
112
  - `text-swap` — keyframe utilities
100
- - `texts-reveal` — keyframe utilities
101
113
  - `thinking-states` — keyframe utilities
102
- - `tooltip` — keyframe utilities
103
114
 
104
115
  ## AI and chat (`src/ai`)
105
116
 
106
- - `composer` — Composer, ComposerProps
117
+ - `ai-button` — AiButton
107
118
  - `confirmation` — Confirmation, ConfirmationTitle, ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected, ConfirmationActions, ConfirmationAction
119
+ - `generation-progress` — GenerationProgress
108
120
  - `loading-state` — PixelLoader
109
121
  - `prompt-input` — PromptInput, PromptInputBody, PromptInputToolbar, PromptInputTextarea, PromptInputSubmit
110
122
  - `reasoning` — Reasoning, ReasoningTrigger, ReasoningContent
@@ -116,7 +128,6 @@ The style layer lives in [`src/tokens.css`](./src/tokens.css) and the motion CSS
116
128
 
117
129
  ---
118
130
 
119
- Skipped on purpose: brand assets (logos, marks, brand renderer, language flags,
120
- locale switch), kanban, list, gantt and status (they live in `@heyramzi/tasks`), and charts
131
+ Skipped on purpose: brand assets (logos, marks, brand renderer), kanban, list, gantt and status (they live in `@heyramzi/tasks`), and charts
121
132
  (they live in `@heyramzi/charts`). The bot avatars
122
133
  are in `src/agents`, imported from `@heyramzi/ui/agents`.
package/README.md CHANGED
@@ -11,11 +11,12 @@ only a convenience.
11
11
  ## What is inside
12
12
 
13
13
  - `src/shadcn` shadcn-svelte primitives, generalized.
14
+ - `src/components` generic pieces on the primitives: segmented control, picker, copy button, data table, list page, hold to confirm and more.
14
15
  - `src/effects` animated borders, beams, marquee, spotlight, image reveal,
15
16
  image comparison, shimmer gradients and a theme toggle.
16
17
  - `src/animations` keyframe CSS for dropdowns, modals, tooltips, text swaps,
17
18
  success checks and the rest of the motion vocabulary.
18
- - `src/ai` chat and AI surfaces: composer, prompt input, response, reasoning, confirmation and loaders.
19
+ - `src/ai` chat and AI surfaces: prompt input, response, reasoning, confirmation and loaders.
19
20
  - `src/agents` bot avatars and a cast grid, imported from `@heyramzi/ui/agents`:
20
21
  `AgentAvatar` draws an eyes-only face from a name, role, shape and colour, and
21
22
  `AgentCast` lays a list of agents out as cards you can pick from.
@@ -41,6 +42,9 @@ Retheme by overriding any variable on `:root`, for example
41
42
  `--color-brand-indigo` or `--radius-lg`. Dark mode keys off a `.dark`
42
43
  class on `html`.
43
44
 
45
+ Rich text lives in [`@heyramzi/rich-editor`](https://github.com/heyramzi/ui/tree/main/packages/rich-editor),
46
+ so the kit doesn't pull Tiptap into every app.
47
+
44
48
  Charts live in their own package now,
45
49
  [`@heyramzi/charts`](https://github.com/heyramzi/ui/tree/main/packages/charts). It reads
46
50
  the same tokens, so import `tokens.css` once and both work.
@@ -125,7 +129,7 @@ Inspired by [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte).
125
129
  The reveal, marquee and beam effects are inspired by
126
130
  [motion-primitives](https://github.com/ibelick/motion-primitives), and the
127
131
  motion vocabulary by [transitions-dev](https://transitions.dev). The agent avatars
128
- vendor [bot-avatars](https://github.com/jakubantalik/bot-avatars); its licence is in
132
+ port [bot-avatars](https://github.com/Jakubantalik/Libraries.dev/tree/main/packages/bot-avatars) 0.2.2; its licence is in
129
133
  `src/agents/vendor/LICENSE`.
130
134
 
131
135
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "7.0.0",
3
+ "version": "8.1.0",
4
4
  "description": "Svelte 5 UI primitives: effects, animations, and shared component CSS.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,16 @@
29
29
  "default": "./src/index.ts"
30
30
  },
31
31
  "./tokens.css": "./src/tokens.css",
32
- "./lib/*": "./src/lib/*",
32
+ "./lib/*.js": {
33
+ "svelte": "./src/lib/*.ts",
34
+ "types": "./src/lib/*.ts",
35
+ "default": "./src/lib/*.ts"
36
+ },
37
+ "./lib/*": {
38
+ "svelte": "./src/lib/*.ts",
39
+ "types": "./src/lib/*.ts",
40
+ "default": "./src/lib/*.ts"
41
+ },
33
42
  "./border-beam": {
34
43
  "svelte": "./src/effects/border-beam/BorderBeam.svelte",
35
44
  "default": "./src/effects/border-beam/BorderBeam.svelte"
@@ -55,6 +64,10 @@
55
64
  "svelte": "./src/effects/spotlight/Spotlight.svelte",
56
65
  "default": "./src/effects/spotlight/Spotlight.svelte"
57
66
  },
67
+ "./effects/magic-card": {
68
+ "svelte": "./src/effects/magic-card/MagicCard.svelte",
69
+ "default": "./src/effects/magic-card/MagicCard.svelte"
70
+ },
58
71
  "./effects/marquee": {
59
72
  "svelte": "./src/effects/marquee/Marquee.svelte",
60
73
  "default": "./src/effects/marquee/Marquee.svelte"
@@ -78,6 +91,10 @@
78
91
  "svelte": "./src/effects/progressive-blur/ProgressiveBlur.svelte",
79
92
  "default": "./src/effects/progressive-blur/ProgressiveBlur.svelte"
80
93
  },
94
+ "./effects/interactive-hover-button": {
95
+ "svelte": "./src/effects/interactive-hover-button/InteractiveHoverButton.svelte",
96
+ "default": "./src/effects/interactive-hover-button/InteractiveHoverButton.svelte"
97
+ },
81
98
  "./effects/image-comparison": {
82
99
  "svelte": "./src/effects/image-comparison/ImageComparison.svelte",
83
100
  "default": "./src/effects/image-comparison/ImageComparison.svelte"
@@ -115,16 +132,6 @@
115
132
  "./animations/*": {
116
133
  "default": "./src/animations/*"
117
134
  },
118
- "./showcase": {
119
- "svelte": "./src/showcase/index.ts",
120
- "types": "./src/showcase/index.ts",
121
- "default": "./src/showcase/index.ts"
122
- },
123
- "./showcase/kit": {
124
- "svelte": "./src/showcase/kit.ts",
125
- "types": "./src/showcase/kit.ts",
126
- "default": "./src/showcase/kit.ts"
127
- },
128
135
  "./shadcn/hooks/*": {
129
136
  "svelte": "./src/shadcn/hooks/*.ts",
130
137
  "types": "./src/shadcn/hooks/*.ts",
@@ -135,6 +142,11 @@
135
142
  "types": "./src/shadcn/*/index.ts",
136
143
  "default": "./src/shadcn/*/index.ts"
137
144
  },
145
+ "./components/*": {
146
+ "svelte": "./src/components/*/index.ts",
147
+ "types": "./src/components/*/index.ts",
148
+ "default": "./src/components/*/index.ts"
149
+ },
138
150
  "./shadcn/utilities.css": {
139
151
  "style": "./src/shadcn/utilities.css",
140
152
  "default": "./src/shadcn/utilities.css"
@@ -158,18 +170,16 @@
158
170
  },
159
171
  "dependencies": {
160
172
  "@heyramzi/utils": "^1.3.3",
161
- "@lucide/svelte": "^1.52.0",
162
173
  "@shadcn-svelte/primitives": "^0.0.1",
163
- "clsx": "^2.1.1",
164
174
  "marked": "^18.1.0",
165
175
  "phosphor-svelte": "^3.1.0",
166
176
  "shiki": "^4.5.0",
167
- "tailwind-merge": "^3.7.0",
168
177
  "tailwind-variants": "^3.3.1"
169
178
  },
170
179
  "devDependencies": {
171
180
  "@internationalized/date": "^3.12.4",
172
181
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
182
+ "@types/node": "^26.6.4",
173
183
  "bits-ui": "^2.19.5",
174
184
  "jsdom": "^30.1.2",
175
185
  "mode-watcher": "^1.1.0",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /* Sync shadcn-svelte into src/shadcn and its demos into the showcase, three-way
2
+ /* Sync shadcn-svelte into src/shadcn and its demos into the site, three-way
3
3
  * per file against `.upstream/`. The rules and why: ../docs/decisions.md. */
4
4
 
5
5
  // pnpm sync:shadcn fetch, vendor, apply, report
@@ -8,7 +8,7 @@
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { existsSync, readFileSync, readdirSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
- import { basename, dirname, resolve } from "node:path";
11
+ import { basename, dirname, relative, resolve } from "node:path";
12
12
  import {
13
13
  CHECK,
14
14
  classify,
@@ -56,16 +56,16 @@ const REROUTE = {
56
56
 
57
57
  const DOCS = "https://raw.githubusercontent.com/huntabyte/shadcn-svelte/main/docs";
58
58
  const DEMO_VENDOR = `${PKG}/.upstream/shadcn-demos`;
59
- const DEMO_LOCAL = `${PKG}/src/showcase/demos/shadcn`;
59
+ const DEMO_LOCAL = `${ROOT}/site/src/lib/shadcn-demos`;
60
60
  const DEMO_MANIFEST = `${DEMO_LOCAL}/index.ts`;
61
61
 
62
62
  /* The docs site's aliases. A demo is a docs file, not a registry payload, so it
63
63
  * carries `$lib/...` rather than the registry's `$UI$`. */
64
64
  const DEMO_ALIASES = [
65
- ["$lib/registry/ui/", "../src/shadcn"],
65
+ ["$lib/registry/ui/", "@heyramzi/ui/shadcn/"],
66
66
  // A handful reach their neighbour by relative path instead of the alias.
67
- ["../ui/", "../src/shadcn"],
68
- ["$lib/registry/hooks/", "../src/shadcn/hooks"],
67
+ ["../ui/", "@heyramzi/ui/shadcn/"],
68
+ ["$lib/registry/hooks/", "@heyramzi/ui/shadcn/hooks/"],
69
69
  ["$lib/utils.js", "@heyramzi/utils"],
70
70
  // The peer here is the runes build, published under its own name.
71
71
  ["@tabler/icons-svelte/", "@tabler/icons-svelte-runes/"],
@@ -107,12 +107,16 @@ const NO_DEMO = new Set([
107
107
  ]);
108
108
 
109
109
  const adapt = (text) => ALIASES.reduce((acc, [from, to]) => acc.replaceAll(from, to), text);
110
+ /* The site imports the kit the way a consumer does, so the exports map names a
111
+ * primitive without its `/index.js` and a hook without its `.js`. */
110
112
  const adaptDemo = (text) =>
111
- DEMO_ALIASES.reduce((acc, [from, to]) => acc.replaceAll(from, to), text);
113
+ DEMO_ALIASES.reduce((acc, [from, to]) => acc.replaceAll(from, to), text)
114
+ .replace(/(@heyramzi\/ui\/shadcn\/[a-z-]+)\/index\.js/g, "$1")
115
+ .replace(/(@heyramzi\/ui\/shadcn\/hooks\/[a-z-]+(?:\.svelte)?)\.js/g, "$1");
112
116
  const readIf = (path) => (existsSync(path) ? readFileSync(path, "utf8") : null);
113
117
  const route = (name, target) => `${PKG}/${REROUTE[`${name}/${target}`] ?? `src/shadcn/${target}`}`;
114
118
  /** Report a file by where it lives here, not by its upstream target. */
115
- const label = (path) => path.slice(PKG.length + 1);
119
+ const label = (path) => (path.startsWith(`${PKG}/`) ? path.slice(PKG.length + 1) : relative(ROOT, path));
116
120
 
117
121
  const pascal = (name) => name.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
118
122
 
@@ -127,10 +131,10 @@ const meta = (doc, name) => {
127
131
  return { title: field("title") ?? pascal(name), note: field("description") ?? "" };
128
132
  };
129
133
 
130
- /** The demo's own shadcn imports, respelled as a consumer writes them. */
134
+ /** The demo's own shadcn imports, as it spells them for a consumer. */
131
135
  const publicImports = (demo) =>
132
- [...demo.matchAll(/import[^;]*?from\s+"#src\/shadcn\/([^"]+?)(?:\/index\.js)?";/g)]
133
- .map((m) => m[0].replace(/"[^"]+"/, `"@heyramzi/ui/shadcn/${m[1]}"`))
136
+ [...demo.matchAll(/import[^;]*?from\s+"@heyramzi\/ui\/shadcn\/[^"]+";/g)]
137
+ .map((m) => m[0])
134
138
  .join("\n");
135
139
 
136
140
  /* The section's list, generated: a primitive reaches the page by being synced,
@@ -145,7 +149,7 @@ async function writeManifest(demos) {
145
149
  " * own, and the sync rewrites this list. */",
146
150
  'import type { Component } from "svelte";',
147
151
  ...demos.map(
148
- (d) => `import ${pascal(d.name)}Demo from "../src/showcase/demos/shadcn/${d.name}.svelte";`,
152
+ (d) => `import ${pascal(d.name)}Demo from "./${d.name}.svelte";`,
149
153
  ),
150
154
  "",
151
155
  "export type ShadcnDemo = {",
@@ -267,7 +271,7 @@ const ours = readdirSync(LOCAL, { withFileTypes: true })
267
271
  for (const name of ours) buckets.unported.push(`${name} — in src/shadcn, not in the registry`);
268
272
 
269
273
  /* The docs site's own demo for each primitive, vendored on the same three-way
270
- * rule. Upstream writes them; the showcase renders them. */
274
+ * rule. Upstream writes them; the site renders them. */
271
275
  const demoNames = [...Object.keys(entries), ...LOCAL_ONLY]
272
276
  .filter((name) => !NO_DEMO.has(name))
273
277
  .toSorted();
@@ -46,7 +46,7 @@ const SRC = `${PKG}/src`;
46
46
  */
47
47
 
48
48
  /*
49
- * Most land in src/animations. Five do not, because a component already did the job and one behaviour gets one implementation: the last five entries name where each went instead.
49
+ * Fourteen of these name the component that owns the motion instead of a file in src/animations.
50
50
  */
51
51
 
52
52
  /*
@@ -55,31 +55,31 @@ const SRC = `${PKG}/src`;
55
55
  const PORTED = {
56
56
  "card-resize": "animations/resize.css",
57
57
  "number-pop-in": "animations/digit-pop-in.css",
58
- "notification-badge": "animations/notification-badge.css",
58
+ "notification-badge": "shadcn/badge/badge.svelte",
59
59
  "text-states-swap": "animations/text-swap.css",
60
60
  "menu-dropdown": "animations/dropdown.css",
61
61
  "modal-open-close": "animations/modal.css",
62
- "panel-reveal": "animations/panel.css",
62
+ "panel-reveal": "shadcn/sheet/sheet-content.svelte",
63
63
  "page-side-by-side": "animations/page-side-by-side.css",
64
- "icon-swap": "animations/icon-swap.css",
64
+ "icon-swap": "effects/theme-toggle/ThemeToggle.svelte",
65
65
  "success-check": "animations/success-check.css",
66
66
  "avatar-group-hover": "animations/avatar-group-hover.css",
67
67
  "error-state-shake": "animations/input-shake.css",
68
68
  "input-clear-with-dissolve": "animations/input-clear-dissolve.css",
69
- "skeleton-loader-and-reveal": "animations/skeleton-reveal.css",
70
- "texts-reveal": "animations/texts-reveal.css",
71
- "tabs-sliding": "animations/tabs-sliding.css",
69
+ "skeleton-loader-and-reveal": "shadcn/skeleton/skeleton.svelte",
70
+ "texts-reveal": "effects/reveal/TextEffect.svelte",
71
+ "tabs-sliding": "shadcn/tabs/tabs-list.svelte",
72
72
  "shimmer-text": "animations/shimmer-text.css",
73
- "tooltip-open-close": "animations/tooltip.css",
73
+ "tooltip-open-close": "shadcn/tooltip/tooltip-content.svelte",
74
74
  "3d-tilt": "animations/card-tilt.css",
75
75
  "dropdown-menu-morph": "animations/plus-menu-morph.css",
76
- accordion: "animations/accordion.css",
76
+ accordion: "shadcn/accordion/accordion-content.svelte",
77
77
  "like-button": "animations/like-button.css",
78
78
  "learn-more-hover": "animations/learn-more-hover.css",
79
79
  "spinning-counter": "animations/spinning-counter.css",
80
80
  "thinking-states": "animations/thinking-states.css",
81
- "reasoning-stream": "animations/reasoning-stream.css",
82
- "streaming-text": "animations/streaming-text.css",
81
+ "reasoning-stream": "ai/reasoning/ReasoningContent.svelte",
82
+ "streaming-text": "effects/reveal/TextEffect.svelte",
83
83
  toggle: "shadcn/switch/switch.svelte",
84
84
  "checkbox-check": "shadcn/checkbox/checkbox.svelte",
85
85
  "toast-open-close": "shadcn/sonner/sonner.svelte",
@@ -1,55 +1,5 @@
1
- <script module lang="ts">
2
- /* The batching shown-gate and the path caches are shared by every avatar
3
- on the page, so a row of them comes in together. Engine: ./vendor. */
4
- import { bakesPending as bakesPendingGlobal } from './vendor/plastic';
5
-
6
- const SHOW_WITHIN = 4000;
7
- const waiters = new Map<object, { ready: () => boolean; show: () => void }>();
8
-
9
- function showReady() {
10
- if (bakesPendingGlobal()) return;
11
- for (const w of waiters.values()) if (!w.ready()) return;
12
- const all = [...waiters.values()];
13
- waiters.clear();
14
- for (const w of all) w.show();
15
- }
16
-
17
- const pathCache = new Map<string, Path2D>();
18
- function bodyPath(d: string): Path2D {
19
- let p = pathCache.get(d);
20
- if (!p) {
21
- p = new Path2D(d);
22
- pathCache.set(d, p);
23
- }
24
- return p;
25
- }
26
-
27
- const partsCache = new Map<string, Path2D[]>();
28
- function partPaths(d: string): Path2D[] {
29
- let p = partsCache.get(d);
30
- if (!p) {
31
- p = d
32
- .split(/(?=M)/)
33
- .filter((s) => s.trim())
34
- .map((s) => new Path2D(s));
35
- partsCache.set(d, p);
36
- }
37
- return p;
38
- }
39
-
40
- let uidSeq = 0;
41
- function hashSeed(id: string): number {
42
- let h = 2166136261;
43
- for (let i = 0; i < id.length; i++) h = Math.imul(h ^ id.charCodeAt(i), 16777619);
44
- return ((h >>> 0) % 1000) / 1000;
45
- }
46
-
47
- export const reducedMotion = (): boolean =>
48
- typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
49
- </script>
50
-
51
1
  <script lang="ts">
52
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } from 'svelte';
53
3
  import type {
54
4
  BotAvatarShading,
55
5
  BotAvatarState,
@@ -61,16 +11,29 @@
61
11
  import { autoInk, richer, shade } from './vendor/color';
62
12
  import { Sim, restPose } from './vendor/engine';
63
13
  import { draw, LIGHT_DEFAULTS, OVERSCAN, RISE, type DrawConfig } from './vendor/draw';
64
- import {
65
- bakesPending,
66
- hurry,
67
- onLanded,
68
- stockLights,
69
- takeMissed,
70
- warmPlastic,
71
- type FurStyle,
72
- } from './vendor/plastic';
14
+ import { bakesPending, hurry, onLanded, takeMissed, warmPlastic, type FurStyle } from './vendor/plastic';
73
15
  import { subscribe, pointer } from './vendor/ticker';
16
+ import {
17
+ SHOW_WITHIN,
18
+ bodyPath,
19
+ nextUid,
20
+ onThemeChange,
21
+ partPaths,
22
+ showReady,
23
+ waiters,
24
+ } from './page-state';
25
+ import {
26
+ clamp,
27
+ clampOpt,
28
+ deviceDpr,
29
+ hashSeed,
30
+ lightsFor,
31
+ pointerStrength,
32
+ q,
33
+ reducedMotion,
34
+ resolveTheme as themeOf,
35
+ warmIdle,
36
+ } from './helpers';
74
37
 
75
38
  type Props = AgentAvatarProps;
76
39
 
@@ -83,7 +46,7 @@
83
46
  state: botState = 'default',
84
47
  size,
85
48
  color,
86
- ink = '#1E1A33',
49
+ ink,
87
50
  brightness: brightnessProp,
88
51
  saturation: saturationProp,
89
52
  speed = 1,
@@ -140,20 +103,14 @@
140
103
  class: className,
141
104
  style,
142
105
  'aria-label': ariaLabel,
106
+ 'aria-hidden': ariaHidden,
143
107
  onclick: userClick,
144
108
  ...rest
145
109
  }: Props = $props();
146
110
 
147
- const clamp = (v: number, lo: number, hi: number) =>
148
- Math.min(hi, Math.max(lo, Number.isFinite(v) ? v : 1));
149
- const q = (v: number, lo: number, hi: number) => Math.round(clamp(v, lo, hi) * 20) / 20;
150
- const clampOpt = (v: number | undefined, d: number, lo: number, hi: number) =>
151
- clamp(v ?? d, lo, hi);
152
111
  const shapePath = (t: BotAvatarType) => SHAPE_PATHS[t] ?? SHAPE_PATHS.clover;
153
- const deviceDpr = () =>
154
- Math.min(2, (typeof devicePixelRatio === 'number' && devicePixelRatio) || 1);
155
112
 
156
- const uid = `ba-${++uidSeq}`;
113
+ const uid = nextUid();
157
114
 
158
115
  /* The avatar draws at 75% of its slot so an idle jump has room in the box
159
116
  and never clips. A numeric `size` overrides the measurement. */
@@ -205,6 +162,10 @@
205
162
  } satisfies FurStyle);
206
163
  const material = $derived(shadingMode === 'plastic' || shadingMode === 'fabric');
207
164
  const lookKey = $derived(`${shadingMode}|${outlineKey}|${Math.round(drawnSize)}`);
165
+ /* What the bakes depend on, as strings, so the warm effect re-runs only
166
+ when one of them changes, not on every prop or every resized pixel. */
167
+ const warmSize = $derived(Math.round(drawnSize));
168
+ const furKey = $derived(`${fur.length}|${fur.density}|${fur.fuzz}|${fur.clumps}|${fur.curl}|${fur.gravity}`);
208
169
  const interactiveNow = $derived(interactive && !heldPose);
209
170
 
210
171
  /* Captured once, on purpose: `shown` never returns to false, so a later
@@ -278,19 +239,7 @@
278
239
  let holding: (() => void) | null = null;
279
240
  const meRef = {};
280
241
 
281
- function resolveTheme(el: HTMLElement | null): 'dark' | 'light' {
282
- if (theme !== 'auto') return theme;
283
- const host = el?.closest('[data-theme], .dark, .light') as HTMLElement | null;
284
- if (host) {
285
- const v = host.getAttribute('data-theme');
286
- if (v === 'dark' || v === 'light') return v;
287
- if (host.classList.contains('dark')) return 'dark';
288
- if (host.classList.contains('light')) return 'light';
289
- }
290
- return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: light)').matches
291
- ? 'light'
292
- : 'dark';
293
- }
242
+ const resolveTheme = (el: HTMLElement | null) => themeOf(theme, el);
294
243
 
295
244
  function layoutPx(canvas: HTMLCanvasElement) {
296
245
  return canvas.clientWidth / OVERSCAN || cssSize || drawnSize || 64;
@@ -365,6 +314,9 @@
365
314
 
366
315
  function showWhenBaked(paintNow: () => void) {
367
316
  showPaint = paintNow;
317
+ /* No canvas means nothing was drawn, so nothing could report a missed
318
+ bake. Calling that ready would skip the fade. */
319
+ if (!canvasEl) return;
368
320
  takeMissed();
369
321
  paintNow();
370
322
  ready = !takeMissed();
@@ -381,30 +333,13 @@
381
333
  });
382
334
  }
383
335
 
384
- function lightsFor(lightAt: number) {
385
- const a = (lightAt * Math.PI) / 180;
386
- const b = ((backLight ?? 0) * Math.PI) / 180;
387
- const lights = stockLights(Math.sin(a), -Math.cos(a));
388
- if (backLight !== undefined) [lights.bx, lights.by] = [Math.sin(b), -Math.cos(b)];
389
- if (lightFront !== undefined) lights.front = (Math.min(85, Math.max(0, lightFront)) * Math.PI) / 180;
390
- return lights;
391
- }
392
-
393
336
  function warm() {
394
337
  if (!material || !cfg?.path) return;
395
338
  const c = cfg;
396
339
  const dev = (drawnSize || 64) * deviceDpr();
397
340
  const lightAt = c.light ?? 295;
398
- warmPlastic(outlineKey, c.path, dev, depth, shadingMode === 'fabric', fur, lightAt, lightsFor(lightAt));
399
- }
400
-
401
- function warmIdle(fn: () => void): () => void {
402
- if (typeof requestIdleCallback === 'function') {
403
- const id = requestIdleCallback(fn);
404
- return () => cancelIdleCallback(id);
405
- }
406
- const id = setTimeout(fn, 1);
407
- return () => clearTimeout(id);
341
+ const lights = lightsFor(lightAt, backLight, lightFront);
342
+ warmPlastic(outlineKey, c.path, dev, depth, shadingMode === 'fabric', fur, lightAt, lights);
408
343
  }
409
344
 
410
345
  function hold() {
@@ -415,7 +350,7 @@
415
350
  if (!holding) return;
416
351
  holding();
417
352
  holding = null;
418
- paint();
353
+ applyPose();
419
354
  };
420
355
  const off = onLanded(() => {
421
356
  if (!bakesPending()) release();
@@ -471,6 +406,8 @@
471
406
  /* Sync the rig with the props on every meaningful change: this is the
472
407
  React `useLayoutEffect` (which ran each render) made reactive. */
473
408
  $effect(() => {
409
+ /* Until the slot is measured there's no canvas to draw on. */
410
+ if (drawnSize <= 0) return;
474
411
  if (!sim) sim = new Sim(seedValue, stateKey);
475
412
  else sim.setState(stateKey);
476
413
  sim.setTurn(clamp(turn, 0, 2));
@@ -513,25 +450,36 @@
513
450
  };
514
451
  });
515
452
 
453
+ /* Once shown, a paused or reduced-motion avatar draws only when asked, so
454
+ it repaints here and keeps repainting as its bakes land. */
455
+ $effect(() => {
456
+ if (!shown) return;
457
+ untrack(() => {
458
+ if (canvasEl && drawnSize > 0) applyPose();
459
+ });
460
+ });
461
+
462
+ $effect(() => {
463
+ if (theme !== 'auto') return;
464
+ return onThemeChange(() => {
465
+ if (canvasEl && drawnSize > 0) applyPose();
466
+ });
467
+ });
468
+
516
469
  /* Start the material's bakes on idle time as soon as the shape is known. */
517
470
  $effect(() => {
518
471
  void shadingMode;
519
472
  void outlineKey;
520
- void drawnSize;
473
+ void warmSize;
521
474
  void depth;
522
475
  void light;
523
476
  void backLight;
524
477
  void lightFront;
525
- if (!material || !cfg?.path) return;
478
+ void furKey;
479
+ if (!material || !warmSize || !untrack(() => cfg?.path)) return;
526
480
  return warmIdle(warm);
527
481
  });
528
482
 
529
- function pointerStrength(d: number) {
530
- if (d < 1) return 1;
531
- if (d > 3) return 0;
532
- return 1 - (d - 1) / 2;
533
- }
534
-
535
483
  function updatePointer(s: Sim, canvas: HTMLCanvasElement) {
536
484
  if (!interactiveNow || Number.isNaN(pointer.x)) {
537
485
  s.setPointer(0, 0, 0);
@@ -604,8 +552,10 @@
604
552
 
605
553
  onMount(() => {
606
554
  if (!hostEl) return;
555
+ /* A hidden parent (a closed tab or panel) reports 0. Keep the last width
556
+ so the canvas, its picture and its gate survive being hidden. */
607
557
  const observer = new ResizeObserver(([entry]) => {
608
- if (entry) hostWidth = entry.contentRect.width;
558
+ if (entry && entry.contentRect.width > 0) hostWidth = entry.contentRect.width;
609
559
  });
610
560
  observer.observe(hostEl);
611
561
  return () => observer.disconnect();
@@ -628,9 +578,12 @@
628
578
  data-bot-avatar={type}
629
579
  data-face={faceKind}
630
580
  data-state={stateKey}
631
- role="img"
632
- aria-label={ariaLabel ??
633
- (name ? `${name}${role ? `, ${role}` : ''}` : `${preset.label} agent, ${stateLabels[stateKey]}`)}
581
+ role={ariaHidden ? undefined : 'img'}
582
+ aria-hidden={ariaHidden ? 'true' : undefined}
583
+ aria-label={ariaHidden
584
+ ? undefined
585
+ : (ariaLabel ??
586
+ (name ? `${name}${role ? `, ${role}` : ''}` : `${preset.label} agent, ${stateLabels[stateKey]}`))}
634
587
  style={canvasStyle}
635
588
  onclick={handleClick}
636
589
  {...rest}