@ctrl-ui/react 0.8.7 → 0.8.9

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 (341) hide show
  1. package/dist/action-bar.d.ts +15 -7
  2. package/dist/action-bar.js +39 -19
  3. package/dist/activity.js +7 -15
  4. package/dist/audio-recorder.d.ts +14 -4
  5. package/dist/audio-recorder.js +82 -29
  6. package/dist/audio-visualizer.js +1 -1
  7. package/dist/blocks/coding-agent.js +39 -39
  8. package/dist/blocks/design-canvas.d.ts +5 -1
  9. package/dist/blocks/design-canvas.js +198 -92
  10. package/dist/blocks/discussion.d.ts +28 -0
  11. package/dist/blocks/discussion.js +67 -0
  12. package/dist/blocks/file-explorer-data.d.ts +1 -0
  13. package/dist/blocks/file-explorer-data.js +4 -1
  14. package/dist/blocks/file-explorer.d.ts +9 -1
  15. package/dist/blocks/file-explorer.js +461 -192
  16. package/dist/blocks/settings.d.ts +2 -1
  17. package/dist/blocks/settings.js +101 -79
  18. package/dist/blocks/team-chat.d.ts +12 -0
  19. package/dist/blocks/team-chat.js +26 -0
  20. package/dist/blocks/theme-toggle.js +23 -31
  21. package/dist/chat-composer-attachment.d.ts +4 -1
  22. package/dist/chat-composer-attachment.js +35 -10
  23. package/dist/chat-composer-editor/extensions/mention.d.ts +5 -1
  24. package/dist/chat-composer-editor/extensions/mention.js +59 -28
  25. package/dist/chat-composer-editor/ghost.js +2 -4
  26. package/dist/chat-composer-editor/types.d.ts +5 -0
  27. package/dist/chat-composer-editor.d.ts +1 -1
  28. package/dist/chat-composer-editor.js +57 -30
  29. package/dist/chat-composer.d.ts +13 -32
  30. package/dist/chat-composer.js +52 -12
  31. package/dist/chat-layout.d.ts +19 -0
  32. package/dist/chat-layout.js +91 -40
  33. package/dist/chat-message/social.d.ts +26 -0
  34. package/dist/chat-message/social.js +122 -0
  35. package/dist/chat-message.d.ts +28 -29
  36. package/dist/chat-message.js +106 -25
  37. package/dist/context.d.ts +2 -2
  38. package/dist/context.js +38 -23
  39. package/dist/dynamic-notification-canvas-loop.d.ts +24 -0
  40. package/dist/dynamic-notification-canvas-loop.js +113 -0
  41. package/dist/dynamic-notification-glass.js +23 -114
  42. package/dist/dynamic-notification-liquid.js +14 -118
  43. package/dist/dynamic-notification.d.ts +16 -8
  44. package/dist/dynamic-notification.js +85 -22
  45. package/dist/email/email-brand.d.ts +7 -4
  46. package/dist/email/email-brand.js +9 -6
  47. package/dist/email/email-code.d.ts +3 -2
  48. package/dist/email/email-code.js +11 -9
  49. package/dist/email/email-footer.d.ts +5 -3
  50. package/dist/email/email-footer.js +11 -5
  51. package/dist/email/email.d.ts +31 -9
  52. package/dist/email/email.js +50 -26
  53. package/dist/email/templates.d.ts +10 -9
  54. package/dist/email/templates.js +102 -38
  55. package/dist/email/theme.js +1 -1
  56. package/dist/environment-variables.d.ts +4 -2
  57. package/dist/environment-variables.js +52 -32
  58. package/dist/filter-bar/editor.js +20 -8
  59. package/dist/filter-bar/model.d.ts +1 -0
  60. package/dist/filter-bar/model.js +10 -1
  61. package/dist/filter-bar/motion.js +4 -4
  62. package/dist/filter-bar/parts.js +19 -8
  63. package/dist/filter-bar/types.d.ts +1 -0
  64. package/dist/filter-bar.js +19 -13
  65. package/dist/hooks/use-audio-recorder.js +12 -6
  66. package/dist/hooks/use-chat-composer.d.ts +19 -7
  67. package/dist/hooks/use-chat-composer.js +32 -5
  68. package/dist/hooks/use-chat-message.d.ts +28 -2
  69. package/dist/hooks/use-chat-message.js +41 -10
  70. package/dist/hooks/use-chat-thread-scroll.js +43 -7
  71. package/dist/hooks/use-color-area.d.ts +3 -3
  72. package/dist/hooks/use-color-area.js +13 -12
  73. package/dist/hooks/use-copy-to-clipboard.d.ts +2 -1
  74. package/dist/hooks/use-copy-to-clipboard.js +20 -20
  75. package/dist/hooks/use-dynamic-notification.d.ts +6 -2
  76. package/dist/hooks/use-dynamic-notification.js +26 -10
  77. package/dist/hooks/use-mobile.d.ts +3 -2
  78. package/dist/hooks/use-mobile.js +8 -4
  79. package/dist/hooks/use-textarea-trigger-menu.js +2 -1
  80. package/dist/hooks/use-trigger-menu.d.ts +54 -0
  81. package/dist/hooks/use-trigger-menu.js +102 -47
  82. package/dist/hooks/use-user-ask.d.ts +2 -0
  83. package/dist/hooks/use-user-ask.js +35 -41
  84. package/dist/inline-attachment.d.ts +6 -2
  85. package/dist/inline-attachment.js +36 -15
  86. package/dist/inline-citation.d.ts +12 -5
  87. package/dist/inline-citation.js +25 -16
  88. package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
  89. package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
  90. package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
  91. package/dist/knob-contracts/audio-visualizer-knobs.js +2 -1
  92. package/dist/knob-contracts/avatar-knobs.d.ts +1 -1
  93. package/dist/knob-contracts/avatar-knobs.js +6 -1
  94. package/dist/knob-contracts/badge-knobs.d.ts +1 -1
  95. package/dist/knob-contracts/badge-knobs.js +2 -1
  96. package/dist/knob-contracts/breadcrumb-knobs.d.ts +4 -0
  97. package/dist/knob-contracts/breadcrumb-knobs.js +8 -0
  98. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  99. package/dist/knob-contracts/button-knobs.js +1 -0
  100. package/dist/knob-contracts/chat-message-knobs.d.ts +1 -1
  101. package/dist/knob-contracts/chat-message-knobs.js +8 -1
  102. package/dist/knob-contracts/code-diff-knobs.d.ts +1 -1
  103. package/dist/knob-contracts/code-diff-knobs.js +2 -0
  104. package/dist/knob-contracts/code-knobs.d.ts +1 -1
  105. package/dist/knob-contracts/code-knobs.js +2 -1
  106. package/dist/knob-contracts/dynamic-notification-knobs.d.ts +1 -1
  107. package/dist/knob-contracts/dynamic-notification-knobs.js +2 -0
  108. package/dist/knob-contracts/emoji-picker-knobs.d.ts +4 -0
  109. package/dist/knob-contracts/emoji-picker-knobs.js +10 -0
  110. package/dist/knob-contracts/field-knobs.d.ts +1 -1
  111. package/dist/knob-contracts/field-knobs.js +5 -1
  112. package/dist/knob-contracts/filter-bar-knobs.d.ts +1 -1
  113. package/dist/knob-contracts/filter-bar-knobs.js +1 -2
  114. package/dist/knob-contracts/markdown-knobs.d.ts +1 -1
  115. package/dist/knob-contracts/markdown-knobs.js +3 -1
  116. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  117. package/dist/knob-contracts/page-layout-knobs.js +2 -1
  118. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  119. package/dist/knob-contracts/popup-knobs.js +27 -0
  120. package/dist/knob-contracts/range-knobs.d.ts +1 -1
  121. package/dist/knob-contracts/range-knobs.js +1 -0
  122. package/dist/knob-contracts/rich-tooltip-knobs.d.ts +1 -1
  123. package/dist/knob-contracts/rich-tooltip-knobs.js +2 -1
  124. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  125. package/dist/knob-contracts/sidebar-knobs.js +27 -2
  126. package/dist/knob-contracts/spinner-knobs.d.ts +1 -1
  127. package/dist/knob-contracts/spinner-knobs.js +1 -1
  128. package/dist/knob-contracts/stepper-knobs.d.ts +1 -1
  129. package/dist/knob-contracts/stepper-knobs.js +4 -1
  130. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  131. package/dist/knob-contracts/switch-knobs.js +5 -0
  132. package/dist/knob-contracts/table-of-contents-knobs.d.ts +1 -1
  133. package/dist/knob-contracts/table-of-contents-knobs.js +2 -1
  134. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  135. package/dist/knob-contracts/tabs-knobs.js +1 -0
  136. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  137. package/dist/knob-contracts/tree-knobs.js +2 -1
  138. package/dist/lib/cn.js +0 -4
  139. package/dist/lib/contrast.d.ts +1 -0
  140. package/dist/lib/contrast.js +17 -5
  141. package/dist/lib/dropzone-validation.js +41 -28
  142. package/dist/lib/env-file.d.ts +1 -1
  143. package/dist/lib/env-file.js +5 -5
  144. package/dist/lib/motion.d.ts +4 -0
  145. package/dist/lib/motion.js +15 -0
  146. package/dist/lib/trigger-detect.js +6 -8
  147. package/dist/markdown-block.js +2 -1
  148. package/dist/popup-parts.d.ts +8 -0
  149. package/dist/popup-parts.js +43 -0
  150. package/dist/source-badge.js +3 -1
  151. package/dist/styles/code.css +0 -2
  152. package/dist/styles/effects.css +6 -32
  153. package/dist/styles/index.css +10 -5
  154. package/dist/styles/recipes/accordion.css +9 -6
  155. package/dist/styles/recipes/action-bar.css +0 -15
  156. package/dist/styles/recipes/activity.css +5 -11
  157. package/dist/styles/recipes/alert.css +4 -4
  158. package/dist/styles/recipes/audio-recorder.css +15 -3
  159. package/dist/styles/recipes/audio-visualizer.css +13 -2
  160. package/dist/styles/recipes/avatar.css +78 -1
  161. package/dist/styles/recipes/badge.css +14 -4
  162. package/dist/styles/recipes/breadcrumb.css +77 -0
  163. package/dist/styles/recipes/button-group.css +5 -7
  164. package/dist/styles/recipes/button-states.css +66 -0
  165. package/dist/styles/recipes/button.css +24 -83
  166. package/dist/styles/recipes/calendar.css +17 -23
  167. package/dist/styles/recipes/card.css +2 -2
  168. package/dist/styles/recipes/chat-composer-attachment.css +9 -3
  169. package/dist/styles/recipes/chat-composer.css +7 -10
  170. package/dist/styles/recipes/chat-layout.css +35 -0
  171. package/dist/styles/recipes/chat-message-social.css +123 -0
  172. package/dist/styles/recipes/chat-message.css +157 -37
  173. package/dist/styles/recipes/choice.css +18 -21
  174. package/dist/styles/recipes/code-diff.css +46 -33
  175. package/dist/styles/recipes/code.css +36 -16
  176. package/dist/styles/recipes/collapsible.css +7 -2
  177. package/dist/styles/recipes/color-picker-details.css +3 -4
  178. package/dist/styles/recipes/color-picker-values.css +9 -8
  179. package/dist/styles/recipes/color-picker.css +11 -2
  180. package/dist/styles/recipes/context.css +3 -3
  181. package/dist/styles/recipes/dockable-panel.css +7 -4
  182. package/dist/styles/recipes/dropzone.css +7 -8
  183. package/dist/styles/recipes/dynamic-notification-input.css +8 -5
  184. package/dist/styles/recipes/dynamic-notification-motion.css +12 -21
  185. package/dist/styles/recipes/dynamic-notification.css +40 -26
  186. package/dist/styles/recipes/emoji-picker.css +132 -0
  187. package/dist/styles/recipes/empty.css +5 -4
  188. package/dist/styles/recipes/environment-variables.css +4 -15
  189. package/dist/styles/recipes/field-parts.css +30 -24
  190. package/dist/styles/recipes/field.css +61 -26
  191. package/dist/styles/recipes/filter-bar.css +18 -31
  192. package/dist/styles/recipes/gradient-editor.css +18 -7
  193. package/dist/styles/recipes/infinite-canvas.css +0 -1
  194. package/dist/styles/recipes/inline-attachment.css +3 -3
  195. package/dist/styles/recipes/inline-citation.css +10 -4
  196. package/dist/styles/recipes/item.css +23 -9
  197. package/dist/styles/recipes/markdown-block.css +5 -4
  198. package/dist/styles/recipes/markdown.css +68 -26
  199. package/dist/styles/recipes/morphing-panel.css +8 -8
  200. package/dist/styles/recipes/page-layout.css +20 -12
  201. package/dist/styles/recipes/pagination.css +4 -5
  202. package/dist/styles/recipes/phone-input.css +12 -8
  203. package/dist/styles/recipes/popup-controls.css +166 -33
  204. package/dist/styles/recipes/popup-drawer.css +3 -14
  205. package/dist/styles/recipes/popup-lists.css +128 -27
  206. package/dist/styles/recipes/popup-motion.css +4 -45
  207. package/dist/styles/recipes/popup-toast.css +55 -26
  208. package/dist/styles/recipes/popup.css +139 -141
  209. package/dist/styles/recipes/range.css +54 -18
  210. package/dist/styles/recipes/resizable.css +2 -7
  211. package/dist/styles/recipes/rich-tooltip.css +24 -34
  212. package/dist/styles/recipes/scroll-area.css +8 -1
  213. package/dist/styles/recipes/sidebar-menu.css +166 -34
  214. package/dist/styles/recipes/sidebar-parts.css +192 -0
  215. package/dist/styles/recipes/sidebar.css +188 -128
  216. package/dist/styles/recipes/source-badge.css +1 -1
  217. package/dist/styles/recipes/spinner.css +8 -1
  218. package/dist/styles/recipes/stepper.css +31 -13
  219. package/dist/styles/recipes/switch.css +68 -23
  220. package/dist/styles/recipes/table-of-contents.css +9 -1
  221. package/dist/styles/recipes/table.css +14 -10
  222. package/dist/styles/recipes/tabs.css +17 -12
  223. package/dist/styles/recipes/task-list.css +7 -14
  224. package/dist/styles/recipes/thread-rail.css +42 -19
  225. package/dist/styles/recipes/timeline.css +2 -0
  226. package/dist/styles/recipes/toolbar.css +27 -25
  227. package/dist/styles/recipes/track-highlight.css +10 -7
  228. package/dist/styles/recipes/tree.css +31 -4
  229. package/dist/styles/recipes/user-ask-details.css +0 -1
  230. package/dist/styles/recipes/user-ask.css +4 -4
  231. package/dist/styles/theme.css +37 -16
  232. package/dist/task-list.d.ts +4 -1
  233. package/dist/task-list.js +24 -8
  234. package/dist/thread-rail.d.ts +1 -1
  235. package/dist/thread-rail.js +61 -19
  236. package/dist/transcript-divider.js +2 -1
  237. package/dist/ui/alert.d.ts +3 -1
  238. package/dist/ui/alert.js +14 -8
  239. package/dist/ui/autocomplete.js +3 -1
  240. package/dist/ui/avatar.d.ts +8 -0
  241. package/dist/ui/avatar.js +23 -3
  242. package/dist/ui/badge.js +1 -1
  243. package/dist/ui/breadcrumb.d.ts +27 -0
  244. package/dist/ui/breadcrumb.js +88 -0
  245. package/dist/ui/button.d.ts +2 -1
  246. package/dist/ui/button.js +1 -1
  247. package/dist/ui/calendar.js +2 -2
  248. package/dist/ui/card.d.ts +3 -1
  249. package/dist/ui/card.js +12 -7
  250. package/dist/ui/checkbox.d.ts +2 -0
  251. package/dist/ui/code-diff.d.ts +1 -1
  252. package/dist/ui/code-diff.js +71 -27
  253. package/dist/ui/code.d.ts +5 -3
  254. package/dist/ui/code.js +26 -27
  255. package/dist/ui/color-picker.d.ts +5 -2
  256. package/dist/ui/color-picker.js +84 -36
  257. package/dist/ui/combobox-disabled-selection.d.ts +1 -2
  258. package/dist/ui/combobox-disabled-selection.js +7 -9
  259. package/dist/ui/combobox.js +13 -5
  260. package/dist/ui/command.d.ts +8 -2
  261. package/dist/ui/command.js +30 -14
  262. package/dist/ui/context-menu.js +3 -0
  263. package/dist/ui/dialog.d.ts +2 -1
  264. package/dist/ui/dialog.js +3 -22
  265. package/dist/ui/dockable-panel-geometry.d.ts +1 -0
  266. package/dist/ui/dockable-panel-geometry.js +7 -1
  267. package/dist/ui/dockable-panel.d.ts +9 -6
  268. package/dist/ui/dockable-panel.js +63 -23
  269. package/dist/ui/dropdown-menu.js +4 -3
  270. package/dist/ui/dropzone.d.ts +2 -2
  271. package/dist/ui/dropzone.js +75 -16
  272. package/dist/ui/emoji-picker.d.ts +18 -0
  273. package/dist/ui/emoji-picker.js +136 -0
  274. package/dist/ui/empty.d.ts +3 -1
  275. package/dist/ui/empty.js +12 -7
  276. package/dist/ui/gradient-editor.d.ts +1 -1
  277. package/dist/ui/gradient-editor.js +37 -21
  278. package/dist/ui/infinite-canvas.d.ts +3 -2
  279. package/dist/ui/infinite-canvas.js +63 -19
  280. package/dist/ui/input-group.d.ts +0 -3
  281. package/dist/ui/input-group.js +3 -13
  282. package/dist/ui/input-otp.d.ts +1 -1
  283. package/dist/ui/input-otp.js +32 -15
  284. package/dist/ui/input.js +4 -4
  285. package/dist/ui/item.d.ts +2 -1
  286. package/dist/ui/item.js +11 -7
  287. package/dist/ui/live-status.d.ts +7 -0
  288. package/dist/ui/live-status.js +17 -0
  289. package/dist/ui/markdown-elements.d.ts +6 -0
  290. package/dist/ui/markdown-elements.js +31 -1
  291. package/dist/ui/menubar.js +2 -2
  292. package/dist/ui/morphing-panel.js +2 -1
  293. package/dist/ui/native-select.d.ts +1 -1
  294. package/dist/ui/native-select.js +24 -11
  295. package/dist/ui/page-layout.d.ts +2 -2
  296. package/dist/ui/page-layout.js +2 -4
  297. package/dist/ui/pagination.d.ts +7 -5
  298. package/dist/ui/pagination.js +19 -13
  299. package/dist/ui/phone-input.d.ts +1 -0
  300. package/dist/ui/phone-input.js +56 -58
  301. package/dist/ui/radio-group.d.ts +2 -0
  302. package/dist/ui/resizable.d.ts +3 -2
  303. package/dist/ui/resizable.js +5 -3
  304. package/dist/ui/responsive-dialog.d.ts +1 -1
  305. package/dist/ui/responsive-dialog.js +4 -21
  306. package/dist/ui/rich-tooltip.d.ts +10 -4
  307. package/dist/ui/rich-tooltip.js +81 -33
  308. package/dist/ui/scroll-area.d.ts +1 -1
  309. package/dist/ui/scroll-area.js +43 -28
  310. package/dist/ui/sidebar-menu.d.ts +5 -1
  311. package/dist/ui/sidebar-menu.js +34 -34
  312. package/dist/ui/sidebar-provider.d.ts +10 -1
  313. package/dist/ui/sidebar-provider.js +38 -13
  314. package/dist/ui/sidebar-resize-rail.js +7 -3
  315. package/dist/ui/sidebar.d.ts +11 -6
  316. package/dist/ui/sidebar.js +48 -24
  317. package/dist/ui/slider.js +31 -22
  318. package/dist/ui/spinner.d.ts +3 -2
  319. package/dist/ui/spinner.js +3 -2
  320. package/dist/ui/stepper.d.ts +8 -1
  321. package/dist/ui/stepper.js +74 -25
  322. package/dist/ui/switch.d.ts +1 -0
  323. package/dist/ui/table-of-contents.d.ts +2 -2
  324. package/dist/ui/table-of-contents.js +20 -8
  325. package/dist/ui/table.js +4 -2
  326. package/dist/ui/textarea.d.ts +1 -1
  327. package/dist/ui/textarea.js +20 -9
  328. package/dist/ui/timeline.d.ts +3 -1
  329. package/dist/ui/timeline.js +17 -9
  330. package/dist/ui/toast.d.ts +11 -5
  331. package/dist/ui/toast.js +100 -54
  332. package/dist/ui/toolbar.d.ts +2 -1
  333. package/dist/ui/toolbar.js +3 -1
  334. package/dist/ui/tooltip.js +3 -20
  335. package/dist/ui/tree.js +120 -80
  336. package/dist/ui/trigger-menu.js +1 -0
  337. package/dist/user-ask.d.ts +10 -5
  338. package/dist/user-ask.js +31 -11
  339. package/package.json +5 -1
  340. package/dist/hooks/use-keyboard-navigation.d.ts +0 -3
  341. package/dist/hooks/use-keyboard-navigation.js +0 -22
