@heyramzi/ui 2.1.0 → 2.2.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 (312) hide show
  1. package/package.json +7 -8
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/card/card-frame.svelte +43 -39
  31. package/src/canvas/card/resize-handles.svelte +89 -80
  32. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  33. package/src/canvas/chrome/lock-badge.svelte +8 -8
  34. package/src/canvas/chrome/side-handles.svelte +24 -19
  35. package/src/canvas/chrome/size-chip.svelte +30 -30
  36. package/src/canvas/chrome/snap-guides.svelte +20 -20
  37. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  38. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  39. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  40. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  41. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  42. package/src/canvas/edges/bend-handle.svelte +46 -46
  43. package/src/canvas/edges/edge-gradient.svelte +10 -10
  44. package/src/canvas/edges/gradient-edge.svelte +45 -29
  45. package/src/canvas/editor/lock.test.ts +6 -1
  46. package/src/canvas/editor/style.ts +7 -1
  47. package/src/canvas/headless/svg-figure.test.ts +5 -1
  48. package/src/canvas/lod/lod-switch.svelte +111 -106
  49. package/src/canvas/pen/PenField.svelte +87 -81
  50. package/src/canvas/pen/PenOverlay.svelte +127 -127
  51. package/src/canvas/pen/ShapeNode.svelte +31 -32
  52. package/src/canvas/pen/TextNode.svelte +54 -53
  53. package/src/canvas/pen/host-field.test.svelte +7 -7
  54. package/src/canvas/pen/shapes.ts +7 -1
  55. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  56. package/src/canvas/screen/PartView.svelte +138 -72
  57. package/src/canvas/screen/ScreenBody.svelte +81 -60
  58. package/src/canvas/screen/ScreenCard.svelte +20 -20
  59. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  60. package/src/canvas/ui/FrameLayer.svelte +59 -59
  61. package/src/canvas/ui/PenLayer.svelte +39 -30
  62. package/src/canvas/ui/StylePanel.svelte +119 -102
  63. package/src/canvas/ui/canvas-grid.svelte +137 -129
  64. package/src/charts/animated-value.svelte +1 -1
  65. package/src/charts/area-chart.css +2 -5
  66. package/src/charts/area-chart.svelte +1 -1
  67. package/src/charts/bar-chart.css +2 -3
  68. package/src/charts/bar-chart.svelte +1 -1
  69. package/src/charts/charts.css +5 -17
  70. package/src/charts/crosshair.svelte +1 -1
  71. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  72. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  73. package/src/charts/dashboard/data-panel.svelte +50 -50
  74. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  75. package/src/charts/dashboard/data-table.svelte +138 -138
  76. package/src/charts/dashboard/panel-grid.svelte +30 -30
  77. package/src/charts/dashboard/period-filter.svelte +25 -25
  78. package/src/charts/dashboard/rating-chip.svelte +19 -19
  79. package/src/charts/dashboard/stat-band.svelte +32 -32
  80. package/src/charts/dashboard/stat-tile.svelte +106 -99
  81. package/src/charts/donut-chart.svelte +1 -1
  82. package/src/charts/edge-fade.svelte +1 -1
  83. package/src/charts/figure.svelte +1 -1
  84. package/src/charts/gauge-chart.css +4 -7
  85. package/src/charts/gauge-chart.svelte +1 -1
  86. package/src/charts/hover-focus.svelte +1 -1
  87. package/src/charts/legend.css +1 -3
  88. package/src/charts/legend.svelte +1 -1
  89. package/src/charts/line-chart.svelte +1 -1
  90. package/src/charts/number-chart.css +6 -9
  91. package/src/charts/number-chart.svelte +1 -1
  92. package/src/charts/reference-line.svelte +19 -19
  93. package/src/charts/reveal.svelte +1 -1
  94. package/src/charts/ring-chart.css +2 -4
  95. package/src/charts/ring-chart.svelte +1 -1
  96. package/src/charts/sparkline.svelte +5 -2
  97. package/src/charts/stat-card.svelte +1 -1
  98. package/src/charts/time-series-chart.css +2 -5
  99. package/src/charts/trend-pill.css +2 -4
  100. package/src/charts/trend-pill.svelte +1 -1
  101. package/src/components/copy/copy-button.svelte +93 -98
  102. package/src/components/copy/copy-button.test.ts +10 -2
  103. package/src/components/data-table/infinite-scroll.svelte +42 -42
  104. package/src/components/data-table/pagination.svelte +34 -22
  105. package/src/components/data-table/row-arrow.svelte +19 -16
  106. package/src/components/data-table/row-context-menu.svelte +52 -52
  107. package/src/components/data-table/sortable-head.svelte +25 -25
  108. package/src/components/data-table/table-skeleton.svelte +41 -36
  109. package/src/components/docs/doc-page.svelte +18 -9
  110. package/src/components/docs/on-this-page.svelte +16 -3
  111. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  112. package/src/components/field-row/field-row.svelte +10 -8
  113. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  114. package/src/components/icon/icon.svelte +14 -14
  115. package/src/components/language-flag/language-flag.svelte +3 -1
  116. package/src/components/list-page/detail-empty.svelte +34 -34
  117. package/src/components/list-page/detail-header.svelte +55 -47
  118. package/src/components/list-page/detail-panel.svelte +51 -59
  119. package/src/components/list-page/detail-row.svelte +19 -19
  120. package/src/components/list-page/detail-stack.svelte +21 -21
  121. package/src/components/list-page/empty-state.svelte +30 -30
  122. package/src/components/list-page/filter-item.svelte +20 -20
  123. package/src/components/list-page/filter-popover.svelte +60 -55
  124. package/src/components/list-page/grouped-list.svelte +4 -2
  125. package/src/components/list-page/index.ts +5 -1
  126. package/src/components/list-page/list-page-shell.svelte +2 -5
  127. package/src/components/list-page/master-detail.svelte +87 -88
  128. package/src/components/list-page/notice-bar.svelte +38 -38
  129. package/src/components/list-page/prev-next.svelte +63 -63
  130. package/src/components/list-page/rail-body.svelte +18 -18
  131. package/src/components/list-page/rail-empty.svelte +23 -30
  132. package/src/components/list-page/rail-head.svelte +24 -24
  133. package/src/components/list-page/rail-row.svelte +77 -73
  134. package/src/components/list-page/section-host.ts +2 -3
  135. package/src/components/locale-switch/locale-switch.svelte +41 -41
  136. package/src/components/mark/mark.svelte +34 -38
  137. package/src/components/open-in/index.ts +1 -5
  138. package/src/components/open-in/open-in.svelte +40 -40
  139. package/src/components/picker/mark-picker.svelte +49 -49
  140. package/src/components/picker/picker.svelte +5 -3
  141. package/src/components/section-header/section-header.test.ts +7 -1
  142. package/src/components/section-header/section-label.svelte +20 -20
  143. package/src/components/segmented/animated-tabs.svelte +31 -2
  144. package/src/components/segmented/collapse.test.ts +2 -1
  145. package/src/components/segmented/index.ts +2 -0
  146. package/src/components/segmented/segment.svelte +52 -49
  147. package/src/components/segmented/segmented-filter.svelte +79 -80
  148. package/src/components/segmented/segmented-nav.svelte +34 -40
  149. package/src/components/segmented/segmented-overflow.svelte +53 -50
  150. package/src/components/segmented/segmented-track.svelte +62 -46
  151. package/src/components/segmented/segmented.ts +2 -3
  152. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  153. package/src/components/svg/svg.svelte +4 -4
  154. package/src/components/table-view/table-view.svelte +17 -2
  155. package/src/components/table-view/table-view.test.ts +7 -1
  156. package/src/components/table-view/types.ts +11 -11
  157. package/src/components/tags-input/index.ts +13 -0
  158. package/src/components/tags-input/tags-input.svelte +305 -0
  159. package/src/components/tags-input/tags.test.ts +64 -0
  160. package/src/components/tags-input/tags.ts +142 -0
  161. package/src/components/theme/index.ts +2 -4
  162. package/src/components/theme/theme-provider.svelte +1 -3
  163. package/src/components/toolbar/toolbar-search.svelte +25 -25
  164. package/src/components/toolbar/toolbar.svelte +11 -7
  165. package/src/components/toolbar/toolbar.test.ts +10 -1
  166. package/src/design/index.css +21 -78
  167. package/src/design/polish.css +26 -35
  168. package/src/editor/editor-toolbar.svelte +104 -99
  169. package/src/editor/editor.svelte +296 -266
  170. package/src/editor/image-toolbar.svelte +20 -22
  171. package/src/editor/index.ts +1 -4
  172. package/src/editor/inline-editor.svelte +280 -238
  173. package/src/editor/markdown.ts +15 -15
  174. package/src/editor/renderer.svelte +71 -71
  175. package/src/editor/slash-menu.svelte +24 -27
  176. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  177. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  178. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  179. package/src/effects/magic-card/MagicCard.css +14 -7
  180. package/src/effects/marquee/Marquee.svelte +3 -5
  181. package/src/effects/reveal/observe.ts +0 -3
  182. package/src/effects/spotlight/Spotlight.svelte +1 -1
  183. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  184. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  185. package/src/gallery.test.ts +14 -4
  186. package/src/inbox/README.md +1 -3
  187. package/src/inbox/inbox.test.ts +181 -138
  188. package/src/inbox/index.ts +20 -20
  189. package/src/inbox/logic.ts +114 -111
  190. package/src/inbox/thread-inbox.svelte +227 -151
  191. package/src/inbox/types.ts +130 -108
  192. package/src/lib/theme-transition.ts +2 -4
  193. package/src/lib/tone.ts +2 -4
  194. package/src/lib/utils.ts +2 -2
  195. package/src/modules.test.ts +79 -69
  196. package/src/onboarding/define-tours.test.ts +55 -40
  197. package/src/onboarding/define-tours.ts +23 -23
  198. package/src/onboarding/demo.svelte +24 -5
  199. package/src/onboarding/index.ts +1 -8
  200. package/src/onboarding/onboarding-provider.svelte +19 -7
  201. package/src/onboarding/onboarding.svelte.ts +16 -16
  202. package/src/onboarding/pick.test.ts +61 -52
  203. package/src/onboarding/pick.ts +16 -13
  204. package/src/onboarding/stores.test.ts +80 -80
  205. package/src/onboarding/stores.ts +41 -39
  206. package/src/onboarding/tour-card.svelte +30 -5
  207. package/src/onboarding/tour-runner.svelte +12 -3
  208. package/src/onboarding/types.ts +20 -12
  209. package/src/onboarding/wait.ts +22 -22
  210. package/src/panel/side-panel.svelte +94 -93
  211. package/src/record/demo-data.ts +291 -225
  212. package/src/record/field-section.svelte +38 -34
  213. package/src/record/fields.ts +148 -96
  214. package/src/record/format.ts +63 -56
  215. package/src/record/index.ts +49 -35
  216. package/src/record/info-list.svelte +145 -114
  217. package/src/record/kinds.ts +31 -31
  218. package/src/record/logic.ts +216 -214
  219. package/src/record/next-up.svelte +45 -34
  220. package/src/record/project-list.svelte +56 -50
  221. package/src/record/record-demo.svelte +130 -110
  222. package/src/record/record-header.svelte +37 -28
  223. package/src/record/record-shell.svelte +54 -54
  224. package/src/record/record-stats.svelte +25 -25
  225. package/src/record/record-timeline.svelte +134 -114
  226. package/src/record/record.test.ts +430 -309
  227. package/src/record/steps-rail.svelte +225 -216
  228. package/src/record/types.ts +234 -215
  229. package/src/record/where-we-are.svelte +62 -58
  230. package/src/shadcn/button/button.svelte +1 -1
  231. package/src/shadcn/card/card-title.svelte +1 -5
  232. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  233. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  234. package/src/shadcn/table/table-cell.svelte +2 -3
  235. package/src/shadcn/utilities.css +3 -8
  236. package/src/slides/Controls.svelte +150 -124
  237. package/src/slides/Deck.svelte +243 -238
  238. package/src/slides/Fragment.svelte +64 -57
  239. package/src/slides/Overview.svelte +106 -101
  240. package/src/slides/Presenter.svelte +300 -266
  241. package/src/slides/Slide.svelte +185 -170
  242. package/src/slides/SlideList.svelte +16 -12
  243. package/src/slides/ambient.d.ts +1 -1
  244. package/src/slides/bridges.ts +42 -23
  245. package/src/slides/deck-controller.svelte.ts +47 -35
  246. package/src/slides/demo-slides.svelte +330 -306
  247. package/src/slides/demo.svelte +164 -160
  248. package/src/slides/export/capture.ts +20 -16
  249. package/src/slides/export/ffmpeg.ts +43 -38
  250. package/src/slides/export/index.ts +5 -5
  251. package/src/slides/export/launch.ts +3 -1
  252. package/src/slides/export/pdf.ts +30 -27
  253. package/src/slides/export/types.ts +10 -8
  254. package/src/slides/index.ts +11 -11
  255. package/src/slides/labels.ts +22 -19
  256. package/src/slides/layout.ts +9 -2
  257. package/src/slides/model.ts +4 -4
  258. package/src/slides/navigation.ts +43 -31
  259. package/src/slides/theme.css +5 -7
  260. package/src/slides/types.ts +9 -10
  261. package/src/status/define-statuses.ts +17 -24
  262. package/src/status/index.ts +5 -5
  263. package/src/status/status-badge.svelte +3 -1
  264. package/src/status/status-dot.svelte +1 -3
  265. package/src/status/status-icon.svelte +1 -1
  266. package/src/status/status-types.ts +21 -21
  267. package/src/tasks/board-order.ts +2 -4
  268. package/src/tasks/demo.svelte +189 -31
  269. package/src/tasks/detail/detail-demo.svelte +73 -13
  270. package/src/tasks/detail/detail-types.ts +45 -45
  271. package/src/tasks/detail/task-detail.svelte +40 -10
  272. package/src/tasks/detail/task-property.svelte +8 -2
  273. package/src/tasks/detail/task-thread.svelte +53 -12
  274. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  275. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  276. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  277. package/src/tasks/gantt/gantt-header.svelte +9 -3
  278. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  279. package/src/tasks/gantt/gantt.css +2 -1
  280. package/src/tasks/gantt/gantt.test.ts +3 -1
  281. package/src/tasks/gantt/logic.test.ts +110 -20
  282. package/src/tasks/gantt/logic.ts +42 -6
  283. package/src/tasks/inbox/inbox-card.svelte +13 -5
  284. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  285. package/src/tasks/inbox/inbox-logic.ts +6 -40
  286. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  287. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  288. package/src/tasks/inbox/inbox-types.ts +54 -58
  289. package/src/tasks/inbox/index.ts +22 -21
  290. package/src/tasks/index.ts +49 -52
  291. package/src/tasks/kanban-board.svelte +16 -12
  292. package/src/tasks/kanban-types.ts +2 -3
  293. package/src/tasks/presets/sales.ts +6 -6
  294. package/src/tasks/task-gantt.svelte +28 -8
  295. package/src/tasks/task-list.svelte +41 -17
  296. package/src/tasks/time/duration.ts +40 -40
  297. package/src/tasks/time/time-chip.svelte +9 -2
  298. package/src/tasks/time/time-dot.svelte +1 -1
  299. package/src/tasks/time/time-ring.svelte +11 -3
  300. package/src/tasks/time/time-tracker.svelte +57 -15
  301. package/src/tasks/time/time-types.ts +57 -57
  302. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  303. package/src/thread/index.ts +7 -7
  304. package/src/thread/logic.ts +36 -33
  305. package/src/thread/thread.svelte +82 -16
  306. package/src/thread/thread.test.ts +58 -47
  307. package/src/thread/types.ts +68 -68
  308. package/src/tokens/craft.css +17 -53
  309. package/src/tokens/index.css +3 -13
  310. package/src/tokens/palette.css +13 -11
  311. package/src/tokens/primitives.css +10 -23
  312. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -1,591 +1,620 @@
