@djangocfg/ui-tools 2.1.407 → 2.1.409

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 (297) hide show
  1. package/README.md +9 -10
  2. package/dist/file-icon/index.cjs +449 -61
  3. package/dist/file-icon/index.cjs.map +1 -1
  4. package/dist/file-icon/index.d.cts +56 -18
  5. package/dist/file-icon/index.d.ts +56 -18
  6. package/dist/file-icon/index.mjs +448 -62
  7. package/dist/file-icon/index.mjs.map +1 -1
  8. package/dist/tree/index.cjs +49 -22
  9. package/dist/tree/index.cjs.map +1 -1
  10. package/dist/tree/index.d.cts +9 -3
  11. package/dist/tree/index.d.ts +9 -3
  12. package/dist/tree/index.mjs +49 -22
  13. package/dist/tree/index.mjs.map +1 -1
  14. package/dist/{types-B_zhyAqR.d.cts → types-eEu8SeiQ.d.cts} +4 -0
  15. package/dist/{types-B_zhyAqR.d.ts → types-eEu8SeiQ.d.ts} +4 -0
  16. package/package.json +8 -13
  17. package/src/components/FloatingToolbar/index.tsx +37 -3
  18. package/src/lib/page-snapshot/__tests__/capture-integration.test.ts +85 -0
  19. package/src/lib/page-snapshot/__tests__/engine.test.ts +36 -0
  20. package/src/lib/page-snapshot/__tests__/redaction-integration.test.ts +99 -0
  21. package/src/lib/page-snapshot/__tests__/tokens.test.ts +17 -0
  22. package/src/lib/page-snapshot/capture/__tests__/budget.test.ts +49 -0
  23. package/src/lib/page-snapshot/capture/__tests__/chrome-filter.test.ts +47 -0
  24. package/src/lib/page-snapshot/capture/__tests__/fold.test.ts +66 -0
  25. package/src/lib/page-snapshot/capture/__tests__/scope.test.ts +74 -0
  26. package/src/lib/page-snapshot/capture/__tests__/walk.test.ts +129 -0
  27. package/src/lib/page-snapshot/capture/accessible-name.ts +73 -0
  28. package/src/lib/page-snapshot/capture/budget.ts +95 -0
  29. package/src/lib/page-snapshot/capture/chrome-filter.ts +81 -0
  30. package/src/lib/page-snapshot/capture/classify.ts +111 -0
  31. package/src/lib/page-snapshot/capture/dom-utils.ts +111 -0
  32. package/src/lib/page-snapshot/capture/fold.ts +96 -0
  33. package/src/lib/page-snapshot/capture/scope.ts +169 -0
  34. package/src/lib/page-snapshot/capture/walk.ts +250 -0
  35. package/src/lib/page-snapshot/cst/__tests__/serialize.test.ts +50 -0
  36. package/src/lib/page-snapshot/cst/directives.ts +47 -0
  37. package/src/lib/page-snapshot/cst/payload.ts +50 -0
  38. package/src/lib/page-snapshot/cst/serialize.ts +84 -0
  39. package/src/lib/page-snapshot/cst/types.ts +115 -0
  40. package/src/lib/page-snapshot/engine.ts +176 -0
  41. package/src/lib/page-snapshot/index.ts +93 -0
  42. package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +72 -0
  43. package/src/lib/page-snapshot/react/PageSnapshotPreview.tsx +78 -0
  44. package/src/lib/page-snapshot/react/__tests__/PageSnapshotChip.test.tsx +54 -0
  45. package/src/lib/page-snapshot/react/__tests__/provider.test.tsx +103 -0
  46. package/src/lib/page-snapshot/react/__tests__/use-page-snapshot-toggle.test.tsx +62 -0
  47. package/src/lib/page-snapshot/react/provider.tsx +162 -0
  48. package/src/lib/page-snapshot/react/use-page-snapshot-toggle.ts +47 -0
  49. package/src/lib/page-snapshot/react/use-page-snapshot.ts +67 -0
  50. package/src/lib/page-snapshot/redaction/__tests__/audit.test.ts +25 -0
  51. package/src/lib/page-snapshot/redaction/__tests__/heuristics.test.ts +73 -0
  52. package/src/lib/page-snapshot/redaction/__tests__/luhn.test.ts +26 -0
  53. package/src/lib/page-snapshot/redaction/__tests__/patterns.test.ts +60 -0
  54. package/src/lib/page-snapshot/redaction/audit.ts +58 -0
  55. package/src/lib/page-snapshot/redaction/heuristics.ts +75 -0
  56. package/src/lib/page-snapshot/redaction/index.ts +75 -0
  57. package/src/lib/page-snapshot/redaction/luhn.ts +25 -0
  58. package/src/lib/page-snapshot/redaction/patterns.ts +111 -0
  59. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +24 -0
  60. package/src/lib/page-snapshot/refs/registry.ts +46 -0
  61. package/src/lib/page-snapshot/staleness/__tests__/hash.test.ts +34 -0
  62. package/src/lib/page-snapshot/staleness/hash.ts +20 -0
  63. package/src/lib/page-snapshot/tokens.ts +15 -0
  64. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +13 -14
  65. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +55 -6
  66. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +2 -5
  67. package/src/tools/Chat/README.md +277 -39
  68. package/src/tools/Chat/composer/Composer.tsx +471 -0
  69. package/src/tools/Chat/composer/ComposerActionBar.tsx +65 -0
  70. package/src/tools/Chat/composer/ComposerBanner.tsx +128 -0
  71. package/src/tools/Chat/composer/ComposerButton.tsx +64 -0
  72. package/src/tools/Chat/composer/ComposerFooter.tsx +90 -0
  73. package/src/tools/Chat/composer/ComposerMenuButton.tsx +62 -0
  74. package/src/tools/Chat/composer/ComposerModelPicker.tsx +104 -0
  75. package/src/tools/Chat/composer/ComposerRichTextarea.tsx +88 -0
  76. package/src/tools/Chat/composer/ComposerToolPill.tsx +95 -0
  77. package/src/tools/Chat/composer/index.ts +45 -0
  78. package/src/tools/Chat/composer/size-context.tsx +26 -0
  79. package/src/tools/Chat/composer/types.ts +143 -0
  80. package/src/tools/Chat/composer/useComposerActions.tsx +164 -0
  81. package/src/tools/Chat/context/ChatProvider.tsx +54 -3
  82. package/src/tools/Chat/core/__tests__/metadata.test.ts +69 -0
  83. package/src/tools/Chat/core/index.ts +23 -1
  84. package/src/tools/Chat/core/markdown.ts +1 -1
  85. package/src/tools/Chat/core/metadata.ts +47 -0
  86. package/src/tools/Chat/core/payload-dispatch.ts +1 -1
  87. package/src/tools/Chat/core/transport/http.ts +71 -32
  88. package/src/tools/Chat/core/transport/sse.ts +18 -10
  89. package/src/tools/Chat/highlight/HighlightOverlay.tsx +101 -0
  90. package/src/tools/Chat/highlight/README.md +103 -0
  91. package/src/tools/Chat/highlight/SpotlightCanvas.tsx +153 -0
  92. package/src/tools/Chat/highlight/__tests__/HighlightOverlay.test.tsx +112 -0
  93. package/src/tools/Chat/highlight/__tests__/resolveRef.test.ts +55 -0
  94. package/src/tools/Chat/highlight/index.ts +21 -0
  95. package/src/tools/Chat/highlight/resolveRef.ts +42 -0
  96. package/src/tools/Chat/highlight/types.ts +49 -0
  97. package/src/tools/Chat/highlight/useHighlightTargets.ts +128 -0
  98. package/src/tools/Chat/hooks/index.ts +0 -5
  99. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +28 -47
  100. package/src/tools/Chat/hooks/useChat.ts +47 -14
  101. package/src/tools/Chat/hooks/useChatComposer.ts +2 -2
  102. package/src/tools/Chat/hooks/useChatLayout.ts +1 -1
  103. package/src/tools/Chat/hooks/useStreamEndFocus.ts +54 -0
  104. package/src/tools/Chat/index.ts +25 -219
  105. package/src/tools/Chat/launcher/ChatDock.tsx +1 -1
  106. package/src/tools/Chat/launcher/ChatLauncher.tsx +1 -1
  107. package/src/tools/Chat/launcher/{ChatHeader.tsx → header/ChatHeader.tsx} +24 -11
  108. package/src/tools/Chat/launcher/{ChatHeaderActionButton.tsx → header/ChatHeaderActionButton.tsx} +34 -3
  109. package/src/tools/Chat/launcher/{ChatHeaderLanguageButton.tsx → header/ChatHeaderLanguageButton.tsx} +2 -2
  110. package/src/tools/Chat/launcher/{ChatHeaderModeToggle.tsx → header/ChatHeaderModeToggle.tsx} +1 -1
  111. package/src/tools/Chat/launcher/{ChatHeaderResetButton.tsx → header/ChatHeaderResetButton.tsx} +2 -1
  112. package/src/tools/Chat/launcher/{HeaderSlots.tsx → header/HeaderSlots.tsx} +3 -3
  113. package/src/tools/Chat/launcher/header/index.ts +26 -0
  114. package/src/tools/Chat/launcher/index.ts +3 -10
  115. package/src/tools/Chat/lazy.tsx +38 -284
  116. package/src/tools/Chat/{components → messages}/MessageBubble.tsx +58 -5
  117. package/src/tools/Chat/{components → messages}/MessageList.tsx +8 -25
  118. package/src/tools/Chat/messages/blocks/MessageBlocks.tsx +131 -0
  119. package/src/tools/Chat/messages/blocks/builtin.tsx +91 -0
  120. package/src/tools/Chat/messages/blocks/index.ts +12 -0
  121. package/src/tools/Chat/messages/blocks/registry.tsx +42 -0
  122. package/src/tools/Chat/messages/blocks/renderers/AudioBlock.tsx +20 -0
  123. package/src/tools/Chat/messages/blocks/renderers/CodeBlock.tsx +19 -0
  124. package/src/tools/Chat/messages/blocks/renderers/GalleryBlock.tsx +26 -0
  125. package/src/tools/Chat/messages/blocks/renderers/ImageBlock.tsx +27 -0
  126. package/src/tools/Chat/messages/blocks/renderers/JsonBlock.tsx +12 -0
  127. package/src/tools/Chat/messages/blocks/renderers/LottieBlock.tsx +11 -0
  128. package/src/tools/Chat/messages/blocks/renderers/MapBlock.tsx +36 -0
  129. package/src/tools/Chat/messages/blocks/renderers/MermaidBlock.tsx +11 -0
  130. package/src/tools/Chat/messages/blocks/renderers/VideoBlock.tsx +24 -0
  131. package/src/tools/Chat/messages/blocks/renderers/types.ts +8 -0
  132. package/src/tools/Chat/{components → messages}/index.ts +11 -5
  133. package/src/tools/Chat/public.ts +212 -0
  134. package/src/tools/Chat/shell/ChatRoot.tsx +345 -0
  135. package/src/tools/Chat/{components → shell}/EmptyState.tsx +4 -2
  136. package/src/tools/Chat/shell/index.ts +15 -0
  137. package/src/tools/Chat/types/block.ts +120 -0
  138. package/src/tools/Chat/types/config.ts +0 -5
  139. package/src/tools/Chat/types/index.ts +17 -0
  140. package/src/tools/Chat/types/message.ts +3 -0
  141. package/src/tools/Chat/utils/index.ts +4 -0
  142. package/src/tools/CodeEditor/README.md +4 -6
  143. package/src/tools/CodeEditor/components/DiffEditor.tsx +48 -13
  144. package/src/tools/CodeEditor/components/Editor.tsx +96 -44
  145. package/src/tools/CodeEditor/context/EditorProvider.tsx +34 -17
  146. package/src/tools/CodeEditor/hooks/useEditorTheme.ts +92 -99
  147. package/src/tools/CodeEditor/hooks/useMonaco.ts +37 -22
  148. package/src/tools/CodeEditor/lazy.tsx +6 -0
  149. package/src/tools/CodeEditor/lib/index.ts +1 -1
  150. package/src/tools/CodeEditor/lib/themes.ts +3 -39
  151. package/src/tools/CronScheduler/CronScheduler.client.tsx +230 -61
  152. package/src/tools/CronScheduler/components/CustomInput.tsx +21 -4
  153. package/src/tools/CronScheduler/components/DayChips.tsx +13 -11
  154. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +4 -4
  155. package/src/tools/CronScheduler/components/SchedulePreview.tsx +7 -3
  156. package/src/tools/CronScheduler/components/TimeSelector.tsx +1 -1
  157. package/src/tools/CronScheduler/index.tsx +1 -1
  158. package/src/tools/CronScheduler/types/index.ts +8 -3
  159. package/src/tools/CronScheduler/utils/cron-humanize.ts +61 -16
  160. package/src/tools/CronScheduler/utils/cron-parser.ts +13 -4
  161. package/src/tools/FileIcon/FileIcon.tsx +24 -39
  162. package/src/tools/FileIcon/get-file-icon.ts +73 -0
  163. package/src/tools/FileIcon/icons/icon-data.ts +399 -0
  164. package/src/tools/FileIcon/index.ts +4 -0
  165. package/src/tools/FileIcon/loader.ts +17 -35
  166. package/src/tools/FileIcon/specialFolders.ts +18 -0
  167. package/src/tools/Gallery/components/lightbox/GalleryLightbox.tsx +112 -35
  168. package/src/tools/Gallery/components/media/GalleryVideo.tsx +21 -2
  169. package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +11 -1
  170. package/src/tools/Gallery/hooks/usePreloadImages.ts +54 -7
  171. package/src/tools/ImageViewer/components/ImageInfo.tsx +12 -1
  172. package/src/tools/ImageViewer/components/ImageToolbar.tsx +51 -43
  173. package/src/tools/ImageViewer/components/ImageViewer.tsx +96 -24
  174. package/src/tools/ImageViewer/hooks/useImageLoading.ts +13 -0
  175. package/src/tools/ImageViewer/utils/constants.ts +3 -0
  176. package/src/tools/ImageViewer/utils/index.ts +1 -0
  177. package/src/tools/JsonForm/JsonSchemaForm.tsx +4 -1
  178. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +5 -3
  179. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +7 -4
  180. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +3 -1
  181. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +23 -3
  182. package/src/tools/JsonForm/widgets/ColorWidget.tsx +20 -12
  183. package/src/tools/JsonForm/widgets/NumberWidget.tsx +14 -9
  184. package/src/tools/JsonForm/widgets/RadioWidget.tsx +78 -0
  185. package/src/tools/JsonForm/widgets/SelectWidget.tsx +1 -0
  186. package/src/tools/JsonForm/widgets/SliderWidget.tsx +7 -4
  187. package/src/tools/JsonForm/widgets/TextWidget.tsx +41 -17
  188. package/src/tools/JsonForm/widgets/index.ts +1 -0
  189. package/src/tools/JsonTree/components/JsonContent.tsx +115 -40
  190. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +177 -72
  191. package/src/tools/LottiePlayer/index.tsx +14 -4
  192. package/src/tools/LottiePlayer/lazy.tsx +11 -3
  193. package/src/tools/LottiePlayer/types.ts +31 -1
  194. package/src/tools/LottiePlayer/useLottie.ts +32 -9
  195. package/src/tools/LottiePlayer/usePrefersReducedMotion.ts +46 -0
  196. package/src/tools/Map/components/LayerSwitcher.tsx +54 -21
  197. package/src/tools/Map/components/MapCluster.tsx +28 -21
  198. package/src/tools/Map/components/MapContainer.tsx +11 -4
  199. package/src/tools/Map/components/MapLegend.tsx +46 -15
  200. package/src/tools/Map/components/MapMarker.tsx +31 -2
  201. package/src/tools/Map/hooks/useMapEvents.ts +64 -105
  202. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +61 -6
  203. package/src/tools/MarkdownEditor/MentionList.tsx +37 -4
  204. package/src/tools/MarkdownEditor/createMentionSuggestion.ts +11 -0
  205. package/src/tools/MarkdownEditor/lazy.tsx +32 -7
  206. package/src/tools/MarkdownEditor/styles.css +13 -0
  207. package/src/tools/MarkdownMessage/CodeBlock.tsx +40 -17
  208. package/src/tools/MarkdownMessage/MarkdownMessage.tsx +26 -6
  209. package/src/tools/MarkdownMessage/components.tsx +22 -9
  210. package/src/tools/MarkdownMessage/types.ts +24 -1
  211. package/src/tools/Mermaid/Mermaid.client.tsx +27 -5
  212. package/src/tools/Mermaid/components/MermaidErrorPanel.tsx +31 -0
  213. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +14 -17
  214. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +264 -168
  215. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +76 -10
  216. package/src/tools/Mermaid/index.tsx +6 -0
  217. package/src/tools/Mermaid/utils/mermaid-helpers.ts +141 -18
  218. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +11 -1
  219. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +49 -20
  220. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +7 -0
  221. package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +7 -4
  222. package/src/tools/OpenapiViewer/constants.ts +3 -0
  223. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +73 -11
  224. package/src/tools/OpenapiViewer/utils/schemaExport.ts +26 -6
  225. package/src/tools/PrettyCode/PrettyCode.client.tsx +23 -16
  226. package/src/tools/PrettyCode/lazy.tsx +1 -1
  227. package/src/tools/SpeechRecognition/README.md +1 -1
  228. package/src/tools/SpeechRecognition/__tests__/language.test.ts +9 -3
  229. package/src/tools/SpeechRecognition/components/RecordingPulse.tsx +59 -0
  230. package/src/tools/SpeechRecognition/components/index.ts +2 -0
  231. package/src/tools/SpeechRecognition/core/engine/external.ts +24 -7
  232. package/src/tools/SpeechRecognition/core/language.ts +23 -6
  233. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +36 -5
  234. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +18 -11
  235. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +94 -26
  236. package/src/tools/SpeechRecognition/widgets/index.ts +1 -1
  237. package/src/tools/Tree/README.md +4 -8
  238. package/src/tools/Tree/TreeRoot.tsx +22 -10
  239. package/src/tools/Tree/components/TreeContent.tsx +24 -4
  240. package/src/tools/Tree/components/TreeLabel.tsx +8 -2
  241. package/src/tools/Tree/components/TreeRow.tsx +16 -6
  242. package/src/tools/Tree/data/flatten.ts +10 -4
  243. package/src/tools/Tree/types.ts +4 -0
  244. package/src/tools/Uploader/components/UploadAddButton.tsx +29 -6
  245. package/src/tools/Uploader/components/UploadDropzone.tsx +63 -7
  246. package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +19 -5
  247. package/src/tools/Uploader/components/UploadPreviewItem.tsx +47 -17
  248. package/src/tools/Uploader/components/UploadPreviewList.tsx +24 -12
  249. package/src/tools/Uploader/utils/formatters.ts +8 -3
  250. package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +1 -0
  251. package/src/tools/VideoPlayer/canvas/{jsx.d.ts → jsx-augmentation.ts} +12 -19
  252. package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +1 -0
  253. package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +1 -0
  254. package/src/tools/VideoPlayer/parts/fullscreen.tsx +1 -1
  255. package/src/tools/VideoPlayer/parts/pip.tsx +1 -1
  256. package/src/tools/VideoPlayer/parts/playback-rate.tsx +1 -1
  257. package/src/tools/VideoPlayer/parts/seek-bar.tsx +2 -2
  258. package/src/tools/VideoPlayer/parts/volume.tsx +2 -2
  259. package/src/tools/index.ts +2 -1
  260. package/src/tools/Chat/components/AudioToggle.tsx +0 -78
  261. package/src/tools/Chat/components/ChatRoot.tsx +0 -305
  262. package/src/tools/Chat/components/Composer.tsx +0 -216
  263. package/src/tools/Chat/hooks/useChatScroll.ts +0 -145
  264. package/src/tools/Chat/types.ts +0 -9
  265. package/src/tools/JsonTree/components/JsonToolbar.tsx +0 -95
  266. package/src/tools/JsonTree/hooks/useElementCorner.ts +0 -84
  267. package/src/tools/JsonTree/hooks/useNavbarHeight.ts +0 -83
  268. package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +0 -121
  269. package/src/tools/Tour/README.md +0 -373
  270. package/src/tools/Tour/components/Tour.tsx +0 -12
  271. package/src/tools/Tour/components/TourContent.tsx +0 -171
  272. package/src/tools/Tour/components/TourNavigation.tsx +0 -77
  273. package/src/tools/Tour/components/TourProgress.tsx +0 -88
  274. package/src/tools/Tour/components/TourSpotlight.tsx +0 -199
  275. package/src/tools/Tour/components/index.ts +0 -5
  276. package/src/tools/Tour/context/TourContext.ts +0 -19
  277. package/src/tools/Tour/context/TourProvider.tsx +0 -292
  278. package/src/tools/Tour/context/index.ts +0 -2
  279. package/src/tools/Tour/hooks/index.ts +0 -3
  280. package/src/tools/Tour/hooks/useKeyboardNavigation.ts +0 -59
  281. package/src/tools/Tour/hooks/useStepTarget.ts +0 -121
  282. package/src/tools/Tour/hooks/useTour.ts +0 -42
  283. package/src/tools/Tour/index.ts +0 -38
  284. package/src/tools/Tour/types/index.ts +0 -224
  285. package/src/tools/Tour/utils/dom.ts +0 -98
  286. package/src/tools/Tour/utils/index.ts +0 -3
  287. package/src/tools/Tour/utils/logger.ts +0 -3
  288. package/src/tools/Tour/utils/scrollIntoView.ts +0 -24
  289. /package/src/tools/Chat/{config.ts → constants.ts} +0 -0
  290. /package/src/tools/Chat/launcher/{ChatHeaderAudioToggle.tsx → header/ChatHeaderAudioToggle.tsx} +0 -0
  291. /package/src/tools/Chat/{components → messages}/Attachments.tsx +0 -0
  292. /package/src/tools/Chat/{components → messages}/JumpToLatest.tsx +0 -0
  293. /package/src/tools/Chat/{components → messages}/MessageActions.tsx +0 -0
  294. /package/src/tools/Chat/{components → messages}/Sources.tsx +0 -0
  295. /package/src/tools/Chat/{components → messages}/StreamingIndicator.tsx +0 -0
  296. /package/src/tools/Chat/{components → messages}/ToolCalls.tsx +0 -0
  297. /package/src/tools/Chat/{components → shell}/ErrorBanner.tsx +0 -0
