@liorian/sdk 0.28.0-alpha.56010ca

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 (362) hide show
  1. package/README.md +483 -0
  2. package/package.json +94 -0
  3. package/schemas/module.schema.json +818 -0
  4. package/src/application/index.ts +3 -0
  5. package/src/application/module-instance.ts +36 -0
  6. package/src/application/service/auth-api-service.ts +291 -0
  7. package/src/application/service/auth-user.service.ts +108 -0
  8. package/src/application/service/index.ts +12 -0
  9. package/src/application/service/module-activation-api.service.ts +105 -0
  10. package/src/application/service/notifications-api-service.ts +38 -0
  11. package/src/application/service/notifications-push.service.ts +299 -0
  12. package/src/application/service/notifications-socket-service.ts +76 -0
  13. package/src/application/service/organizations-api-service.ts +104 -0
  14. package/src/application/service/otp-api-service.ts +11 -0
  15. package/src/application/service/storage-api-service.ts +77 -0
  16. package/src/application/service/storefront-api.service.ts +72 -0
  17. package/src/application/service/user-preferences-api.service.ts +57 -0
  18. package/src/domain/config/app.config.ts +35 -0
  19. package/src/domain/config/auth.config.ts +30 -0
  20. package/src/domain/config/index.ts +4 -0
  21. package/src/domain/config/logo.config.ts +17 -0
  22. package/src/domain/entities/activities.interface.ts +18 -0
  23. package/src/domain/entities/catalog.interface.ts +96 -0
  24. package/src/domain/entities/index.ts +13 -0
  25. package/src/domain/entities/media.ts +22 -0
  26. package/src/domain/entities/module-activation.interface.ts +173 -0
  27. package/src/domain/entities/module.interface.ts +101 -0
  28. package/src/domain/entities/notification.interface.ts +23 -0
  29. package/src/domain/entities/objectable.type.ts +3 -0
  30. package/src/domain/entities/organization.interface.ts +81 -0
  31. package/src/domain/entities/user-auth.interface.ts +17 -0
  32. package/src/domain/entities/user-phone.interface.ts +11 -0
  33. package/src/domain/entities/user-profile.interface.ts +23 -0
  34. package/src/domain/entities/user.interface.ts +79 -0
  35. package/src/domain/enums/coloring.enum.ts +6 -0
  36. package/src/domain/enums/domains.enum.ts +282 -0
  37. package/src/domain/enums/index.ts +8 -0
  38. package/src/domain/enums/module-category.enum.ts +57 -0
  39. package/src/domain/enums/module.enum.ts +97 -0
  40. package/src/domain/enums/routine.enum.ts +12 -0
  41. package/src/domain/enums/user-profile.enum.ts +25 -0
  42. package/src/domain/enums/user-status.enum.ts +7 -0
  43. package/src/domain/index.ts +6 -0
  44. package/src/domain/typing/auth-caches.ts +12 -0
  45. package/src/domain/typing/auth-oauth.types.ts +172 -0
  46. package/src/domain/typing/devise.ts +2 -0
  47. package/src/domain/typing/index.ts +10 -0
  48. package/src/domain/typing/response.ts +30 -0
  49. package/src/domain/typing/routine.types.ts +113 -0
  50. package/src/domain/typing/session.interface.ts +28 -0
  51. package/src/domain/typing/statisticals.ts +11 -0
  52. package/src/domain/typing/user-media-upload.ts +9 -0
  53. package/src/domain/typing/values.ts +6 -0
  54. package/src/index.tsx +168 -0
  55. package/src/infrastructure/capabilities/cache-storage/capability.ts +129 -0
  56. package/src/infrastructure/capabilities/cache-storage/enum.ts +5 -0
  57. package/src/infrastructure/capabilities/cache-storage/index.ts +4 -0
  58. package/src/infrastructure/capabilities/cache-storage/types.ts +13 -0
  59. package/src/infrastructure/capabilities/index.ts +3 -0
  60. package/src/infrastructure/capabilities/stackable/capability.ts +55 -0
  61. package/src/infrastructure/capabilities/stackable/index.ts +3 -0
  62. package/src/infrastructure/capabilities/stackable/types.ts +24 -0
  63. package/src/infrastructure/common-classname.constant.ts +4 -0
  64. package/src/infrastructure/data/countries.ts +208 -0
  65. package/src/infrastructure/data/gender.ts +5 -0
  66. package/src/infrastructure/data/index.ts +3 -0
  67. package/src/infrastructure/dataset/index.ts +3 -0
  68. package/src/infrastructure/dataset/sign-in.dataset.ts +21 -0
  69. package/src/infrastructure/dataset/sign-up.dataset.ts +64 -0
  70. package/src/infrastructure/hooks/index.ts +10 -0
  71. package/src/infrastructure/hooks/use-activity-lock-draft.ts +69 -0
  72. package/src/infrastructure/hooks/use-auth.ts +24 -0
  73. package/src/infrastructure/hooks/use-elastic-interaction.ts +180 -0
  74. package/src/infrastructure/hooks/use-is-desktop.ts +25 -0
  75. package/src/infrastructure/hooks/use-is-tauri.ts +17 -0
  76. package/src/infrastructure/hooks/use-mobile.ts +19 -0
  77. package/src/infrastructure/hooks/use-notifications.ts +128 -0
  78. package/src/infrastructure/hooks/use-recent-colors.ts +113 -0
  79. package/src/infrastructure/hooks/use-recent-modules.ts +76 -0
  80. package/src/infrastructure/index.tsx +11 -0
  81. package/src/infrastructure/library/design-system.ts +95 -0
  82. package/src/infrastructure/library/index.tsx +5 -0
  83. package/src/infrastructure/library/motion-utils.ts +77 -0
  84. package/src/infrastructure/library/motion.tsx +31 -0
  85. package/src/infrastructure/library/scroll-progress.tsx +93 -0
  86. package/src/infrastructure/providers/auth-guard.provider.tsx +47 -0
  87. package/src/infrastructure/providers/auth.provider.tsx +168 -0
  88. package/src/infrastructure/providers/index.tsx +8 -0
  89. package/src/infrastructure/providers/module-usage.provider.tsx +95 -0
  90. package/src/infrastructure/providers/modules-guard.provider.tsx +64 -0
  91. package/src/infrastructure/providers/modules-routines.provider.tsx +33 -0
  92. package/src/infrastructure/providers/modules-switcher.provider.tsx +25 -0
  93. package/src/infrastructure/providers/query.provider.tsx +27 -0
  94. package/src/infrastructure/routines/__tests__/routine.test.ts +368 -0
  95. package/src/infrastructure/routines/index.tsx +3 -0
  96. package/src/infrastructure/routines/routine.hook.ts +121 -0
  97. package/src/infrastructure/routines/routine.ts +283 -0
  98. package/src/infrastructure/routines/routine.tsx +283 -0
  99. package/src/infrastructure/stores/activity-lock.store.ts +22 -0
  100. package/src/infrastructure/stores/auth-user-connected.store.ts +24 -0
  101. package/src/infrastructure/stores/dataset.store.ts +106 -0
  102. package/src/infrastructure/stores/index.ts +6 -0
  103. package/src/infrastructure/stores/module.store.ts +266 -0
  104. package/src/infrastructure/stores/upload.store.ts +198 -0
  105. package/src/infrastructure/utilities/access-label.util.ts +158 -0
  106. package/src/infrastructure/utilities/activities.util.tsx +54 -0
  107. package/src/infrastructure/utilities/activity-lock.service.ts +263 -0
  108. package/src/infrastructure/utilities/activity-tracker.service.ts +205 -0
  109. package/src/infrastructure/utilities/api-service.ts +237 -0
  110. package/src/infrastructure/utilities/auth-logout.util.ts +23 -0
  111. package/src/infrastructure/utilities/classname.util.ts +7 -0
  112. package/src/infrastructure/utilities/encryption.service.ts +125 -0
  113. package/src/infrastructure/utilities/form-snapshot.service.ts +304 -0
  114. package/src/infrastructure/utilities/format.util.ts +40 -0
  115. package/src/infrastructure/utilities/index.tsx +23 -0
  116. package/src/infrastructure/utilities/media-label.service.ts +47 -0
  117. package/src/infrastructure/utilities/modules.ts +11 -0
  118. package/src/infrastructure/utilities/permission.util.ts +22 -0
  119. package/src/infrastructure/utilities/push-permission.util.ts +43 -0
  120. package/src/infrastructure/utilities/route-access.ts +38 -0
  121. package/src/infrastructure/utilities/socket-service.ts +123 -0
  122. package/src/infrastructure/utilities/sso-auth.util.ts +188 -0
  123. package/src/infrastructure/utilities/stringable.util.ts +16 -0
  124. package/src/infrastructure/utilities/strings.util.ts +18 -0
  125. package/src/infrastructure/utilities/tauri.util.ts +15 -0
  126. package/src/infrastructure/utilities/utils.ts +6 -0
  127. package/src/infrastructure/utilities/values.ts +5 -0
  128. package/src/presentation/8starlabs-ui/button.tsx +94 -0
  129. package/src/presentation/8starlabs-ui/flip-clock.tsx +368 -0
  130. package/src/presentation/8starlabs-ui/heatmap.tsx +430 -0
  131. package/src/presentation/8starlabs-ui/hooks/index.tsx +2 -0
  132. package/src/presentation/8starlabs-ui/hooks/use-mobile.ts +21 -0
  133. package/src/presentation/8starlabs-ui/index.tsx +20 -0
  134. package/src/presentation/8starlabs-ui/json-viewer.tsx +972 -0
  135. package/src/presentation/8starlabs-ui/marquee.tsx +134 -0
  136. package/src/presentation/8starlabs-ui/partition-bar.tsx +198 -0
  137. package/src/presentation/8starlabs-ui/scroll-fade.tsx +158 -0
  138. package/src/presentation/8starlabs-ui/status-indicator.tsx +86 -0
  139. package/src/presentation/8starlabs-ui/system-banner.tsx +50 -0
  140. package/src/presentation/8starlabs-ui/timeline.tsx +576 -0
  141. package/src/presentation/8starlabs-ui/transport-badge.tsx +249 -0
  142. package/src/presentation/8starlabs-ui/ui/copy-button.tsx +60 -0
  143. package/src/presentation/8starlabs-ui/ui/index.tsx +2 -0
  144. package/src/presentation/analytics-section.tsx +85 -0
  145. package/src/presentation/app-sidebar.tsx +234 -0
  146. package/src/presentation/charts/area-widget.chart-skeleton.tsx +61 -0
  147. package/src/presentation/charts/area-widget.chart.tsx +128 -0
  148. package/src/presentation/charts/chart-widgets.types.ts +81 -0
  149. package/src/presentation/charts/index.tsx +16 -0
  150. package/src/presentation/charts/line-widget.chart-skeleton.tsx +64 -0
  151. package/src/presentation/charts/line-widget.chart.tsx +105 -0
  152. package/src/presentation/charts/pie-widget.chart-skeleton.tsx +56 -0
  153. package/src/presentation/charts/pie-widget.chart.tsx +168 -0
  154. package/src/presentation/charts/radar-widget.chart-skeleton.tsx +67 -0
  155. package/src/presentation/charts/radar-widget.chart.tsx +80 -0
  156. package/src/presentation/charts/radial-stacked-widget.chart-skeleton.tsx +66 -0
  157. package/src/presentation/charts/radial-stacked-widget.chart.tsx +120 -0
  158. package/src/presentation/charts/radial-widget.chart-skeleton.tsx +61 -0
  159. package/src/presentation/charts/radial-widget.chart.tsx +87 -0
  160. package/src/presentation/charts/tooltip-widget.chart-skeleton.tsx +64 -0
  161. package/src/presentation/charts/tooltip-widget.chart.tsx +89 -0
  162. package/src/presentation/components/activity-actions.tsx +20 -0
  163. package/src/presentation/components/activity-container.tsx +55 -0
  164. package/src/presentation/components/activity-content.tsx +20 -0
  165. package/src/presentation/components/activity-descriptor-parser.tsx +37 -0
  166. package/src/presentation/components/activity-header.tsx +20 -0
  167. package/src/presentation/components/activity-loader.tsx +19 -0
  168. package/src/presentation/components/activity-title.tsx +32 -0
  169. package/src/presentation/components/activity.tsx +22 -0
  170. package/src/presentation/components/auth/index.tsx +4 -0
  171. package/src/presentation/components/auth/sso-callback.view.tsx +44 -0
  172. package/src/presentation/components/auth/sso-disconnect.view.tsx +41 -0
  173. package/src/presentation/components/auth/sso-sign-in.view.tsx +29 -0
  174. package/src/presentation/components/auth-lock-screen.tsx +309 -0
  175. package/src/presentation/components/auth-session.view.tsx +17 -0
  176. package/src/presentation/components/data-table.tsx +814 -0
  177. package/src/presentation/components/dynamic-icon.tsx +16 -0
  178. package/src/presentation/components/floating-upload.tsx +291 -0
  179. package/src/presentation/components/index.tsx +40 -0
  180. package/src/presentation/components/local-time.tsx +170 -0
  181. package/src/presentation/components/markdown-parser.tsx +201 -0
  182. package/src/presentation/components/message-scroller.tsx +131 -0
  183. package/src/presentation/components/module-first-access-config.view.tsx +181 -0
  184. package/src/presentation/components/module-first-access-pending.view.tsx +38 -0
  185. package/src/presentation/components/module-guard.tsx +34 -0
  186. package/src/presentation/components/modules-first-access.guard.tsx +88 -0
  187. package/src/presentation/components/modules-guard-forbidden.tsx +46 -0
  188. package/src/presentation/components/notification-details.modal.tsx +145 -0
  189. package/src/presentation/components/notification-item.tsx +127 -0
  190. package/src/presentation/components/notifications-popover.tsx +128 -0
  191. package/src/presentation/components/push-notifications.provider.tsx +65 -0
  192. package/src/presentation/components/upload-progress-dialog.tsx +140 -0
  193. package/src/presentation/components/waiting-bar.tsx +22 -0
  194. package/src/presentation/components/waiting-section.tsx +15 -0
  195. package/src/presentation/components/waiting.tsx +14 -0
  196. package/src/presentation/data-grid/data-grid-empty.tsx +24 -0
  197. package/src/presentation/data-grid/data-grid-search-engine.tsx +32 -0
  198. package/src/presentation/data-grid/data-grid.tsx +892 -0
  199. package/src/presentation/data-grid/index.tsx +4 -0
  200. package/src/presentation/form-screen.tsx +95 -0
  201. package/src/presentation/icons/custom.tsx +20 -0
  202. package/src/presentation/icons/glyphIcons.tsx +53 -0
  203. package/src/presentation/icons/icons.enum.ts +4 -0
  204. package/src/presentation/icons/icons.util.ts +6 -0
  205. package/src/presentation/icons/index.tsx +8 -0
  206. package/src/presentation/icons/lucide.tsx +17 -0
  207. package/src/presentation/icons/remix.tsx +33 -0
  208. package/src/presentation/icons/types.ts +27 -0
  209. package/src/presentation/index.tsx +45 -0
  210. package/src/presentation/modals/components/ModalBox.tsx +74 -0
  211. package/src/presentation/modals/components/ModalPortal.tsx +31 -0
  212. package/src/presentation/modals/components/ModalStepper.tsx +319 -0
  213. package/src/presentation/modals/components/ModalWrapper.tsx +65 -0
  214. package/src/presentation/modals/components/index.tsx +5 -0
  215. package/src/presentation/modals/hooks/index.ts +2 -0
  216. package/src/presentation/modals/hooks/useModal.ts +38 -0
  217. package/src/presentation/modals/index.tsx +5 -0
  218. package/src/presentation/modals/stores/index.ts +2 -0
  219. package/src/presentation/modals/stores/useModalStore.ts +90 -0
  220. package/src/presentation/modals/types/index.ts +2 -0
  221. package/src/presentation/modals/types/modal.type.ts +33 -0
  222. package/src/presentation/module-menubar.tsx +300 -0
  223. package/src/presentation/module-widget.tsx +233 -0
  224. package/src/presentation/nav-documents.tsx +89 -0
  225. package/src/presentation/nav-main.tsx +62 -0
  226. package/src/presentation/nav-secondary.tsx +41 -0
  227. package/src/presentation/nav-user.tsx +109 -0
  228. package/src/presentation/permission-action-badge.tsx +35 -0
  229. package/src/presentation/reui/index.tsx +3 -0
  230. package/src/presentation/reui/stepper.tsx +508 -0
  231. package/src/presentation/reui/timeline.tsx +226 -0
  232. package/src/presentation/sheets/index.tsx +2 -0
  233. package/src/presentation/sheets/legacy-sheet-header.tsx +0 -0
  234. package/src/presentation/sheets/legacy-sheet.tsx +37 -0
  235. package/src/presentation/size.enum.ts +19 -0
  236. package/src/presentation/system/index.tsx +3 -0
  237. package/src/presentation/system/logo.theme.tsx +21 -0
  238. package/src/presentation/system/types.ts +10 -0
  239. package/src/presentation/theme/index.tsx +6 -0
  240. package/src/presentation/theme/prefer-color-scheme.tsx +50 -0
  241. package/src/presentation/theme/switch-color-scheme.tsx +16 -0
  242. package/src/presentation/theme/theme-switcher-button.tsx +20 -0
  243. package/src/presentation/theme/theme.enum.ts +4 -0
  244. package/src/presentation/theme/theme.store.ts +87 -0
  245. package/src/presentation/themes/index.tsx +2 -0
  246. package/src/presentation/themes/katon/dashboard-layout.tsx +1080 -0
  247. package/src/presentation/themes/katon/edge-section.tsx +19 -0
  248. package/src/presentation/themes/katon/header-menubar.tsx +270 -0
  249. package/src/presentation/themes/katon/header-modules-available.tsx +44 -0
  250. package/src/presentation/themes/katon/header-modules-recents.tsx +52 -0
  251. package/src/presentation/themes/katon/header-modules-store.tsx +25 -0
  252. package/src/presentation/themes/katon/header-options.tsx +13 -0
  253. package/src/presentation/themes/katon/header-organization-select.tsx +146 -0
  254. package/src/presentation/themes/katon/header-routines.tsx +74 -0
  255. package/src/presentation/themes/katon/header-tasks-connected-user.tsx +150 -0
  256. package/src/presentation/themes/katon/index.tsx +16 -0
  257. package/src/presentation/themes/katon/main-wrapper.tsx +34 -0
  258. package/src/presentation/themes/katon/module-item-link.tsx +32 -0
  259. package/src/presentation/themes/katon/view-frame.tsx +25 -0
  260. package/src/presentation/themes/katon/view-helmet.tsx +53 -0
  261. package/src/presentation/themes/katon/view-status.tsx +32 -0
  262. package/src/presentation/themes/katon/view-wrapper.tsx +15 -0
  263. package/src/presentation/themes/katon/view.tsx +42 -0
  264. package/src/presentation/ui/accordion-multiselect.tsx +206 -0
  265. package/src/presentation/ui/accordion.tsx +81 -0
  266. package/src/presentation/ui/alert-dialog.tsx +199 -0
  267. package/src/presentation/ui/alert.tsx +76 -0
  268. package/src/presentation/ui/aspect-ratio.tsx +11 -0
  269. package/src/presentation/ui/attachment.tsx +204 -0
  270. package/src/presentation/ui/avatar.tsx +112 -0
  271. package/src/presentation/ui/badge.tsx +49 -0
  272. package/src/presentation/ui/breadcrumb.tsx +122 -0
  273. package/src/presentation/ui/bubble.tsx +125 -0
  274. package/src/presentation/ui/button-group.tsx +83 -0
  275. package/src/presentation/ui/button.tsx +87 -0
  276. package/src/presentation/ui/calendar.tsx +222 -0
  277. package/src/presentation/ui/card.tsx +106 -0
  278. package/src/presentation/ui/carousel.tsx +242 -0
  279. package/src/presentation/ui/chart.tsx +373 -0
  280. package/src/presentation/ui/checkbox.tsx +33 -0
  281. package/src/presentation/ui/clickable.tsx +58 -0
  282. package/src/presentation/ui/collapsible.tsx +33 -0
  283. package/src/presentation/ui/color-swatch-selector.tsx +81 -0
  284. package/src/presentation/ui/combobox.tsx +299 -0
  285. package/src/presentation/ui/command.tsx +195 -0
  286. package/src/presentation/ui/context-menu.tsx +263 -0
  287. package/src/presentation/ui/dialog.tsx +168 -0
  288. package/src/presentation/ui/direction.tsx +22 -0
  289. package/src/presentation/ui/drawer.tsx +134 -0
  290. package/src/presentation/ui/dropdown-menu.tsx +269 -0
  291. package/src/presentation/ui/empty.tsx +104 -0
  292. package/src/presentation/ui/field.tsx +238 -0
  293. package/src/presentation/ui/granularity-selector.tsx +58 -0
  294. package/src/presentation/ui/hover-card.tsx +44 -0
  295. package/src/presentation/ui/index.tsx +74 -0
  296. package/src/presentation/ui/input-group.tsx +156 -0
  297. package/src/presentation/ui/input-otp.tsx +87 -0
  298. package/src/presentation/ui/input.tsx +19 -0
  299. package/src/presentation/ui/item.tsx +196 -0
  300. package/src/presentation/ui/kpi-card.tsx +203 -0
  301. package/src/presentation/ui/label-selector.tsx +100 -0
  302. package/src/presentation/ui/label.tsx +24 -0
  303. package/src/presentation/ui/legacy-birth-date-input.tsx +43 -0
  304. package/src/presentation/ui/legacy-color-picker/index.tsx +42 -0
  305. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.content.tsx +37 -0
  306. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.context.tsx +42 -0
  307. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.hex-input.tsx +97 -0
  308. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.picker.tsx +266 -0
  309. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.presets.ts +22 -0
  310. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.quick-list.tsx +49 -0
  311. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.recent.tsx +43 -0
  312. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.swatches.tsx +111 -0
  313. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.trigger.tsx +128 -0
  314. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.tsx +110 -0
  315. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.types.ts +1 -0
  316. package/src/presentation/ui/legacy-color-picker/legacy-color-picker.utils.ts +114 -0
  317. package/src/presentation/ui/legacy-country-input.tsx +171 -0
  318. package/src/presentation/ui/legacy-gender-input.tsx +22 -0
  319. package/src/presentation/ui/legacy-input.tsx +106 -0
  320. package/src/presentation/ui/legacy-phone-input.tsx +213 -0
  321. package/src/presentation/ui/legacy-select-input.tsx +114 -0
  322. package/src/presentation/ui/legacy-text-area.tsx +111 -0
  323. package/src/presentation/ui/list-items.tsx +59 -0
  324. package/src/presentation/ui/marker.tsx +69 -0
  325. package/src/presentation/ui/media-player/index.tsx +12 -0
  326. package/src/presentation/ui/media-player/media-player.context.tsx +36 -0
  327. package/src/presentation/ui/media-player/media-player.tsx +167 -0
  328. package/src/presentation/ui/media-player/player-controls.tsx +207 -0
  329. package/src/presentation/ui/media-player/players/audio-player.tsx +78 -0
  330. package/src/presentation/ui/media-player/players/index.tsx +4 -0
  331. package/src/presentation/ui/media-player/players/video-player.tsx +193 -0
  332. package/src/presentation/ui/media-player/players/voice-note-player.tsx +108 -0
  333. package/src/presentation/ui/media-player/use-media-player.hook.ts +263 -0
  334. package/src/presentation/ui/menubar.tsx +281 -0
  335. package/src/presentation/ui/message.tsx +92 -0
  336. package/src/presentation/ui/navigation-menu.tsx +172 -0
  337. package/src/presentation/ui/pagination.tsx +129 -0
  338. package/src/presentation/ui/popover.tsx +89 -0
  339. package/src/presentation/ui/progress.tsx +31 -0
  340. package/src/presentation/ui/resizable.tsx +50 -0
  341. package/src/presentation/ui/select.tsx +192 -0
  342. package/src/presentation/ui/separator.tsx +28 -0
  343. package/src/presentation/ui/sheet.tsx +147 -0
  344. package/src/presentation/ui/sidebar.tsx +702 -0
  345. package/src/presentation/ui/skeleton.tsx +13 -0
  346. package/src/presentation/ui/slide-to-confirm.tsx +129 -0
  347. package/src/presentation/ui/slider.tsx +59 -0
  348. package/src/presentation/ui/sonner.tsx +58 -0
  349. package/src/presentation/ui/switch.tsx +33 -0
  350. package/src/presentation/ui/table.tsx +116 -0
  351. package/src/presentation/ui/tabs.tsx +90 -0
  352. package/src/presentation/ui/text-gradient.tsx +84 -0
  353. package/src/presentation/ui/textarea.tsx +18 -0
  354. package/src/presentation/ui/timeline-steps.tsx +235 -0
  355. package/src/presentation/ui/toggle-group.tsx +89 -0
  356. package/src/presentation/ui/toggle.tsx +47 -0
  357. package/src/presentation/ui/tooltip.tsx +57 -0
  358. package/src/presentation/uploading/index.tsx +3 -0
  359. package/src/presentation/uploading/media-upload-field.tsx +152 -0
  360. package/src/presentation/uploading/storage-media.tsx +210 -0
  361. package/src/styles.css +403 -0
  362. package/src/themes.css +1038 -0