1
1
  <script lang="ts">
2
- import { onMount, untrack } from 'svelte';
3
- import type {
4
- BotAvatarShading,
5
- BotAvatarState,
6
- BotAvatarType,
7
- } from './vendor/types.js';
8
- import type { AgentAvatarProps } from './types.js';
9
- import { botAvatarPresets, stateLabels } from './vendor/presets.js';
10
- import { SHAPE_PATHS, SHAPE_PARTS } from './vendor/shapes.js';
11
- import { richer, shade } from './vendor/color.js';
12
- import { Sim, restPose } from './vendor/engine.js';
13
- import { draw, LIGHT_DEFAULTS, OVERSCAN, RISE, type DrawConfig } from './vendor/draw.js';
14
- import { bakesPending, hurry, onLanded, takeMissed, warmPlastic, type FurStyle } from './vendor/plastic.js';
15
- import { subscribe, pointer } from './vendor/ticker.js';
16
- import {
17
- SHOW_WITHIN,
18
- bodyPath,
19
- nextUid,
20
- onThemeChange,
21
- partPaths,
22
- showReady,
23
- waiters,
24
- } from './page-state.js';
25
- import {
26
- clamp,
27
- clampOpt,
28
- deviceDpr,
29
- hashSeed,
30
- inkFor,
31
- lightsFor,
32
- pointerStrength,
33
- q,
34
- reducedMotion,
35
- resolveTheme as themeOf,
36
- warmIdle,
37
- } from './helpers.js';
38
-
39
- let {
40
- name,
41
- role,
42
- shape = 'clover',
43
- path,
44
- face = 'eyes',
45
- state: botState = 'default',
46
- size,
47
- color,
48
- ink,
49
- brightness: brightnessProp,
50
- saturation: saturationProp,
51
- speed = 1,
52
- paused = false,
53
- seed,
54
- shading = 'fabric',
55
- hat = 'none',
56
- glasses = 'none',
57
- headphones = false,
58
- bowTie = false,
59
- accessoryColor = '#27272b',
60
- shadow,
61
- highlight,
62
- depth = 0.65,
63
- roundness = 1,
64
- furLength = 1,
65
- furDensity = 1.6,
66
- furFuzz = 0.9,
67
- furClumps = 0.4,
68
- furCurl = 0.7,
69
- furGravity = 0.9,
70
- light,
71
- rim,
72
- spread,
73
- backLight,
74
- lightFront,
75
- shine,
76
- sheen,
77
- backSoftness,
78
- pose: heldPose,
79
- interactive = true,
80
- turn = 1,
81
- theme = 'auto',
82
- whirl = 0,
83
- whirlSize = 1,
84
- whirlWidth = 1,
85
- whirlLength = 1,
86
- whirlTilt = 1,
87
- jumpHeight = 26,
88
- jumpTime = 0.68,
89
- jumpStretch = 1,
90
- jumpSpin = 1,
91
- jumpLean = 6,
92
- jumpEvery = 8,
93
- jumpLand = 0,
94
- jumpSquash = 1.15,
95
- jumpSquashTime = 0.37,
96
- jumpSquashEase = 'pulse',
97
- jumpGroundTime = 0.11,
98
- jumpGroundEase = 'pulse',
99
- jumpRiseTime = 0.33,
100
- jumpRiseEase = 'pulse',
101
- jumpClickSquashTime = 0.24,
102
- class: className,
103
- style,
104
- 'aria-label': ariaLabel,
105
- 'aria-hidden': ariaHidden,
106
- onclick: userClick,
107
- ...rest
108
- }: AgentAvatarProps = $props();
109
-
110
- const shapePath = (t: BotAvatarType) => SHAPE_PATHS[t] ?? SHAPE_PATHS.clover;
111
-
112
- const uid = nextUid();
113
-
114
- /* The avatar draws at 75% of its slot so an idle jump has room in the box
2
+ import { onMount, untrack } from "svelte";
3
+ import type {
4
+ BotAvatarShading,
5
+ BotAvatarState,
6
+ BotAvatarType,
7
+ } from "./vendor/types.js";
8
+ import type { AgentAvatarProps } from "./types.js";
9
+ import { botAvatarPresets, stateLabels } from "./vendor/presets.js";
10
+ import { SHAPE_PATHS, SHAPE_PARTS } from "./vendor/shapes.js";
11
+ import { richer, shade } from "./vendor/color.js";
12
+ import { Sim, restPose } from "./vendor/engine.js";
13
+ import {
14
+ draw,
15
+ LIGHT_DEFAULTS,
16
+ OVERSCAN,
17
+ RISE,
18
+ type DrawConfig,
19
+ } from "./vendor/draw.js";
20
+ import {
21
+ bakesPending,
22
+ hurry,
23
+ onLanded,
24
+ takeMissed,
25
+ warmPlastic,
26
+ type FurStyle,
27
+ } from "./vendor/plastic.js";
28
+ import { subscribe, pointer } from "./vendor/ticker.js";
29
+ import {
30
+ SHOW_WITHIN,
31
+ bodyPath,
32
+ nextUid,
33
+ onThemeChange,
34
+ partPaths,
35
+ showReady,
36
+ waiters,
37
+ } from "./page-state.js";
38
+ import {
39
+ clamp,
40
+ clampOpt,
41
+ deviceDpr,
42
+ hashSeed,
43
+ inkFor,
44
+ lightsFor,
45
+ pointerStrength,
46
+ q,
47
+ reducedMotion,
48
+ resolveTheme as themeOf,
49
+ warmIdle,
50
+ } from "./helpers.js";
51
+
52
+ let {
53
+ name,
54
+ role,
55
+ shape = "clover",
56
+ path,
57
+ face = "eyes",
58
+ state: botState = "default",
59
+ size,
60
+ color,
61
+ ink,
62
+ brightness: brightnessProp,
63
+ saturation: saturationProp,
64
+ speed = 1,
65
+ paused = false,
66
+ seed,
67
+ shading = "fabric",
68
+ hat = "none",
69
+ glasses = "none",
70
+ headphones = false,
71
+ bowTie = false,
72
+ accessoryColor = "#27272b",
73
+ shadow,
74
+ highlight,
75
+ depth = 0.65,
76
+ roundness = 1,
77
+ furLength = 1,
78
+ furDensity = 1.6,
79
+ furFuzz = 0.9,
80
+ furClumps = 0.4,
81
+ furCurl = 0.7,
82
+ furGravity = 0.9,
83
+ light,
84
+ rim,
85
+ spread,
86
+ backLight,
87
+ lightFront,
88
+ shine,
89
+ sheen,
90
+ backSoftness,
91
+ pose: heldPose,
92
+ interactive = true,
93
+ turn = 1,
94
+ theme = "auto",
95
+ whirl = 0,
96
+ whirlSize = 1,
97
+ whirlWidth = 1,
98
+ whirlLength = 1,
99
+ whirlTilt = 1,
100
+ jumpHeight = 26,
101
+ jumpTime = 0.68,
102
+ jumpStretch = 1,
103
+ jumpSpin = 1,
104
+ jumpLean = 6,
105
+ jumpEvery = 8,
106
+ jumpLand = 0,
107
+ jumpSquash = 1.15,
108
+ jumpSquashTime = 0.37,
109
+ jumpSquashEase = "pulse",
110
+ jumpGroundTime = 0.11,
111
+ jumpGroundEase = "pulse",
112
+ jumpRiseTime = 0.33,
113
+ jumpRiseEase = "pulse",
114
+ jumpClickSquashTime = 0.24,
115
+ class: className,
116
+ style,
117
+ "aria-label": ariaLabel,
118
+ "aria-hidden": ariaHidden,
119
+ onclick: userClick,
120
+ ...rest
121
+ }: AgentAvatarProps = $props();
122
+
123
+ const shapePath = (t: BotAvatarType) => SHAPE_PATHS[t] ?? SHAPE_PATHS.clover;
124
+
125
+ const uid = nextUid();
126
+
127
+ /* The avatar draws at 75% of its slot so an idle jump has room in the box
115
128
  and never clips. A numeric `size` overrides the measurement. */