@@ -11,6 +11,7 @@ import Lottie from 'react-lottie-player';
11
11
 
12
12
  import { LottiePlayerProps } from './types';
13
13
  import { useLottie } from './useLottie';
14
+ import { usePrefersReducedMotion } from './usePrefersReducedMotion';
14
15
 
15
16
  // Size presets mapping
16
17
  const SIZE_PRESETS = {
@@ -22,42 +23,33 @@ const SIZE_PRESETS = {
22
23
  full: { width: '100%', height: '100%' },
23
24
  } as const;
24
25
 
26
+ const SPEED_STEPS = [0.5, 1, 1.5, 2] as const;
27
+
25
28
  /**
26
29
  * LottiePlayer component for displaying Lottie animations
27
30
  *
28
31
  * Features:
29
32
  * - Loads animations from URLs or objects
30
33
  * - Size presets or custom dimensions
31
- * - Playback controls (speed, direction, loop)
34
+ * - Interactive playback controls (play/pause, loop, speed)
35
+ * - Segment playback and hover-to-play
32
36
  * - Loading and error states
37
+ * - Respects `prefers-reduced-motion`
33
38
  * - Event callbacks
34
39
  *
35
40
  * Usage:
36
41
  * ```tsx
37
42
  * // From URL with size preset
38
- * <LottiePlayer
39
- * src="https://example.com/animation.json"
40
- * size="md"
41
- * autoplay
42
- * loop
43
- * />
43
+ * <LottiePlayer src="https://example.com/animation.json" size="md" autoplay loop />
44
+ *
45
+ * // Interactive controls
46
+ * <LottiePlayer src={animationData} controls />
44
47
  *
45
- * // From object with custom size
46
- * <LottiePlayer
47
- * src={animationData}
48
- * width={300}
49
- * height={300}
50
- * speed={1.5}
51
- * controls
52
- * />
48
+ * // Play a frame range only
49
+ * <LottiePlayer src={animationData} segment={[30, 90]} />
53
50
  *
54
- * // With callbacks
55
- * <LottiePlayer
56
- * src="https://example.com/animation.json"
57
- * onLoad={() => console.log('Animation loaded')}
58
- * onComplete={() => console.log('Animation completed')}
59
- * onError={(err) => console.error('Error:', err)}
60
- * />
51
+ * // Play on hover
52
+ * <LottiePlayer src={animationData} hoverToPlay autoplay={false} />
61
53
  * ```
62
54
  */
63
55
  export function LottiePlayer({
@@ -70,8 +62,12 @@ export function LottiePlayer({
70
62
  speed = 1,
71
63
  direction = 1,
72
64
  controls = false,
65
+ segment,
66
+ hoverToPlay = false,
73
67
  background,
74
68
  className,
69
+ ariaLabel,
70
+ respectReducedMotion = true,
75
71
  showLoading = true,
76
72
  onComplete,
77
73
  onLoad,
@@ -79,18 +75,38 @@ export function LottiePlayer({
79
75
  }: LottiePlayerProps) {
80
76
  // Load animation data using our custom hook
81
77
  const { animationData, isLoading, error, retry } = useLottie({ src });
78
+ const prefersReducedMotion = usePrefersReducedMotion();
79
+ const reduceMotion = respectReducedMotion && prefersReducedMotion;
80
+
81
+ // Interactive playback state (only used when `controls` is enabled).
82
+ const [isPlaying, setIsPlaying] = React.useState(autoplay);
83
+ const [loopEnabled, setLoopEnabled] = React.useState(Boolean(loop));
84
+ const [currentSpeed, setCurrentSpeed] = React.useState<number>(speed);
85
+ const [hovered, setHovered] = React.useState(false);
82
86
 
83
- // Notify parent about load state
87
+ // Keep internal state in sync if controlling props change.
88
+ React.useEffect(() => setIsPlaying(autoplay), [autoplay]);
89
+ React.useEffect(() => setLoopEnabled(Boolean(loop)), [loop]);
90
+ React.useEffect(() => setCurrentSpeed(speed), [speed]);
91
+
92
+ // Notify parent about load state once per loaded animation.
93
+ const notifiedLoadRef = React.useRef<object | null>(null);
84
94
  React.useEffect(() => {
85
- if (animationData && onLoad) {
86
- onLoad();
95
+ if (animationData && notifiedLoadRef.current !== animationData) {
96
+ notifiedLoadRef.current = animationData;
97
+ onLoad?.();
87
98
  }
88
99
  }, [animationData, onLoad]);
89
100
 
90
- // Notify parent about errors
101
+ // Notify parent about errors once per error instance.
102
+ const notifiedErrorRef = React.useRef<Error | null>(null);
91
103
  React.useEffect(() => {
92
- if (error && onError) {
93
- onError(error);
104
+ if (error && notifiedErrorRef.current !== error) {
105
+ notifiedErrorRef.current = error;
106
+ onError?.(error);
107
+ }
108
+ if (!error) {
109
+ notifiedErrorRef.current = null;
94
110
  }
95
111
  }, [error, onError]);
96
112
 
@@ -103,32 +119,71 @@ export function LottiePlayer({
103
119
  height: height ?? SIZE_PRESETS[size].height,
104
120
  };
105
121
  }
106
-
107
- // Use size preset
108
122
  return SIZE_PRESETS[size];
109
123
  }, [size, width, height]);
110
124
 
111
125
  // Handle complete event
112
126
  const handleComplete = React.useCallback(() => {
113
- if (onComplete) {
114
- onComplete();
127
+ // When not looping, reflect the finished state in the controls.
128
+ if (!loopEnabled) {
129
+ setIsPlaying(false);
115
130
  }
116
- }, [onComplete]);
131
+ onComplete?.();
132
+ }, [loopEnabled, onComplete]);
133
+
134
+ const togglePlay = React.useCallback(() => {
135
+ setIsPlaying((prev) => !prev);
136
+ }, []);
137
+
138
+ const cycleSpeed = React.useCallback(() => {
139
+ setCurrentSpeed((prev) => {
140
+ const idx = SPEED_STEPS.indexOf(prev as (typeof SPEED_STEPS)[number]);
141
+ return SPEED_STEPS[(idx + 1) % SPEED_STEPS.length];
142
+ });
143
+ }, []);
144
+
145
+ // Resolve the effective play / loop / speed values.
146
+ // Reduced motion forces a paused, non-looping static frame.
147
+ const effectivePlaying = reduceMotion
148
+ ? false
149
+ : hoverToPlay
150
+ ? hovered
151
+ : controls
152
+ ? isPlaying
153
+ : autoplay;
154
+ const effectiveLoop = reduceMotion ? false : controls ? loopEnabled : loop;
155
+ const effectiveSpeed = controls ? currentSpeed : speed;
156
+
157
+ // Segment playback: react-lottie-player expects [start, end].
158
+ const segments = React.useMemo(
159
+ () => (segment ? ([segment[0], segment[1]] as [number, number]) : undefined),
160
+ [segment]
161
+ );
162
+
163
+ const containerStyle = React.useMemo<React.CSSProperties>(
164
+ () => ({
165
+ width: dimensions.width,
166
+ height: dimensions.height,
167
+ background: background || 'transparent',
168
+ }),
169
+ [dimensions.width, dimensions.height, background]
170
+ );
117
171
 
118
172
  // Loading state
119
173
  if (isLoading && showLoading) {
120
174
  return (
121
175
  <div
122
176
  className={`flex items-center justify-center ${className || ''}`}
123
- style={{
124
- width: dimensions.width,
125
- height: dimensions.height,
126
- background: background || 'transparent',
127
- }}
177
+ style={containerStyle}
178
+ role="status"
179
+ aria-live="polite"
128
180
  >
129
181
  <div className="flex flex-col items-center gap-2">
130
- <div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900" />
131
- <span className="text-sm text-gray-500">Loading animation...</span>
182
+ <div
183
+ className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-foreground motion-reduce:animate-[spin_1.5s_linear_infinite]"
184
+ aria-hidden="true"
185
+ />
186
+ <span className="text-sm text-muted-foreground">Loading animation...</span>
132
187
  </div>
133
188
  </div>
134
189
  );
@@ -139,18 +194,16 @@ export function LottiePlayer({
139
194
  return (
140
195
  <div
141
196
  className={`flex items-center justify-center ${className || ''}`}
142
- style={{
143
- width: dimensions.width,
144
- height: dimensions.height,
145
- background: background || 'transparent',
146
- }}
197
+ style={containerStyle}
198
+ role="alert"
147
199
  >
148
200
  <div className="flex flex-col items-center gap-2 p-4 text-center">
149
201
  <svg
150
- className="h-8 w-8 text-red-500"
202
+ className="h-8 w-8 text-destructive"
151
203
  fill="none"
152
204
  stroke="currentColor"
153
205
  viewBox="0 0 24 24"
206
+ aria-hidden="true"
154
207
  >
155
208
  <path
156
209
  strokeLinecap="round"
@@ -159,10 +212,11 @@ export function LottiePlayer({
159
212
  d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
160
213
  />
161
214
  </svg>
162
- <div className="text-sm text-red-600">{error.message}</div>
215
+ <div className="text-sm text-destructive">{error.message}</div>
163
216
  <button
217
+ type="button"
164
218
  onClick={retry}
165
- className="rounded bg-red-100 px-3 py-1 text-sm text-red-700 hover:bg-red-200"
219
+ className="rounded bg-destructive/10 px-3 py-1 text-sm text-destructive transition-colors hover:bg-destructive/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
166
220
  >
167
221
  Retry
168
222
  </button>
@@ -180,32 +234,83 @@ export function LottiePlayer({
180
234
  return (
181
235
  <div
182
236
  className={className}
183
- style={{
184
- width: dimensions.width,
185
- height: dimensions.height,
186
- background: background || 'transparent',
187
- }}
237
+ role="img"
238
+ aria-label={ariaLabel ?? 'Animation'}
239
+ onMouseEnter={hoverToPlay ? () => setHovered(true) : undefined}
240
+ onMouseLeave={hoverToPlay ? () => setHovered(false) : undefined}
188
241
  >
189
- <Lottie
190
- animationData={animationData}
191
- play={autoplay}
192
- loop={loop}
193
- speed={speed}
194
- direction={direction}
195
- style={{
196
- width: '100%',
197
- height: '100%',
198
- }}
199
- onComplete={handleComplete}
200
- rendererSettings={{
201
- preserveAspectRatio: 'xMidYMid meet',
202
- }}
203
- />
242
+ <div style={containerStyle}>
243
+ <Lottie
244
+ animationData={animationData}
245
+ play={effectivePlaying}
246
+ loop={effectiveLoop}
247
+ speed={effectiveSpeed}
248
+ direction={direction}
249
+ segments={segments}
250
+ goTo={reduceMotion ? (segment ? segment[0] : 0) : undefined}
251
+ style={{ width: '100%', height: '100%' }}
252
+ onComplete={handleComplete}
253
+ rendererSettings={{
254
+ preserveAspectRatio: 'xMidYMid meet',
255
+ }}
256
+ />
257
+ </div>
204
258
  {controls && (
205
- <div className="mt-2 flex items-center justify-center gap-2">
206
- <span className="text-xs text-gray-500">
207
- Speed: {speed}x | Direction: {direction === 1 ? 'Forward' : 'Reverse'}
208
- </span>
259
+ <div className="mt-2 flex items-center justify-center gap-1">
260
+ <button
261
+ type="button"
262
+ onClick={togglePlay}
263
+ disabled={reduceMotion}
264
+ aria-label={effectivePlaying ? 'Pause animation' : 'Play animation'}
265
+ aria-pressed={effectivePlaying}
266
+ className="inline-flex h-7 w-7 items-center justify-center rounded text-foreground transition-colors hover:bg-muted disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
267
+ >
268
+ {effectivePlaying ? (
269
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
270
+ <rect x="6" y="5" width="4" height="14" rx="1" />
271
+ <rect x="14" y="5" width="4" height="14" rx="1" />
272
+ </svg>
273
+ ) : (
274
+ <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
275
+ <path d="M8 5v14l11-7z" />
276
+ </svg>
277
+ )}
278
+ </button>
279
+ <button
280
+ type="button"
281
+ onClick={() => setLoopEnabled((prev) => !prev)}
282
+ aria-label={loopEnabled ? 'Disable loop' : 'Enable loop'}
283
+ aria-pressed={loopEnabled}
284
+ className={`inline-flex h-7 w-7 items-center justify-center rounded transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
285
+ loopEnabled ? 'text-foreground' : 'text-muted-foreground'
286
+ }`}
287
+ >
288
+ <svg
289
+ className="h-4 w-4"
290
+ viewBox="0 0 24 24"
291
+ fill="none"
292
+ stroke="currentColor"
293
+ strokeWidth={2}
294
+ aria-hidden="true"
295
+ >
296
+ <path
297
+ strokeLinecap="round"
298
+ strokeLinejoin="round"
299
+ d="M17 1l4 4-4 4M3 11V9a4 4 0 014-4h14M7 23l-4-4 4-4M21 13v2a4 4 0 01-4 4H3"
300
+ />
301
+ </svg>
302
+ </button>
303
+ <button
304
+ type="button"
305
+ onClick={cycleSpeed}
306
+ aria-label={`Playback speed ${currentSpeed}x, click to change`}
307
+ className="inline-flex h-7 min-w-9 items-center justify-center rounded px-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
308
+ >
309
+ {currentSpeed}x
310
+ </button>
311
+ {reduceMotion && (
312
+ <span className="ml-1 text-xs text-muted-foreground">Reduced motion</span>
313
+ )}
209
314
  </div>
210
315
  )}
211
316
  </div>
@@ -16,10 +16,13 @@ const LottiePlayerClient = lazy(() =>
16
16
 
17
17
  // Loading fallback component
18
18
  const LoadingFallback = () => (
19
- <div className="flex items-center justify-center p-8">
19
+ <div className="flex items-center justify-center p-8" role="status" aria-live="polite">
20
20
  <div className="flex flex-col items-center gap-2">
21
- <div className="h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-gray-900" />
22
- <span className="text-sm text-gray-500">Loading player...</span>
21
+ <div
22
+ className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-foreground motion-reduce:animate-[spin_1.5s_linear_infinite]"
23
+ aria-hidden="true"
24
+ />
25
+ <span className="text-sm text-muted-foreground">Loading player...</span>
23
26
  </div>
24
27
  </div>
25
28
  );
@@ -51,6 +54,13 @@ export function LottiePlayer(props: LottiePlayerProps) {
51
54
  }
52
55
 
53
56
  // Re-export types for convenience
54
- export type { LottiePlayerProps, LottieSize, LottieSpeed, LottieDirection } from './types';
57
+ export type {
58
+ LottiePlayerProps,
59
+ LottieSize,
60
+ LottieSpeed,
61
+ LottieDirection,
62
+ LottieSegment,
63
+ } from './types';
55
64
  export { useLottie } from './useLottie';
56
65
  export type { UseLottieOptions, UseLottieReturn } from './useLottie';
66
+ export { usePrefersReducedMotion } from './usePrefersReducedMotion';
@@ -10,7 +10,7 @@
10
10
  * import { LottiePlayer } from '@djangocfg/ui-tools/lottie'
11
11
  */
12
12
 
13
- import { createLazyComponent, LoadingFallback } from '../../components';
13
+ import { createLazyComponent } from '../../components';
14
14
  import type { LottiePlayerProps } from './types';
15
15
 
16
16
  // ============================================================================
@@ -22,6 +22,7 @@ export type {
22
22
  LottieSize,
23
23
  LottieSpeed,
24
24
  LottieDirection,
25
+ LottieSegment,
25
26
  } from './types';
26
27
 
27
28
  // ============================================================================
@@ -30,9 +31,16 @@ export type {
30
31
 
31
32
  function LottieLoadingFallback() {
32
33
  return (
33
- <div className="flex items-center justify-center p-8">
34
+ <div
35
+ className="flex items-center justify-center p-8"
36
+ role="status"
37
+ aria-live="polite"
38
+ >
34
39
  <div className="flex flex-col items-center gap-2">
35
- <div className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary" />
40
+ <div
41
+ className="h-8 w-8 animate-spin rounded-full border-4 border-muted border-t-primary motion-reduce:animate-[spin_1.5s_linear_infinite]"
42
+ aria-hidden="true"
43
+ />
36
44
  <span className="text-sm text-muted-foreground">Loading animation...</span>
37
45
  </div>
38
46
  </div>
@@ -10,6 +10,11 @@ export type LottieSpeed = 0.5 | 1 | 1.5 | 2;
10
10
 
11
11
  export type LottieDirection = 1 | -1;
12
12
 
13
+ /**
14
+ * Inclusive [start, end] frame range for segment playback.
15
+ */
16
+ export type LottieSegment = [number, number];
17
+
13
18
  export interface LottiePlayerProps {
14
19
  /**
15
20
  * Animation data (JSON object) or URL to load from
@@ -57,11 +62,23 @@ export interface LottiePlayerProps {
57
62
  direction?: LottieDirection;
58
63
 
59
64
  /**
60
- * Show playback controls
65
+ * Show interactive playback controls (play/pause, loop, speed)
61
66
  * @default false
62
67
  */
63
68
  controls?: boolean;
64
69
 
70
+ /**
71
+ * Play only a frame range, e.g. [30, 90]. Overrides full-clip playback.
72
+ */
73
+ segment?: LottieSegment;
74
+
75
+ /**
76
+ * Play the animation only while the pointer is over it. When the
77
+ * pointer leaves, playback pauses. Ignored when `controls` drive playback.
78
+ * @default false
79
+ */
80
+ hoverToPlay?: boolean;
81
+
65
82
  /**
66
83
  * Background color
67
84
  */
@@ -72,6 +89,19 @@ export interface LottiePlayerProps {
72
89
  */
73
90
  className?: string;
74
91
 
92
+ /**
93
+ * Accessible label describing the animation for screen readers.
94
+ */
95
+ ariaLabel?: string;
96
+
97
+ /**
98
+ * Respect the user's `prefers-reduced-motion` setting. When the user
99
+ * prefers reduced motion the animation renders a static first frame
100
+ * instead of playing.
101
+ * @default true
102
+ */
103
+ respectReducedMotion?: boolean;
104
+
75
105
  /**
76
106
  * Show loading state
77
107
  * @default true
@@ -8,8 +8,6 @@
8
8
 
9
9
  import { useEffect, useRef, useState } from 'react';
10
10
 
11
- import { LottieAnimationData } from './types';
12
-
13
11
  export interface UseLottieOptions {
14
12
  /**
15
13
  * Animation data (JSON object) or URL to load from
@@ -54,6 +52,7 @@ const animationCache = new Map<string, object>();
54
52
  * Features:
55
53
  * - Loads animations from URLs or accepts animation objects directly
56
54
  * - Caching support to prevent re-fetching the same animation
55
+ * - Aborts in-flight requests on unmount / src change
57
56
  * - Error handling with retry capability
58
57
  * - Loading states
59
58
  *
@@ -89,9 +88,12 @@ export function useLottie(options: UseLottieOptions): UseLottieReturn {
89
88
  }, []);
90
89
 
91
90
  useEffect(() => {
92
- // If src is already an object, use it directly
91
+ // If src is already an object, use it directly.
92
+ // Only update state when the reference actually changes so that
93
+ // callers passing an inline object literal do not trigger an
94
+ // infinite render loop.
93
95
  if (typeof src === 'object' && src !== null) {
94
- setAnimationData(src);
96
+ setAnimationData((prev) => (prev === src ? prev : src));
95
97
  setIsLoading(false);
96
98
  setError(null);
97
99
  return;
@@ -99,6 +101,8 @@ export function useLottie(options: UseLottieOptions): UseLottieReturn {
99
101
 
100
102
  // If src is a string (URL), fetch it
101
103
  if (typeof src === 'string') {
104
+ const abortController = new AbortController();
105
+
102
106
  const loadAnimation = async () => {
103
107
  // Check cache first
104
108
  if (cache && animationCache.has(src)) {
@@ -117,29 +121,44 @@ export function useLottie(options: UseLottieOptions): UseLottieReturn {
117
121
  }
118
122
 
119
123
  try {
120
- const response = await fetch(src);
124
+ const response = await fetch(src, { signal: abortController.signal });
121
125
 
122
126
  if (!response.ok) {
123
127
  throw new Error(`Failed to load animation: ${response.status} ${response.statusText}`);
124
128
  }
125
129
 
126
- const data = await response.json();
130
+ let data: unknown;
131
+ try {
132
+ data = await response.json();
133
+ } catch {
134
+ throw new Error('Animation file is not valid JSON');
135
+ }
127
136
 
128
137
  // Validate that it's a valid Lottie animation
129
- if (!data || typeof data !== 'object' || !data.v || !data.layers) {
138
+ if (
139
+ !data ||
140
+ typeof data !== 'object' ||
141
+ !('v' in data) ||
142
+ !('layers' in data) ||
143
+ !Array.isArray((data as { layers: unknown }).layers)
144
+ ) {
130
145
  throw new Error('Invalid Lottie animation data');
131
146
  }
132
147
 
133
148
  // Cache the loaded animation
134
149
  if (cache) {
135
- animationCache.set(src, data);
150
+ animationCache.set(src, data as object);
136
151
  }
137
152
 
138
153
  if (isMountedRef.current) {
139
- setAnimationData(data);
154
+ setAnimationData(data as object);
140
155
  setIsLoading(false);
141
156
  }
142
157
  } catch (err) {
158
+ // Ignore aborts triggered by unmount / src change.
159
+ if (abortController.signal.aborted) {
160
+ return;
161
+ }
143
162
  if (isMountedRef.current) {
144
163
  setError(err instanceof Error ? err : new Error('Failed to load animation'));
145
164
  setIsLoading(false);
@@ -148,6 +167,10 @@ export function useLottie(options: UseLottieOptions): UseLottieReturn {
148
167
  };
149
168
 
150
169
  loadAnimation();
170
+
171
+ return () => {
172
+ abortController.abort();
173
+ };
151
174
  }
152
175
  }, [src, cache, retryCount]);
153
176
 
@@ -0,0 +1,46 @@
1
+ /**
2
+ * usePrefersReducedMotion Hook
3
+ *
4
+ * Tracks the `prefers-reduced-motion` media query so animations can be
5
+ * suppressed for users who request reduced motion (a11y).
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useEffect, useState } from 'react';
11
+
12
+ const QUERY = '(prefers-reduced-motion: reduce)';
13
+
14
+ /**
15
+ * Returns `true` when the user has requested reduced motion.
16
+ *
17
+ * SSR-safe: returns `false` on the server and during the first client
18
+ * render, then syncs with the actual media query after mount.
19
+ */
20
+ export function usePrefersReducedMotion(): boolean {
21
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
22
+
23
+ useEffect(() => {
24
+ if (typeof window === 'undefined' || !window.matchMedia) {
25
+ return;
26
+ }
27
+
28
+ const mediaQuery = window.matchMedia(QUERY);
29
+ setPrefersReducedMotion(mediaQuery.matches);
30
+
31
+ const handleChange = (event: MediaQueryListEvent) => {
32
+ setPrefersReducedMotion(event.matches);
33
+ };
34
+
35
+ // Safari < 14 only supports the deprecated addListener API.
36
+ if (typeof mediaQuery.addEventListener === 'function') {
37
+ mediaQuery.addEventListener('change', handleChange);
38
+ return () => mediaQuery.removeEventListener('change', handleChange);
39
+ }
40
+
41
+ mediaQuery.addListener(handleChange);
42
+ return () => mediaQuery.removeListener(handleChange);
43
+ }, []);
44
+
45
+ return prefersReducedMotion;
46
+ }