@fusion.dev/helix-headless 0.0.0-dev

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 (424) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +75202 -0
  3. package/dist/accordion/index.d.ts +2 -0
  4. package/dist/accordion/index.js +2 -0
  5. package/dist/accordion/item/accordion-item.d.ts +141 -0
  6. package/dist/accordion/item/accordion-item.js +214 -0
  7. package/dist/accordion/root/accordion-root.d.ts +57 -0
  8. package/dist/accordion/root/accordion-root.js +96 -0
  9. package/dist/announcement/index.d.ts +1 -0
  10. package/dist/announcement/index.js +1 -0
  11. package/dist/announcement/root/announcement-root.d.ts +106 -0
  12. package/dist/announcement/root/announcement-root.js +144 -0
  13. package/dist/app-header/index.d.ts +1 -0
  14. package/dist/app-header/index.js +1 -0
  15. package/dist/app-header/root/app-header-root.d.ts +113 -0
  16. package/dist/app-header/root/app-header-root.js +147 -0
  17. package/dist/avatar/group/avatar-group.d.ts +116 -0
  18. package/dist/avatar/group/avatar-group.js +136 -0
  19. package/dist/avatar/index.d.ts +2 -0
  20. package/dist/avatar/index.js +2 -0
  21. package/dist/avatar/root/avatar-root.d.ts +71 -0
  22. package/dist/avatar/root/avatar-root.js +147 -0
  23. package/dist/badge/index.d.ts +1 -0
  24. package/dist/badge/index.js +1 -0
  25. package/dist/badge/root/badge-root.d.ts +61 -0
  26. package/dist/badge/root/badge-root.js +121 -0
  27. package/dist/bar-chart/index.d.ts +1 -0
  28. package/dist/bar-chart/index.js +1 -0
  29. package/dist/bar-chart/root/bar-chart-root.d.ts +266 -0
  30. package/dist/bar-chart/root/bar-chart-root.js +515 -0
  31. package/dist/blockquote/index.d.ts +1 -0
  32. package/dist/blockquote/index.js +1 -0
  33. package/dist/blockquote/root/blockquote-root.d.ts +51 -0
  34. package/dist/blockquote/root/blockquote-root.js +84 -0
  35. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +104 -0
  36. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +137 -0
  37. package/dist/breadcrumbs/index.d.ts +2 -0
  38. package/dist/breadcrumbs/index.js +2 -0
  39. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +77 -0
  40. package/dist/breadcrumbs/root/breadcrumbs-root.js +94 -0
  41. package/dist/button/index.d.ts +1 -0
  42. package/dist/button/index.js +1 -0
  43. package/dist/button/root/button-root.d.ts +251 -0
  44. package/dist/button/root/button-root.js +401 -0
  45. package/dist/button-group/index.d.ts +1 -0
  46. package/dist/button-group/index.js +1 -0
  47. package/dist/button-group/root/button-group-root.d.ts +62 -0
  48. package/dist/button-group/root/button-group-root.js +62 -0
  49. package/dist/callout/index.d.ts +1 -0
  50. package/dist/callout/index.js +1 -0
  51. package/dist/callout/root/callout-root.d.ts +72 -0
  52. package/dist/callout/root/callout-root.js +90 -0
  53. package/dist/card/index.d.ts +1 -0
  54. package/dist/card/index.js +1 -0
  55. package/dist/card/root/card-root.d.ts +54 -0
  56. package/dist/card/root/card-root.js +71 -0
  57. package/dist/carousel/index.d.ts +1 -0
  58. package/dist/carousel/index.js +1 -0
  59. package/dist/carousel/root/carousel-root.d.ts +399 -0
  60. package/dist/carousel/root/carousel-root.js +723 -0
  61. package/dist/chat/composer/chat-composer.d.ts +160 -0
  62. package/dist/chat/composer/chat-composer.js +273 -0
  63. package/dist/chat/index.d.ts +3 -0
  64. package/dist/chat/index.js +3 -0
  65. package/dist/chat/message/chat-message.d.ts +170 -0
  66. package/dist/chat/message/chat-message.js +271 -0
  67. package/dist/chat/root/chat-root.d.ts +216 -0
  68. package/dist/chat/root/chat-root.js +389 -0
  69. package/dist/checkbox/index.d.ts +1 -0
  70. package/dist/checkbox/index.js +1 -0
  71. package/dist/checkbox/root/checkbox-root.d.ts +108 -0
  72. package/dist/checkbox/root/checkbox-root.js +207 -0
  73. package/dist/checkbox-group/index.d.ts +1 -0
  74. package/dist/checkbox-group/index.js +1 -0
  75. package/dist/checkbox-group/root/checkbox-group-root.d.ts +155 -0
  76. package/dist/checkbox-group/root/checkbox-group-root.js +293 -0
  77. package/dist/code/index.d.ts +1 -0
  78. package/dist/code/index.js +1 -0
  79. package/dist/code/root/code-root.d.ts +36 -0
  80. package/dist/code/root/code-root.js +43 -0
  81. package/dist/code-block/index.d.ts +1 -0
  82. package/dist/code-block/index.js +1 -0
  83. package/dist/code-block/root/code-block-root.d.ts +112 -0
  84. package/dist/code-block/root/code-block-root.js +155 -0
  85. package/dist/color-input/index.d.ts +1 -0
  86. package/dist/color-input/index.js +1 -0
  87. package/dist/color-input/root/color-input-root.d.ts +227 -0
  88. package/dist/color-input/root/color-input-root.js +456 -0
  89. package/dist/color-picker/color.d.ts +74 -0
  90. package/dist/color-picker/color.js +238 -0
  91. package/dist/color-picker/index.d.ts +2 -0
  92. package/dist/color-picker/index.js +2 -0
  93. package/dist/color-picker/root/color-picker-root.d.ts +303 -0
  94. package/dist/color-picker/root/color-picker-root.js +576 -0
  95. package/dist/combobox/index.d.ts +1 -0
  96. package/dist/combobox/index.js +1 -0
  97. package/dist/combobox/root/combobox-root.d.ts +396 -0
  98. package/dist/combobox/root/combobox-root.js +808 -0
  99. package/dist/command/index.d.ts +1 -0
  100. package/dist/command/index.js +1 -0
  101. package/dist/command/root/command-root.d.ts +166 -0
  102. package/dist/command/root/command-root.js +353 -0
  103. package/dist/copy-button/index.d.ts +1 -0
  104. package/dist/copy-button/index.js +1 -0
  105. package/dist/copy-button/root/copy-button-root.d.ts +154 -0
  106. package/dist/copy-button/root/copy-button-root.js +225 -0
  107. package/dist/data-table/index.d.ts +2 -0
  108. package/dist/data-table/index.js +2 -0
  109. package/dist/data-table/root/data-table-root.d.ts +392 -0
  110. package/dist/data-table/root/data-table-root.js +729 -0
  111. package/dist/data-table/root/order.d.ts +116 -0
  112. package/dist/data-table/root/order.js +175 -0
  113. package/dist/date-input/index.d.ts +1 -0
  114. package/dist/date-input/index.js +1 -0
  115. package/dist/date-input/root/date-input-root.d.ts +361 -0
  116. package/dist/date-input/root/date-input-root.js +791 -0
  117. package/dist/date-picker/index.d.ts +1 -0
  118. package/dist/date-picker/index.js +1 -0
  119. package/dist/date-picker/root/date-picker-root.d.ts +361 -0
  120. package/dist/date-picker/root/date-picker-root.js +837 -0
  121. package/dist/dialog/index.d.ts +1 -0
  122. package/dist/dialog/index.js +1 -0
  123. package/dist/dialog/root/dialog-root.d.ts +185 -0
  124. package/dist/dialog/root/dialog-root.js +319 -0
  125. package/dist/divider/index.d.ts +1 -0
  126. package/dist/divider/index.js +1 -0
  127. package/dist/divider/root/divider-root.d.ts +68 -0
  128. package/dist/divider/root/divider-root.js +116 -0
  129. package/dist/donut-chart/index.d.ts +1 -0
  130. package/dist/donut-chart/index.js +1 -0
  131. package/dist/donut-chart/root/donut-chart-root.d.ts +209 -0
  132. package/dist/donut-chart/root/donut-chart-root.js +395 -0
  133. package/dist/drawer/index.d.ts +1 -0
  134. package/dist/drawer/index.js +1 -0
  135. package/dist/drawer/root/drawer-root.d.ts +144 -0
  136. package/dist/drawer/root/drawer-root.js +222 -0
  137. package/dist/drilldown/index.d.ts +2 -0
  138. package/dist/drilldown/index.js +2 -0
  139. package/dist/drilldown/item/drilldown-item.d.ts +87 -0
  140. package/dist/drilldown/item/drilldown-item.js +152 -0
  141. package/dist/drilldown/root/drilldown-root.d.ts +115 -0
  142. package/dist/drilldown/root/drilldown-root.js +263 -0
  143. package/dist/element-events.generated.d.ts +1 -0
  144. package/dist/element-events.generated.js +125 -0
  145. package/dist/empty/index.d.ts +1 -0
  146. package/dist/empty/index.js +1 -0
  147. package/dist/empty/root/empty-root.d.ts +47 -0
  148. package/dist/empty/root/empty-root.js +68 -0
  149. package/dist/file-input/index.d.ts +1 -0
  150. package/dist/file-input/index.js +1 -0
  151. package/dist/file-input/root/file-input-root.d.ts +196 -0
  152. package/dist/file-input/root/file-input-root.js +488 -0
  153. package/dist/format-date/index.d.ts +1 -0
  154. package/dist/format-date/index.js +1 -0
  155. package/dist/format-date/root/format-date-root.d.ts +151 -0
  156. package/dist/format-date/root/format-date-root.js +266 -0
  157. package/dist/heatmap/index.d.ts +2 -0
  158. package/dist/heatmap/index.js +2 -0
  159. package/dist/heatmap/root/buckets.d.ts +62 -0
  160. package/dist/heatmap/root/buckets.js +85 -0
  161. package/dist/heatmap/root/heatmap-root.d.ts +330 -0
  162. package/dist/heatmap/root/heatmap-root.js +526 -0
  163. package/dist/icon/index.d.ts +1 -0
  164. package/dist/icon/index.js +1 -0
  165. package/dist/icon/root/icon-root.d.ts +53 -0
  166. package/dist/icon/root/icon-root.js +93 -0
  167. package/dist/icon-button/index.d.ts +1 -0
  168. package/dist/icon-button/index.js +1 -0
  169. package/dist/icon-button/root/icon-button-root.d.ts +40 -0
  170. package/dist/icon-button/root/icon-button-root.js +46 -0
  171. package/dist/image/index.d.ts +1 -0
  172. package/dist/image/index.js +1 -0
  173. package/dist/image/root/image-root.d.ts +213 -0
  174. package/dist/image/root/image-root.js +335 -0
  175. package/dist/index.d.ts +99 -0
  176. package/dist/index.js +99 -0
  177. package/dist/input/index.d.ts +1 -0
  178. package/dist/input/index.js +1 -0
  179. package/dist/input/root/input-root.d.ts +281 -0
  180. package/dist/input/root/input-root.js +472 -0
  181. package/dist/kbd/index.d.ts +1 -0
  182. package/dist/kbd/index.js +1 -0
  183. package/dist/kbd/root/kbd-root.d.ts +98 -0
  184. package/dist/kbd/root/kbd-root.js +230 -0
  185. package/dist/legend/index.d.ts +1 -0
  186. package/dist/legend/index.js +1 -0
  187. package/dist/legend/root/legend-root.d.ts +83 -0
  188. package/dist/legend/root/legend-root.js +125 -0
  189. package/dist/line-chart/index.d.ts +1 -0
  190. package/dist/line-chart/index.js +1 -0
  191. package/dist/line-chart/root/line-chart-root.d.ts +238 -0
  192. package/dist/line-chart/root/line-chart-root.js +365 -0
  193. package/dist/list/index.d.ts +2 -0
  194. package/dist/list/index.js +2 -0
  195. package/dist/list/item/list-item.d.ts +95 -0
  196. package/dist/list/item/list-item.js +148 -0
  197. package/dist/list/root/list-root.d.ts +72 -0
  198. package/dist/list/root/list-root.js +61 -0
  199. package/dist/listbox/index.d.ts +1 -0
  200. package/dist/listbox/index.js +1 -0
  201. package/dist/listbox/root/listbox-root.d.ts +357 -0
  202. package/dist/listbox/root/listbox-root.js +890 -0
  203. package/dist/loader/index.d.ts +1 -0
  204. package/dist/loader/index.js +1 -0
  205. package/dist/loader/root/loader-root.d.ts +52 -0
  206. package/dist/loader/root/loader-root.js +74 -0
  207. package/dist/megamenu/index.d.ts +1 -0
  208. package/dist/megamenu/index.js +1 -0
  209. package/dist/megamenu/root/megamenu-root.d.ts +254 -0
  210. package/dist/megamenu/root/megamenu-root.js +526 -0
  211. package/dist/menu/index.d.ts +2 -0
  212. package/dist/menu/index.js +2 -0
  213. package/dist/menu/item/menu-item.d.ts +231 -0
  214. package/dist/menu/item/menu-item.js +405 -0
  215. package/dist/menu/root/menu-root.d.ts +158 -0
  216. package/dist/menu/root/menu-root.js +422 -0
  217. package/dist/meter/index.d.ts +1 -0
  218. package/dist/meter/index.js +1 -0
  219. package/dist/meter/root/meter-root.d.ts +234 -0
  220. package/dist/meter/root/meter-root.js +316 -0
  221. package/dist/navigation/index.d.ts +1 -0
  222. package/dist/navigation/index.js +1 -0
  223. package/dist/navigation/root/navigation-root.d.ts +55 -0
  224. package/dist/navigation/root/navigation-root.js +130 -0
  225. package/dist/notifications/index.d.ts +2 -0
  226. package/dist/notifications/index.js +2 -0
  227. package/dist/notifications/notification/notification.d.ts +181 -0
  228. package/dist/notifications/notification/notification.js +305 -0
  229. package/dist/notifications/root/notifications-root.d.ts +168 -0
  230. package/dist/notifications/root/notifications-root.js +291 -0
  231. package/dist/number-input/index.d.ts +1 -0
  232. package/dist/number-input/index.js +1 -0
  233. package/dist/number-input/root/number-input-root.d.ts +282 -0
  234. package/dist/number-input/root/number-input-root.js +592 -0
  235. package/dist/option/group/option-group.d.ts +38 -0
  236. package/dist/option/group/option-group.js +58 -0
  237. package/dist/option/index.d.ts +2 -0
  238. package/dist/option/index.js +2 -0
  239. package/dist/option/root/option-root.d.ts +194 -0
  240. package/dist/option/root/option-root.js +247 -0
  241. package/dist/outline/index.d.ts +1 -0
  242. package/dist/outline/index.js +1 -0
  243. package/dist/outline/root/outline-root.d.ts +237 -0
  244. package/dist/outline/root/outline-root.js +519 -0
  245. package/dist/pagination/index.d.ts +3 -0
  246. package/dist/pagination/index.js +3 -0
  247. package/dist/pagination/root/pagination-root.d.ts +390 -0
  248. package/dist/pagination/root/pagination-root.js +695 -0
  249. package/dist/pagination/size/pagination-size.d.ts +127 -0
  250. package/dist/pagination/size/pagination-size.js +232 -0
  251. package/dist/pagination/summary/pagination-summary.d.ts +90 -0
  252. package/dist/pagination/summary/pagination-summary.js +145 -0
  253. package/dist/pin-input/index.d.ts +1 -0
  254. package/dist/pin-input/index.js +1 -0
  255. package/dist/pin-input/root/pin-input-root.d.ts +292 -0
  256. package/dist/pin-input/root/pin-input-root.js +553 -0
  257. package/dist/popover/index.d.ts +1 -0
  258. package/dist/popover/index.js +1 -0
  259. package/dist/popover/root/popover-root.d.ts +108 -0
  260. package/dist/popover/root/popover-root.js +231 -0
  261. package/dist/progress/index.d.ts +1 -0
  262. package/dist/progress/index.js +1 -0
  263. package/dist/progress/root/progress-root.d.ts +125 -0
  264. package/dist/progress/root/progress-root.js +212 -0
  265. package/dist/qr-code/index.d.ts +1 -0
  266. package/dist/qr-code/index.js +1 -0
  267. package/dist/qr-code/root/qr-code-root.d.ts +93 -0
  268. package/dist/qr-code/root/qr-code-root.js +233 -0
  269. package/dist/quote/index.d.ts +1 -0
  270. package/dist/quote/index.js +1 -0
  271. package/dist/quote/root/quote-root.d.ts +36 -0
  272. package/dist/quote/root/quote-root.js +50 -0
  273. package/dist/radio/group/radio-group.d.ts +84 -0
  274. package/dist/radio/group/radio-group.js +170 -0
  275. package/dist/radio/index.d.ts +2 -0
  276. package/dist/radio/index.js +2 -0
  277. package/dist/radio/root/radio-root.d.ts +102 -0
  278. package/dist/radio/root/radio-root.js +151 -0
  279. package/dist/rating/index.d.ts +1 -0
  280. package/dist/rating/index.js +1 -0
  281. package/dist/rating/root/rating-root.d.ts +242 -0
  282. package/dist/rating/root/rating-root.js +484 -0
  283. package/dist/scroll-area/index.d.ts +1 -0
  284. package/dist/scroll-area/index.js +1 -0
  285. package/dist/scroll-area/root/scroll-area-root.d.ts +96 -0
  286. package/dist/scroll-area/root/scroll-area-root.js +215 -0
  287. package/dist/select/index.d.ts +1 -0
  288. package/dist/select/index.js +1 -0
  289. package/dist/select/root/select-root.d.ts +170 -0
  290. package/dist/select/root/select-root.js +419 -0
  291. package/dist/skeleton/index.d.ts +1 -0
  292. package/dist/skeleton/index.js +1 -0
  293. package/dist/skeleton/root/skeleton-root.d.ts +31 -0
  294. package/dist/skeleton/root/skeleton-root.js +42 -0
  295. package/dist/slider/index.d.ts +1 -0
  296. package/dist/slider/index.js +1 -0
  297. package/dist/slider/root/slider-root.d.ts +235 -0
  298. package/dist/slider/root/slider-root.js +488 -0
  299. package/dist/sparkline/index.d.ts +1 -0
  300. package/dist/sparkline/index.js +1 -0
  301. package/dist/sparkline/root/sparkline-root.d.ts +147 -0
  302. package/dist/sparkline/root/sparkline-root.js +278 -0
  303. package/dist/speed-dial/index.d.ts +2 -0
  304. package/dist/speed-dial/index.js +2 -0
  305. package/dist/speed-dial/item/speed-dial-item.d.ts +71 -0
  306. package/dist/speed-dial/item/speed-dial-item.js +113 -0
  307. package/dist/speed-dial/root/speed-dial-root.d.ts +122 -0
  308. package/dist/speed-dial/root/speed-dial-root.js +251 -0
  309. package/dist/splitter/index.d.ts +1 -0
  310. package/dist/splitter/index.js +1 -0
  311. package/dist/splitter/root/splitter-root.d.ts +491 -0
  312. package/dist/splitter/root/splitter-root.js +1030 -0
  313. package/dist/stat/group/stat-group.d.ts +30 -0
  314. package/dist/stat/group/stat-group.js +47 -0
  315. package/dist/stat/index.d.ts +2 -0
  316. package/dist/stat/index.js +2 -0
  317. package/dist/stat/root/stat-root.d.ts +62 -0
  318. package/dist/stat/root/stat-root.js +51 -0
  319. package/dist/steps/index.d.ts +2 -0
  320. package/dist/steps/index.js +2 -0
  321. package/dist/steps/root/steps-root.d.ts +110 -0
  322. package/dist/steps/root/steps-root.js +130 -0
  323. package/dist/steps/step/step.d.ts +139 -0
  324. package/dist/steps/step/step.js +215 -0
  325. package/dist/switch/index.d.ts +1 -0
  326. package/dist/switch/index.js +1 -0
  327. package/dist/switch/root/switch-root.d.ts +74 -0
  328. package/dist/switch/root/switch-root.js +153 -0
  329. package/dist/table/cell/table-cell.d.ts +34 -0
  330. package/dist/table/cell/table-cell.js +52 -0
  331. package/dist/table/column/table-column.d.ts +165 -0
  332. package/dist/table/column/table-column.js +222 -0
  333. package/dist/table/index.d.ts +4 -0
  334. package/dist/table/index.js +4 -0
  335. package/dist/table/root/table-root.d.ts +98 -0
  336. package/dist/table/root/table-root.js +161 -0
  337. package/dist/table/row/table-row.d.ts +27 -0
  338. package/dist/table/row/table-row.js +42 -0
  339. package/dist/tabs/index.d.ts +3 -0
  340. package/dist/tabs/index.js +3 -0
  341. package/dist/tabs/panel/tab-panel.d.ts +67 -0
  342. package/dist/tabs/panel/tab-panel.js +98 -0
  343. package/dist/tabs/root/tabs-root.d.ts +123 -0
  344. package/dist/tabs/root/tabs-root.js +217 -0
  345. package/dist/tabs/tab/tab.d.ts +64 -0
  346. package/dist/tabs/tab/tab.js +116 -0
  347. package/dist/tag/index.d.ts +1 -0
  348. package/dist/tag/index.js +1 -0
  349. package/dist/tag/root/tag-root.d.ts +41 -0
  350. package/dist/tag/root/tag-root.js +67 -0
  351. package/dist/textarea/index.d.ts +1 -0
  352. package/dist/textarea/index.js +1 -0
  353. package/dist/textarea/root/textarea-root.d.ts +215 -0
  354. package/dist/textarea/root/textarea-root.js +380 -0
  355. package/dist/time-input/index.d.ts +1 -0
  356. package/dist/time-input/index.js +1 -0
  357. package/dist/time-input/root/time-input-root.d.ts +322 -0
  358. package/dist/time-input/root/time-input-root.js +661 -0
  359. package/dist/time-picker/index.d.ts +1 -0
  360. package/dist/time-picker/index.js +1 -0
  361. package/dist/time-picker/root/time-picker-root.d.ts +380 -0
  362. package/dist/time-picker/root/time-picker-root.js +669 -0
  363. package/dist/timeline/index.d.ts +2 -0
  364. package/dist/timeline/index.js +2 -0
  365. package/dist/timeline/item/timeline-item.d.ts +81 -0
  366. package/dist/timeline/item/timeline-item.js +128 -0
  367. package/dist/timeline/root/timeline-root.d.ts +52 -0
  368. package/dist/timeline/root/timeline-root.js +72 -0
  369. package/dist/toast/index.d.ts +2 -0
  370. package/dist/toast/index.js +2 -0
  371. package/dist/toast/item/toast-item.d.ts +183 -0
  372. package/dist/toast/item/toast-item.js +345 -0
  373. package/dist/toast/root/toast-root.d.ts +72 -0
  374. package/dist/toast/root/toast-root.js +87 -0
  375. package/dist/toggle/group/toggle-group.d.ts +140 -0
  376. package/dist/toggle/group/toggle-group.js +278 -0
  377. package/dist/toggle/index.d.ts +2 -0
  378. package/dist/toggle/index.js +2 -0
  379. package/dist/toggle/root/toggle-root.d.ts +99 -0
  380. package/dist/toggle/root/toggle-root.js +146 -0
  381. package/dist/toolbar/index.d.ts +1 -0
  382. package/dist/toolbar/index.js +1 -0
  383. package/dist/toolbar/root/toolbar-root.d.ts +105 -0
  384. package/dist/toolbar/root/toolbar-root.js +260 -0
  385. package/dist/tooltip/index.d.ts +1 -0
  386. package/dist/tooltip/index.js +1 -0
  387. package/dist/tooltip/root/tooltip-root.d.ts +388 -0
  388. package/dist/tooltip/root/tooltip-root.js +707 -0
  389. package/dist/tour/index.d.ts +2 -0
  390. package/dist/tour/index.js +2 -0
  391. package/dist/tour/root/tour-root.d.ts +161 -0
  392. package/dist/tour/root/tour-root.js +351 -0
  393. package/dist/tour/stop/tour-stop.d.ts +131 -0
  394. package/dist/tour/stop/tour-stop.js +249 -0
  395. package/dist/tree/index.d.ts +2 -0
  396. package/dist/tree/index.js +2 -0
  397. package/dist/tree/item/tree-item.d.ts +208 -0
  398. package/dist/tree/item/tree-item.js +381 -0
  399. package/dist/tree/root/tree-root.d.ts +88 -0
  400. package/dist/tree/root/tree-root.js +287 -0
  401. package/dist/trend/index.d.ts +1 -0
  402. package/dist/trend/index.js +1 -0
  403. package/dist/trend/root/trend-root.d.ts +67 -0
  404. package/dist/trend/root/trend-root.js +106 -0
  405. package/dist/video/index.d.ts +1 -0
  406. package/dist/video/index.js +1 -0
  407. package/dist/video/root/video-root.d.ts +711 -0
  408. package/dist/video/root/video-root.js +1821 -0
  409. package/dist/video-playlist/index.d.ts +3 -0
  410. package/dist/video-playlist/index.js +3 -0
  411. package/dist/video-playlist/item/video-playlist-item.d.ts +101 -0
  412. package/dist/video-playlist/item/video-playlist-item.js +162 -0
  413. package/dist/video-playlist/menu/video-playlist-menu.d.ts +121 -0
  414. package/dist/video-playlist/menu/video-playlist-menu.js +237 -0
  415. package/dist/video-playlist/root/video-playlist-root.d.ts +108 -0
  416. package/dist/video-playlist/root/video-playlist-root.js +218 -0
  417. package/dist/virtual-list/index.d.ts +1 -0
  418. package/dist/virtual-list/index.js +1 -0
  419. package/dist/virtual-list/root/virtual-list-root.d.ts +121 -0
  420. package/dist/virtual-list/root/virtual-list-root.js +221 -0
  421. package/jsx.d.ts +49767 -0
  422. package/package.json +77 -0
  423. package/svelte.d.ts +129 -0
  424. package/vue.d.ts +9242 -0