package/dist/context.js CHANGED
@@ -4,7 +4,7 @@ import { Button } from "./ui/button.js";
4
4
  import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "./ui/popover.js";
5
5
  import { ScrollArea } from "./ui/scroll-area.js";
6
6
  import { deriveContextModel } from "./context-model.js";
7
- import { createContext, useContext } from "react";
7
+ import { createContext, useContext, useId, useMemo } from "react";
8
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { X } from "lucide-react";
10
10
  //#region src/context.tsx
@@ -16,14 +16,20 @@ function useContextValue() {
16
16
  }
17
17
  function Context({ segments, maxTokens, model: modelName, locale, open, defaultOpen, onOpenChange, className, children, ...props }) {
18
18
  const model = deriveContextModel(segments, maxTokens);
19
+ const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
20
+ const percentageFormatter = useMemo(() => new Intl.NumberFormat(locale, {
21
+ style: "percent",
22
+ maximumFractionDigits: 1
23
+ }), [locale]);
24
+ const percentage = model.ratio === null ? null : percentageFormatter.format(model.ratio);
25
+ const usedTokens = numberFormatter.format(model.usedTokens);
19
26
  const context = {
20
27
  model,
21
28
  modelName,
22
- numberFormatter: new Intl.NumberFormat(locale),
23
- percentageFormatter: new Intl.NumberFormat(locale, {
24
- style: "percent",
25
- maximumFractionDigits: 1
26
- })
29
+ numberFormatter,
30
+ percentageFormatter,
31
+ percentage,
32
+ usage: model.maxTokens === null ? `${usedTokens} tokens used; limit unavailable` : `${usedTokens} of ${numberFormatter.format(model.maxTokens)} tokens used (${percentage})${model.overageTokens > 0 ? `; ${numberFormatter.format(model.overageTokens)} tokens over limit` : ""}`
27
33
  };
28
34
  return /* @__PURE__ */ jsx(ContextValueContext.Provider, {
29
35
  value: context,
@@ -43,24 +49,22 @@ function Context({ segments, maxTokens, model: modelName, locale, open, defaultO
43
49
  })
44
50
  });
45
51
  }