116
- const JUMP_CLEARANCE = 0.75;
117
- let hostEl = $state<HTMLElement | null>(null);
118
- let hostWidth = $state(0);
119
- const drawnSize = $derived(
120
- typeof size === 'number' ? size : hostWidth > 0 ? hostWidth * JUMP_CLEARANCE : 0,
121
- );
122
-
123
- const type = $derived(shape);
124
- const preset = $derived(botAvatarPresets[type] ?? botAvatarPresets.clover);
125
- const faceKind = $derived(face ?? preset.face);
126
- const picked = $derived(color ?? preset.color);
127
- const brightness = $derived(brightnessProp ?? (preset.brightness ?? 1));
128
- const saturation = $derived(saturationProp ?? (preset.saturation ?? 1.5));
129
- const vivid = $derived(clamp(saturation, 1.5, 2.5) - 1.5);
130
- const body = $derived(
131
- richer(
132
- brightness === 1 && saturation === 1
133
- ? picked
134
- : shade(
135
- picked,
136
- (Math.min(2, Math.max(0, brightness)) - 1) * 0.35,
137
- (Math.min(1.5, Math.max(0, saturation)) - 1) * 0.5,
138
- ),
139
- vivid,
140
- ),
141
- );
142
- const inkColor = $derived(ink ?? inkFor(body));
143
- const seedValue = $derived(Math.min(1, Math.max(0, seed ?? hashSeed(uid))));
144
- const stateKey = $derived((botState in stateLabels ? botState : 'default') as BotAvatarState);
145
- const frozen = $derived(paused || !(speed > 0));
146
- const shadingMode = $derived(
147
- (shading === true ? 'crisp' : shading === false ? 'flat' : shading) as BotAvatarShading,
148
- );
149
- const customPath = $derived(typeof path === 'string' && path.trim() ? path.trim() : null);
150
- const outlineKey = $derived(
151
- customPath ? `path:${hashSeed(customPath)}:${customPath.length}` : type,
152
- );
153
- const lit = $derived(LIGHT_DEFAULTS[shadingMode === 'fabric' ? 'fabric' : 'other']);
154
- const fur = $derived({
155
- length: q(furLength, 0.3, 2.5),
156
- density: q(furDensity, 0.3, 2),
157
- fuzz: q(furFuzz, 0, 1),
158
- clumps: q(furClumps, 0, 1),
159
- curl: q(furCurl, 0, 1),
160
- gravity: q(furGravity, 0, 1),
161
- } satisfies FurStyle);
162
- const material = $derived(shadingMode === 'plastic' || shadingMode === 'fabric');
163
- const lookKey = $derived(`${shadingMode}|${outlineKey}|${Math.round(drawnSize)}`);
164
- /* What the bakes depend on, as strings, so the warm effect re-runs only
129
+ const JUMP_CLEARANCE = 0.75;
130
+ let hostEl = $state<HTMLElement | null>(null);
131
+ let hostWidth = $state(0);
132
+ const drawnSize = $derived(
133
+ typeof size === "number" ? size : hostWidth > 0 ? hostWidth * JUMP_CLEARANCE : 0,
134
+ );
135
+
136
+ const type = $derived(shape);
137
+ const preset = $derived(botAvatarPresets[type] ?? botAvatarPresets.clover);
138
+ const faceKind = $derived(face ?? preset.face);
139
+ const picked = $derived(color ?? preset.color);
140
+ const brightness = $derived(brightnessProp ?? preset.brightness ?? 1);
141
+ const saturation = $derived(saturationProp ?? preset.saturation ?? 1.5);
142
+ const vivid = $derived(clamp(saturation, 1.5, 2.5) - 1.5);
143
+ const body = $derived(
144
+ richer(
145
+ brightness === 1 && saturation === 1
146
+ ? picked
147
+ : shade(
148
+ picked,
149
+ (Math.min(2, Math.max(0, brightness)) - 1) * 0.35,
150
+ (Math.min(1.5, Math.max(0, saturation)) - 1) * 0.5,
151
+ ),
152
+ vivid,
153
+ ),
154
+ );
155
+ const inkColor = $derived(ink ?? inkFor(body));
156
+ const seedValue = $derived(Math.min(1, Math.max(0, seed ?? hashSeed(uid))));
157
+ const stateKey = $derived((botState in stateLabels ? botState : "default") as BotAvatarState);
158
+ const frozen = $derived(paused || !(speed > 0));
159
+ const shadingMode = $derived(
160
+ (shading === true ? "crisp" : shading === false ? "flat" : shading) as BotAvatarShading,
161
+ );
162
+ const customPath = $derived(typeof path === "string" && path.trim() ? path.trim() : null);
163
+ const outlineKey = $derived(
164
+ customPath ? `path:${hashSeed(customPath)}:${customPath.length}` : type,
165
+ );
166
+ const lit = $derived(LIGHT_DEFAULTS[shadingMode === "fabric" ? "fabric" : "other"]);
167
+ const fur = $derived({
168
+ length: q(furLength, 0.3, 2.5),
169
+ density: q(furDensity, 0.3, 2),
170
+ fuzz: q(furFuzz, 0, 1),
171
+ clumps: q(furClumps, 0, 1),
172
+ curl: q(furCurl, 0, 1),
173
+ gravity: q(furGravity, 0, 1),
174
+ } satisfies FurStyle);
175
+ const material = $derived(shadingMode === "plastic" || shadingMode === "fabric");
176
+ const lookKey = $derived(`${shadingMode}|${outlineKey}|${Math.round(drawnSize)}`);
177
+ /* What the bakes depend on, as strings, so the warm effect re-runs only
165
178
  when one of them changes, not on every prop or every resized pixel. */