@@ -0,0 +1,1821 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, drawIcon, HelixElement, HelixNextEvent, HelixPreviousEvent, onUnlessFalse, } from '@fusion.dev/helix-core';
8
+ import { airplay, arrowClockwise, arrowCounterClockwise, closedCaptioning, cornersIn, cornersOut, dotsThreeVertical, download, gauge, pause, pictureInPicture, play, skipBack, skipForward, speakerHigh, speakerX, } from '@fusion.dev/helix-icons';
9
+ import { nothing } from 'lit';
10
+ import { property, state } from 'lit/decorators.js';
11
+ import { html } from 'lit/static-html.js';
12
+ /* Side-effect, for the menu rows this renders. Both tiers import it, because
13
+ this tier writes the template and the styled tier inherits it without having
14
+ a copy of its own to remind anyone. */
15
+ import '../../menu/index.js';
16
+ /**
17
+ * Seconds as a clock, with an hour only when there is one.
18
+ *
19
+ * `0:42` and not `00:00:42`. A reader comparing the position to the duration is
20
+ * reading two numbers at a glance, and leading zeroes that never change are
21
+ * digits in the way of the ones that do.
22
+ */
23
+ const clock = (seconds) => {
24
+ if (!Number.isFinite(seconds) || seconds < 0) {
25
+ return '0:00';
26
+ }
27
+ const whole = Math.floor(seconds);
28
+ const hours = Math.floor(whole / 3600);
29
+ const minutes = Math.floor((whole % 3600) / 60);
30
+ const rest = whole % 60;
31
+ const padded = `${String(rest).padStart(2, '0')}`;
32
+ return hours > 0
33
+ ? `${hours}:${String(minutes).padStart(2, '0')}:${padded}`
34
+ : `${minutes}:${padded}`;
35
+ };
36
+ /**
37
+ * Which player currently owns the page's one media session.
38
+ *
39
+ * There is exactly one `navigator.mediaSession` per page, and it decides what
40
+ * the lock screen shows and where a headset's buttons land. Two players cannot
41
+ * both have it, so the rule is written down rather than left to whichever
42
+ * rendered last: **whoever played most recently owns it**, which is what a
43
+ * reader means by "this one" when they press play on their headphones.
44
+ *
45
+ * Module scope, because the thing being shared is the page's and not any
46
+ * element's. A player gives it up when it leaves the document, and only if it
47
+ * still holds it: one that already lost the session to another must not clear
48
+ * that other one's metadata on its way out.
49
+ */
50
+ let owner;
51
+ /**
52
+ * How far a skip control moves, in seconds.
53
+ *
54
+ * Ten, which is the number every player that has one uses, and the number the
55
+ * platform names in `seekbackward` and `seekforward` when it hands over a
56
+ * default. A different length here would be correct only on this player.
57
+ */
58
+ const SKIP_SECONDS = 10;
59
+ /**
60
+ * @summary Plays a video, with controls this library draws rather than the
61
+ * browser's.
62
+ *
63
+ * @description
64
+ * **The picture is a real `<video>`.** Decoding, buffering, captions, Picture
65
+ * in Picture, AirPlay and the codecs a device happens to have are all the
66
+ * platform's, and none of them can be had from anything else. What is replaced
67
+ * is the control bar, which is the one part of a video element that cannot be
68
+ * styled and looks different in every browser.
69
+ *
70
+ * **Every control is a real control.** The scrubber and the volume are
71
+ * `<input type="range">`, so a reader gets arrow keys, Home and End, the
72
+ * platform's own touch target and its screen reader announcement without this
73
+ * element implementing any of it. Drawing a div and listening for pointer moves
74
+ * is how a media player ends up unusable from a keyboard.
75
+ *
76
+ * **Native controls are the fallback, not the default.** Where a consumer sets
77
+ * `controls`, the browser's bar is used and this one is not drawn: two sets of
78
+ * controls over one video is worse than either alone, and a consumer who asked
79
+ * for the native bar has a reason.
80
+ *
81
+ * The keys are the ones every player uses: Space and `k` to play or pause, the
82
+ * arrows to seek and to change the volume, `m` to mute, `f` for fullscreen.
83
+ * They are bound on this element rather than on the document, because a video
84
+ * is not a palette: keys belong to it only while a reader is on it.
85
+ *
86
+ * **And none of them where `controls` is set.** The browser's own bar owns
87
+ * those keys, and two handlers on one press is a video that seeks twice or
88
+ * plays and immediately pauses. The focus stop goes with them, since the native
89
+ * controls bring their own.
90
+ *
91
+ * **`<source>` and `<track>` are copied into the video, not slotted into it.**
92
+ * A media element runs its resource selection over its own child *nodes*, and a
93
+ * slot is one node: the sources and the caption tracks a consumer wrote were
94
+ * assigned into the flattened tree and the video never saw any of them. It had
95
+ * no text tracks, no current source and a `readyState` of zero, so a video
96
+ * given sources this way did not merely lack captions, it did not play.
97
+ *
98
+ * They are copied rather than moved, because the consumer's light DOM is theirs
99
+ * and a framework that re-renders it would find its children gone. The copies
100
+ * are rebuilt whenever the slot changes and the video is asked to load again,
101
+ * which is what makes a `<track>` added later take effect.
102
+ *
103
+ * **The lock screen and a headset's buttons work, and one player owns them.**
104
+ * There is one `navigator.mediaSession` per page, so the rule is stated rather
105
+ * than left to render order: whichever player was started last owns it, which
106
+ * is what a reader means by "this one" when they press play on their
107
+ * headphones. The title comes from `label` and the artwork from `poster`.
108
+ *
109
+ * **`remote` sends the picture somewhere else.** The Remote Playback API is
110
+ * what Safari implements for AirPlay and Chrome for Cast, so one control covers
111
+ * both and the platform supplies the device list. It is offered only once
112
+ * something has answered `watchAvailability`: a button that opens an empty list
113
+ * is worse than no button, and a consumer who wants none can set
114
+ * `disableremoteplayback` on this element.
115
+ *
116
+ * The properties below are written by this component and read by whoever draws
117
+ * it. A consumer sets none of them: they carry a number this element computed
118
+ * and CSS cannot, which is what lets a stylesheet on this tier draw the thing
119
+ * without measuring anything itself.
120
+ *
121
+ * @since 0.1.0
122
+ * @status experimental
123
+ * @category media
124
+ * @aliases Player, Media, Movie
125
+ * @documentation https://helix.fusion.dev/components/video
126
+ *
127
+ * @slot - `<source>` and `<track>` elements, which are copied into the video.
128
+ * @slot back - Top left over the frame, for a way out of a set.
129
+ * @slot controls - Added to the control bar, before fullscreen.
130
+ * @slot poster - Shown over the video until it starts.
131
+ *
132
+ * @event {Event} ended - Relayed for the same reason.
133
+ * @event {HelixNextEvent} helix-next - Fired when the reader asks for the next item.
134
+ * @event {HelixPreviousEvent} helix-previous - Fired when they ask for the previous one.
135
+ * @event {Event} pause - Relayed for the same reason.
136
+ * @event {Event} play - Relayed, because it does not cross a shadow boundary.
137
+ *
138
+ * @csspart back - The region top left, for a way out of a set.
139
+ * @csspart captions - Subtitles on and off.
140
+ * @csspart captions-menu - The language menu, where there is more than one.
141
+ * @csspart captions-menu-back - The control that returns from a submenu.
142
+ * @csspart captions-menu-back-mark - The glyph on the back control of a submenu.
143
+ * @csspart captions-menu-empty - Shown when there are no items to show.
144
+ * @csspart captions-menu-header - The bar above a submenu, holding the way back and its name.
145
+ * @csspart captions-menu-loading - Shown while the items are still on their way.
146
+ * @csspart captions-menu-surface - The floating panel the menu is drawn on.
147
+ * @csspart captions-menu-title - The name of the submenu that is showing.
148
+ * @csspart controls - The bar along the bottom.
149
+ * @csspart cue - The caption on screen now, drawn by this element.
150
+ * @csspart download - Saves the file, where it was asked for.
151
+ * @csspart entry - One row in a menu this player opens.
152
+ * @csspart entry-item - The row, which is an `<a>` where `href` is set.
153
+ * @csspart entry-mark - The trailing mark: a tick when checked, a caret when it opens.
154
+ * @csspart entry-start - The leading column, which is a slot and is always there.
155
+ * @csspart failure - What is said when the source will not play.
156
+ * @csspart forward - The control that goes on ten seconds.
157
+ * @csspart fullscreen - The whole screen, and back.
158
+ * @csspart icon - A glyph this component draws.
159
+ * @csspart loading - What is said while the picture waits for more of itself.
160
+ * @csspart loading-mark - The part of that which spins.
161
+ * @csspart more - The button that opens it.
162
+ * @csspart mute - Sound on and off.
163
+ * @csspart next - Asks for the next item, where it was asked for.
164
+ * @csspart overflow - The menu holding the controls kept off the bar.
165
+ * @csspart overflow-back - The control that returns from a submenu.
166
+ * @csspart overflow-back-mark - The glyph on the back control of a submenu.
167
+ * @csspart overflow-empty - Shown when there are no items to show.
168
+ * @csspart overflow-header - The bar above a submenu, holding the way back and its name.
169
+ * @csspart overflow-loading - Shown while the items are still on their way.
170
+ * @csspart overflow-surface - The floating panel the menu is drawn on.
171
+ * @csspart overflow-title - The name of the submenu that is showing.
172
+ * @csspart pip - Picture in picture, where it was asked for and exists.
173
+ * @csspart play - Play and pause.
174
+ * @csspart poster - What covers it before it plays.
175
+ * @csspart previous - Asks for the previous one.
176
+ * @csspart remote - Play on another device, where one is available.
177
+ * @csspart rewind - The control that goes back ten seconds.
178
+ * @csspart scrubber - Where it is, and where to go.
179
+ * @csspart sound - The speaker and its level, which are one control.
180
+ * @csspart speed - Playback speed, where it was asked for.
181
+ * @csspart stage - The box holding the picture and its controls, and the focus
182
+ * target the keys are bound to.
183
+ * @csspart start - The mark over the picture before anything has played.
184
+ * @csspart time - The position and the duration.
185
+ * @csspart video - The video itself.
186
+ * @csspart volume - How loud.
187
+ *
188
+ * @cssproperty --video-intrinsic-ratio - The media's own aspect ratio, once it is known. `--video-aspect-ratio` overrides it.
189
+ * @cssproperty --video-level - The current volume, 0 to 1.
190
+ * @cssproperty --video-progress - How far playback has reached, as a fraction of the duration.
191
+ */
192
+ let HelixHeadlessVideo = class HelixHeadlessVideo extends HelixElement {
193
+ constructor() {
194
+ super(...arguments);
195
+ /**
196
+ * Where the video is, for the common case of one file.
197
+ */
198
+ this.src = '';
199
+ /**
200
+ * A still shown before it plays, which the platform draws itself.
201
+ */
202
+ this.poster = '';
203
+ /**
204
+ * Accessible name, since a video is not described by its filename.
205
+ */
206
+ this.label = '';
207
+ /**
208
+ * Whether it starts again at the end.
209
+ */
210
+ this.loop = false;
211
+ /**
212
+ * Whether it *starts* silent, which is what an autoplaying video must do.
213
+ *
214
+ * **Renamed from `muted`, because that is what it always was.** The platform
215
+ * splits these two: the `muted` content attribute reflects to `defaultMuted`,
216
+ * and the `muted` property is live state a reader's click moves without
217
+ * touching the attribute. This element declared one property called `muted`,
218
+ * bound it to the attribute, and tracked the live state privately as
219
+ * `silent`, so `<helix-video>.muted` answered what the author wrote rather
220
+ * than whether there was sound. A name matching the platform's while meaning
221
+ * something else is worse than a name the platform does not have, and
222
+ * `check:platform-fidelity` could not see it: it compares names.
223
+ */
224
+ this.defaultMuted = false;
225
+ /**
226
+ * Whether it plays where it sits rather than taking over the screen.
227
+ */
228
+ this.playsInline = true;
229
+ /**
230
+ * Whether the browser draws the controls instead of this element.
231
+ *
232
+ * A fallback rather than a default. Two bars over one video is worse than
233
+ * either alone, so this one is not drawn at all when it is set.
234
+ */
235
+ this.controls = false;
236
+ /**
237
+ * How much to fetch before anything is asked for.
238
+ */
239
+ this.preload = 'metadata';
240
+ /**
241
+ * Whether it starts on its own, which a browser only allows while silent.
242
+ *
243
+ * Forwarded rather than acted on. Whether an unprompted play is permitted is
244
+ * the browser's policy and not this element's, and `play()` already swallows
245
+ * the refusal: what follows a refused autoplay is a paused player showing its
246
+ * start control, which is the correct thing to be looking at.
247
+ */
248
+ this.autoplay = false;
249
+ /**
250
+ * How the media is fetched across origins, which decides whether a track can
251
+ * be read.
252
+ */
253
+ this.crossOrigin = null;
254
+ /**
255
+ * Whether the browser may offer picture in picture.
256
+ */
257
+ this.disablePictureInPicture = false;
258
+ /**
259
+ * Whether the browser may offer playback on a remote device.
260
+ */
261
+ this.disableRemotePlayback = false;
262
+ /**
263
+ * Controls beyond the ones every player has, named in the order they appear.
264
+ *
265
+ * A list rather than a property apiece. A bar is a row with a budget, and a
266
+ * boolean per feature invites a consumer to switch all of them on: naming
267
+ * them makes the row a decision, and makes the order theirs. Anything
268
+ * unrecognised is skipped rather than throwing, since the list is markup and
269
+ * a typo should cost a control, not a page.
270
+ *
271
+ * `pip` is offered only where the platform has it, checked rather than
272
+ * assumed: it is absent on iPhone and behind a flag in Firefox, and a button
273
+ * that cannot work is worse than one that is not there.
274
+ *
275
+ * The names are `speed`, `pip`, `remote` and `download`.
276
+ *
277
+ * **`download` is named and never assumed.** Offering the file is a decision
278
+ * about the content rather than about the player, and plenty of consumers
279
+ * decline it deliberately. It is also the one control whose success is not
280
+ * ours to promise: the attribute is ignored cross-origin, so a file served
281
+ * from somewhere else opens rather than saves, which is the browser's rule
282
+ * and not something a player can work around.
283
+ */
284
+ this.extraControls = '';
285
+ /**
286
+ * Controls that go behind one button instead of onto the bar, named the same
287
+ * way.
288
+ *
289
+ * **Named rather than measured, and the reason is that a width cannot answer
290
+ * this.** A media query asks about the window, and the bar's problem is its
291
+ * own width: a video in a 320px sidebar on a desktop is cramped and a
292
+ * full-bleed hero on a phone is not. A container query asks the right
293
+ * question and still cannot answer it, because how many controls there are is
294
+ * the consumer's, so the threshold is not a width but a width per control,
295
+ * which is a guess. And a control that moves between the bar and a hidden
296
+ * menu as the layout shifts is a control the reader has to find twice.
297
+ *
298
+ * So the consumer says where the fold is, which they can, having already
299
+ * named the controls in order. A name in both lists is drawn once, on the
300
+ * bar: two of the same control is worse than either placement.
301
+ */
302
+ this.overflowControls = '';
303
+ /**
304
+ * The speeds offered, slowest first, as multipliers of normal.
305
+ */
306
+ this.speeds = '0.5 1 1.5 2';
307
+ /**
308
+ * How long the bar waits after the pointer stops before it goes, in ms.
309
+ *
310
+ * It used to leave the moment the pointer did, which is not how a player
311
+ * behaves: a reader moving across the picture to reach a control watched it
312
+ * vanish under them. Zero turns the wait off and the bar goes immediately;
313
+ * a large number keeps it up for as long as anyone is likely to look.
314
+ *
315
+ * The wait is only ever about *idling*. A video that is paused keeps its bar
316
+ * regardless, because a reader who stopped it is deciding what to do next.
317
+ */
318
+ this.idleDelay = 2500;
319
+ /**
320
+ * What the play control is called while it would start the video.
321
+ */
322
+ this.playLabel = 'Play';
323
+ /**
324
+ * And while it would stop it.
325
+ */
326
+ this.pauseLabel = 'Pause';
327
+ /**
328
+ * What a reader is told while the picture is waiting for more of itself.
329
+ */
330
+ this.loadingLabel = 'Loading';
331
+ /**
332
+ * And what they are told when it will not play at all.
333
+ */
334
+ this.errorLabel = 'This video could not be played';
335
+ /**
336
+ * Whether playback has stopped to wait for data.
337
+ *
338
+ * **Only ever after somebody asked for playback**, which is what `waiting`
339
+ * means: the element wanted the next frame and did not have it. Before that,
340
+ * a video that has loaded nothing is a poster with a play control over it,
341
+ * which looks deliberate and is. A spinner there would say something is
342
+ * happening when nothing is, on every video on the page.
343
+ *
344
+ * The case this covers is the one that looks like a bug: a picture frozen
345
+ * mid-play, the scrubber stopped, and nothing on screen saying why.
346
+ */
347
+ this.buffering = false;
348
+ /**
349
+ * Whether the source failed, which is the other thing a frozen frame can
350
+ * mean.
351
+ */
352
+ this.failed = false;
353
+ this.playing = false;
354
+ this.position = 0;
355
+ this.knownDuration = 0;
356
+ this.loudness = 1;
357
+ this.silent = false;
358
+ this.full = false;
359
+ this.captioned = false;
360
+ /**
361
+ * Which caption track is showing, or `-1` for none.
362
+ *
363
+ * `@state`, because it is read off the video rather than declared: a consumer
364
+ * asking for a language would be naming a track that may not have loaded, and
365
+ * `default` on the `<track>` is the platform's own way to say it.
366
+ */
367
+ this.captionIndex = -1;
368
+ /**
369
+ * How many caption tracks there are, which decides which control is drawn.
370
+ *
371
+ * A count and not a boolean, and that is the whole reason it changed. Tracks
372
+ * resolve after the element renders, so this is what schedules the update
373
+ * that draws the control at all; as a boolean it went false to true when the
374
+ * first one arrived and then never moved again, so a second track landing
375
+ * left the toggle in place and the language menu was unreachable on exactly
376
+ * the videos it exists for.
377
+ */
378
+ this.captionCount = 0;
379
+ this.rate = 1;
380
+ /**
381
+ * Whether the reader has been still long enough for the bar to go.
382
+ */
383
+ this.idle = false;
384
+ /**
385
+ * Whether anything on the network offers to play this.
386
+ */
387
+ this.remoteAvailable = false;
388
+ /**
389
+ * Whether it is playing somewhere else right now.
390
+ */
391
+ this.remoteConnected = false;
392
+ /**
393
+ * The sources and caption tracks to copy into the video, as the consumer
394
+ * wrote them.
395
+ */
396
+ this.media = [];
397
+ /**
398
+ * Rebuilds the copies from whatever the consumer has slotted.
399
+ *
400
+ * The attribute list is explicit rather than a blind copy of every attribute:
401
+ * these are two elements with a known and small surface, and copying anything
402
+ * a consumer happened to put on one would carry their framework's own markers
403
+ * into a shadow root that has no use for them.
404
+ */
405
+ this.handleMedia = (event) => {
406
+ const assigned = event.target.assignedElements();
407
+ this.media = assigned
408
+ .filter((node) => node instanceof HTMLSourceElement || node instanceof HTMLTrackElement)
409
+ .map((node) => {
410
+ const tag = node instanceof HTMLTrackElement ? 'track' : 'source';
411
+ const names = tag === 'track'
412
+ ? ['default', 'kind', 'label', 'src', 'srclang']
413
+ : ['media', 'sizes', 'src', 'srcset', 'type'];
414
+ const attrs = {};
415
+ for (const name of names) {
416
+ const value = node.getAttribute(name);
417
+ if (value !== null) {
418
+ attrs[name] = value;
419
+ }
420
+ }
421
+ return { attrs, tag };
422
+ });
423
+ };
424
+ this.handleLoaded = () => {
425
+ const video = this.video;
426
+ if (!video) {
427
+ return;
428
+ }
429
+ this.knownDuration = video.duration;
430
+ /* The video's own shape, handed to CSS as a number.
431
+ *
432
+ * `aspect-ratio: auto` cannot be used for this, and that is the whole
433
+ * reason this exists: `auto` takes the element's intrinsic ratio, and a
434
+ * `<video>` carrying a poster reports the *poster's*. A three-by-two still
435
+ * over a sixteen-by-nine clip made the box three-by-two, so the poster
436
+ * filled it and the video letterboxed inside, standing fifty pixels
437
+ * shorter the moment it started. Measured at 488 by 325 where the video
438
+ * wanted 488 by 275.
439
+ *
440
+ * Set once the metadata is in, which is the first moment the real ratio is
441
+ * knowable at all. Until then the stylesheet's own default holds. */
442
+ if (video.videoWidth > 0 && video.videoHeight > 0) {
443
+ this.style.setProperty('--video-intrinsic-ratio', `${video.videoWidth} / ${video.videoHeight}`);
444
+ }
445
+ /* Read once the video has its tracks, not from the slot. A `<track>` is
446
+ only a caption once the platform has parsed it, and a consumer can add
447
+ one later. */
448
+ this.captionCount = this.captionTracks.length;
449
+ /* And what the first one is doing, which is not always nothing: a
450
+ `<track default>` starts showing without anyone pressing anything, so a
451
+ button that began at "off" reported the opposite of what was on screen. */
452
+ const track = video.textTracks[0];
453
+ if (track) {
454
+ /* A `<track default>` arrives `showing`, which is the browser drawing it.
455
+ Moved to `hidden` so the cues keep coming and we draw them instead. */
456
+ if (track.mode === 'showing') {
457
+ track.mode = 'hidden';
458
+ }
459
+ this.captioned = track.mode === 'hidden';
460
+ /* Taken off whatever it was on before adding it here. This runs from
461
+ `loadedmetadata`, which fires again for every source a reader moves to,
462
+ and a consumer adding a `<track>` later gets another. Added without
463
+ removing, a playlist of six left six listeners on tracks that between
464
+ them fire for every cue in the video. */
465
+ this.cued?.removeEventListener('cuechange', this.handleCue);
466
+ this.cued = track;
467
+ track.addEventListener('cuechange', this.handleCue);
468
+ this.handleCue();
469
+ }
470
+ };
471
+ this.handleTime = () => {
472
+ this.position = this.video?.currentTime ?? 0;
473
+ };
474
+ /**
475
+ * Relayed, because none of these cross a shadow boundary on their own.
476
+ *
477
+ * The video is inside this element's shadow root, so a consumer listening on
478
+ * the element they wrote hears nothing at all without this. Input relays
479
+ * `change` for the same reason.
480
+ */
481
+ /* `emptied` is in the list for a reason worth keeping. Assigning `src` runs
482
+ the media load algorithm, which stops playback and sets `paused` without
483
+ firing `pause`: the only events are `emptied` and `loadstart`. Listening for
484
+ `play` and `pause` alone, this element went on believing it was playing
485
+ after a playlist pointed it at the next entry, so the start control it draws
486
+ only when paused stayed hidden and there was nothing at all to press.
487
+ Measured: `paused` true, `playing` true, no `pause` event in the log. */
488
+ this.handleWaiting = () => {
489
+ this.buffering = true;
490
+ };
491
+ /* Anything that means the picture can move again. `loadstart` is in the list
492
+ because a new source clears the last one's failure: a playlist pointing the
493
+ player somewhere else must not keep showing the error from the entry
494
+ before. */
495
+ this.handleReady = (event) => {
496
+ this.buffering = false;
497
+ if (event.type === 'loadstart') {
498
+ this.failed = false;
499
+ }
500
+ };
501
+ this.handleError = () => {
502
+ this.buffering = false;
503
+ this.failed = true;
504
+ };
505
+ this.handleState = (event) => {
506
+ const video = this.video;
507
+ this.playing = video ? !video.paused : false;
508
+ if (event.type === 'emptied' || event.type === 'pause') {
509
+ this.buffering = false;
510
+ }
511
+ if (event.type === 'play') {
512
+ this.claimSession();
513
+ }
514
+ if (owner === this && typeof navigator !== 'undefined' && navigator.mediaSession) {
515
+ navigator.mediaSession.playbackState = this.playing ? 'playing' : 'paused';
516
+ }
517
+ /* Non-bubbling, which is what the platform sends and what makes these reach
518
+ a React consumer at all.
519
+ *
520
+ * `play`, `pause` and `ended` are in react-dom's `nonDelegatedEvents`, read
521
+ * out of the installed 19.2.8 rather than remembered: it concatenates the
522
+ * whole media list onto `beforetoggle cancel close invalid load scroll
523
+ * scrollend toggle`. React attaches those directly to `<video>`, `<audio>`
524
+ * and `<img>` and never to a custom element, so nothing at the root hears
525
+ * them. `bindReactHandler` is what closes that, and `reactPropFor` skips
526
+ * anything that bubbles, on the grounds that a bubbling relay is already
527
+ * reaching React's delegated listener and a second binding would double
528
+ * every call.
529
+ *
530
+ * So these three were sending `bubbles: true` into the one branch that
531
+ * refuses to bind, and `onPlay`, `onPause` and `onEnded` on `<helix-video>`
532
+ * were silent in React. The comment beside `reactPropFor` named this exact
533
+ * trio as fixed while this line kept it broken. Matching the platform's own
534
+ * propagation fixes it and is what `Image` already does with `load` and
535
+ * `error`. */
536
+ this.dispatchEvent(new Event(event.type, { bubbles: false, composed: false }));
537
+ };
538
+ this.handleVolume = () => {
539
+ const video = this.video;
540
+ if (!video) {
541
+ return;
542
+ }
543
+ this.loudness = video.volume;
544
+ this.silent = video.muted;
545
+ };
546
+ /**
547
+ * Draws the active cue ourselves, so it can be put where we want it.
548
+ *
549
+ * The browser renders cues inside the video's own box and there is no
550
+ * standard way to inset that box: a caption lands under the control bar and
551
+ * stays there. `::cue` styles the text and not the region, and the property
552
+ * that moves the region is a `-webkit-` internal that two of the three
553
+ * engines do not have.
554
+ *
555
+ * So the track is set to `hidden` rather than `showing`, which is the mode
556
+ * that keeps the cues coming without drawing them, and what it reports is
557
+ * rendered into a part of our own. That also makes captions styleable, which
558
+ * `::cue` only half allows.
559
+ */
560
+ this.handleCue = () => {
561
+ /* The track that is showing, not the first one. With one track those are
562
+ the same and with two they are not, so a reader on the second language
563
+ was shown the first one's words. */
564
+ const track = this.captionTracks[this.captionIndex] ?? this.video?.textTracks[0];
565
+ const active = track?.activeCues?.[0];
566
+ this.cue = active instanceof VTTCue ? active.getCueAsHTML() : undefined;
567
+ };
568
+ /**
569
+ * Restarts the wait, on anything that counts as the reader being here.
570
+ *
571
+ * Pointer movement and focus both count, and both have to: a reader using the
572
+ * keyboard never moves a pointer, and a bar that only answered the mouse
573
+ * would leave them nothing to look at.
574
+ */
575
+ this.stir = () => {
576
+ this.idle = false;
577
+ clearTimeout(this.idleTimer);
578
+ if (this.idleDelay > 0) {
579
+ this.idleTimer = setTimeout(() => {
580
+ this.idle = true;
581
+ }, this.idleDelay);
582
+ }
583
+ };
584
+ this.handleFullscreenChange = () => {
585
+ this.full = document.fullscreenElement === this;
586
+ };
587
+ /**
588
+ * The keys every player uses, bound to this element rather than the document.
589
+ *
590
+ * A video is not a command palette: its keys belong to it while a reader is
591
+ * on it, and a page with two videos on it would otherwise have both answering
592
+ * the same press.
593
+ *
594
+ * Nothing is taken while focus is in a range. The scrubber and the volume are
595
+ * real inputs and the arrows are already theirs, so intercepting would mean
596
+ * seeking twice for one press.
597
+ */
598
+ this.handleKeydown = (event) => {
599
+ /* Nothing at all where the browser draws the controls. Its own bar owns
600
+ Space, the arrows and the rest, and two handlers on one key is a video
601
+ that seeks twice or plays and immediately pauses. A consumer who asked
602
+ for the native controls asked for the native keyboard with them. */
603
+ if (this.controls) {
604
+ return;
605
+ }
606
+ if (event.target?.tagName === 'INPUT') {
607
+ return;
608
+ }
609
+ const video = this.video;
610
+ if (!video) {
611
+ return;
612
+ }
613
+ const step = event.shiftKey ? 10 : 5;
614
+ switch (event.key) {
615
+ case ' ':
616
+ case 'k': {
617
+ this.toggle();
618
+ break;
619
+ }
620
+ case 'ArrowRight': {
621
+ this.seek(video.currentTime + step);
622
+ break;
623
+ }
624
+ case 'ArrowLeft': {
625
+ this.seek(video.currentTime - step);
626
+ break;
627
+ }
628
+ case 'ArrowUp': {
629
+ video.volume = Math.min(video.volume + 0.1, 1);
630
+ break;
631
+ }
632
+ case 'ArrowDown': {
633
+ video.volume = Math.max(video.volume - 0.1, 0);
634
+ break;
635
+ }
636
+ case 'j': {
637
+ this.skipBy(-SKIP_SECONDS);
638
+ break;
639
+ }
640
+ case 'l': {
641
+ this.skipBy(SKIP_SECONDS);
642
+ break;
643
+ }
644
+ case 'm': {
645
+ video.muted = !video.muted;
646
+ break;
647
+ }
648
+ case 'f': {
649
+ void this.toggleFullscreen();
650
+ break;
651
+ }
652
+ default: {
653
+ return;
654
+ }
655
+ }
656
+ event.preventDefault();
657
+ };
658
+ this.handleRemoteState = () => {
659
+ this.remoteConnected = this.video?.remote?.state === 'connected';
660
+ };
661
+ }
662
+ /**
663
+ * Whether the file this points at is ours to hand over.
664
+ *
665
+ * `blob:` and `data:` named rather than compared, because the rule `download`
666
+ * actually follows is same-origin *or one of those two*: a clip a consumer
667
+ * generated in the page has no origin to match and is theirs to save by
668
+ * definition. Comparing origins alone called both foreign, so the one case
669
+ * where a download certainly works was the one case it was not offered.
670
+ */
671
+ get localFile() {
672
+ if (this.src === '') {
673
+ return false;
674
+ }
675
+ if (this.src.startsWith('blob:') || this.src.startsWith('data:')) {
676
+ return true;
677
+ }
678
+ if (typeof location === 'undefined') {
679
+ return false;
680
+ }
681
+ try {
682
+ return new URL(this.src, location.href).origin === location.origin;
683
+ }
684
+ catch {
685
+ return false;
686
+ }
687
+ }
688
+ /** Whether this platform can do a named control at all. */
689
+ offers(one) {
690
+ if (one === 'pip') {
691
+ return typeof document !== 'undefined' && document.pictureInPictureEnabled;
692
+ }
693
+ /* Availability, not support. A device that can play this has to exist for
694
+ the button to mean anything: an AirPlay control with nothing to send to
695
+ is a control that opens an empty list, which is worse than no control.
696
+ `watchAvailability` is what answers that, and it answers late, so this
697
+ reads a flag the watch keeps rather than asking. */
698
+ if (one === 'remote') {
699
+ return this.remoteAvailable;
700
+ }
701
+ /* A download needs something to download. `src` and not a slotted
702
+ `<source>`: the element chose one of those and which one is the
703
+ browser's business, so offering a specific file would be a guess about a
704
+ decision already made. */
705
+ if (one === 'download') {
706
+ return this.src !== '';
707
+ }
708
+ /* Always offered, because there is nothing to check. Neither of these does
709
+ anything to the video: they say the reader asked, and whatever holds the
710
+ set answers. A player that hid them until it could prove a set existed
711
+ would be a player that cannot know. */
712
+ if (one === 'next' || one === 'previous') {
713
+ return true;
714
+ }
715
+ /* A skip needs only a timeline, and a video that has none cannot be played
716
+ at all, so there is nothing here to check either. */
717
+ if (one === 'forward' || one === 'rewind') {
718
+ return true;
719
+ }
720
+ return one === 'speed';
721
+ }
722
+ /** The extras asked for, in order, and only the ones this platform can do. */
723
+ get extras() {
724
+ return this.extraControls
725
+ .split(/\s+/)
726
+ .filter(Boolean)
727
+ .filter((one) => this.offers(one));
728
+ }
729
+ /**
730
+ * The controls asked for behind the overflow button, in order.
731
+ *
732
+ * A name in both lists is on the bar and not here. Drawing it twice is worse
733
+ * than either placement, and the bar is the one a reader can see.
734
+ */
735
+ get overflow() {
736
+ const onBar = new Set(this.extras);
737
+ return this.overflowControls
738
+ .split(/\s+/)
739
+ .filter(Boolean)
740
+ .filter((one) => !onBar.has(one) && this.offers(one));
741
+ }
742
+ /**
743
+ * The media's own state, under the platform's own names.
744
+ *
745
+ * **This element already knew every one of these and kept them private.** It
746
+ * tracks `currentTime` as `position`, `duration` as `knownDuration`, `volume`
747
+ * as `loudness`, `paused` as `playing` and `playbackRate` as `rate`, paints
748
+ * them, and published none of them, so a consumer asking where the video had
749
+ * reached had nowhere to ask: the `<video>` is inside this shadow root and
750
+ * `querySelector` does not cross it. 295 is that finding across the library
751
+ * and this is its clearest case.
752
+ *
753
+ * **Read from the element, with the mirror as the fallback.** The element is
754
+ * the truth and the mirror is a render cache that a media event refreshes, so
755
+ * reading the mirror alone would answer with whatever the last `timeupdate`
756
+ * said rather than with where the video is now. The fallback is what answers
757
+ * on the server and before the first render, where there is no element and
758
+ * the mirror holds what markup asked for.
759
+ *
760
+ * **Accessors rather than reactive properties**, because none of these is a
761
+ * setting: they are the media's state, and a `@property` would give each one
762
+ * an attribute that a consumer could write and the element would then have to
763
+ * disagree with. Where the platform makes one writable this writes it
764
+ * through, and where the platform does not neither does this.
765
+ */
766
+ get currentSrc() {
767
+ return this.video?.currentSrc ?? '';
768
+ }
769
+ /**
770
+ * Where playback has reached, in seconds.
771
+ *
772
+ * Writing seeks, through the same clamp `seek` uses, because a `currentTime`
773
+ * past the end is the one write the platform answers with an exception.
774
+ */
775
+ get currentTime() {
776
+ return this.video?.currentTime ?? this.position;
777
+ }
778
+ set currentTime(seconds) {
779
+ this.position = seconds;
780
+ this.seek(seconds);
781
+ }
782
+ /** The rate playback returns to, which `defaultPlaybackRate` names natively. */
783
+ get defaultPlaybackRate() {
784
+ return this.video?.defaultPlaybackRate ?? 1;
785
+ }
786
+ set defaultPlaybackRate(rate) {
787
+ const video = this.video;
788
+ if (video) {
789
+ video.defaultPlaybackRate = rate;
790
+ }
791
+ }
792
+ /** How long the media runs, in seconds, and `NaN` until that is known. */
793
+ get duration() {
794
+ return this.video?.duration ?? this.knownDuration;
795
+ }
796
+ /** Whether playback has run off the end. */
797
+ get ended() {
798
+ return this.video?.ended ?? false;
799
+ }
800
+ /** Whether playback is stopped, which is the resting state and not an error. */
801
+ get paused() {
802
+ return this.video?.paused ?? !this.playing;
803
+ }
804
+ /** How fast it plays, where 1 is the recorded speed. */
805
+ get playbackRate() {
806
+ return this.video?.playbackRate ?? this.rate;
807
+ }
808
+ set playbackRate(rate) {
809
+ this.rate = rate;
810
+ const video = this.video;
811
+ if (video) {
812
+ video.playbackRate = rate;
813
+ }
814
+ }
815
+ /** Whether pitch is held steady when the rate changes. */
816
+ get preservesPitch() {
817
+ return this.video?.preservesPitch ?? true;
818
+ }
819
+ set preservesPitch(preserve) {
820
+ const video = this.video;
821
+ if (video) {
822
+ video.preservesPitch = preserve;
823
+ }
824
+ }
825
+ /** Whether it is currently moving to a new position. */
826
+ get seeking() {
827
+ return this.video?.seeking ?? false;
828
+ }
829
+ /** The media's own height in pixels, and zero until the metadata has loaded. */
830
+ get videoHeight() {
831
+ return this.video?.videoHeight ?? 0;
832
+ }
833
+ /** The media's own width in pixels, and zero until the metadata has loaded. */
834
+ get videoWidth() {
835
+ return this.video?.videoWidth ?? 0;
836
+ }
837
+ /**
838
+ * Whether there is sound, now, which a reader's click moves.
839
+ *
840
+ * The live half of the pair above. Writing mutes or unmutes the media itself
841
+ * rather than the attribute, which is what the platform does and what the
842
+ * mute control here has always done directly.
843
+ */
844
+ get muted() {
845
+ return this.video?.muted ?? this.defaultMuted;
846
+ }
847
+ set muted(quiet) {
848
+ this.silent = quiet;
849
+ const video = this.video;
850
+ if (video) {
851
+ video.muted = quiet;
852
+ }
853
+ }
854
+ /** How loud it plays, 0 to 1. */
855
+ get volume() {
856
+ return this.video?.volume ?? this.loudness;
857
+ }
858
+ set volume(level) {
859
+ this.loudness = level;
860
+ const video = this.video;
861
+ if (video) {
862
+ video.volume = level;
863
+ }
864
+ }
865
+ get video() {
866
+ return this.renderRoot?.querySelector('[part="video"]') ?? null;
867
+ }
868
+ /**
869
+ * Starts playing, where the platform allows it.
870
+ *
871
+ * The rejection is swallowed on purpose: a browser refuses `play()` when
872
+ * nothing the reader did asked for it, and that is a policy answer rather than
873
+ * a fault. What follows a refused play is a paused player showing its start
874
+ * control, which is the correct thing to be looking at.
875
+ */
876
+ play() {
877
+ void this.video?.play().catch(() => { });
878
+ }
879
+ /** Stops, leaving the frame up. */
880
+ pause() {
881
+ this.video?.pause();
882
+ }
883
+ /** Toggles playback, which is what every key and the button both come to. */
884
+ toggle() {
885
+ const video = this.video;
886
+ if (!video) {
887
+ return;
888
+ }
889
+ if (video.paused) {
890
+ this.play();
891
+ }
892
+ else {
893
+ this.pause();
894
+ }
895
+ }
896
+ /** Moves to a point in seconds, clamped to what exists. */
897
+ seek(seconds) {
898
+ const video = this.video;
899
+ if (!video || !Number.isFinite(video.duration)) {
900
+ return;
901
+ }
902
+ video.currentTime = Math.min(Math.max(seconds, 0), video.duration);
903
+ }
904
+ /**
905
+ * The scrubber's new position, and the reason these two are named at all.
906
+ *
907
+ * **The stop is the first line.** `input` is composed by specification, so a
908
+ * range inside this shadow root sends its own straight past the host to
909
+ * whoever is listening on it, and this component declares no `input`: a
910
+ * consumer binding one received the scrubber's and the volume rail's under a
911
+ * name Video never promised, carrying a seconds count or a level between zero
912
+ * and one rather than anything about the element.
913
+ *
914
+ * Found by sweeping every family that renders a native control, which is what
915
+ * 308 said and not what it was read as: the first two passes both took "form
916
+ * control" as the population and a media component with two rails in its
917
+ * chrome is neither a form control nor exempt from the mechanism. See 308.
918
+ */
919
+ takeScrub(event) {
920
+ event.stopPropagation();
921
+ this.seek(Number(event.target.value));
922
+ }
923
+ /** The volume rail's new level, stopped for the reason `takeScrub` carries. */
924
+ takeVolume(event) {
925
+ event.stopPropagation();
926
+ const video = this.video;
927
+ if (!video) {
928
+ return;
929
+ }
930
+ video.volume = Number(event.target.value);
931
+ video.muted = false;
932
+ }
933
+ /**
934
+ * Takes the page's media session, and tells the platform what is playing.
935
+ *
936
+ * On play rather than on render, which is the ownership rule: a page with two
937
+ * players gives the session to whichever one the reader started, so a
938
+ * headset's buttons land on what they are listening to rather than on
939
+ * whichever came first in the markup.
940
+ *
941
+ * The handlers are what make a headset's play, pause and skip work at all.
942
+ * Without them a platform falls back to guessing which element was meant,
943
+ * which is right often enough to hide the fault and wrong exactly when there
944
+ * are two.
945
+ */
946
+ claimSession() {
947
+ if (typeof navigator === 'undefined' || !navigator.mediaSession) {
948
+ return;
949
+ }
950
+ owner = this;
951
+ const session = navigator.mediaSession;
952
+ session.metadata = new MediaMetadata({
953
+ artwork: this.poster === '' ? [] : [{ src: this.poster }],
954
+ title: this.label,
955
+ });
956
+ session.setActionHandler('play', () => this.toggle());
957
+ session.setActionHandler('pause', () => this.toggle());
958
+ session.setActionHandler('seekbackward', (details) => {
959
+ this.seek((this.video?.currentTime ?? 0) - (details.seekOffset ?? 10));
960
+ });
961
+ session.setActionHandler('seekforward', (details) => {
962
+ this.seek((this.video?.currentTime ?? 0) + (details.seekOffset ?? 10));
963
+ });
964
+ session.setActionHandler('seekto', (details) => {
965
+ if (details.seekTime !== undefined && details.seekTime !== null) {
966
+ this.seek(details.seekTime);
967
+ }
968
+ });
969
+ /* The skip keys, and only where the consumer has said there is something to
970
+ skip to. A headset whose skip buttons do nothing is worse than one whose
971
+ skip buttons are not offered: the platform draws them from whether a
972
+ handler is set, so setting one unconditionally would put two dead keys on
973
+ every lock screen showing this video. */
974
+ const skips = `${this.extraControls} ${this.overflowControls}`.split(/\s+/);
975
+ /* The platform asks for a length and we answer with ours, or with its
976
+ default where it did not say. A lock screen offering a skip that moves a
977
+ different distance than the button on the bar is one player with two
978
+ answers to the same gesture. */
979
+ session.setActionHandler('seekbackward', (details) => {
980
+ this.skipBy(-(details.seekOffset ?? SKIP_SECONDS));
981
+ });
982
+ session.setActionHandler('seekforward', (details) => {
983
+ this.skipBy(details.seekOffset ?? SKIP_SECONDS);
984
+ });
985
+ session.setActionHandler('nexttrack', skips.includes('next') ? () => this.skip(false) : null);
986
+ session.setActionHandler('previoustrack', skips.includes('previous') ? () => this.skip(true) : null);
987
+ }
988
+ /**
989
+ * Moves by a fixed length, clamped by `seek`.
990
+ *
991
+ * Separate from the arrow keys, which move by 5 and by 10 with Shift. Those
992
+ * are a scrub; this is the gesture for "I missed that", and a reader who
993
+ * presses it twice expects twenty seconds rather than an accelerating jump.
994
+ */
995
+ skipBy(seconds) {
996
+ const video = this.video;
997
+ if (!video) {
998
+ return;
999
+ }
1000
+ this.seek(video.currentTime + seconds);
1001
+ }
1002
+ /**
1003
+ * The tracks a reader could choose between, in the order they were written.
1004
+ *
1005
+ * Filtered by kind, because `textTracks` also carries `chapters`, `metadata`
1006
+ * and `descriptions`, and a menu offering "Chapters" as a subtitle language
1007
+ * would be offering something that draws no words. Read off the video rather
1008
+ * than off the slot: a `<track>` added later is in one and not the other, and
1009
+ * the video is the one that has resolved them.
1010
+ */
1011
+ get captionTracks() {
1012
+ const tracks = this.video?.textTracks;
1013
+ if (!tracks) {
1014
+ return [];
1015
+ }
1016
+ return [...tracks].filter((one) => one.kind === 'subtitles' || one.kind === 'captions');
1017
+ }
1018
+ /**
1019
+ * Shows one track and disables every other, or shows none at `-1`.
1020
+ *
1021
+ * Every other, rather than only the one that was on. Two tracks in `hidden`
1022
+ * both fire `cuechange`, so the rendered cue becomes whichever fired last and
1023
+ * a reader who switched language gets the two interleaved. Measured as two
1024
+ * subtitles alternating line by line.
1025
+ *
1026
+ * `hidden`, never `showing`. Hidden is the mode where a track still fires
1027
+ * `cuechange` and still fills `activeCues`, and the browser draws nothing,
1028
+ * which is exactly what is wanted when something else is doing the drawing.
1029
+ */
1030
+ showCaption(index) {
1031
+ const tracks = this.captionTracks;
1032
+ tracks.forEach((track, at) => {
1033
+ track.mode = at === index ? 'hidden' : 'disabled';
1034
+ });
1035
+ this.captionIndex = index;
1036
+ this.captioned = index >= 0;
1037
+ this.cue = undefined;
1038
+ }
1039
+ /** Turns captions on or off, which is what the button means with one track. */
1040
+ toggleCaptions() {
1041
+ if (this.captionTracks.length === 0) {
1042
+ return;
1043
+ }
1044
+ this.showCaption(this.captioned ? -1 : 0);
1045
+ }
1046
+ /**
1047
+ * Says the reader asked for another item, and does nothing else.
1048
+ *
1049
+ * There is nothing else it could do. A `<video>` plays one resource, so what
1050
+ * comes before or after it is a fact this element does not hold: a playlist
1051
+ * has it, or the consumer does. Announcing is the whole of the contribution,
1052
+ * and the event bubbles so a wrapper hears it.
1053
+ *
1054
+ * The same call answers the button, the row in the overflow menu, and the
1055
+ * skip keys on a headset, which is why the media session's handlers point
1056
+ * here rather than at anything of their own.
1057
+ */
1058
+ skip(back) {
1059
+ this.dispatchEvent(back ? new HelixPreviousEvent() : new HelixNextEvent());
1060
+ }
1061
+ /** Steps through the speeds offered, which is what one button can do. */
1062
+ cycleSpeed() {
1063
+ const speeds = this.speeds
1064
+ .split(/\s+/)
1065
+ .map(Number)
1066
+ .filter((one) => one > 0);
1067
+ const video = this.video;
1068
+ if (!video || speeds.length === 0) {
1069
+ return;
1070
+ }
1071
+ const at = speeds.indexOf(video.playbackRate);
1072
+ const next = speeds[(at + 1) % speeds.length] ?? 1;
1073
+ video.playbackRate = next;
1074
+ this.rate = next;
1075
+ }
1076
+ /**
1077
+ * Starts watching for something to play this on, and stops on the way out.
1078
+ *
1079
+ * The Remote Playback API is the standard one, and it is what Safari
1080
+ * implements for AirPlay and Chrome for Cast: one call covers both, where the
1081
+ * older `webkitShowPlaybackTargetPicker` covers only the first. Firefox has
1082
+ * neither, and `remote` simply being absent is what turns the control off
1083
+ * there.
1084
+ *
1085
+ * `watchAvailability` returns an id that has to be canceled, or the callback
1086
+ * outlives the element that owns it.
1087
+ */
1088
+ async watchRemote() {
1089
+ const remote = this.video?.remote;
1090
+ if (!remote?.watchAvailability) {
1091
+ return;
1092
+ }
1093
+ try {
1094
+ this.remoteWatch = await remote.watchAvailability((available) => {
1095
+ this.remoteAvailable = available;
1096
+ });
1097
+ }
1098
+ catch {
1099
+ /* Refused where the media is not remotable, which is a fact about the
1100
+ source rather than an error to report: the control stays off. */
1101
+ }
1102
+ }
1103
+ /** Opens the platform's own device picker. It has no list of its own. */
1104
+ async promptRemote() {
1105
+ try {
1106
+ await this.video?.remote?.prompt();
1107
+ }
1108
+ catch {
1109
+ /* Dismissing the picker rejects, which is a reader saying no rather than
1110
+ anything going wrong. */
1111
+ }
1112
+ }
1113
+ async togglePip() {
1114
+ const video = this.video;
1115
+ if (!video) {
1116
+ return;
1117
+ }
1118
+ if (document.pictureInPictureElement === video) {
1119
+ await document.exitPictureInPicture();
1120
+ return;
1121
+ }
1122
+ await video.requestPictureInPicture();
1123
+ }
1124
+ /**
1125
+ * Into fullscreen and back, and quiet about being refused.
1126
+ *
1127
+ * `requestFullscreen` rejects rather than returning false, and the two
1128
+ * refusals a consumer will actually meet are a Permissions Policy that does
1129
+ * not grant `fullscreen` to the frame the player is in, and a call without
1130
+ * user activation. Both are `TypeError: Permissions check failed`, which is
1131
+ * the same message for two different causes.
1132
+ *
1133
+ * It was `void`ed, so a refusal became an unhandled rejection: the button did
1134
+ * nothing and said nothing, and the console entry it left named neither the
1135
+ * element nor the reason. Caught here, the state stays honest because
1136
+ * `aria-pressed` follows `fullscreenchange` rather than this call, so a
1137
+ * refused request leaves a button that correctly reports "not fullscreen".
1138
+ */
1139
+ async toggleFullscreen() {
1140
+ try {
1141
+ if (document.fullscreenElement === this) {
1142
+ await document.exitFullscreen();
1143
+ return;
1144
+ }
1145
+ await this.requestFullscreen();
1146
+ }
1147
+ catch (error) {
1148
+ /* Reported rather than swallowed, and reported once. A player embedded in
1149
+ a frame without the permission is the common case, and the fix is on
1150
+ the frame rather than here, so the message has to say which. */
1151
+ console.warn(`<${this.localName}> could not enter fullscreen. If it is inside an iframe, that frame needs allow="fullscreen".`, error);
1152
+ }
1153
+ }
1154
+ connectedCallback() {
1155
+ super.connectedCallback();
1156
+ document.addEventListener('fullscreenchange', this.handleFullscreenChange);
1157
+ }
1158
+ disconnectedCallback() {
1159
+ super.disconnectedCallback();
1160
+ clearTimeout(this.idleTimer);
1161
+ this.cued?.removeEventListener('cuechange', this.handleCue);
1162
+ this.cued = undefined;
1163
+ document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
1164
+ /* The watch outlives the element unless it is canceled, and a callback
1165
+ holding a removed element is a leak with a `@state` write on the end. */
1166
+ if (this.remoteWatch !== undefined) {
1167
+ void this.video?.remote?.cancelWatchAvailability?.(this.remoteWatch);
1168
+ this.remoteWatch = undefined;
1169
+ }
1170
+ /* And the page's one media session goes back to nobody, but only if this
1171
+ element is the one holding it. */
1172
+ if (owner === this) {
1173
+ owner = undefined;
1174
+ if (typeof navigator !== 'undefined' && navigator.mediaSession) {
1175
+ navigator.mediaSession.metadata = null;
1176
+ navigator.mediaSession.playbackState = 'none';
1177
+ }
1178
+ }
1179
+ }
1180
+ firstUpdated(changed) {
1181
+ super.firstUpdated?.(changed);
1182
+ const video = this.video;
1183
+ if (video) {
1184
+ this.silent = video.muted;
1185
+ this.loudness = video.volume;
1186
+ video.remote?.addEventListener?.('connect', this.handleRemoteState);
1187
+ video.remote?.addEventListener?.('disconnect', this.handleRemoteState);
1188
+ void this.watchRemote();
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Mirrors playback onto the host as `data-playing`.
1193
+ *
1194
+ * Because CSS cannot ask. `paused` is a property of a `<video>` and never an
1195
+ * attribute, so `video:not([paused])` matches every video that has ever
1196
+ * existed: the first version of the rule that hides the bar used exactly that
1197
+ * and hid it always, including on a paused video, which is the one state it
1198
+ * was written to keep it in.
1199
+ *
1200
+ * **A `data-` attribute is right here because of when it is written.** A fact
1201
+ * that can be true on the first update mirrors to a custom state instead, or
1202
+ * a consumer's server sends markup the element then grows an attribute on and
1203
+ * React reports a mismatch it will not patch up. Nothing plays until somebody
1204
+ * presses play, so this one is written long past hydration and there is
1205
+ * nothing for it to disagree with. `data-closed` on `Toast.Item` and
1206
+ * `data-closed` on `Toast.Item` is the same case; `:state(open)` and
1207
+ * `:state(checked)` are the other one, and were `data-` attributes until
1208
+ * 27 Aug 2026.
1209
+ */
1210
+ updated(changed) {
1211
+ super.updated?.(changed);
1212
+ this.toggleAttribute('data-playing', this.playing);
1213
+ /* `data-idle` rather than a hover rule alone, so the stylesheet can say
1214
+ "playing, and nobody has moved for a while" instead of "playing and the
1215
+ pointer is not on it this instant". */
1216
+ this.toggleAttribute('data-idle', this.idle && this.playing);
1217
+ /* A media element decides what to play once, when it is created or when it
1218
+ is told to look again. Copies that arrive after that are markup it has
1219
+ already walked past. */
1220
+ if (changed.has('media')) {
1221
+ const video = this.video;
1222
+ video?.load();
1223
+ this.captionCount = this.captionTracks.length;
1224
+ }
1225
+ }
1226
+ /**
1227
+ * One named control, as a button on the bar.
1228
+ *
1229
+ * A method rather than four branches inlined, because the same four names are
1230
+ * asked for twice now, once for the bar and once for the menu behind it, and
1231
+ * two lists that have to stay in step is how a control ends up offered in one
1232
+ * place and not the other.
1233
+ */
1234
+ /* `protected` rather than `private`: the method was already the seam and a
1235
+ tier above simply could not reach it. */
1236
+ renderExtra(one) {
1237
+ if (one === 'remote') {
1238
+ return html `<button
1239
+ part="remote"
1240
+ type="button"
1241
+ aria-label="Play on another device"
1242
+ aria-pressed=${this.remoteConnected ? 'true' : 'false'}
1243
+ @click=${() => void this.promptRemote()}
1244
+ >
1245
+ ${drawIcon(airplay)}
1246
+ </button>`;
1247
+ }
1248
+ /* Two templates rather than one with the name bound in, for the reason
1249
+ written under `previous` below: a part is read from the source, so
1250
+ `part=${one}` is a part no check can prove anything about. These two were
1251
+ left bound when that pair was split, and `check:parts` had them exactly
1252
+ right: both were documented and neither could be found. */
1253
+ if (one === 'rewind') {
1254
+ return html `<button
1255
+ part="rewind"
1256
+ type="button"
1257
+ aria-label=${`Back ${SKIP_SECONDS} seconds`}
1258
+ @click=${() => this.skipBy(-SKIP_SECONDS)}
1259
+ >
1260
+ ${drawIcon(arrowCounterClockwise)}
1261
+ </button>`;
1262
+ }
1263
+ if (one === 'forward') {
1264
+ return html `<button
1265
+ part="forward"
1266
+ type="button"
1267
+ aria-label=${`Forward ${SKIP_SECONDS} seconds`}
1268
+ @click=${() => this.skipBy(SKIP_SECONDS)}
1269
+ >
1270
+ ${drawIcon(arrowClockwise)}
1271
+ </button>`;
1272
+ }
1273
+ if (one === 'speed') {
1274
+ return html `<button
1275
+ part="speed"
1276
+ type="button"
1277
+ aria-label="Playback speed"
1278
+ @click=${() => this.cycleSpeed()}
1279
+ >
1280
+ ${this.rate}x
1281
+ </button>`;
1282
+ }
1283
+ /* Two templates rather than one with the name bound in, because a part is
1284
+ read from the source: `check:selectors` scans for the literal, so a part
1285
+ written as `part=${one}` is a part no stylesheet can be proved to reach,
1286
+ and `check:parts` cannot find it to match against the documentation
1287
+ either. It reported both of these as matching nothing. */
1288
+ if (one === 'previous') {
1289
+ return html `<button
1290
+ part="previous"
1291
+ type="button"
1292
+ aria-label="Previous"
1293
+ @click=${() => this.skip(true)}
1294
+ >
1295
+ ${drawIcon(skipBack)}
1296
+ </button>`;
1297
+ }
1298
+ if (one === 'next') {
1299
+ return html `<button
1300
+ part="next"
1301
+ type="button"
1302
+ aria-label="Next"
1303
+ @click=${() => this.skip(false)}
1304
+ >
1305
+ ${drawIcon(skipForward)}
1306
+ </button>`;
1307
+ }
1308
+ if (one === 'download') {
1309
+ /* A real anchor, because saving a file is the browser's job and there is
1310
+ no scripted version of it worth having.
1311
+ *
1312
+ * **`download` only where it means something.** The attribute is honoured
1313
+ * for a same-origin file and ignored for any other, whatever headers come
1314
+ * back, so on a video served from a CDN it does nothing and the press
1315
+ * *navigates*: measured, clicking it took the whole page to the mp4, which
1316
+ * reads as the player disappearing. Writing an attribute the browser will
1317
+ * ignore is a lie in the markup, so it is written only when it holds.
1318
+ *
1319
+ * A new tab where it does not. That is the best a page can do with someone
1320
+ * else's file, and it is the difference between handing the reader the
1321
+ * video and taking away the one they were watching. */
1322
+ const local = this.localFile;
1323
+ return html `<a
1324
+ part="download"
1325
+ ?download=${local}
1326
+ href=${this.src}
1327
+ target=${local ? nothing : '_blank'}
1328
+ rel=${local ? nothing : 'noreferrer'}
1329
+ aria-label="Download"
1330
+ >
1331
+ ${drawIcon(download)}
1332
+ </a>`;
1333
+ }
1334
+ return html `<button
1335
+ part="pip"
1336
+ type="button"
1337
+ aria-label="Picture in picture"
1338
+ @click=${() => void this.togglePip()}
1339
+ >
1340
+ ${drawIcon(pictureInPicture)}
1341
+ </button>`;
1342
+ }
1343
+ /*
1344
+ * The same control as a row in the overflow menu.
1345
+ *
1346
+ * The label is words rather than a mark, which is the difference between a
1347
+ * bar and a menu: a row has the width for a name and a reader scanning one is
1348
+ * reading rather than recognising. `download` is a linked row, which is what
1349
+ * `Menu.Item`'s `href` is for.
1350
+ */
1351
+ /**
1352
+ * A toggle for one track, a menu for several, and nothing for none.
1353
+ *
1354
+ * No menu at one, because a menu of a single language plus "Off" is a
1355
+ * two-state control wearing a popover, and the button already says which
1356
+ * state it is in through `aria-pressed`. The moment there are two the button
1357
+ * cannot say which one is on, so it stops being a button.
1358
+ *
1359
+ * The label comes off the track: `label` where the markup gave one, and
1360
+ * `srclang` where it did not, because a `<track>` with neither is a track
1361
+ * nobody can choose between and that is the markup's fault to fix, not this
1362
+ * component's to paper over with "Track 2".
1363
+ */
1364
+ /* `protected` rather than `private`: the method was already the seam and a
1365
+ tier above simply could not reach it. */
1366
+ renderCaptions() {
1367
+ /* Branched on the state rather than on the getter, so the render that draws
1368
+ this is one the state scheduled. Reading the tracks for their labels is
1369
+ fine either way: by the time this runs, the count that got us here was
1370
+ taken from the same list. */
1371
+ const tracks = this.captionTracks;
1372
+ if (this.captionCount === 0) {
1373
+ return nothing;
1374
+ }
1375
+ if (this.captionCount === 1) {
1376
+ return html `<button
1377
+ part="captions"
1378
+ type="button"
1379
+ aria-label="Captions"
1380
+ aria-pressed=${this.captioned ? 'true' : 'false'}
1381
+ @click=${() => this.toggleCaptions()}
1382
+ >
1383
+ ${drawIcon(closedCaptioning)}
1384
+ </button>`;
1385
+ }
1386
+ return html `<${this.tag('menu')} part="captions-menu"
1387
+ exportparts="back: captions-menu-back, back-mark: captions-menu-back-mark, empty: captions-menu-empty,
1388
+ header: captions-menu-header, loading: captions-menu-loading, surface: captions-menu-surface,
1389
+ title: captions-menu-title" label="Subtitles" placement="top-end">
1390
+ <button part="captions" slot="trigger" type="button" aria-label="Subtitles">
1391
+ ${drawIcon(closedCaptioning)}
1392
+ </button>
1393
+ <${this.tag('menu-item')}
1394
+ part="entry"
1395
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start"
1396
+ value="off"
1397
+ aria-checked=${this.captionIndex < 0 ? 'true' : 'false'}
1398
+ role="menuitemradio"
1399
+ @helix-select=${() => this.showCaption(-1)}
1400
+ >
1401
+ Off
1402
+ </${this.tag('menu-item')}>
1403
+ ${tracks.map((track, at) => html `<${this.tag('menu-item')} part="entry"
1404
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start"
1405
+ value=${track.language || String(at)}
1406
+ aria-checked=${this.captionIndex === at ? 'true' : 'false'}
1407
+ role="menuitemradio"
1408
+ @helix-select=${() => this.showCaption(at)}
1409
+ >
1410
+ ${track.label || track.language}
1411
+ </${this.tag('menu-item')}>`)}
1412
+ </${this.tag('menu')}>`;
1413
+ }
1414
+ /* `protected` rather than `private`: the method was already the seam and a
1415
+ tier above simply could not reach it. */
1416
+ renderOverflowItem(one) {
1417
+ if (one === 'remote') {
1418
+ return html `<${this.tag('menu-item')} part="entry"
1419
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start"
1420
+ value="remote"
1421
+ @helix-select=${() => void this.promptRemote()}
1422
+ >
1423
+ ${drawIcon(airplay, { slot: 'start' })}
1424
+ Play on another device
1425
+ </${this.tag('menu-item')}>`;
1426
+ }
1427
+ if (one === 'speed') {
1428
+ return html `<${this.tag('menu-item')} part="entry"
1429
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="speed" @helix-select=${() => this.cycleSpeed()}>
1430
+ ${drawIcon(gauge, { slot: 'start' })}
1431
+ Playback speed: ${this.rate}x
1432
+ </${this.tag('menu-item')}>`;
1433
+ }
1434
+ if (one === 'rewind' || one === 'forward') {
1435
+ const back = one === 'rewind';
1436
+ return html `<${this.tag('menu-item')} part="entry"
1437
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start"
1438
+ value=${one}
1439
+ @helix-select=${() => this.skipBy(back ? -SKIP_SECONDS : SKIP_SECONDS)}
1440
+ >
1441
+ ${drawIcon(back ? arrowCounterClockwise : arrowClockwise, { slot: 'start' })}
1442
+ ${back ? `Back ${SKIP_SECONDS} seconds` : `Forward ${SKIP_SECONDS} seconds`}
1443
+ </${this.tag('menu-item')}>`;
1444
+ }
1445
+ if (one === 'previous') {
1446
+ return html `<${this.tag('menu-item')} part="entry"
1447
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="previous" @helix-select=${() => this.skip(true)}>
1448
+ ${drawIcon(skipBack, { slot: 'start' })}
1449
+ Previous
1450
+ </${this.tag('menu-item')}>`;
1451
+ }
1452
+ if (one === 'next') {
1453
+ return html `<${this.tag('menu-item')} part="entry"
1454
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="next" @helix-select=${() => this.skip(false)}>
1455
+ ${drawIcon(skipForward, { slot: 'start' })}
1456
+ Next
1457
+ </${this.tag('menu-item')}>`;
1458
+ }
1459
+ if (one === 'download') {
1460
+ const local = this.localFile;
1461
+ return html `<${this.tag('menu-item')} part="entry"
1462
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start"
1463
+ ?download=${local}
1464
+ href=${this.src}
1465
+ rel=${local ? nothing : 'noreferrer'}
1466
+ target=${local ? nothing : '_blank'}
1467
+ value="download"
1468
+ >
1469
+ ${drawIcon(download, { slot: 'start' })}
1470
+ Download
1471
+ </${this.tag('menu-item')}>`;
1472
+ }
1473
+ return html `<${this.tag('menu-item')} part="entry"
1474
+ exportparts="item: entry-item, mark: entry-mark, start: entry-start" value="pip" @helix-select=${() => void this.togglePip()}>
1475
+ ${drawIcon(pictureInPicture, { slot: 'start' })}
1476
+ Picture in picture
1477
+ </${this.tag('menu-item')}>`;
1478
+ }
1479
+ /* `protected` rather than `private`: the method was already the seam and a
1480
+ tier above simply could not reach it. */
1481
+ renderBar() {
1482
+ const done = this.knownDuration > 0 ? this.position / this.knownDuration : 0;
1483
+ return html `<div part="controls">
1484
+ <button
1485
+ part="play"
1486
+ type="button"
1487
+ aria-label=${this.playing ? this.pauseLabel : this.playLabel}
1488
+ @click=${() => this.toggle()}
1489
+ >
1490
+ ${drawIcon(this.playing ? pause : play)}
1491
+ </button>
1492
+ <span part="time">${clock(this.position)} / ${clock(this.knownDuration)}</span>
1493
+ <input
1494
+ part="scrubber"
1495
+ type="range"
1496
+ min="0"
1497
+ max=${Number.isFinite(this.knownDuration) && this.knownDuration > 0 ? this.knownDuration : 0}
1498
+ step="0.01"
1499
+ .value=${String(this.position)}
1500
+ aria-label="Seek"
1501
+ aria-valuetext=${`${clock(this.position)} of ${clock(this.knownDuration)}`}
1502
+ style=${`--video-progress: ${done}`}
1503
+ @input=${this.takeScrub}
1504
+ />
1505
+ <span part="sound">
1506
+ <button
1507
+ part="mute"
1508
+ type="button"
1509
+ aria-label=${this.silent ? 'Unmute' : 'Mute'}
1510
+ aria-pressed=${this.silent ? 'true' : 'false'}
1511
+ @click=${() => {
1512
+ const video = this.video;
1513
+ if (video) {
1514
+ video.muted = !video.muted;
1515
+ }
1516
+ }}
1517
+ >
1518
+ ${drawIcon(this.silent || this.loudness === 0 ? speakerX : speakerHigh)}
1519
+ </button>
1520
+ <input
1521
+ part="volume"
1522
+ type="range"
1523
+ min="0"
1524
+ max="1"
1525
+ step="0.05"
1526
+ .value=${String(this.silent ? 0 : this.loudness)}
1527
+ aria-label="Volume"
1528
+ aria-valuetext=${`${Math.round((this.silent ? 0 : this.loudness) * 100)} percent`}
1529
+ style=${`--video-level: ${this.silent ? 0 : this.loudness}`}
1530
+ @input=${this.takeVolume}
1531
+ />
1532
+ </span>
1533
+ ${this.renderCaptions()}
1534
+ ${this.extras.map((one) => this.renderExtra(one))}
1535
+ ${this.overflow.length === 0
1536
+ ? nothing
1537
+ : html `<${this.tag('menu')} part="overflow"
1538
+ exportparts="back: overflow-back, back-mark: overflow-back-mark, empty: overflow-empty,
1539
+ header: overflow-header, loading: overflow-loading, surface: overflow-surface,
1540
+ title: overflow-title" label="More controls" placement="top-end">
1541
+ <button part="more" slot="trigger" type="button" aria-label="More controls">
1542
+ ${drawIcon(dotsThreeVertical)}
1543
+ </button>
1544
+ ${this.overflow.map((one) => this.renderOverflowItem(one))}
1545
+ </${this.tag('menu')}>`}
1546
+ ${
1547
+ /* Whatever else belongs on this bar, put here by whoever knows.
1548
+ A playlist's dropdown is the case this was written for and the video
1549
+ deliberately knows nothing about it: `helix-video-playlist` renders a
1550
+ menu of its own entries into this slot, so the popover machinery and
1551
+ the notion of a set of clips both stay in the component that is about
1552
+ a set of clips. Before fullscreen, which is always the last thing on
1553
+ a control bar because it is the one that changes what the bar is
1554
+ attached to. */
1555
+ ''}
1556
+ <slot name="controls"></slot>
1557
+ <button
1558
+ part="fullscreen"
1559
+ type="button"
1560
+ aria-label=${this.full ? 'Exit fullscreen' : 'Fullscreen'}
1561
+ aria-pressed=${this.full ? 'true' : 'false'}
1562
+ @click=${() => void this.toggleFullscreen()}
1563
+ >
1564
+ ${drawIcon(this.full ? cornersIn : cornersOut)}
1565
+ </button>
1566
+ </div>`;
1567
+ }
1568
+ render() {
1569
+ /* One focusable box around the lot, which is where the keys land. A video
1570
+ without native controls is not focusable at all, and the buttons under it
1571
+ are, so without this a reader could tab to Play and never to the picture
1572
+ the keys are about. `role="group"` with a name so what they landed on
1573
+ announces as something rather than as an unnamed div. */
1574
+ return html `<div
1575
+ part="stage"
1576
+ tabindex=${this.controls ? nothing : '0'}
1577
+ role=${this.controls ? nothing : 'group'}
1578
+ aria-label=${this.controls ? nothing : this.label || 'Video'}
1579
+ @keydown=${this.handleKeydown}
1580
+ @pointermove=${this.stir}
1581
+ @pointerleave=${() => {
1582
+ /* Leaving is the one case that does not wait: the reader has gone
1583
+ somewhere else, and there is nothing to reach for. */
1584
+ clearTimeout(this.idleTimer);
1585
+ this.idle = true;
1586
+ }}
1587
+ @focusin=${this.stir}
1588
+ >
1589
+ <video
1590
+ part="video"
1591
+ aria-label=${this.label || nothing}
1592
+ src=${this.src || nothing}
1593
+ poster=${this.poster || nothing}
1594
+ preload=${this.preload}
1595
+ crossorigin=${this.crossOrigin ?? nothing}
1596
+ ?autoplay=${this.autoplay}
1597
+ ?controls=${this.controls}
1598
+ ?disablepictureinpicture=${this.disablePictureInPicture}
1599
+ ?disableremoteplayback=${this.disableRemotePlayback}
1600
+ ?loop=${this.loop}
1601
+ ?muted=${this.defaultMuted}
1602
+ ?playsinline=${this.playsInline}
1603
+ @loadedmetadata=${this.handleLoaded}
1604
+ @timeupdate=${this.handleTime}
1605
+ @volumechange=${this.handleVolume}
1606
+ @play=${this.handleState}
1607
+ @pause=${this.handleState}
1608
+ @ended=${this.handleState}
1609
+ @emptied=${this.handleState}
1610
+ @waiting=${this.handleWaiting}
1611
+ @playing=${this.handleReady}
1612
+ @canplay=${this.handleReady}
1613
+ @loadstart=${this.handleReady}
1614
+ @error=${this.handleError}
1615
+ >
1616
+ ${this.media.map((one) => one.tag === 'track'
1617
+ ? html `<track
1618
+ ?default=${'default' in one.attrs}
1619
+ kind=${one.attrs.kind ?? nothing}
1620
+ label=${one.attrs.label ?? nothing}
1621
+ src=${one.attrs.src ?? nothing}
1622
+ srclang=${one.attrs.srclang ?? nothing}
1623
+ />`
1624
+ : html `<source
1625
+ media=${one.attrs.media ?? nothing}
1626
+ sizes=${one.attrs.sizes ?? nothing}
1627
+ src=${one.attrs.src ?? nothing}
1628
+ srcset=${one.attrs.srcset ?? nothing}
1629
+ type=${one.attrs.type ?? nothing}
1630
+ />`)}
1631
+ </video>
1632
+ <!-- Outside the video, and hidden. It exists to notice what the consumer
1633
+ wrote, not to be rendered: a slot inside a media element is the one
1634
+ child its resource selection sees, and a slot is not a source.
1635
+ No backticks in here: this comment is inside a template literal, and
1636
+ the first one ends it. -->
1637
+ <span aria-hidden="true" hidden><slot @slotchange=${this.handleMedia}></slot></span>
1638
+ ${this.cue === undefined ? nothing : html `<p aria-live="off" part="cue">${this.cue}</p>`}
1639
+ ${
1640
+ /* Top left, over the frame, and empty unless something is slotted.
1641
+ A player inside a set has somewhere to go back to and a player on its
1642
+ own does not, so the video draws whatever it is handed and decides
1643
+ nothing: a `back` property would be this component holding an opinion
1644
+ about navigation, which is the consumer's. */
1645
+ ''}
1646
+ <div part="back"><slot name="back"></slot></div>
1647
+ <slot name="poster" part="poster"></slot>
1648
+ ${
1649
+ /* A live region rather than a spinning box, because the reader who most
1650
+ needs to be told the picture is waiting is the one who cannot see it
1651
+ spin. `polite`, so it waits its turn, and it goes when the frame moves
1652
+ again. */
1653
+ this.buffering && !this.failed
1654
+ ? html `<div
1655
+ part="loading"
1656
+ role="status"
1657
+ aria-live="polite"
1658
+ >
1659
+ <span part="loading-mark" aria-hidden="true"></span>
1660
+ ${this.loadingLabel}
1661
+ </div>`
1662
+ : nothing}
1663
+ ${
1664
+ /* And the other thing a frozen frame can mean. `alert`, because nothing
1665
+ further is going to happen and a reader waiting politely for a video
1666
+ that will never load is waiting forever. */
1667
+ this.failed
1668
+ ? html `<div
1669
+ part="failure"
1670
+ role="alert"
1671
+ >
1672
+ ${this.errorLabel}
1673
+ </div>`
1674
+ : nothing}
1675
+ ${
1676
+ /* Whenever it is not playing, which is what the mark means: press this
1677
+ to play. It said that only before the first play, on the reasoning
1678
+ that a paused video keeps its bar so the small control is already
1679
+ there. True, and it makes the mark mean two different things at two
1680
+ moments that are the same state. Every player a reader has used shows
1681
+ it on pause, and the one who paused to study a frame clears it by
1682
+ pressing the thing in the way.
1683
+
1684
+ Not drawn at all where the browser draws its own controls, since that
1685
+ bar has a play button and two would be two. */
1686
+ this.controls ||
1687
+ this.playing ||
1688
+ this.buffering ||
1689
+ this.failed ||
1690
+ this.startControl === 'none'
1691
+ ? nothing
1692
+ : html `<button
1693
+ part="start"
1694
+ type="button"
1695
+ aria-label=${this.playLabel}
1696
+ @click=${() => this.toggle()}
1697
+ >
1698
+ ${drawIcon(play)}
1699
+ </button>`}
1700
+ ${this.controls ? nothing : this.renderBar()}
1701
+ </div>`;
1702
+ }
1703
+ };
1704
+ __decorate([
1705
+ property()
1706
+ ], HelixHeadlessVideo.prototype, "src", void 0);
1707
+ __decorate([
1708
+ property()
1709
+ ], HelixHeadlessVideo.prototype, "poster", void 0);
1710
+ __decorate([
1711
+ property()
1712
+ ], HelixHeadlessVideo.prototype, "label", void 0);
1713
+ __decorate([
1714
+ property({ reflect: true, type: Boolean })
1715
+ ], HelixHeadlessVideo.prototype, "loop", void 0);
1716
+ __decorate([
1717
+ property({ attribute: 'muted', reflect: true, type: Boolean })
1718
+ ], HelixHeadlessVideo.prototype, "defaultMuted", void 0);
1719
+ __decorate([
1720
+ property({ attribute: 'playsinline', converter: onUnlessFalse })
1721
+ ], HelixHeadlessVideo.prototype, "playsInline", void 0);
1722
+ __decorate([
1723
+ property({ reflect: true, type: Boolean })
1724
+ ], HelixHeadlessVideo.prototype, "controls", void 0);
1725
+ __decorate([
1726
+ property()
1727
+ ], HelixHeadlessVideo.prototype, "preload", void 0);
1728
+ __decorate([
1729
+ property({ reflect: true, type: Boolean })
1730
+ ], HelixHeadlessVideo.prototype, "autoplay", void 0);
1731
+ __decorate([
1732
+ property({ attribute: 'crossorigin' })
1733
+ ], HelixHeadlessVideo.prototype, "crossOrigin", void 0);
1734
+ __decorate([
1735
+ property({ attribute: 'disablepictureinpicture', reflect: true, type: Boolean })
1736
+ ], HelixHeadlessVideo.prototype, "disablePictureInPicture", void 0);
1737
+ __decorate([
1738
+ property({ attribute: 'disableremoteplayback', reflect: true, type: Boolean })
1739
+ ], HelixHeadlessVideo.prototype, "disableRemotePlayback", void 0);
1740
+ __decorate([
1741
+ property({ attribute: 'extra-controls' })
1742
+ ], HelixHeadlessVideo.prototype, "extraControls", void 0);
1743
+ __decorate([
1744
+ property({ attribute: 'overflow-controls' })
1745
+ ], HelixHeadlessVideo.prototype, "overflowControls", void 0);
1746
+ __decorate([
1747
+ property({ attribute: 'start-control' })
1748
+ ], HelixHeadlessVideo.prototype, "startControl", void 0);
1749
+ __decorate([
1750
+ property({ attribute: 'speeds' })
1751
+ ], HelixHeadlessVideo.prototype, "speeds", void 0);
1752
+ __decorate([
1753
+ property({ attribute: 'idle-delay', type: Number })
1754
+ ], HelixHeadlessVideo.prototype, "idleDelay", void 0);
1755
+ __decorate([
1756
+ property({ attribute: 'play-label' })
1757
+ ], HelixHeadlessVideo.prototype, "playLabel", void 0);
1758
+ __decorate([
1759
+ property({ attribute: 'pause-label' })
1760
+ ], HelixHeadlessVideo.prototype, "pauseLabel", void 0);
1761
+ __decorate([
1762
+ property({ attribute: 'loading-label' })
1763
+ ], HelixHeadlessVideo.prototype, "loadingLabel", void 0);
1764
+ __decorate([
1765
+ property({ attribute: 'error-label' })
1766
+ ], HelixHeadlessVideo.prototype, "errorLabel", void 0);
1767
+ __decorate([
1768
+ state()
1769
+ ], HelixHeadlessVideo.prototype, "buffering", void 0);
1770
+ __decorate([
1771
+ state()
1772
+ ], HelixHeadlessVideo.prototype, "failed", void 0);
1773
+ __decorate([
1774
+ state()
1775
+ ], HelixHeadlessVideo.prototype, "playing", void 0);
1776
+ __decorate([
1777
+ state()
1778
+ ], HelixHeadlessVideo.prototype, "position", void 0);
1779
+ __decorate([
1780
+ state()
1781
+ ], HelixHeadlessVideo.prototype, "knownDuration", void 0);
1782
+ __decorate([
1783
+ state()
1784
+ ], HelixHeadlessVideo.prototype, "loudness", void 0);
1785
+ __decorate([
1786
+ state()
1787
+ ], HelixHeadlessVideo.prototype, "silent", void 0);
1788
+ __decorate([
1789
+ state()
1790
+ ], HelixHeadlessVideo.prototype, "full", void 0);
1791
+ __decorate([
1792
+ state()
1793
+ ], HelixHeadlessVideo.prototype, "captioned", void 0);
1794
+ __decorate([
1795
+ state()
1796
+ ], HelixHeadlessVideo.prototype, "captionIndex", void 0);
1797
+ __decorate([
1798
+ state()
1799
+ ], HelixHeadlessVideo.prototype, "captionCount", void 0);
1800
+ __decorate([
1801
+ state()
1802
+ ], HelixHeadlessVideo.prototype, "cue", void 0);
1803
+ __decorate([
1804
+ state()
1805
+ ], HelixHeadlessVideo.prototype, "rate", void 0);
1806
+ __decorate([
1807
+ state()
1808
+ ], HelixHeadlessVideo.prototype, "idle", void 0);
1809
+ __decorate([
1810
+ state()
1811
+ ], HelixHeadlessVideo.prototype, "remoteAvailable", void 0);
1812
+ __decorate([
1813
+ state()
1814
+ ], HelixHeadlessVideo.prototype, "remoteConnected", void 0);
1815
+ __decorate([
1816
+ state()
1817
+ ], HelixHeadlessVideo.prototype, "media", void 0);
1818
+ HelixHeadlessVideo = __decorate([
1819
+ customElement('helix-headless-video')
1820
+ ], HelixHeadlessVideo);
1821
+ export { HelixHeadlessVideo };