46
- function ContextTrigger({ "aria-label": ariaLabel, variant = "surface", size = "sm", className, children, ...props }) {
47
- const { model, numberFormatter, percentageFormatter } = useContextValue();
48
- const percentage = model.ratio === null ? null : percentageFormatter.format(model.ratio);
52
+ function ContextTrigger({ "aria-describedby": describedBy, variant = "surface", size = "sm", className, children, ...props }) {
53
+ const { model, percentage, usage } = useContextValue();
54
+ const usageId = useId();
49
55
  const visualPercentage = Math.min(100, Math.max(0, (model.ratio ?? 0) * 100));
50
- const shortLabel = percentage === null ? "Context unavailable" : `${percentage} context`;
51
- const accessibleLabel = model.maxTokens === null ? `Context window: ${numberFormatter.format(model.usedTokens)} tokens used; limit unavailable` : `Context window: ${numberFormatter.format(model.usedTokens)} of ${numberFormatter.format(model.maxTokens)} tokens used (${percentage})${model.overageTokens > 0 ? `; ${numberFormatter.format(model.overageTokens)} tokens over limit` : ""}`;
52
- return /* @__PURE__ */ jsx("span", {
56
+ return /* @__PURE__ */ jsxs("span", {
53
57
  "data-control-ui": "context",
54
58
  "data-control-family": "context",
55
59
  "data-slot": "trigger",
56
60
  "data-status": model.status,
57
61
  className: "relative inline-flex",
58
- children: /* @__PURE__ */ jsx(Button, {
62
+ children: [/* @__PURE__ */ jsx(Button, {
59
63
  ...props,
60
64
  render: /* @__PURE__ */ jsx(PopoverTrigger, {}),
61
65
  variant,
62
66
  size,
63
- "aria-label": ariaLabel ?? accessibleLabel,
67
+ "aria-describedby": describedBy ? `${usageId} ${describedBy}` : usageId,
64
68
  className: cn("group/context after:absolute after:-inset-1.5 after:content-['']", className),
65
69
  children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("svg", {
66
70
  "data-control-ui": "context",
@@ -96,9 +100,13 @@ function ContextTrigger({ "aria-label": ariaLabel, variant = "surface", size = "
96
100
  "data-control-ui": "context",
97
101
  "data-control-family": "context",
98
102
  "data-slot": "trigger-label",
99
- children: shortLabel
103
+ children: percentage === null ? "Context unavailable" : `${percentage} context`
100
104
  })] })
101
- })
105
+ }), /* @__PURE__ */ jsx("span", {
106
+ id: usageId,
107
+ className: "sr-only",
108
+ children: usage
109
+ })]
102
110
  });
103
111
  }
104
112
  function ContextContent({ "aria-label": ariaLabel, side = "top", align = "end", sideOffset = 8, collisionPadding = 16, padding = "none", className, children, ...props }) {
@@ -170,8 +178,7 @@ function ContextDescription({ className, children, ...props }) {
170
178
  });
171
179
  }
172
180
  function ContextSummary({ className, children, ...props }) {
173
- const { model, numberFormatter, percentageFormatter } = useContextValue();
174
- const percentage = model.ratio === null ? null : percentageFormatter.format(model.ratio);
181
+ const { model, numberFormatter, percentage } = useContextValue();
175
182
  return /* @__PURE__ */ jsx("div", {
176
183
  ...props,
177
184
  "data-control-ui": "context",
@@ -183,21 +190,29 @@ function ContextSummary({ className, children, ...props }) {
183
190
  "data-control-ui": "context",
184
191
  "data-control-family": "context",
185
192
  "data-slot": "summary-value",
186
- children: model.maxTokens === null ? "Limit unavailable" : `${numberFormatter.format(model.usedTokens)} / ${numberFormatter.format(model.maxTokens)} tokens`
193
+ children: model.maxTokens === null ? "Limit unavailable" : `${numberFormatter.format(model.usedTokens)} of ${numberFormatter.format(model.maxTokens)} tokens`
187
194
  })] })