166
- const warmSize = $derived(Math.round(drawnSize));
167
- const furKey = $derived(`${fur.length}|${fur.density}|${fur.fuzz}|${fur.clumps}|${fur.curl}|${fur.gravity}`);
168
- const interactiveNow = $derived(interactive && !heldPose);
179
+ const warmSize = $derived(Math.round(drawnSize));
180
+ const furKey = $derived(
181
+ `${fur.length}|${fur.density}|${fur.fuzz}|${fur.clumps}|${fur.curl}|${fur.gravity}`,
182
+ );
183
+ const interactiveNow = $derived(interactive && !heldPose);
169
184
 
170
- /* Captured once, on purpose: `shown` never returns to false, so a later
185
+ /* Captured once, on purpose: `shown` never returns to false, so a later
171
186
  shading change must not reset it. */
172
- function beginsMaterial() {
173
- return shading === 'plastic' || shading === 'fabric';
174
- }
175
-
176
- function partsFor(t: BotAvatarType) {
177
- return !customPath && SHAPE_PARTS[t] ? partPaths(SHAPE_PARTS[t] as string) : undefined;
178
- }
179
-
180
- function configure(): DrawConfig {
181
- return {
182
- path: bodyPath(customPath ?? shapePath(type)),
183
- face: faceKind,
184
- faceX: preset.faceX,
185
- faceY: preset.faceY,
186
- faceScale: preset.faceScale,
187
- color: body,
188
- ink: inkColor,
189
- shading: shadingMode,
190
- shadow: clampOpt(shadow, lit.shadow, 0, 2),
191
- highlight: clampOpt(highlight, lit.highlight, 0, 2),
192
- depth: clamp(depth, 0.2, 2),
193
- roundness: clamp(roundness, 0, 1),
194
- fur,
195
- vivid,
196
- backLight,
197
- lightFront,
198
- shine,
199
- sheen,
200
- backSoftness,
201
- light: light ?? lit.light,
202
- rim: clampOpt(rim, lit.rim, 0, 2),
203
- spread: clampOpt(spread, lit.spread, 0.4, 2.5),
204
- typeKey: outlineKey,
205
- still: frozen || reducedMotion(),
206
- furOnIdle: true,
207
- whirl: {
208
- strength: clamp(whirl, 0, 2),
209
- size: clamp(whirlSize, 0.6, 1.6),
210
- width: clamp(whirlWidth, 0.4, 2),
211
- length: clamp(whirlLength, 0.4, 1.6),
212
- tilt: clamp(whirlTilt, 0.5, 1.8),
213
- },
214
- parts: partsFor(type),
215
- wear: { hat, glasses, headphones, bowTie, color: accessoryColor, fabric: shadingMode === 'fabric' },
216
- };
217
- }
218
-
219
- const cfg = $derived(configure());
220
-
221
- let canvasEl = $state<HTMLCanvasElement | null>(null);
222
- let sim: Sim | null = null;
223
- let cssSize = 0;
224
- let lastLook: string | null = null;
225
-
226
- let shown = $state(!beginsMaterial());
227
- let shownRef = !beginsMaterial();
228
- function markShown() {
229
- shownRef = true;
230
- shown = true;
231
- }
232
-
233
- let stillPaint: (() => void) | null = null;
234
- let waiting: (() => void) | null = null;
235
- let watch: (() => void) | null = null;
236
- let showPaint: (() => void) | null = null;
237
- let ready = false;
238
- let holding: (() => void) | null = null;
239
- const meRef = {};
240
-
241
- const resolveTheme = (el: HTMLElement | null) => themeOf(theme, el);
242
-
243
- function layoutPx(canvas: HTMLCanvasElement) {
244
- return canvas.clientWidth / OVERSCAN || cssSize || drawnSize || 64;
245
- }
246
-
247
- function poseFor(rest0: ReturnType<typeof restPose>) {
248
- const held = heldPose;
249
- if (held)
250
- return { ...rest0, yaw: held.yaw ?? 0, pitch: held.pitch ?? rest0.pitch, roll: held.roll ?? rest0.roll };
251
- return sim ? sim.pose : rest0;
252
- }
253
-
254
- function paint() {
255
- const canvas = canvasEl;
256
- const c = cfg;
257
- if (!canvas || !c?.path) return;
258
- const px = layoutPx(canvas);
259
- if (!px) return;
260
- const dpr = deviceDpr();
261
- const want = Math.round(px * OVERSCAN * dpr);
262
- if (canvas.width !== want || canvas.height !== want) {
263
- canvas.width = want;
264
- canvas.height = want;
265
- }
266
- cssSize = px;
267
- const ctx = canvas.getContext('2d');
268
- if (!ctx) return;
269
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
270
- c.dpr = dpr;
271
- draw(ctx, px, poseFor(restPose(stateKey)), c);
272
- }
273
-
274
- function paintStill() {
275
- const canvas = canvasEl;
276
- const c = cfg;
277
- if (!canvas || !c?.path) return;
278
- const px = layoutPx(canvas);
279
- if (!px) return;
280
- cssSize = px;
281
- const dpr = deviceDpr();
282
- canvas.width = canvas.height = Math.round(px * OVERSCAN * dpr);
283
- const ctx = canvas.getContext('2d');
284
- if (ctx) {
285
- c.theme = resolveTheme(canvas);
286
- c.dpr = dpr;
287
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
288
- draw(ctx, px, restPose(stateKey), c);
289
- }
290
- }
291
-
292
- function stopWaiting() {
293
- if (waiting) waiting();
294
- waiting = null;
295
- }
296
-
297
- function settle(paintNow: () => void) {
298
- takeMissed();
299
- paintNow();
300
- if (!takeMissed()) stopWaiting();
301
- else if (!waiting)
302
- waiting = onLanded(() => {
303
- const p = stillPaint;
304
- if (canvasEl && p) settle(p);
305
- else stopWaiting();
306
- });
307
- }
308
-
309
- function stopWatch() {
310
- if (watch) watch();
311
- watch = null;
312
- }
313
-
314
- function showWhenBaked(paintNow: () => void) {
315
- showPaint = paintNow;
316
- /* No canvas means nothing was drawn, so nothing could report a missed
187
+ function beginsMaterial() {
188
+ return shading === "plastic" || shading === "fabric";
189
+ }
190
+
191
+ function partsFor(t: BotAvatarType) {
192
+ return !customPath && SHAPE_PARTS[t] ? partPaths(SHAPE_PARTS[t] as string) : undefined;
193
+ }
194
+
195
+ function configure(): DrawConfig {
196
+ return {
197
+ path: bodyPath(customPath ?? shapePath(type)),
198
+ face: faceKind,
199
+ faceX: preset.faceX,
200
+ faceY: preset.faceY,
201
+ faceScale: preset.faceScale,
202
+ color: body,
203
+ ink: inkColor,
204
+ shading: shadingMode,
205
+ shadow: clampOpt(shadow, lit.shadow, 0, 2),
206
+ highlight: clampOpt(highlight, lit.highlight, 0, 2),
207
+ depth: clamp(depth, 0.2, 2),
208
+ roundness: clamp(roundness, 0, 1),
209
+ fur,
210
+ vivid,
211
+ backLight,
212
+ lightFront,
213
+ shine,
214
+ sheen,
215
+ backSoftness,
216
+ light: light ?? lit.light,
217
+ rim: clampOpt(rim, lit.rim, 0, 2),
218
+ spread: clampOpt(spread, lit.spread, 0.4, 2.5),
219
+ typeKey: outlineKey,
220
+ still: frozen || reducedMotion(),
221
+ furOnIdle: true,
222
+ whirl: {
223
+ strength: clamp(whirl, 0, 2),
224
+ size: clamp(whirlSize, 0.6, 1.6),
225
+ width: clamp(whirlWidth, 0.4, 2),
226
+ length: clamp(whirlLength, 0.4, 1.6),
227
+ tilt: clamp(whirlTilt, 0.5, 1.8),
228
+ },
229
+ parts: partsFor(type),
230
+ wear: {
231
+ hat,
232
+ glasses,
233
+ headphones,
234
+ bowTie,
235
+ color: accessoryColor,
236
+ fabric: shadingMode === "fabric",
237
+ },
238
+ };
239
+ }
240
+
241
+ const cfg = $derived(configure());
242
+
243
+ let canvasEl = $state<HTMLCanvasElement | null>(null);
244
+ let sim: Sim | null = null;
245
+ let cssSize = 0;
246
+ let lastLook: string | null = null;
247
+
248
+ let shown = $state(!beginsMaterial());
249
+ let shownRef = !beginsMaterial();
250
+ function markShown() {
251
+ shownRef = true;
252
+ shown = true;
253
+ }
254
+
255
+ let stillPaint: (() => void) | null = null;
256
+ let waiting: (() => void) | null = null;
257
+ let watch: (() => void) | null = null;
258
+ let showPaint: (() => void) | null = null;
259
+ let ready = false;
260
+ let holding: (() => void) | null = null;
261
+ const meRef = {};
262
+
263
+ const resolveTheme = (el: HTMLElement | null) => themeOf(theme, el);
264
+
265
+ function layoutPx(canvas: HTMLCanvasElement) {
266
+ return canvas.clientWidth / OVERSCAN || cssSize || drawnSize || 64;
267
+ }
268
+
269
+ function poseFor(rest0: ReturnType<typeof restPose>) {
270
+ const held = heldPose;
271
+ if (held)
272
+ return {
273
+ ...rest0,
274
+ yaw: held.yaw ?? 0,
275
+ pitch: held.pitch ?? rest0.pitch,
276
+ roll: held.roll ?? rest0.roll,
277
+ };
278
+ return sim ? sim.pose : rest0;
279
+ }
280
+
281
+ function paint() {
282
+ const canvas = canvasEl;
283
+ const c = cfg;
284
+ if (!canvas || !c?.path) return;
285
+ const px = layoutPx(canvas);
286
+ if (!px) return;
287
+ const dpr = deviceDpr();
288
+ const want = Math.round(px * OVERSCAN * dpr);
289
+ if (canvas.width !== want || canvas.height !== want) {
290
+ canvas.width = want;
291
+ canvas.height = want;
292
+ }
293
+ cssSize = px;
294
+ const ctx = canvas.getContext("2d");
295
+ if (!ctx) return;
296
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
297
+ c.dpr = dpr;
298
+ draw(ctx, px, poseFor(restPose(stateKey)), c);
299
+ }
300
+
301
+ function paintStill() {
302
+ const canvas = canvasEl;
303
+ const c = cfg;
304
+ if (!canvas || !c?.path) return;
305
+ const px = layoutPx(canvas);
306
+ if (!px) return;
307
+ cssSize = px;
308
+ const dpr = deviceDpr();
309
+ canvas.width = canvas.height = Math.round(px * OVERSCAN * dpr);
310
+ const ctx = canvas.getContext("2d");
311
+ if (ctx) {
312
+ c.theme = resolveTheme(canvas);
313
+ c.dpr = dpr;
314
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
315
+ draw(ctx, px, restPose(stateKey), c);
316
+ }
317
+ }
318
+
319
+ function stopWaiting() {
320
+ if (waiting) waiting();
321
+ waiting = null;
322
+ }
323
+
324
+ function settle(paintNow: () => void) {
325
+ takeMissed();
326
+ paintNow();
327
+ if (!takeMissed()) stopWaiting();
328
+ else if (!waiting)
329
+ waiting = onLanded(() => {
330
+ const p = stillPaint;
331
+ if (canvasEl && p) settle(p);
332
+ else stopWaiting();
333
+ });
334
+ }
335
+
336
+ function stopWatch() {
337
+ if (watch) watch();
338
+ watch = null;
339
+ }
340
+
341
+ function showWhenBaked(paintNow: () => void) {
342
+ showPaint = paintNow;
343
+ /* No canvas means nothing was drawn, so nothing could report a missed
317
344
  bake. Calling that ready would skip the fade. */
318
- if (!canvasEl) return;
319
- takeMissed();
320
- paintNow();
321
- ready = !takeMissed();
322
- if (ready && !bakesPending()) {
323
- showReady();
324
- return;
325
- }
326
- if (!watch)
327
- watch = onLanded(() => {
328
- const p = showPaint;
329
- if (canvasEl && p && !shownRef) showWhenBaked(p);
330
- else stopWatch();
331
- showReady();
332
- });
333
- }
334
-
335
- function warm() {
336
- if (!material || !cfg?.path) return;
337
- const c = cfg;
338
- const dev = (drawnSize || 64) * deviceDpr();
339
- const lightAt = c.light ?? 295;
340
- const lights = lightsFor(lightAt, backLight, lightFront);
341
- warmPlastic(outlineKey, c.path, dev, depth, shadingMode === 'fabric', fur, lightAt, lights);
342
- }
343
-
344
- function hold() {
345
- warm();
346
- if (!bakesPending()) return;
347
- hurry(true);
348
- const release = () => {
349
- if (!holding) return;
350
- holding();
351
- holding = null;
352
- applyPose();
353
- };
354
- const off = onLanded(() => {
355
- if (!bakesPending()) release();
356
- });
357
- const t = setTimeout(release, SHOW_WITHIN);
358
- holding = () => {
359
- off();
360
- clearTimeout(t);
361
- hurry(false);
362
- };
363
- }
364
-
365
- function handleClick(e: MouseEvent) {
366
- if (interactive && !frozen && !heldPose) sim?.poke();
367
- userClick?.(e);
368
- }
369
-
370
- onMount(() => () => {
371
- stopWaiting();
372
- holding?.();
373
- holding = null;
374
- });
375
-
376
- function shouldHold() {
377
- return shownRef && material && lastLook !== null && lastLook !== lookKey && !holding;
378
- }
379
-
380
- function applyPose() {
381
- if (holding) return;
382
- if (reducedMotion()) {
383
- stillPaint = paintStill;
384
- if (shownRef) settle(paintStill);
385
- else showWhenBaked(paintStill);
386
- return;
387
- }
388
- if (canvasEl) {
389
- const c = cfg;
390
- if (c) c.theme = resolveTheme(canvasEl);
391
- }
392
- if (!shownRef) {
393
- stillPaint = frozen ? paint : null;
394
- showWhenBaked(paint);
395
- } else if (frozen) {
396
- stillPaint = paint;
397
- settle(paint);
398
- } else {
399
- stillPaint = null;
400
- stopWaiting();
401
- paint();
402
- }
403
- }
404
-
405
- /* Sync the rig with the props on every meaningful change: this is the
345
+ if (!canvasEl) return;
346
+ takeMissed();
347
+ paintNow();
348
+ ready = !takeMissed();
349
+ if (ready && !bakesPending()) {
350
+ showReady();
351
+ return;
352
+ }
353
+ if (!watch)
354
+ watch = onLanded(() => {
355
+ const p = showPaint;
356
+ if (canvasEl && p && !shownRef) showWhenBaked(p);
357
+ else stopWatch();
358
+ showReady();
359
+ });
360
+ }
361
+
362
+ function warm() {
363
+ if (!material || !cfg?.path) return;
364
+ const c = cfg;
365
+ const dev = (drawnSize || 64) * deviceDpr();
366
+ const lightAt = c.light ?? 295;
367
+ const lights = lightsFor(lightAt, backLight, lightFront);
368
+ warmPlastic(outlineKey, c.path, dev, depth, shadingMode === "fabric", fur, lightAt, lights);
369
+ }
370
+
371
+ function hold() {
372
+ warm();
373
+ if (!bakesPending()) return;
374
+ hurry(true);
375
+ const release = () => {
376
+ if (!holding) return;
377
+ holding();
378
+ holding = null;
379
+ applyPose();
380
+ };
381
+ const off = onLanded(() => {
382
+ if (!bakesPending()) release();
383
+ });
384
+ const t = setTimeout(release, SHOW_WITHIN);
385
+ holding = () => {
386
+ off();
387
+ clearTimeout(t);
388
+ hurry(false);
389
+ };
390
+ }
391
+
392
+ function handleClick(e: MouseEvent) {
393
+ if (interactive && !frozen && !heldPose) sim?.poke();
394
+ userClick?.(e);
395
+ }
396
+
397
+ onMount(() => () => {
398
+ stopWaiting();
399
+ holding?.();
400
+ holding = null;
401
+ });
402
+
403
+ function shouldHold() {
404
+ return shownRef && material && lastLook !== null && lastLook !== lookKey && !holding;
405
+ }
406
+
407
+ function applyPose() {
408
+ if (holding) return;
409
+ if (reducedMotion()) {
410
+ stillPaint = paintStill;
411
+ if (shownRef) settle(paintStill);
412
+ else showWhenBaked(paintStill);
413
+ return;
414
+ }
415
+ if (canvasEl) {
416
+ const c = cfg;
417
+ if (c) c.theme = resolveTheme(canvasEl);
418
+ }
419
+ if (!shownRef) {
420
+ stillPaint = frozen ? paint : null;
421
+ showWhenBaked(paint);
422
+ } else if (frozen) {
423
+ stillPaint = paint;
424
+ settle(paint);
425
+ } else {
426
+ stillPaint = null;
427
+ stopWaiting();
428
+ paint();
429
+ }
430
+ }
431
+
432
+ /* Sync the rig with the props on every meaningful change: this is the
406
433
  React `useLayoutEffect` (which ran each render) made reactive. */