@@ -0,0 +1,167 @@
1
+ "use client"
2
+
3
+ import * as React from "react";
4
+
5
+ import {cn} from "../../../infrastructure/utilities/utils";
6
+ import {AudioPlayer} from "./players/audio-player";
7
+ import {VideoPlayer} from "./players/video-player";
8
+ import {VoiceNotePlayer} from "./players/voice-note-player";
9
+ import {MediaPlayerProvider, type MediaPlayerVariant} from "./media-player.context";
10
+ import {useMediaPlayer, type UseMediaPlayerOptions} from "./use-media-player.hook";
11
+
12
+ export interface MediaPlayerProps
13
+ extends Omit<UseMediaPlayerOptions, "src">,
14
+ Pick<React.ComponentProps<"div">, "className" | "id"> {
15
+ /** URL du média : fichier ou flux live (.m3u8 géré automatiquement). */
16
+ src: string;
17
+ /** Apparence : « voice-note » (bulle WhatsApp), « audio » (classique) ou « video » (YouTube). */
18
+ variant?: MediaPlayerVariant;
19
+ /** Titre annoncé aux lecteurs d'écran et affiché selon la variante. */
20
+ label?: string;
21
+ /** Pochette (audio) ou image d'aperçu (video). */
22
+ cover?: string;
23
+ /** Classe utilitaire pour personnaliser la couleur d'accent (progression, poignée…). */
24
+ accentClassName?: string;
25
+ /** Déclenché à la fin de la lecture (jamais en direct). */
26
+ onEnded?: () => void;
27
+ }
28
+
29
+ /**
30
+ Lecteur multimédia unifié du design system : une seule source de logique
31
+ (useMediaPlayer) et trois apparences partageant la même direction
32
+ artistique, les mêmes commandes clavier (inspirées de YouTube) et le même
33
+ support des flux live HLS.
34
+ */
35
+ export function MediaPlayer({
36
+ src,
37
+ variant = "audio",
38
+ label,
39
+ cover,
40
+ autoPlay = false,
41
+ rates,
42
+ live,
43
+ accentClassName,
44
+ className,
45
+ id,
46
+ onEnded,
47
+ }: MediaPlayerProps) {
48
+ const controller = useMediaPlayer({src, live, autoPlay, rates});
49
+ const {mediaRef, containerRef} = controller;
50
+
51
+ // Propagation optionnelle de l'événement « ended ».
52
+ React.useEffect(() => {
53
+ const element = mediaRef.current;
54
+ if (!element || !onEnded) return;
55
+ element.addEventListener("ended", onEnded);
56
+ return () => element.removeEventListener("ended", onEnded);
57
+ }, [mediaRef, onEnded]);
58
+
59
+ /**
60
+ Raccourcis inspirés de YouTube : Espace/K lecture, ←/→ ±5 s, J/L ±10 s,
61
+ ↑/↓ volume, M muet, F plein écran, I PiP (vidéo), 0-9 saut à x %,
62
+ Début/Fin. Les éléments interactifs internes gardent leurs touches
63
+ natives (Espace sur un bouton ne double-déclenche pas).
64
+ */
65
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
66
+ const target = event.target as HTMLElement;
67
+ const interactiveTarget = Boolean(target.closest("button, a, input, textarea, select, [role='slider']"));
68
+ const key = event.key;
69
+ const lowerKey = key.toLowerCase();
70
+
71
+ switch (true) {
72
+ case (key === " " || lowerKey === "k") && !interactiveTarget:
73
+ event.preventDefault();
74
+ controller.actions.togglePlay();
75
+ break;
76
+ case key === "ArrowLeft":
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ controller.actions.seekBy(-5);
80
+ break;
81
+ case key === "ArrowRight":
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ controller.actions.seekBy(5);
85
+ break;
86
+ case lowerKey === "j":
87
+ event.preventDefault();
88
+ controller.actions.seekBy(-10);
89
+ break;
90
+ case lowerKey === "l":
91
+ event.preventDefault();
92
+ controller.actions.seekBy(10);
93
+ break;
94
+ case key === "ArrowUp":
95
+ event.preventDefault();
96
+ controller.actions.setVolume(controller.state.volume + 0.05);
97
+ break;
98
+ case key === "ArrowDown":
99
+ event.preventDefault();
100
+ controller.actions.setVolume(controller.state.volume - 0.05);
101
+ break;
102
+ case lowerKey === "m":
103
+ event.preventDefault();
104
+ controller.actions.toggleMute();
105
+ break;
106
+ case lowerKey === "f":
107
+ event.preventDefault();
108
+ controller.actions.toggleFullscreen();
109
+ break;
110
+ case lowerKey === "i" && variant === "video":
111
+ event.preventDefault();
112
+ void controller.actions.togglePictureInPicture();
113
+ break;
114
+ case /^[0-9]$/.test(key) && !controller.state.isLive:
115
+ event.preventDefault();
116
+ controller.actions.jumpToFraction(Number(key) / 10);
117
+ break;
118
+ case key === "Home":
119
+ event.preventDefault();
120
+ controller.actions.seekTo(0);
121
+ break;
122
+ case key === "End" && !controller.state.isLive:
123
+ event.preventDefault();
124
+ controller.actions.seekTo(controller.state.duration);
125
+ break;
126
+ default:
127
+ break;
128
+ }
129
+ };
130
+
131
+ return (
132
+ <MediaPlayerProvider value={{controller, variant, accentClassName, label}}>
133
+ <div
134
+ ref={containerRef}
135
+ id={id}
136
+ role="group"
137
+ aria-label={label ?? "Lecteur multimédia"}
138
+ tabIndex={-1}
139
+ data-slot="media-player"
140
+ data-variant={variant}
141
+ className={cn(
142
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring",
143
+ variant === "video" ? "group/player relative overflow-hidden bg-black" : "w-full",
144
+ className,
145
+ )}
146
+ onKeyDown={handleKeyDown}
147
+ >
148
+ {variant === "voice-note" && <VoiceNotePlayer/>}
149
+ {variant === "audio" && <AudioPlayer cover={cover}/>}
150
+ {variant === "video" && (
151
+ <VideoPlayer>
152
+ <video
153
+ ref={mediaRef as React.RefObject<HTMLVideoElement>}
154
+ poster={cover}
155
+ preload="metadata"
156
+ playsInline
157
+ className="size-full"
158
+ />
159
+ </VideoPlayer>
160
+ )}
161
+ {variant !== "video" && (
162
+ <audio ref={mediaRef} preload="metadata" className="hidden"/>
163
+ )}
164
+ </div>
165
+ </MediaPlayerProvider>
166
+ );
167
+ }
@@ -0,0 +1,207 @@
1
+ "use client"
2
+
3
+ import * as React from "react";
4
+ import {Volume1Icon, Volume2Icon, VolumeXIcon} from "lucide-react";
5
+
6
+ import {cn} from "../../../infrastructure/utilities/utils";
7
+ import {Slider} from "../slider";
8
+ import {usePlayer} from "./media-player.context";
9
+
10
+ /** Formate une durée en secondes : « 3:05 » ou « 1:02:11 ». */
11
+ export function formatMediaTime(totalSeconds: number): string {
12
+ if (!Number.isFinite(totalSeconds) || totalSeconds < 0) return "0:00";
13
+ const seconds = Math.floor(totalSeconds % 60);
14
+ const minutes = Math.floor((totalSeconds / 60) % 60);
15
+ const hours = Math.floor(totalSeconds / 3600);
16
+ const twoDigits = String(seconds).padStart(2, "0");
17
+ return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${twoDigits}` : `${minutes}:${twoDigits}`;
18
+ }
19
+
20
+ interface PlayerScrubberProps {
21
+ className?: string;
22
+ /** Épaisseur de la piste au repos ; grossit au survol/focus. */
23
+ thick?: boolean;
24
+ }
25
+
26
+ /**
27
+ Barre de progression accessible (role="slider", ARIA complet) avec affichage
28
+ du tampon et poignée visible au survol — inspirée de la barre YouTube.
29
+ Le clavier (←/→/Home/End/0-9) est géré par le conteneur racine du lecteur.
30
+ */
31
+ export function PlayerScrubber({className, thick = false}: PlayerScrubberProps) {
32
+ const {controller} = usePlayer();
33
+ const {currentTime, bufferedEnd, seekMin, seekMax, isLive, duration} = controller.state;
34
+ const {seekTo} = controller.actions;
35
+ const trackRef = React.useRef<HTMLDivElement | null>(null);
36
+ const [dragging, setDragging] = React.useState(false);
37
+
38
+ const span = Math.max(seekMax - seekMin, 0.001);
39
+ const fraction = isLive && !seekWindowUsable(seekMin, seekMax)
40
+ ? 1
41
+ : Math.min(Math.max((currentTime - seekMin) / span, 0), 1);
42
+ const bufferedFraction = Number.isFinite(bufferedEnd) ? Math.min(Math.max((bufferedEnd - seekMin) / span, 0), 1) : 0;
43
+
44
+ const seekFromPointer = (clientX: number) => {
45
+ const track = trackRef.current;
46
+ if (!track) return;
47
+ const rect = track.getBoundingClientRect();
48
+ const ratio = rect.width > 0 ? Math.min(Math.max((clientX - rect.left) / rect.width, 0), 1) : 0;
49
+ seekTo(seekMin + ratio * span);
50
+ };
51
+
52
+ // Direct sans fenêtre DVR : la barre est purement indicative.
53
+ const disabled = isLive && !seekWindowUsable(seekMin, seekMax);
54
+
55
+ return (
56
+ <div
57
+ ref={trackRef}
58
+ role="slider"
59
+ tabIndex={disabled ? -1 : 0}
60
+ aria-label={isLive ? "Position dans le direct" : "Barre de progression"}
61
+ aria-valuemin={seekMin}
62
+ aria-valuemax={Math.round(seekMax)}
63
+ aria-valuenow={Math.round(currentTime)}
64
+ aria-valuetext={`${formatMediaTime(currentTime)} sur ${formatMediaTime(isLive ? seekMax : duration || seekMax)}`}
65
+ aria-disabled={disabled}
66
+ data-slot="player-scrubber"
67
+ className={cn(
68
+ "group/scrubber relative flex touch-none items-center py-2 select-none",
69
+ disabled ? "cursor-default" : "cursor-pointer",
70
+ className,
71
+ )}
72
+ onPointerDown={(event) => {
73
+ if (disabled) return;
74
+ event.currentTarget.setPointerCapture(event.pointerId);
75
+ setDragging(true);
76
+ seekFromPointer(event.clientX);
77
+ }}
78
+ onPointerMove={(event) => {
79
+ if (dragging) seekFromPointer(event.clientX);
80
+ }}
81
+ onPointerUp={() => setDragging(false)}
82
+ onPointerCancel={() => setDragging(false)}
83
+ >
84
+ <div className={cn(
85
+ "relative w-full overflow-hidden rounded-full bg-muted transition-[height]",
86
+ thick ? "h-1.5" : "h-1 group-hover/scrubber:h-1.5 group-focus-within/scrubber:h-1.5",
87
+ )}>
88
+ <div
89
+ data-slot="player-buffered"
90
+ className="absolute inset-y-0 left-0 rounded-full bg-muted-foreground/30"
91
+ style={{width: `${bufferedFraction * 100}%`}}
92
+ />
93
+ <div
94
+ data-slot="player-played"
95
+ className={cn("absolute inset-y-0 left-0 rounded-full bg-primary", dragging && "bg-primary")}
96
+ style={{width: `${fraction * 100}%`}}
97
+ />
98
+ </div>
99
+ {!disabled && (
100
+ <span
101
+ aria-hidden
102
+ data-slot="player-thumb"
103
+ className={cn(
104
+ "absolute top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 scale-0 rounded-full bg-primary shadow transition-transform",
105
+ "group-hover/scrubber:scale-100 group-focus-within/scrubber:scale-100",
106
+ dragging && "scale-125",
107
+ )}
108
+ style={{left: `${fraction * 100}%`}}
109
+ />
110
+ )}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ function seekWindowUsable(start: number, end: number): boolean {
116
+ return Number.isFinite(start) && Number.isFinite(end) && end - start > 1;
117
+ }
118
+
119
+ /**
120
+ Volume façon YouTube : bouton muet + curseur horizontal qui se déplie au
121
+ survol. Accessible via le Slider radix (flèches natives) et ↑/↓ globaux.
122
+ */
123
+ export function PlayerVolumeControl({className}: {className?: string}) {
124
+ const {controller} = usePlayer();
125
+ const {volume, muted} = controller.state;
126
+ const {setVolume, toggleMute} = controller.actions;
127
+
128
+ const effectiveVolume = muted ? 0 : volume;
129
+ const VolumeIcon = effectiveVolume === 0 ? VolumeXIcon : effectiveVolume < 0.5 ? Volume1Icon : Volume2Icon;
130
+
131
+ return (
132
+ <div data-slot="player-volume" className={cn("group/volume flex items-center gap-1", className)}>
133
+ <ButtonGhost
134
+ label={muted ? "Réactiver le son (M)" : "Couper le son (M)"}
135
+ onClick={toggleMute}
136
+ >
137
+ <VolumeIcon/>
138
+ </ButtonGhost>
139
+ <div className="w-0 overflow-hidden opacity-0 transition-all duration-200 group-hover/volume:w-20 group-hover/volume:opacity-100 group-focus-within/volume:w-20 group-focus-within/volume:opacity-100">
140
+ <Slider
141
+ aria-label="Volume"
142
+ value={[effectiveVolume * 100]}
143
+ min={0}
144
+ max={100}
145
+ step={5}
146
+ onValueChange={(values) => setVolume((values[0] ?? 0) / 100)}
147
+ className="h-1 w-20"
148
+ />
149
+ </div>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ /** Bouton rond générique des barres de contrôle (cohérence entre variantes). */
155
+ export function ButtonGhost({
156
+ label,
157
+ className,
158
+ children,
159
+ ...props
160
+ }: React.ComponentProps<"button"> & {label: string}) {
161
+ return (
162
+ <button
163
+ type="button"
164
+ title={label}
165
+ aria-label={label}
166
+ data-slot="player-button"
167
+ className={cn(
168
+ "inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-current opacity-90 outline-hidden transition hover:bg-accent hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring",
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ {children}
174
+ </button>
175
+ );
176
+ }
177
+
178
+ /** Badge « EN DIRECT » avec point rouge pulsant (style plateformes live). */
179
+ export function LiveBadge({className}: {className?: string}) {
180
+ return (
181
+ <span
182
+ data-slot="player-live-badge"
183
+ className={cn("inline-flex shrink-0 items-center gap-1.5 text-xs font-semibold tracking-wide text-red-500 uppercase", className)}
184
+ >
185
+ <span className="relative flex size-2">
186
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"/>
187
+ <span className="relative inline-flex size-2 rounded-full bg-red-500"/>
188
+ </span>
189
+ Direct
190
+ </span>
191
+ );
192
+ }
193
+
194
+ /** Affichage du temps « 1:23 / 5:00 », remplacé par le badge en direct. */
195
+ export function TimeDisplay({className}: {className?: string}) {
196
+ const {controller} = usePlayer();
197
+ const {currentTime, duration, isLive} = controller.state;
198
+
199
+ if (isLive) return <LiveBadge className={className}/>;
200
+
201
+ return (
202
+ <span data-slot="player-time" className={cn("shrink-0 text-xs tabular-nums select-none", className)}>
203
+ {formatMediaTime(currentTime)}
204
+ <span className="opacity-60"> / {formatMediaTime(duration)}</span>
205
+ </span>
206
+ );
207
+ }
@@ -0,0 +1,78 @@
1
+ "use client"
2
+
3
+ import {PauseIcon, PlayIcon} from "lucide-react";
4
+
5
+ import {cn} from "../../../../infrastructure/utilities/utils";
6
+ import {usePlayer} from "../media-player.context";
7
+ import {ButtonGhost, PlayerScrubber, PlayerVolumeControl, TimeDisplay} from "../player-controls";
8
+
9
+ interface AudioPlayerProps {
10
+ /** Pochette optionnelle affichée à gauche (sinon icône par défaut du consommateur). */
11
+ cover?: string;
12
+ }
13
+
14
+ /**
15
+ Apparence « lecteur audio traditionnel » : pochette, transport, barre de
16
+ progression avec tampon, temps, volume dépliable et vitesse — la même grammaire
17
+ visuelle que les variantes voice-note et video.
18
+ */
19
+ export function AudioPlayer({cover}: AudioPlayerProps) {
20
+ const {controller, accentClassName, label} = usePlayer();
21
+ const {isPlaying, isLoading, duration} = controller.state;
22
+ const {togglePlay} = controller.actions;
23
+
24
+ return (
25
+ <div data-slot="audio-player" className="flex items-center gap-3 rounded-xl border bg-card p-3">
26
+ {/* Pochette */}
27
+ <div className="relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-muted-foreground">
28
+ {cover ? (
29
+ <img src={cover} alt="" className="size-full object-cover"/>
30
+ ) : (
31
+ <span className={cn(
32
+ "flex items-center justify-center transition-colors",
33
+ isPlaying && cn("text-primary", accentClassName),
34
+ )}>
35
+ <AudioGlyph/>
36
+ </span>
37
+ )}
38
+ {isLoading && isPlaying && (
39
+ <span aria-hidden className="absolute inset-0 flex items-center justify-center bg-background/60">
40
+ <span className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"/>
41
+ </span>
42
+ )}
43
+ </div>
44
+
45
+ {/* Transport + progression */}
46
+ <div className="min-w-0 flex-1">
47
+ {label && (
48
+ <p className="mb-0.5 truncate text-xs font-medium" title={label}>{label}</p>
49
+ )}
50
+ <div className="flex items-center gap-2">
51
+ <ButtonGhost
52
+ label={isPlaying ? "Pause (Espace)" : "Lire (Espace)"}
53
+ onClick={togglePlay}
54
+ className={cn("size-9 shrink-0 bg-primary/10 hover:bg-primary/20", accentClassName)}
55
+ >
56
+ {isPlaying ? <PauseIcon className="size-4 fill-current"/> : <PlayIcon className="size-4 fill-current"/>}
57
+ </ButtonGhost>
58
+ <div className="min-w-0 flex-1">
59
+ <PlayerScrubber thick/>
60
+ </div>
61
+ <TimeDisplay className="shrink-0 text-xs tabular-nums text-muted-foreground"/>
62
+ </div>
63
+ <div className="mt-0.5 hidden items-center justify-end gap-2 sm:flex">
64
+ <PlayerVolumeControl/>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ /** Petit glyphe « onde sonore » pour la pochette par défaut. */
72
+ function AudioGlyph() {
73
+ return (
74
+ <svg viewBox="0 0 24 24" className="size-5 fill-none stroke-current stroke-2" aria-hidden>
75
+ <path d="M4 10v4M8 7v10M12 4v16M16 7v10M20 10v4" strokeLinecap="round"/>
76
+ </svg>
77
+ );
78
+ }
@@ -0,0 +1,4 @@
1
+ // @liorian/sdk/presentation/ui/media-player/players — concrete players used by the media player.
2
+ export * from "./audio-player";
3
+ export * from "./video-player";
4
+ export * from "./voice-note-player";
@@ -0,0 +1,193 @@
1
+ "use client"
2
+
3
+ import * as React from "react";
4
+ import {MaximizeIcon, MinimizeIcon, PauseIcon, PictureInPicture2Icon, PlayIcon, SettingsIcon} from "lucide-react";
5
+
6
+ import {cn} from "../../../../infrastructure/utilities/utils";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuLabel,
12
+ DropdownMenuTrigger,
13
+ } from "../../dropdown-menu";
14
+ import {usePlayer} from "../media-player.context";
15
+ import {ButtonGhost, PlayerScrubber, PlayerVolumeControl, TimeDisplay} from "../player-controls";
16
+
17
+ interface VideoPlayerProps {
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ /** Délai avant masquage automatique des contrôles en lecture (YouTube ≈ 3 s). */
22
+ const CONTROLS_HIDE_DELAY_MS = 2700;
23
+
24
+ /**
25
+ Apparence « lecteur vidéo » inspirée de YouTube : vidéo plein cadre, grand
26
+ bouton central au repos, barre de progression fine au-dessus des contrôles,
27
+ contrôles sur dégradé qui se masquent automatiquement en lecture,
28
+ vitesse dans un menu, Picture-in-Picture et plein écran (double-clic).
29
+ */
30
+ export function VideoPlayer({children}: VideoPlayerProps) {
31
+ const {controller} = usePlayer();
32
+ const {isPlaying, isLoading, isFullscreen, isLive, error, rate} = controller.state;
33
+ const {togglePlay, toggleFullscreen, togglePictureInPicture, setRate} = controller.actions;
34
+
35
+ const [controlsVisible, setControlsVisible] = React.useState(true);
36
+ const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
37
+ const clickTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
38
+
39
+ /** Reprogramme le masquage : contrôles toujours visibles en pause. */
40
+ const scheduleHide = React.useCallback(() => {
41
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
42
+ setControlsVisible(true);
43
+ hideTimerRef.current = setTimeout(() => {
44
+ if (controller.state.isPlaying) setControlsVisible(false);
45
+ }, CONTROLS_HIDE_DELAY_MS);
46
+ }, [controller.state.isPlaying]);
47
+
48
+ React.useEffect(() => {
49
+ if (!isPlaying) {
50
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
51
+ setControlsVisible(true);
52
+ return;
53
+ }
54
+ scheduleHide();
55
+ return () => {
56
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
57
+ };
58
+ }, [isPlaying, scheduleHide]);
59
+
60
+ // Clic simple = lecture/pause ; double-clic = plein écran (désambiguïsé).
61
+ const handleSurfaceClick = () => {
62
+ if (clickTimerRef.current) {
63
+ clearTimeout(clickTimerRef.current);
64
+ clickTimerRef.current = null;
65
+ toggleFullscreen();
66
+ return;
67
+ }
68
+ clickTimerRef.current = setTimeout(() => {
69
+ clickTimerRef.current = null;
70
+ togglePlay();
71
+ }, 220);
72
+ };
73
+
74
+ return (
75
+ <div
76
+ data-slot="video-surface"
77
+ className={cn(
78
+ "group/video relative size-full select-none",
79
+ !controlsVisible && isPlaying && "cursor-none",
80
+ )}
81
+ onPointerMove={scheduleHide}
82
+ onPointerLeave={() => {
83
+ if (isPlaying) setControlsVisible(false);
84
+ }}
85
+ >
86
+ {children}
87
+
88
+ {/* Clic / double-clic sur la surface */}
89
+ <div aria-hidden className="absolute inset-0" onClick={handleSurfaceClick}/>
90
+
91
+ {/* Grand bouton central au repos */}
92
+ {!isPlaying && (
93
+ <button
94
+ type="button"
95
+ title="Lire (Espace)"
96
+ aria-label="Lire"
97
+ onClick={() => {
98
+ if (clickTimerRef.current) {
99
+ clearTimeout(clickTimerRef.current);
100
+ clickTimerRef.current = null;
101
+ }
102
+ togglePlay();
103
+ }}
104
+ className="absolute inset-0 z-10 flex cursor-pointer items-center justify-center outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
105
+ >
106
+ <span className="flex size-16 items-center justify-center rounded-full bg-black/60 text-white backdrop-blur transition hover:scale-105 hover:bg-black/70">
107
+ <PlayIcon className="size-8 fill-current"/>
108
+ </span>
109
+ </button>
110
+ )}
111
+
112
+ {/* Tampon de chargement */}
113
+ {isLoading && isPlaying && (
114
+ <div aria-hidden className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
115
+ <span className="size-10 animate-spin rounded-full border-[3px] border-white/30 border-t-white"/>
116
+ </div>
117
+ )}
118
+
119
+ {error && (
120
+ <div className="absolute inset-x-0 top-3 z-20 flex justify-center">
121
+ <span className="rounded-md bg-red-600/90 px-3 py-1 text-xs font-medium text-white">{error}</span>
122
+ </div>
123
+ )}
124
+
125
+ {/* Dégradé + barre de progression + contrôles */}
126
+ <div
127
+ data-slot="video-controls"
128
+ className={cn(
129
+ "absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-3 pt-8 pb-1.5 text-white transition-all duration-200",
130
+ controlsVisible ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-2 opacity-0",
131
+ )}
132
+ >
133
+ <PlayerScrubber className="text-white [&_[data-slot=player-buffered]]:bg-white/30"/>
134
+
135
+ <div className="flex items-center gap-1">
136
+ <ButtonGhost
137
+ label={isPlaying ? "Pause (Espace)" : "Lire (Espace)"}
138
+ onClick={togglePlay}
139
+ className="text-white hover:bg-white/15"
140
+ >
141
+ {isPlaying ? <PauseIcon className="size-5 fill-current"/> : <PlayIcon className="size-5 fill-current"/>}
142
+ </ButtonGhost>
143
+
144
+ <PlayerVolumeControl className="text-white [&_button]:hover:bg-white/15"/>
145
+
146
+ <TimeDisplay className="text-white/90"/>
147
+
148
+ <span className="flex-1"/>
149
+
150
+ {/* Vitesse de lecture */}
151
+ <DropdownMenu>
152
+ <DropdownMenuTrigger asChild>
153
+ <ButtonGhost label="Paramètres de lecture" className="text-white hover:bg-white/15">
154
+ <SettingsIcon className="size-4"/>
155
+ </ButtonGhost>
156
+ </DropdownMenuTrigger>
157
+ <DropdownMenuContent align="end" side="top" sideOffset={8}>
158
+ <DropdownMenuLabel>Vitesse de lecture</DropdownMenuLabel>
159
+ {[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2].map((value) => (
160
+ <DropdownMenuItem
161
+ key={value}
162
+ onSelect={() => setRate(value)}
163
+ className={cn("justify-between", value === rate && "font-semibold")}
164
+ >
165
+ {String(value).replace(".", ",")}×
166
+ {value === rate && <span aria-hidden>✓</span>}
167
+ </DropdownMenuItem>
168
+ ))}
169
+ </DropdownMenuContent>
170
+ </DropdownMenu>
171
+
172
+ {!isLive && (
173
+ <ButtonGhost
174
+ label="Picture-in-Picture (I)"
175
+ onClick={() => void togglePictureInPicture()}
176
+ className="hidden text-white hover:bg-white/15 sm:inline-flex"
177
+ >
178
+ <PictureInPicture2Icon className="size-4"/>
179
+ </ButtonGhost>
180
+ )}
181
+
182
+ <ButtonGhost
183
+ label={isFullscreen ? "Quitter le plein écran (F)" : "Plein écran (F)"}
184
+ onClick={toggleFullscreen}
185
+ className="text-white hover:bg-white/15"
186
+ >
187
+ {isFullscreen ? <MinimizeIcon className="size-4"/> : <MaximizeIcon className="size-4"/>}
188
+ </ButtonGhost>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ );
193
+ }