188
195
  });
189
196
  }
190
- function ContextGraph({ className, ...props }) {
191
- const { model } = useContextValue();
197
+ function ContextGraph({ "aria-label": ariaLabel = "Context usage", className, ...props }) {
198
+ const { model, usage } = useContextValue();
199
+ const meterProps = model.maxTokens === null ? { "aria-hidden": true } : {
200
+ role: "meter",
201
+ "aria-label": ariaLabel,
202
+ "aria-valuemin": 0,
203
+ "aria-valuemax": model.maxTokens,
204
+ "aria-valuenow": Math.min(model.usedTokens, model.maxTokens),
205
+ "aria-valuetext": usage
206
+ };
192
207
  return /* @__PURE__ */ jsxs("svg", {
193
208
  ...props,
209
+ ...meterProps,
194
210
  "data-control-ui": "context",
195
211
  "data-control-family": "context",
196
212
  "data-slot": "graph",
197
213
  "data-status": model.status,
198
214
  viewBox: "0 0 100 10",
199
215
  preserveAspectRatio: "none",
200
- "aria-hidden": "true",
201
216
  className: cn("w-full overflow-hidden", className),
202
217
  children: [
203
218
  /* @__PURE__ */ jsx("rect", {
@@ -322,7 +337,7 @@ function ContextClose({ variant = "ghost", size = "xs", iconOnly = true, "aria-l
322
337
  size,
323
338
  iconOnly,
324
339
  "aria-label": ariaLabel,
325
- className: cn("ml-auto", className),
340
+ className: cn("ms-auto", className),
326
341
  children: children ?? /* @__PURE__ */ jsx(X, {
327
342
  "aria-hidden": "true",
328
343
  className: "size-4"
@@ -0,0 +1,24 @@
1
+ //#region src/dynamic-notification-canvas-loop.d.ts
2
+ export type NotificationCanvasFrame = {
3
+ reduced: boolean;
4
+ now: number;
5
+ deltaMs: number;
6
+ };
7
+ export type NotificationCanvasLoop = {
8
+ invalidate: () => void;
9
+ cancel: () => void;
10
+ destroy: () => void;
11
+ };
12
+ export type NotificationMotionValues = {
13
+ radius: number;
14
+ aurora: number;
15
+ reveal: number;
16
+ settle: number;
17
+ lift: number;
18
+ };
19
+ export declare function createNotificationMotion(initialState: string | undefined): {
20
+ values: NotificationMotionValues;
21
+ advance: (state: string | undefined, radius: number, reduced: boolean, deltaMs: number) => boolean;
22
+ };
23
+ export declare function createNotificationCanvasLoop(canvas: HTMLCanvasElement, render: (frame: NotificationCanvasFrame) => boolean): NotificationCanvasLoop;
24
+ //#endregion
@@ -0,0 +1,113 @@
1
+ import { prefersReducedMotion } from "./lib/motion.js";
2
+ //#region src/dynamic-notification-canvas-loop.ts
3
+ const FRAME_MS_AT_60HZ = 1e3 / 60;
4
+ const MAX_FRAME_DELTA_MS = 100;
5
+ function approachAt60Hz(current, target, response, deltaMs) {
6
+ const frameScale = Math.max(0, deltaMs) / FRAME_MS_AT_60HZ;
7
+ return current + (target - current) * (1 - (1 - response) ** frameScale);
8
+ }
9
+ function motionTargets(state, radius) {
10
+ const thinking = state === "thinking";
11
+ return {
12
+ radius,
13
+ aurora: thinking ? 1 : 0,
14
+ reveal: state === "collapsed" ? 0 : 1,
15
+ settle: state === "expanded" ? 1 : 0,
16
+ lift: thinking ? 0 : 1
17
+ };
18
+ }
19
+ function createNotificationMotion(initialState) {
20
+ const values = motionTargets(initialState, 0);
21
+ function advance(state, radius, reduced, deltaMs) {
22
+ const targets = motionTargets(state, radius);
23
+ if (reduced) {
24
+ Object.assign(values, targets);
25
+ return false;
26
+ }
27
+ values.radius = approachAt60Hz(values.radius, targets.radius, .25, deltaMs);
28
+ values.aurora = approachAt60Hz(values.aurora, targets.aurora, .06, deltaMs);
29
+ values.reveal = approachAt60Hz(values.reveal, targets.reveal, .07, deltaMs);
30
+ values.settle = approachAt60Hz(values.settle, targets.settle, .07, deltaMs);
31
+ values.lift = targets.lift === 1 ? Math.min(1, values.lift + deltaMs * .00216) : approachAt60Hz(values.lift, 0, .28, deltaMs);
32
+ return state === "thinking" || Math.abs(values.radius - targets.radius) > .05 || Math.abs(values.aurora - targets.aurora) > .002 || Math.abs(values.reveal - targets.reveal) > .002 || Math.abs(values.settle - targets.settle) > .002 || Math.abs(values.lift - targets.lift) > .002;
33
+ }
34
+ return {
35
+ values,
36
+ advance
37
+ };
38
+ }
39
+ function createNotificationCanvasLoop(canvas, render) {
40
+ let destroyed = false;
41
+ let intersecting = true;
42
+ let pageVisible = !document.hidden;
43
+ let rafId = 0;
44
+ let staticFrameDrawn = false;
45
+ let previousFrameAt = performance.now();
46
+ const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
47
+ function tick() {
48
+ rafId = 0;
49
+ if (destroyed || !intersecting || !pageVisible) return;
50
+ const now = performance.now();
51
+ const deltaMs = Math.min(MAX_FRAME_DELTA_MS, now - previousFrameAt);
52
+ previousFrameAt = now;
53
+ const reduced = prefersReducedMotion(canvas);
54
+ const keepAnimating = render({
55
+ reduced,
56
+ now,
57
+ deltaMs
58
+ }) && !reduced;
59
+ staticFrameDrawn = !keepAnimating;
60
+ if (keepAnimating) rafId = requestAnimationFrame(tick);
61
+ }
62
+ function invalidate() {
63
+ staticFrameDrawn = false;
64
+ if (destroyed || !intersecting || !pageVisible || rafId !== 0) return;
65
+ previousFrameAt = performance.now();
66
+ rafId = requestAnimationFrame(tick);
67
+ }
68
+ function cancel() {
69
+ if (rafId !== 0) cancelAnimationFrame(rafId);
70
+ rafId = 0;
71
+ }
72
+ function handleVisibility() {
73
+ pageVisible = !document.hidden;
74
+ invalidate();
75
+ }
76
+ const resizeObserver = new ResizeObserver(invalidate);
77
+ resizeObserver.observe(canvas);
78
+ const intersectionObserver = new IntersectionObserver((entries) => {
79
+ for (const entry of entries) intersecting = entry.isIntersecting;
80
+ invalidate();
81
+ }, { rootMargin: "64px" });
82
+ intersectionObserver.observe(canvas);
83
+ const motionObserver = new MutationObserver(() => {
84
+ if (!staticFrameDrawn || !prefersReducedMotion(canvas)) invalidate();
85
+ });
86
+ motionObserver.observe(document.documentElement, {
87
+ attributes: true,
88
+ attributeFilter: ["data-motion"]
89
+ });
90
+ const stateObserver = new MutationObserver(invalidate);
91
+ if (canvas.parentElement) stateObserver.observe(canvas.parentElement, {
92
+ attributes: true,
93
+ attributeFilter: ["data-state"]
94
+ });
95
+ document.addEventListener("visibilitychange", handleVisibility);
96
+ reducedMotionQuery.addEventListener("change", invalidate);
97
+ return {
98
+ invalidate,
99
+ cancel,
100
+ destroy() {
101
+ destroyed = true;
102
+ cancel();
103
+ resizeObserver.disconnect();
104
+ intersectionObserver.disconnect();
105
+ motionObserver.disconnect();
106
+ stateObserver.disconnect();
107
+ document.removeEventListener("visibilitychange", handleVisibility);
108
+ reducedMotionQuery.removeEventListener("change", invalidate);
109
+ }
110
+ };
111
+ }
112
+ //#endregion
113
+ export { createNotificationCanvasLoop, createNotificationMotion };
@@ -1,3 +1,4 @@
1
+ import { createNotificationCanvasLoop, createNotificationMotion } from "./dynamic-notification-canvas-loop.js";
1
2
  import { DYNAMIC_NOTIFICATION_SIRI_WAVE_GLSL } from "./dynamic-notification-siri-wave.js";
2
3
  //#region src/dynamic-notification-glass.ts
3
4
  const VERTEX_SHADER = `
@@ -141,15 +142,6 @@ function buildProgram(gl) {
141
142
  ribbonHorizon: gl.getUniformLocation(program, "u_ribbonHorizon")
142
143
  };
143
144
  }
144
- function auroraTarget(state) {
145
- return state === "thinking" ? 1 : 0;
146
- }
147
- function revealTarget(state) {
148
- return state === "collapsed" ? 0 : 1;
149
- }
150
- function settleTarget(state) {
151
- return state === "expanded" ? 1 : 0;
152
- }
153
145
  function createDynamicNotificationGlass(canvas, options = {}) {
154
146
  const { intensity = 1, maxDpr = 2 } = options;
155
147
  const gl = canvas.getContext("webgl", {
@@ -161,28 +153,9 @@ function createDynamicNotificationGlass(canvas, options = {}) {
161
153
  let contextLost = gl.isContextLost();
162
154
  let glass = contextLost ? null : buildProgram(gl);
163
155
  if (!contextLost && !glass) return () => {};
164
- let destroyed = false;
165
- let intersecting = true;
166
- let pageVisible = !document.hidden;
167
- let rafId = 0;
168
156
  let dpr = 1;
169
- let radius = 0;
170
- let aurora = auroraTarget(canvas.parentElement?.dataset.state);
171
- let lift = 1 - aurora;
172
- let reveal = revealTarget(canvas.parentElement?.dataset.state);
173
- let settle = settleTarget(canvas.parentElement?.dataset.state);
174
- let staticFrameDrawn = false;
157
+ const motion = createNotificationMotion(canvas.parentElement?.dataset.state);
175
158
  const startedAt = performance.now();
176
- const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
177
- function reducedMotion() {
178
- return reducedMotionQuery.matches || canvas.closest("[data-motion=\"reduced\"]") !== null;
179
- }
180
- function targetRadius() {
181
- const host = canvas.parentElement;
182
- if (!host) return 0;
183
- const parsed = Number.parseFloat(getComputedStyle(host).borderTopLeftRadius);
184
- return Number.isFinite(parsed) ? parsed * dpr : 0;
185
- }
186
159
  function resize() {
187
160
  dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
188
161
  const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
@@ -192,111 +165,47 @@ function createDynamicNotificationGlass(canvas, options = {}) {
192
165
  canvas.height = height;
193
166
  }
194
167
  }
195
- function advanceMotion(reduced) {
196
- const target = targetRadius();
197
- radius = reduced ? target : radius + (target - radius) * .25;
198
- const auroraGoal = auroraTarget(canvas.parentElement?.dataset.state);
199
- aurora = reduced ? auroraGoal : aurora + (auroraGoal - aurora) * .06;
200
- const liftGoal = auroraGoal >= .5 ? 0 : 1;
201
- if (reduced) lift = liftGoal;
202
- else if (liftGoal === 1) lift = Math.min(1, lift + .036);
203
- else lift *= .72;
204
- const revealGoal = revealTarget(canvas.parentElement?.dataset.state);
205
- reveal = reduced ? revealGoal : reveal + (revealGoal - reveal) * .07;
206
- const settleGoal = settleTarget(canvas.parentElement?.dataset.state);
207
- settle = reduced ? settleGoal : settle + (settleGoal - settle) * .07;
208
- }
209
- function uploadUniforms(activeGl, activeGlass, reduced) {
210
- activeGl.uniform2f(activeGlass.resolution, canvas.width, canvas.height);
211
- activeGl.uniform1f(activeGlass.time, reduced ? 4.2 : (performance.now() - startedAt) / 1e3);
212
- activeGl.uniform1f(activeGlass.radius, radius);
213
- activeGl.uniform1f(activeGlass.intensity, intensity);
214
- activeGl.uniform1f(activeGlass.aurora, aurora);
215
- activeGl.uniform1f(activeGlass.reveal, reveal);
216
- activeGl.uniform1f(activeGlass.settle, settle);
217
- activeGl.uniform1f(activeGlass.lift, lift);
218
- activeGl.uniform1f(activeGlass.ribbonHorizon, .5);
219
- }
220
- function draw() {
221
- if (!gl || !glass || contextLost) return;
168
+ function render({ reduced, now, deltaMs }) {
169
+ const host = canvas.parentElement;
170
+ if (!gl || !glass || contextLost || !host) return false;
222
171
  resize();
172
+ const hostRadius = Number.parseFloat(getComputedStyle(host).borderTopLeftRadius);
173
+ const keepAnimating = motion.advance(host.dataset.state, Number.isFinite(hostRadius) ? hostRadius * dpr : 0, reduced, deltaMs);
174
+ const { radius, aurora, reveal, settle, lift } = motion.values;
223
175
  gl.viewport(0, 0, canvas.width, canvas.height);
224
176
  gl.clearColor(0, 0, 0, 0);
225
177
  gl.clear(gl.COLOR_BUFFER_BIT);
226
178
  gl.useProgram(glass.program);
227
- const reduced = reducedMotion();
228
- advanceMotion(reduced);
229
- uploadUniforms(gl, glass, reduced);
179
+ gl.uniform2f(glass.resolution, canvas.width, canvas.height);
180
+ gl.uniform1f(glass.time, reduced ? 4.2 : (now - startedAt) / 1e3);
181
+ gl.uniform1f(glass.radius, radius);
182
+ gl.uniform1f(glass.intensity, intensity);
183
+ gl.uniform1f(glass.aurora, aurora);
184
+ gl.uniform1f(glass.reveal, reveal);
185
+ gl.uniform1f(glass.settle, settle);
186
+ gl.uniform1f(glass.lift, lift);
187
+ gl.uniform1f(glass.ribbonHorizon, .5);
230
188
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
231
189
  if (canvas.dataset.glassReady !== "true") canvas.dataset.glassReady = "true";
190
+ return keepAnimating;
232
191
  }
233
- function visible() {
234
- return intersecting && pageVisible;
235
- }
236
- function tick() {
237
- rafId = 0;
238
- if (destroyed || !visible() || contextLost) return;
239
- draw();
240
- if (reducedMotion()) {
241
- staticFrameDrawn = true;
242
- return;
243
- }
244
- staticFrameDrawn = false;
245
- rafId = requestAnimationFrame(tick);
246
- }
247
- function invalidate() {
248
- staticFrameDrawn = false;
249
- if (destroyed || !visible() || contextLost || rafId !== 0) return;
250
- rafId = requestAnimationFrame(tick);
251
- }
252
- function handleVisibility() {
253
- pageVisible = !document.hidden;
254
- invalidate();
255
- }
192
+ const loop = createNotificationCanvasLoop(canvas, render);
256
193
  function handleContextLost(event) {
257
194
  event.preventDefault();
258
195
  contextLost = true;
259
- if (rafId !== 0) cancelAnimationFrame(rafId);
260
- rafId = 0;
196
+ loop.cancel();
261
197
  }
262
198
  function handleContextRestored() {
263
199
  if (!gl) return;
264
200
  contextLost = false;
265
201
  glass = buildProgram(gl);
266
- invalidate();
267
- }
268
- const resizeObserver = new ResizeObserver(() => {
269
- invalidate();
270
- });
271
- resizeObserver.observe(canvas);
272
- const intersectionObserver = new IntersectionObserver((entries) => {
273
- for (const entry of entries) intersecting = entry.isIntersecting;
274
- invalidate();
275
- }, { rootMargin: "64px" });
276
- intersectionObserver.observe(canvas);
277
- const motionObserver = new MutationObserver(() => {
278
- if (!staticFrameDrawn || !reducedMotion()) invalidate();
279
- });
280
- motionObserver.observe(document.documentElement, {
281
- attributes: true,
282
- attributeFilter: ["data-motion"]
283
- });
284
- function handleMotionPreference() {
285
- invalidate();
202
+ loop.invalidate();
286
203
  }
287
- document.addEventListener("visibilitychange", handleVisibility);
288
- reducedMotionQuery.addEventListener("change", handleMotionPreference);
289
204
  canvas.addEventListener("webglcontextlost", handleContextLost);
290
205
  canvas.addEventListener("webglcontextrestored", handleContextRestored);
291
- invalidate();
206
+ loop.invalidate();
292
207
  return () => {
293
- destroyed = true;
294
- if (rafId !== 0) cancelAnimationFrame(rafId);
295
- resizeObserver.disconnect();
296
- intersectionObserver.disconnect();
297
- motionObserver.disconnect();
298
- document.removeEventListener("visibilitychange", handleVisibility);
299
- reducedMotionQuery.removeEventListener("change", handleMotionPreference);
208
+ loop.destroy();
300
209
  canvas.removeEventListener("webglcontextlost", handleContextLost);
301
210
  canvas.removeEventListener("webglcontextrestored", handleContextRestored);
302
211
  delete canvas.dataset.glassReady;
@@ -1,3 +1,4 @@
1
+ import { createNotificationCanvasLoop, createNotificationMotion } from "./dynamic-notification-canvas-loop.js";
1
2
  import { DYNAMIC_NOTIFICATION_SIRI_WAVE_GLSL } from "./dynamic-notification-siri-wave.js";
2
3
  import { LIQUID_GLASS_OPTICS_GLSL } from "./lib/liquid-glass-optics.js";
3
4
  //#region src/dynamic-notification-liquid.ts
@@ -192,19 +193,6 @@ function buildProgram(gl) {
192
193
  ribbonHorizon: gl.getUniformLocation(program, "u_ribbonHorizon")
193
194
  };
194
195
  }
195
- function auroraTarget(state) {
196
- return state === "thinking" ? 1 : 0;
197
- }
198
- function revealTarget(state) {
199
- return state === "collapsed" ? 0 : 1;
200
- }
201
- function settleTarget(state) {
202
- return state === "expanded" ? 1 : 0;
203
- }
204
- function approachAt60Hz(current, target, response, deltaMs) {
205
- const frameScale = Math.max(0, deltaMs) / (1e3 / 60);
206
- return current + (target - current) * (1 - (1 - response) ** frameScale);
207
- }
208
196
  const EXCLUDE_SELECTOR = "[data-control-ui=\"dynamic-notification\"]";
209
197
  function cssImageUrls(value) {
210
198
  return Array.from(value.matchAll(/url\((["']?)(.*?)\1\)/g), (match) => match[2]?.trim()).filter((url) => Boolean(url && !url.startsWith("data:") && !url.startsWith("#")));
@@ -434,31 +422,13 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
434
422
  };
435
423
  }
436
424
  let destroyed = false;
437
- let intersecting = true;
438
- let pageVisible = !document.hidden;
439
- let rafId = 0;
440
425
  let dpr = 1;
441
- let radius = 0;
442
- const initialState = canvas.parentElement?.dataset.state;
443
- let aurora = auroraTarget(initialState);
444
- let reveal = revealTarget(initialState);
445
- let settle = settleTarget(initialState);
446
- let lift = 1 - aurora;
447
- let staticFrameDrawn = false;
426
+ const motion = createNotificationMotion(canvas.parentElement?.dataset.state);
448
427
  let captured = null;
449
428
  let textureFresh = false;
450
429
  let captureToken = 0;
451
430
  let captureTimer = 0;
452
431
  const startedAt = performance.now();
453
- let previousFrameAt = startedAt;
454
- const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
455
- function reducedMotion() {
456
- return reducedMotionQuery.matches || canvas.closest("[data-motion=\"reduced\"]") !== null;
457
- }
458
- function targetRadius(host) {
459
- const parsed = Number.parseFloat(getComputedStyle(host).borderTopLeftRadius);
460
- return Number.isFinite(parsed) ? parsed : 0;
461
- }
462
432
  function resize() {
463
433
  dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
464
434
  const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
@@ -494,36 +464,14 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
494
464
  captured = result;
495
465
  delete canvas.dataset.glassFailed;
496
466
  uploadTexture();
497
- invalidate();
467
+ loop.invalidate();
498
468
  });
499
469
  }
500
470
  function scheduleRecapture() {
501
471
  window.clearTimeout(captureTimer);
502
472
  captureTimer = window.setTimeout(recapture, 200);
503
473
  }
504
- function advanceMotion(host, reduced, deltaMs) {
505
- const state = host.dataset.state;
506
- const targetAurora = auroraTarget(state);
507
- const targets = {
508
- radius: targetRadius(host),
509
- aurora: targetAurora,
510
- reveal: revealTarget(state),
511
- settle: settleTarget(state),
512
- lift: targetAurora >= .5 ? 0 : 1
513
- };
514
- radius = reduced ? targets.radius : approachAt60Hz(radius, targets.radius, .25, deltaMs);
515
- aurora = reduced ? targets.aurora : approachAt60Hz(aurora, targets.aurora, .06, deltaMs);
516
- reveal = reduced ? targets.reveal : approachAt60Hz(reveal, targets.reveal, .07, deltaMs);
517
- settle = reduced ? targets.settle : approachAt60Hz(settle, targets.settle, .07, deltaMs);
518
- if (reduced) lift = targets.lift;
519
- else if (targets.lift === 1) lift = Math.min(1, lift + deltaMs * .00216);
520
- else lift = approachAt60Hz(lift, 0, .28, deltaMs);
521
- return targets;
522
- }
523
- function shouldAnimate(host, reduced, targets) {
524
- return !reduced && (host.dataset.state === "thinking" || Math.abs(radius - targets.radius) > .05 || Math.abs(aurora - targets.aurora) > .002 || Math.abs(reveal - targets.reveal) > .002 || Math.abs(settle - targets.settle) > .002 || Math.abs(lift - targets.lift) > .002);
525
- }
526
- function draw() {
474
+ function render({ reduced, now, deltaMs }) {
527
475
  if (!gl || !liquid || contextLost || !textureFresh) return false;
528
476
  const host = canvas.parentElement;
529
477
  if (!host) return false;
@@ -531,15 +479,13 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
531
479
  const hostRect = host.getBoundingClientRect();
532
480
  const sceneRect = scene.getBoundingClientRect();
533
481
  if (sceneRect.width < 1 || sceneRect.height < 1) return false;
482
+ const hostRadius = Number.parseFloat(getComputedStyle(host).borderTopLeftRadius);
483
+ const keepAnimating = motion.advance(host.dataset.state, Number.isFinite(hostRadius) ? hostRadius : 0, reduced, deltaMs);
484
+ const { radius, aurora, reveal, settle, lift } = motion.values;
534
485
  gl.viewport(0, 0, canvas.width, canvas.height);
535
486
  gl.clearColor(0, 0, 0, 0);
536
487
  gl.clear(gl.COLOR_BUFFER_BIT);
537
488
  gl.useProgram(liquid.program);
538
- const reduced = reducedMotion();
539
- const now = performance.now();
540
- const deltaMs = Math.min(100, now - previousFrameAt);
541
- previousFrameAt = now;
542
- const targets = advanceMotion(host, reduced, deltaMs);
543
489
  gl.activeTexture(gl.TEXTURE0);
544
490
  gl.bindTexture(gl.TEXTURE_2D, liquid.sharpTexture);
545
491
  gl.activeTexture(gl.TEXTURE1);
@@ -566,38 +512,16 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
566
512
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
567
513
  delete canvas.dataset.glassFailed;
568
514
  if (canvas.dataset.glassReady !== "true") canvas.dataset.glassReady = "true";
569
- return shouldAnimate(host, reduced, targets);
570
- }
571
- function visible() {
572
- return intersecting && pageVisible;
573
- }
574
- function tick() {
575
- rafId = 0;
576
- if (destroyed || !visible() || contextLost) return;
577
- if (!draw()) {
578
- staticFrameDrawn = true;
579
- return;
580
- }
581
- staticFrameDrawn = false;
582
- rafId = requestAnimationFrame(tick);
583
- }
584
- function invalidate() {
585
- staticFrameDrawn = false;
586
- if (destroyed || !visible() || contextLost || rafId !== 0) return;
587
- rafId = requestAnimationFrame(tick);
588
- }
589
- function handleVisibility() {
590
- pageVisible = !document.hidden;
591
- invalidate();
515
+ return keepAnimating;
592
516
  }
517
+ const loop = createNotificationCanvasLoop(canvas, render);
593
518
  function handleContextLost(event) {
594
519
  event.preventDefault();
595
520
  contextLost = true;
596
521
  textureFresh = false;
597
522
  delete canvas.dataset.glassReady;
598
523
  canvas.dataset.glassFailed = "context";
599
- if (rafId !== 0) cancelAnimationFrame(rafId);
600
- rafId = 0;
524
+ loop.cancel();
601
525
  }
602
526
  function handleContextRestored() {
603
527
  if (!gl) return;
@@ -608,15 +532,9 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
608
532
  return;
609
533
  }
610
534
  uploadTexture();
611
- invalidate();
535
+ loop.invalidate();
612
536
  }
613
- const resizeObserver = new ResizeObserver(() => {
614
- invalidate();
615
- });
616
- resizeObserver.observe(canvas);
617
- const sceneResizeObserver = new ResizeObserver(() => {
618
- scheduleRecapture();
619
- });
537
+ const sceneResizeObserver = new ResizeObserver(scheduleRecapture);
620
538
  sceneResizeObserver.observe(scene);
621
539
  const sceneMutationObserver = new MutationObserver((mutations) => {
622
540
  for (const mutation of mutations) {
@@ -632,38 +550,16 @@ function createDynamicNotificationLiquid(canvas, options = {}) {
632
550
  childList: true,
633
551
  subtree: true
634
552
  });
635
- const intersectionObserver = new IntersectionObserver((entries) => {
636
- for (const entry of entries) intersecting = entry.isIntersecting;
637
- invalidate();
638
- }, { rootMargin: "64px" });
639
- intersectionObserver.observe(canvas);
640
- const motionObserver = new MutationObserver(() => {
641
- if (!staticFrameDrawn || !reducedMotion()) invalidate();
642
- });
643
- motionObserver.observe(document.documentElement, {
644
- attributes: true,
645
- attributeFilter: ["data-motion"]
646
- });
647
- function handleMotionPreference() {
648
- invalidate();
649
- }
650
- document.addEventListener("visibilitychange", handleVisibility);
651
- reducedMotionQuery.addEventListener("change", handleMotionPreference);
652
553
  canvas.addEventListener("webglcontextlost", handleContextLost);
653
554
  canvas.addEventListener("webglcontextrestored", handleContextRestored);
654
555
  recapture();
655
- invalidate();
556
+ loop.invalidate();
656
557
  return () => {
657
558
  destroyed = true;
658
- if (rafId !== 0) cancelAnimationFrame(rafId);
559
+ loop.destroy();
659
560
  window.clearTimeout(captureTimer);
660
- resizeObserver.disconnect();
661
561
  sceneResizeObserver.disconnect();
662
562
  sceneMutationObserver.disconnect();
663
- intersectionObserver.disconnect();
664
- motionObserver.disconnect();
665
- document.removeEventListener("visibilitychange", handleVisibility);
666
- reducedMotionQuery.removeEventListener("change", handleMotionPreference);
667
563
  canvas.removeEventListener("webglcontextlost", handleContextLost);
668
564
  canvas.removeEventListener("webglcontextrestored", handleContextRestored);
669
565
  delete canvas.dataset.glassReady;