407
- $effect(() => {
408
- /* Until the slot is measured there's no canvas to draw on. */
409
- if (drawnSize <= 0) return;
410
- if (!sim) sim = new Sim(seedValue, stateKey);
411
- else sim.setState(stateKey);
412
- sim.setTurn(clamp(turn, 0, 2));
413
- if (shouldHold()) hold();
414
- lastLook = lookKey;
415
- sim.setJump({
416
- height: jumpHeight,
417
- time: Math.max(0.2, jumpTime),
418
- stretch: jumpStretch,
419
- spin: Math.max(0, Math.round(jumpSpin)),
420
- lean: jumpLean,
421
- every: jumpEvery,
422
- land: jumpLand,
423
- squash: jumpSquash,
424
- squashTime: Math.max(0.05, jumpSquashTime),
425
- squashEase: jumpSquashEase,
426
- groundTime: Math.max(0, jumpGroundTime),
427
- groundEase: jumpGroundEase,
428
- riseTime: Math.max(0.05, jumpRiseTime),
429
- riseEase: jumpRiseEase,
430
- clickSquashTime: Math.max(0.05, jumpClickSquashTime),
431
- });
432
- applyPose();
433
- });
434
-
435
- /* Register with the shared shown-gate so a row of baked avatars appears
434
+ $effect(() => {
435
+ /* Until the slot is measured there's no canvas to draw on. */
436
+ if (drawnSize <= 0) return;
437
+ if (!sim) sim = new Sim(seedValue, stateKey);
438
+ else sim.setState(stateKey);
439
+ sim.setTurn(clamp(turn, 0, 2));
440
+ if (shouldHold()) hold();
441
+ lastLook = lookKey;
442
+ sim.setJump({
443
+ height: jumpHeight,
444
+ time: Math.max(0.2, jumpTime),
445
+ stretch: jumpStretch,
446
+ spin: Math.max(0, Math.round(jumpSpin)),
447
+ lean: jumpLean,
448
+ every: jumpEvery,
449
+ land: jumpLand,
450
+ squash: jumpSquash,
451
+ squashTime: Math.max(0.05, jumpSquashTime),
452
+ squashEase: jumpSquashEase,
453
+ groundTime: Math.max(0, jumpGroundTime),
454
+ groundEase: jumpGroundEase,
455
+ riseTime: Math.max(0.05, jumpRiseTime),
456
+ riseEase: jumpRiseEase,
457
+ clickSquashTime: Math.max(0.05, jumpClickSquashTime),
458
+ });
459
+ applyPose();
460
+ });
461
+
462
+ /* Register with the shared shown-gate so a row of baked avatars appears
436
463
  together, or after SHOW_WITHIN at the latest. */
437
- $effect(() => {
438
- if (shown) return;
439
- waiters.set(meRef, { ready: () => ready, show: markShown });
440
- showReady();
441
- hurry(true);
442
- const t = setTimeout(markShown, SHOW_WITHIN);
443
- return () => {
444
- hurry(false);
445
- clearTimeout(t);
446
- stopWatch();
447
- waiters.delete(meRef);
448
- showReady();
449
- };
450
- });
451
-
452
- /* Once shown, a paused or reduced-motion avatar draws only when asked, so
464
+ $effect(() => {
465
+ if (shown) return;
466
+ waiters.set(meRef, { ready: () => ready, show: markShown });
467
+ showReady();
468
+ hurry(true);
469
+ const t = setTimeout(markShown, SHOW_WITHIN);
470
+ return () => {
471
+ hurry(false);
472
+ clearTimeout(t);
473
+ stopWatch();
474
+ waiters.delete(meRef);
475
+ showReady();
476
+ };
477
+ });
478
+
479
+ /* Once shown, a paused or reduced-motion avatar draws only when asked, so
453
480
  it repaints here and keeps repainting as its bakes land. */
454
- $effect(() => {
455
- if (!shown) return;
456
- untrack(() => {
457
- if (canvasEl && drawnSize > 0) applyPose();
458
- });
459
- });
460
-
461
- $effect(() => {
462
- if (theme !== 'auto') return;
463
- return onThemeChange(() => {
464
- if (canvasEl && drawnSize > 0) applyPose();
465
- });
466
- });
467
-
468
- /* Start the material's bakes on idle time as soon as the shape is known. */
469
- $effect(() => {
470
- void shadingMode;
471
- void outlineKey;
472
- void warmSize;
473
- void depth;
474
- void light;
475
- void backLight;
476
- void lightFront;
477
- void furKey;
478
- if (!material || !warmSize || !untrack(() => cfg?.path)) return;
479
- return warmIdle(warm);
480
- });
481
-
482
- function updatePointer(s: Sim, canvas: HTMLCanvasElement) {
483
- if (!interactiveNow || Number.isNaN(pointer.x)) {
484
- s.setPointer(0, 0, 0);
485
- return;
486
- }
487
- const r = canvas.getBoundingClientRect();
488
- const box = r.width / OVERSCAN || 1;
489
- const dx = (pointer.x - (r.left + r.width / 2)) / box;
490
- const dy = (pointer.y - (r.top + r.height / 2 + RISE * box)) / box;
491
- const d = Math.hypot(dx, dy);
492
- s.setPointer(dx / Math.max(1, d), dy / Math.max(1, d), pointerStrength(d));
493
- }
494
-
495
- /* The loop, only while visible, animated and not reduced. */
496
- $effect(() => {
497
- if (frozen || reducedMotion()) return;
498
- const canvas = canvasEl;
499
- if (!canvas) return;
500
- let unsub: (() => void) | null = null;
501
- const tick = (dt: number) => {
502
- const s = sim;
503
- if (!s) return;
504
- updatePointer(s, canvas);
505
- s.update(dt * speed);
506
- if (shownRef && !holding) paint();
507
- };
508
- const run = () => {
509
- if (!unsub) unsub = subscribe(tick);
510
- };
511
- const stop = () => {
512
- if (unsub) unsub();
513
- unsub = null;
514
- };
515
- let io: IntersectionObserver | null = null;
516
- if (typeof IntersectionObserver === 'function') {
517
- io = new IntersectionObserver((entries) => {
518
- if (entries[0]?.isIntersecting ?? true) run();
519
- else stop();
520
- });
521
- io.observe(canvas);
522
- } else run();
523
- return () => {
524
- stop();
525
- if (io) io.disconnect();
526
- };
527
- });
528
-
529
- const canvasStyle = $derived.by(() => {
530
- const s = drawnSize || 64;
531
- const dim = `${s * OVERSCAN}px`;
532
- const side = (OVERSCAN - 1) / 2;
533
- const pull = (k: number) => `${-s * k}px`;
534
- return (
535
- `display:inline-block;vertical-align:middle;width:${dim};height:${dim};` +
536
- `margin-left:${pull(side)};margin-right:${pull(side)};` +
537
- `margin-top:${pull(side + RISE)};margin-bottom:${pull(side - RISE)};` +
538
- `flex:none;opacity:${shown ? 1 : 0};transition:opacity 0.3s ease-out`
539
- );
540
- });
541
-
542
- /* No box: the slot carries no border, background or padding. Without an
481
+ $effect(() => {
482
+ if (!shown) return;
483
+ untrack(() => {
484
+ if (canvasEl && drawnSize > 0) applyPose();
485
+ });
486
+ });
487
+
488
+ $effect(() => {
489
+ if (theme !== "auto") return;
490
+ return onThemeChange(() => {
491
+ if (canvasEl && drawnSize > 0) applyPose();
492
+ });
493
+ });
494
+
495
+ /* Start the material's bakes on idle time as soon as the shape is known. */
496
+ $effect(() => {
497
+ void shadingMode;
498
+ void outlineKey;
499
+ void warmSize;
500
+ void depth;
501
+ void light;
502
+ void backLight;
503
+ void lightFront;
504
+ void furKey;
505
+ if (!material || !warmSize || !untrack(() => cfg?.path)) return;
506
+ return warmIdle(warm);
507
+ });
508
+
509
+ function updatePointer(s: Sim, canvas: HTMLCanvasElement) {
510
+ if (!interactiveNow || Number.isNaN(pointer.x)) {
511
+ s.setPointer(0, 0, 0);
512
+ return;
513
+ }
514
+ const r = canvas.getBoundingClientRect();
515
+ const box = r.width / OVERSCAN || 1;
516
+ const dx = (pointer.x - (r.left + r.width / 2)) / box;
517
+ const dy = (pointer.y - (r.top + r.height / 2 + RISE * box)) / box;
518
+ const d = Math.hypot(dx, dy);
519
+ s.setPointer(dx / Math.max(1, d), dy / Math.max(1, d), pointerStrength(d));
520
+ }
521
+
522
+ /* The loop, only while visible, animated and not reduced. */
523
+ $effect(() => {
524
+ if (frozen || reducedMotion()) return;
525
+ const canvas = canvasEl;
526
+ if (!canvas) return;
527
+ let unsub: (() => void) | null = null;
528
+ const tick = (dt: number) => {
529
+ const s = sim;
530
+ if (!s) return;
531
+ updatePointer(s, canvas);
532
+ s.update(dt * speed);
533
+ if (shownRef && !holding) paint();
534
+ };
535
+ const run = () => {
536
+ if (!unsub) unsub = subscribe(tick);
537
+ };
538
+ const stop = () => {
539
+ if (unsub) unsub();
540
+ unsub = null;
541
+ };
542
+ let io: IntersectionObserver | null = null;
543
+ if (typeof IntersectionObserver === "function") {
544
+ io = new IntersectionObserver((entries) => {
545
+ if (entries[0]?.isIntersecting ?? true) run();
546
+ else stop();
547
+ });
548
+ io.observe(canvas);
549
+ } else run();
550
+ return () => {
551
+ stop();
552
+ if (io) io.disconnect();
553
+ };
554
+ });
555
+
556
+ const canvasStyle = $derived.by(() => {
557
+ const s = drawnSize || 64;
558
+ const dim = `${s * OVERSCAN}px`;
559
+ const side = (OVERSCAN - 1) / 2;
560
+ const pull = (k: number) => `${-s * k}px`;
561
+ return (
562
+ `display:inline-block;vertical-align:middle;width:${dim};height:${dim};` +
563
+ `margin-left:${pull(side)};margin-right:${pull(side)};` +
564
+ `margin-top:${pull(side + RISE)};margin-bottom:${pull(side - RISE)};` +
565
+ `flex:none;opacity:${shown ? 1 : 0};transition:opacity 0.3s ease-out`
566
+ );
567
+ });
568
+
569
+ /* No box: the slot carries no border, background or padding. Without an
543
570
  explicit size it stays square, and the avatar fills three quarters of it. */
544
- const hostStyle = $derived.by(() => {
545
- const base =
546
- size === undefined
547
- ? 'display:inline-grid;place-items:center;line-height:0;width:100%;aspect-ratio:1/1;'
548
- : 'display:inline-block;line-height:0;';
549
- return style ? `${base}${style}` : base;
550
- });
551
-
552
- onMount(() => {
553
- if (!hostEl) return;
554
- /* A hidden parent (a closed tab or panel) reports 0. Keep the last width
571
+ const hostStyle = $derived.by(() => {
572
+ const base =
573
+ size === undefined
574
+ ? "display:inline-grid;place-items:center;line-height:0;width:100%;aspect-ratio:1/1;"
575
+ : "display:inline-block;line-height:0;";
576
+ return style ? `${base}${style}` : base;
577
+ });
578
+
579
+ onMount(() => {
580
+ if (!hostEl) return;
581
+ /* A hidden parent (a closed tab or panel) reports 0. Keep the last width
555
582
  so the canvas, its picture and its gate survive being hidden. */
556
- const observer = new ResizeObserver(([entry]) => {
557
- if (entry && entry.contentRect.width > 0) hostWidth = entry.contentRect.width;
558
- });
559
- observer.observe(hostEl);
560
- return () => observer.disconnect();
561
- });
562
-
563
- export function getCanvas(): HTMLCanvasElement | null {
564
- return canvasEl;
565
- }
583
+ const observer = new ResizeObserver(([entry]) => {
584
+ if (entry && entry.contentRect.width > 0) hostWidth = entry.contentRect.width;
585
+ });
586
+ observer.observe(hostEl);
587
+ return () => observer.disconnect();
588
+ });
589
+
590
+ export function getCanvas(): HTMLCanvasElement | null {
591
+ return canvasEl;
592
+ }
566
593
  </script>
567
594
 
568
595
  <span
569
- bind:this={hostEl}
570
- class={className ? `agent-avatar ${className}` : 'agent-avatar'}
571
- style={hostStyle}
572
- data-agent-shape={type}
596
+ bind:this={hostEl}
597
+ class={className ? `agent-avatar ${className}` : "agent-avatar"}
598
+ style={hostStyle}
599
+ data-agent-shape={type}
573
600
  >
574
- {#if drawnSize > 0}
575
- <canvas
576
- bind:this={canvasEl}
577
- data-bot-avatar={type}
578
- data-face={faceKind}
579
- data-state={stateKey}
580
- role={ariaHidden ? undefined : 'img'}
581
- aria-hidden={ariaHidden ? 'true' : undefined}
582
- aria-label={ariaHidden
583
- ? undefined
584
- : (ariaLabel ??
585
- (name ? `${name}${role ? `, ${role}` : ''}` : `${preset.label} agent, ${stateLabels[stateKey]}`))}
586
- style={canvasStyle}
587
- onclick={handleClick}
588
- {...rest}
589
- ></canvas>
590
- {/if}
601
+ {#if drawnSize > 0}
602
+ <canvas
603
+ bind:this={canvasEl}
604
+ data-bot-avatar={type}
605
+ data-face={faceKind}
606
+ data-state={stateKey}
607
+ role={ariaHidden ? undefined : "img"}
608
+ aria-hidden={ariaHidden ? "true" : undefined}
609
+ aria-label={ariaHidden
610
+ ? undefined
611
+ : (ariaLabel ??
612
+ (name
613
+ ? `${name}${role ? `, ${role}` : ""}`
614
+ : `${preset.label} agent, ${stateLabels[stateKey]}`))}
615
+ style={canvasStyle}
616
+ onclick={handleClick}
617
+ {...rest}
618
+ ></canvas>
619
+ {/if}
591
620
  </span>