tempest-react-sdk 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +74 -73
  2. package/dist/components/AppBar/AppBar.module.cjs.map +1 -1
  3. package/dist/components/AppBar/AppBar.module.js.map +1 -1
  4. package/dist/components/AudioPlayer/AudioPlayer.cjs +1 -1
  5. package/dist/components/AudioPlayer/AudioPlayer.js +6 -6
  6. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
  7. package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
  8. package/dist/components/VideoPlayer/VideoPlayer.cjs +2 -0
  9. package/dist/components/VideoPlayer/VideoPlayer.cjs.map +1 -0
  10. package/dist/components/VideoPlayer/VideoPlayer.js +229 -0
  11. package/dist/components/VideoPlayer/VideoPlayer.js.map +1 -0
  12. package/dist/components/VideoPlayer/VideoPlayer.module.cjs +2 -0
  13. package/dist/components/VideoPlayer/VideoPlayer.module.cjs.map +1 -0
  14. package/dist/components/VideoPlayer/VideoPlayer.module.js +19 -0
  15. package/dist/components/VideoPlayer/VideoPlayer.module.js.map +1 -0
  16. package/dist/components/VideoPlayer/playback-rates.cjs +2 -0
  17. package/dist/components/VideoPlayer/playback-rates.cjs.map +1 -0
  18. package/dist/components/VideoPlayer/playback-rates.js +11 -0
  19. package/dist/components/VideoPlayer/playback-rates.js.map +1 -0
  20. package/dist/hooks/use-push-to-talk.cjs +1 -1
  21. package/dist/hooks/use-push-to-talk.cjs.map +1 -1
  22. package/dist/hooks/use-push-to-talk.js +2 -2
  23. package/dist/hooks/use-push-to-talk.js.map +1 -1
  24. package/dist/imaging/exceptions.cjs +1 -1
  25. package/dist/imaging/exceptions.cjs.map +1 -1
  26. package/dist/imaging/exceptions.js +5 -1
  27. package/dist/imaging/exceptions.js.map +1 -1
  28. package/dist/imaging/frame.cjs +2 -0
  29. package/dist/imaging/frame.cjs.map +1 -0
  30. package/dist/imaging/frame.js +82 -0
  31. package/dist/imaging/frame.js.map +1 -0
  32. package/dist/imaging.cjs +1 -1
  33. package/dist/imaging.d.ts +611 -494
  34. package/dist/imaging.js +10 -9
  35. package/dist/styles/AppBar.css +1 -1
  36. package/dist/styles/Scheduler.css +2 -2
  37. package/dist/styles/VideoPlayer.css +23 -0
  38. package/dist/styles/core.css +1 -1
  39. package/dist/styles/layout.css +2 -2
  40. package/dist/styles/media.css +22 -0
  41. package/dist/styles/navigation.css +1 -1
  42. package/dist/styles.css +1 -1
  43. package/dist/tempest-react-sdk.cjs +1 -1
  44. package/dist/tempest-react-sdk.d.ts +317 -35
  45. package/dist/tempest-react-sdk.js +213 -211
  46. package/dist/webrtc/link-stats.cjs +1 -1
  47. package/dist/webrtc/link-stats.cjs.map +1 -1
  48. package/dist/webrtc/link-stats.js +134 -44
  49. package/dist/webrtc/link-stats.js.map +1 -1
  50. package/dist/webrtc/mesh-quality.cjs +1 -1
  51. package/dist/webrtc/mesh-quality.cjs.map +1 -1
  52. package/dist/webrtc/mesh-quality.js +8 -2
  53. package/dist/webrtc/mesh-quality.js.map +1 -1
  54. package/dist/webrtc/peer-mesh.cjs +1 -1
  55. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  56. package/dist/webrtc/peer-mesh.js +85 -63
  57. package/dist/webrtc/peer-mesh.js.map +1 -1
  58. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Scheduler.module.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n/*\n * Absolutely positioned, so the inline `grid-column` it is placed with must name\n * BOTH lines. For an abspos child of a grid container an `auto` side does not\n * mean \"span 1\" — CSS Grid §9.2 resolves it to the container's padding edge, so\n * a bare `grid-column: 3` runs from column 3 to the end of the week and every\n * event covers the whole view. A one-day scheduler hides it: there, \"column 1 to\n * the edge\" happens to be one column. Hence `N / span 1` in Scheduler.tsx.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-contrast, #fff);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"Scheduler.module.js","names":[],"sources":["../../../src/components/Scheduler/Scheduler.module.css"],"sourcesContent":[".wrapper {\n /*\n * Column widths are declared once here and reused by the header, the all-day\n * lane and the grid — three separate grids that have to line up exactly, and\n * would drift the moment any of them computed its own tracks.\n */\n --tempest-scheduler-gutter: 3.5rem;\n --tempest-scheduler-height: 32rem;\n\n display: flex;\n flex-direction: column;\n width: 100%;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-bg);\n font-family: var(--tempest-font-sans);\n overflow: hidden;\n}\n\n.head,\n.allDayLane {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) repeat(\n var(--tempest-scheduler-days),\n minmax(0, 1fr)\n );\n border-bottom: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.dayHead {\n padding: var(--tempest-space-2);\n text-align: center;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-semibold);\n letter-spacing: var(--tempest-tracking-wide);\n text-transform: capitalize;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayHead.today {\n color: var(--tempest-primary);\n}\n\n.gutterHead {\n padding: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n text-align: right;\n}\n\n.allDayTrack {\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n grid-column: 2 / -1;\n gap: 2px;\n padding: 2px;\n}\n\n.allDayEvent {\n overflow: hidden;\n padding: 0 var(--tempest-space-2);\n border: none;\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.5rem;\n text-align: left;\n text-overflow: ellipsis;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.allDayEvent:disabled {\n cursor: default;\n}\n\n.body {\n display: grid;\n grid-template-columns: var(--tempest-scheduler-gutter) 1fr;\n height: var(--tempest-scheduler-height);\n overflow-y: auto;\n}\n\n.gutter {\n position: relative;\n border-right: 1px solid var(--tempest-border);\n}\n\n.hourLabel {\n position: absolute;\n right: var(--tempest-space-1);\n /*\n * Nudged up by half a line so the label reads as sitting *on* its hour line\n * rather than hanging below it.\n */\n transform: translateY(-50%);\n color: var(--tempest-text-muted);\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.grid {\n position: relative;\n display: grid;\n grid-template-columns: repeat(var(--tempest-scheduler-days), minmax(0, 1fr));\n}\n\n.dayColumn {\n position: relative;\n grid-row: 1;\n border-left: 1px solid var(--tempest-border);\n}\n\n.dayColumn:first-child {\n border-left: none;\n}\n\n.todayColumn {\n background-color: var(--tempest-surface);\n}\n\n.hourLine {\n position: absolute;\n left: 0;\n right: 0;\n border-top: 1px solid var(--tempest-border);\n pointer-events: none;\n}\n\n/*\n * Events live in the same grid as the day columns, one row deep, positioned inside\n * their column by percentage. Being siblings of the columns rather than children is\n * what lets an event overlay the hour lines without clipping.\n */\n/*\n * Absolutely positioned, so the inline `grid-column` it is placed with must name\n * BOTH lines. For an abspos child of a grid container an `auto` side does not\n * mean \"span 1\" — CSS Grid §9.2 resolves it to the container's padding edge, so\n * a bare `grid-column: 3` runs from column 3 to the end of the week and every\n * event covers the whole view. A one-day scheduler hides it: there, \"column 1 to\n * the edge\" happens to be one column. Hence `N / span 1` in Scheduler.tsx.\n */\n.event {\n position: absolute;\n grid-row: 1;\n display: flex;\n flex-direction: column;\n gap: 1px;\n overflow: hidden;\n padding: 2px var(--tempest-space-1);\n border: 1px solid var(--tempest-bg);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.event:disabled {\n cursor: default;\n}\n\n.event:focus-visible {\n outline: 2px solid var(--tempest-text);\n outline-offset: -2px;\n}\n\n/*\n * No `opacity` here, deliberately. Dimming the time to signal it as secondary drops\n * it below the AA contrast threshold against the event fill — the browser `axe` sweep\n * caught exactly that (jsdom cannot, since it does not paint). The hierarchy comes\n * from size and weight instead, which cost nothing in contrast.\n */\n.eventTime {\n font-size: 0.625rem;\n font-variant-numeric: tabular-nums;\n}\n\n.eventTitle {\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n line-height: var(--tempest-leading-snug);\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.nowLine {\n position: absolute;\n left: 0;\n right: 0;\n grid-row: 1;\n grid-column: 1 / -1;\n height: 0;\n border-top: 2px solid var(--tempest-danger, #dc2626);\n pointer-events: none;\n z-index: 1;\n}\n\n@media (max-width: 640px) {\n .wrapper {\n --tempest-scheduler-gutter: 2.75rem;\n }\n\n .dayHead {\n font-size: 0.625rem;\n padding: var(--tempest-space-1);\n }\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ const e=require("../../utils/cn.cjs"),t=require("../../hooks/use-fullscreen.cjs"),n=require("../../hooks/use-object-url.cjs"),r=require("./playback-rates.cjs"),i=require("../../audio/duration.cjs"),a=require("./VideoPlayer.module.cjs");let o=require("react"),s=require("react/jsx-runtime"),c=require("lucide-react");var l={"pt-BR":{play:`Tocar`,pause:`Pausar`,seek:`Posição`,mute:`Silenciar`,unmute:`Ativar som`,volume:`Volume`,rate:`Velocidade`,enterFullscreen:`Tela cheia`,exitFullscreen:`Sair da tela cheia`},en:{play:`Play`,pause:`Pause`,seek:`Seek`,mute:`Mute`,unmute:`Unmute`,volume:`Volume`,rate:`Speed`,enterFullscreen:`Full screen`,exitFullscreen:`Exit full screen`}};function u(e,t){return`${e.toLocaleString(t===`en`?`en-US`:`pt-BR`)}×`}function d({src:d,durationMs:f,poster:p,tracks:m,aspectRatio:h=16/9,rate:g,rates:_=r.DEFAULT_PLAYBACK_RATES,onRateChange:v,shiftPitch:y=!1,fullscreen:b=!0,autoPlay:x=!1,loop:S=!1,muted:C=!1,locale:w=`pt-BR`,actions:T,onEnded:E,onError:D,disabled:O=!1,className:k,...A}){let j=l[w],M=(0,o.useRef)(null),N=(0,o.useRef)(null),P=(0,o.useRef)(!1),[F,I]=(0,o.useState)(!1),[L,R]=(0,o.useState)(0),[z,B]=(0,o.useState)(1/0),[V,H]=(0,o.useState)(g??1),[U,W]=(0,o.useState)(1),[G,K]=(0,o.useState)(C),q=t.useFullscreen(M),J=n.useObjectUrl(typeof d==`string`?null:d),Y=typeof d==`string`?d:J,X=g??V,Z=Number.isFinite(f)?f:z,Q=Number.isFinite(Z)&&Z>0;(0,o.useEffect)(()=>{R(0),I(!1),B(1/0),P.current=!1},[Y]),(0,o.useEffect)(()=>{let e=N.current;e&&(e.defaultPlaybackRate=X,e.playbackRate=X,e.preservesPitch=!y)},[X,y,Y]),(0,o.useEffect)(()=>{let e=N.current;e&&(e.volume=U,e.muted=G)},[U,G,Y]);let $=()=>{let e=N.current;if(!e)return;if(Number.isFinite(e.duration)){B(e.duration*1e3);return}if(P.current||Number.isFinite(f))return;P.current=!0;let t=()=>{e.removeEventListener(`timeupdate`,t),Number.isFinite(e.duration)&&B(e.duration*1e3),e.currentTime=0};e.addEventListener(`timeupdate`,t),e.currentTime=1e101},ee=()=>{let e=N.current;if(!(!e||!Y)){if(e.paused){Promise.resolve(e.play()).then(()=>I(!0)).catch(e=>D?.(e));return}e.pause(),I(!1)}},te=e=>{let t=N.current;!t||!Q||(t.currentTime=e/1e3,R(e))},ne=e=>{g===void 0&&H(e),v?.(e)};return(0,s.jsxs)(`div`,{ref:M,className:e.cn(a.default.player,q.isFullscreen&&a.default.immersive,k),...A,children:[(0,s.jsx)(`div`,{className:a.default.frame,style:{aspectRatio:h},children:(0,s.jsx)(`video`,{ref:N,className:a.default.video,src:Y??void 0,poster:p,loop:S,autoPlay:x,playsInline:!0,preload:`metadata`,onLoadedMetadata:$,onDurationChange:$,onTimeUpdate:()=>{let e=N.current;e&&R(e.currentTime*1e3)},onPlay:()=>I(!0),onPause:()=>I(!1),onVolumeChange:()=>{let e=N.current;e&&(W(e.volume),K(e.muted))},onEnded:()=>{I(!1),E?.()},onError:e=>D?.(e),children:m?.map(e=>(0,s.jsx)(`track`,{kind:e.kind??`captions`,src:e.src,srcLang:e.srcLang,label:e.label,default:e.default},`${e.kind??`captions`}-${e.srcLang}-${e.src}`))})}),(0,s.jsxs)(`div`,{className:a.default.chrome,children:[(0,s.jsx)(`button`,{type:`button`,className:a.default.transport,onClick:ee,disabled:O||!Y,"aria-label":F?j.pause:j.play,title:F?j.pause:j.play,children:F?(0,s.jsx)(c.Pause,{size:16,"aria-hidden":!0}):(0,s.jsx)(c.Play,{size:16,"aria-hidden":!0})}),(0,s.jsx)(`input`,{type:`range`,className:a.default.seek,min:0,max:Q?Math.round(Z):0,step:100,value:Math.min(Math.round(L),Q?Math.round(Z):0),onChange:e=>te(Number(e.target.value)),disabled:O||!Q,"aria-label":j.seek,"aria-valuetext":`${i.formatDuration(L)} / ${i.formatDuration(Z)}`}),(0,s.jsxs)(`span`,{className:a.default.time,children:[(0,s.jsx)(`span`,{children:i.formatDuration(L)}),(0,s.jsx)(`span`,{"aria-hidden":`true`,children:`/`}),(0,s.jsx)(`span`,{children:i.formatDuration(Z)})]}),(0,s.jsx)(`button`,{type:`button`,className:a.default.icon,onClick:()=>K(e=>!e),disabled:O,"aria-label":G?j.unmute:j.mute,title:G?j.unmute:j.mute,children:G?(0,s.jsx)(c.VolumeX,{size:16,"aria-hidden":!0}):(0,s.jsx)(c.Volume2,{size:16,"aria-hidden":!0})}),(0,s.jsx)(`input`,{type:`range`,className:a.default.volume,min:0,max:1,step:.05,value:G?0:U,onChange:e=>{let t=Number(e.target.value);W(t),K(t===0)},disabled:O,"aria-label":j.volume}),_.length>0&&(0,s.jsx)(`select`,{className:a.default.rate,value:X,onChange:e=>ne(Number(e.target.value)),disabled:O,"aria-label":j.rate,title:j.rate,children:_.map(e=>(0,s.jsx)(`option`,{value:e,children:u(e,w)},e))}),b&&q.supported&&(0,s.jsx)(`button`,{type:`button`,className:a.default.icon,onClick:()=>{q.toggle().catch(e=>D?.(e))},disabled:O,"aria-label":q.isFullscreen?j.exitFullscreen:j.enterFullscreen,title:q.isFullscreen?j.exitFullscreen:j.enterFullscreen,children:q.isFullscreen?(0,s.jsx)(c.Minimize,{size:16,"aria-hidden":!0}):(0,s.jsx)(c.Maximize,{size:16,"aria-hidden":!0})}),T&&(0,s.jsx)(`span`,{className:a.default.actions,children:T})]})]})}exports.VideoPlayer=d;
2
+ //# sourceMappingURL=VideoPlayer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoPlayer.cjs","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,229 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { useFullscreen as t } from "../../hooks/use-fullscreen.js";
3
+ import { useObjectUrl as n } from "../../hooks/use-object-url.js";
4
+ import { DEFAULT_PLAYBACK_RATES as r } from "./playback-rates.js";
5
+ import { formatDuration as i } from "../../audio/duration.js";
6
+ import a from "./VideoPlayer.module.js";
7
+ import { useEffect as o, useRef as s, useState as c } from "react";
8
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
9
+ import { Maximize as d, Minimize as ee, Pause as te, Play as ne, Volume2 as f, VolumeX as p } from "lucide-react";
10
+ //#region src/components/VideoPlayer/VideoPlayer.tsx
11
+ var re = {
12
+ "pt-BR": {
13
+ play: "Tocar",
14
+ pause: "Pausar",
15
+ seek: "Posição",
16
+ mute: "Silenciar",
17
+ unmute: "Ativar som",
18
+ volume: "Volume",
19
+ rate: "Velocidade",
20
+ enterFullscreen: "Tela cheia",
21
+ exitFullscreen: "Sair da tela cheia"
22
+ },
23
+ en: {
24
+ play: "Play",
25
+ pause: "Pause",
26
+ seek: "Seek",
27
+ mute: "Mute",
28
+ unmute: "Unmute",
29
+ volume: "Volume",
30
+ rate: "Speed",
31
+ enterFullscreen: "Full screen",
32
+ exitFullscreen: "Exit full screen"
33
+ }
34
+ };
35
+ function ie(e, t) {
36
+ return `${e.toLocaleString(t === "en" ? "en-US" : "pt-BR")}×`;
37
+ }
38
+ function m({ src: m, durationMs: h, poster: g, tracks: _, aspectRatio: v = 16 / 9, rate: y, rates: b = r, onRateChange: x, shiftPitch: S = !1, fullscreen: C = !0, autoPlay: w = !1, loop: T = !1, muted: E = !1, locale: D = "pt-BR", actions: O, onEnded: k, onError: A, disabled: j = !1, className: ae, ...oe }) {
39
+ let M = re[D], N = s(null), P = s(null), F = s(!1), [I, L] = c(!1), [R, z] = c(0), [B, V] = c(Infinity), [H, U] = c(y ?? 1), [W, G] = c(1), [K, q] = c(E), J = t(N), se = n(typeof m == "string" ? null : m), Y = typeof m == "string" ? m : se, X = y ?? H, Z = Number.isFinite(h) ? h : B, Q = Number.isFinite(Z) && Z > 0;
40
+ o(() => {
41
+ z(0), L(!1), V(Infinity), F.current = !1;
42
+ }, [Y]), o(() => {
43
+ let e = P.current;
44
+ e && (e.defaultPlaybackRate = X, e.playbackRate = X, e.preservesPitch = !S);
45
+ }, [
46
+ X,
47
+ S,
48
+ Y
49
+ ]), o(() => {
50
+ let e = P.current;
51
+ e && (e.volume = W, e.muted = K);
52
+ }, [
53
+ W,
54
+ K,
55
+ Y
56
+ ]);
57
+ let $ = () => {
58
+ let e = P.current;
59
+ if (!e) return;
60
+ if (Number.isFinite(e.duration)) {
61
+ V(e.duration * 1e3);
62
+ return;
63
+ }
64
+ if (F.current || Number.isFinite(h)) return;
65
+ F.current = !0;
66
+ let t = () => {
67
+ e.removeEventListener("timeupdate", t), Number.isFinite(e.duration) && V(e.duration * 1e3), e.currentTime = 0;
68
+ };
69
+ e.addEventListener("timeupdate", t), e.currentTime = 1e101;
70
+ }, ce = () => {
71
+ let e = P.current;
72
+ if (!(!e || !Y)) {
73
+ if (e.paused) {
74
+ Promise.resolve(e.play()).then(() => L(!0)).catch((e) => A?.(e));
75
+ return;
76
+ }
77
+ e.pause(), L(!1);
78
+ }
79
+ }, le = (e) => {
80
+ let t = P.current;
81
+ !t || !Q || (t.currentTime = e / 1e3, z(e));
82
+ }, ue = (e) => {
83
+ y === void 0 && U(e), x?.(e);
84
+ };
85
+ return /* @__PURE__ */ u("div", {
86
+ ref: N,
87
+ className: e(a.player, J.isFullscreen && a.immersive, ae),
88
+ ...oe,
89
+ children: [/* @__PURE__ */ l("div", {
90
+ className: a.frame,
91
+ style: { aspectRatio: v },
92
+ children: /* @__PURE__ */ l("video", {
93
+ ref: P,
94
+ className: a.video,
95
+ src: Y ?? void 0,
96
+ poster: g,
97
+ loop: T,
98
+ autoPlay: w,
99
+ playsInline: !0,
100
+ preload: "metadata",
101
+ onLoadedMetadata: $,
102
+ onDurationChange: $,
103
+ onTimeUpdate: () => {
104
+ let e = P.current;
105
+ e && z(e.currentTime * 1e3);
106
+ },
107
+ onPlay: () => L(!0),
108
+ onPause: () => L(!1),
109
+ onVolumeChange: () => {
110
+ let e = P.current;
111
+ e && (G(e.volume), q(e.muted));
112
+ },
113
+ onEnded: () => {
114
+ L(!1), k?.();
115
+ },
116
+ onError: (e) => A?.(e),
117
+ children: _?.map((e) => /* @__PURE__ */ l("track", {
118
+ kind: e.kind ?? "captions",
119
+ src: e.src,
120
+ srcLang: e.srcLang,
121
+ label: e.label,
122
+ default: e.default
123
+ }, `${e.kind ?? "captions"}-${e.srcLang}-${e.src}`))
124
+ })
125
+ }), /* @__PURE__ */ u("div", {
126
+ className: a.chrome,
127
+ children: [
128
+ /* @__PURE__ */ l("button", {
129
+ type: "button",
130
+ className: a.transport,
131
+ onClick: ce,
132
+ disabled: j || !Y,
133
+ "aria-label": I ? M.pause : M.play,
134
+ title: I ? M.pause : M.play,
135
+ children: l(I ? te : ne, {
136
+ size: 16,
137
+ "aria-hidden": !0
138
+ })
139
+ }),
140
+ /* @__PURE__ */ l("input", {
141
+ type: "range",
142
+ className: a.seek,
143
+ min: 0,
144
+ max: Q ? Math.round(Z) : 0,
145
+ step: 100,
146
+ value: Math.min(Math.round(R), Q ? Math.round(Z) : 0),
147
+ onChange: (e) => le(Number(e.target.value)),
148
+ disabled: j || !Q,
149
+ "aria-label": M.seek,
150
+ "aria-valuetext": `${i(R)} / ${i(Z)}`
151
+ }),
152
+ /* @__PURE__ */ u("span", {
153
+ className: a.time,
154
+ children: [
155
+ /* @__PURE__ */ l("span", { children: i(R) }),
156
+ /* @__PURE__ */ l("span", {
157
+ "aria-hidden": "true",
158
+ children: "/"
159
+ }),
160
+ /* @__PURE__ */ l("span", { children: i(Z) })
161
+ ]
162
+ }),
163
+ /* @__PURE__ */ l("button", {
164
+ type: "button",
165
+ className: a.icon,
166
+ onClick: () => q((e) => !e),
167
+ disabled: j,
168
+ "aria-label": K ? M.unmute : M.mute,
169
+ title: K ? M.unmute : M.mute,
170
+ children: l(K ? p : f, {
171
+ size: 16,
172
+ "aria-hidden": !0
173
+ })
174
+ }),
175
+ /* @__PURE__ */ l("input", {
176
+ type: "range",
177
+ className: a.volume,
178
+ min: 0,
179
+ max: 1,
180
+ step: .05,
181
+ value: K ? 0 : W,
182
+ onChange: (e) => {
183
+ let t = Number(e.target.value);
184
+ G(t), q(t === 0);
185
+ },
186
+ disabled: j,
187
+ "aria-label": M.volume
188
+ }),
189
+ b.length > 0 && /* @__PURE__ */ l("select", {
190
+ className: a.rate,
191
+ value: X,
192
+ onChange: (e) => ue(Number(e.target.value)),
193
+ disabled: j,
194
+ "aria-label": M.rate,
195
+ title: M.rate,
196
+ children: b.map((e) => /* @__PURE__ */ l("option", {
197
+ value: e,
198
+ children: ie(e, D)
199
+ }, e))
200
+ }),
201
+ C && J.supported && /* @__PURE__ */ l("button", {
202
+ type: "button",
203
+ className: a.icon,
204
+ onClick: () => {
205
+ J.toggle().catch((e) => A?.(e));
206
+ },
207
+ disabled: j,
208
+ "aria-label": J.isFullscreen ? M.exitFullscreen : M.enterFullscreen,
209
+ title: J.isFullscreen ? M.exitFullscreen : M.enterFullscreen,
210
+ children: J.isFullscreen ? /* @__PURE__ */ l(ee, {
211
+ size: 16,
212
+ "aria-hidden": !0
213
+ }) : /* @__PURE__ */ l(d, {
214
+ size: 16,
215
+ "aria-hidden": !0
216
+ })
217
+ }),
218
+ O && /* @__PURE__ */ l("span", {
219
+ className: a.actions,
220
+ children: O
221
+ })
222
+ ]
223
+ })]
224
+ });
225
+ }
226
+ //#endregion
227
+ export { m as VideoPlayer };
228
+
229
+ //# sourceMappingURL=VideoPlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoPlayer.js","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count, function-lines — `src` takes a URL or a Blob, and\n * every other prop is a facet of playing it: durationMs (a MediaRecorder WebM may\n * carry none), poster, captions, aspect ratio, the rate control and its presets,\n * pitch, fullscreen, autoPlay/loop/muted, onEnded/onError, plus the actions slot and\n * locale. The body is the transport state machine — object-URL lifecycle, seek, rate,\n * volume — around one video element ref.\n */\nimport { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from \"lucide-react\";\nimport { useEffect, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { formatDuration } from \"@/audio/duration\";\nimport { useFullscreen } from \"@/hooks/use-fullscreen\";\nimport { useObjectUrl } from \"@/hooks/use-object-url\";\nimport { cn } from \"@/utils/cn\";\n\nimport { DEFAULT_PLAYBACK_RATES } from \"./playback-rates\";\nimport styles from \"./VideoPlayer.module.css\";\n\n/**\n * DOM attributes this component redefines.\n *\n * `onRateChange` is a real collision, not tidiness: React types it on every\n * element as the media `ratechange` handler, `ReactEventHandler`. Ours carries\n * the rate the viewer picked, and a `(rate: number) => void` is not assignable\n * to a handler that receives an event — same shape of clash as `Input`'s `size`\n * against `HTMLInputElement.size`. The DOM one is useless on a `<div>` anyway:\n * the event fires on the media element inside, which this component owns.\n */\ntype OverriddenDomProps = \"children\" | \"onRateChange\";\n\n/** One caption or subtitle file, rendered as a `<track>`. */\nexport interface VideoTextTrack {\n /** URL of the WebVTT file. */\n src: string;\n /** BCP 47 language tag, e.g. `\"pt-BR\"`. */\n srcLang: string;\n /** Name shown in the browser's own track menu. */\n label: string;\n /** Track kind. Default `\"captions\"`. */\n kind?: \"subtitles\" | \"captions\" | \"descriptions\" | \"chapters\" | \"metadata\";\n /** Whether this track is on unless the viewer picks another. */\n default?: boolean;\n}\n\nexport interface VideoPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /**\n * What to play — a URL, or a `Blob`/`File` straight from a recorder.\n *\n * A `Blob` is wrapped in an object URL that is revoked when it changes or the\n * component unmounts, so a session that records twenty clips does not leak\n * twenty URLs for the lifetime of the tab.\n */\n src: string | Blob | null;\n /**\n * Known length in milliseconds.\n *\n * Pass it whenever you have it — a recording from `useVideoRecorder` always\n * does. Without it the element's own `duration` is used, and a fresh\n * `MediaRecorder` blob may report `Infinity` until it is probed for one.\n */\n durationMs?: number;\n /** Still shown before playback. Pair it with `captureFrame` from `/imaging`. */\n poster?: string;\n /** Caption and subtitle files. */\n tracks?: readonly VideoTextTrack[];\n /** Frame shape while the video has no intrinsic size yet. Default `16 / 9`. */\n aspectRatio?: number;\n /**\n * Playback rate. Controlled when passed with `onRateChange`.\n *\n * Written to both `playbackRate` and `defaultPlaybackRate`, because the\n * element resets the first to the second when a source loads — a player that\n * only writes `playbackRate` silently drops back to 1× on every clip change.\n */\n rate?: number;\n /** Presets the control offers. `[]` hides it. Default {@link DEFAULT_PLAYBACK_RATES}. */\n rates?: readonly number[];\n /** Called with the rate the viewer picked. */\n onRateChange?: (rate: number) => void;\n /** Let the pitch rise with the rate (`preservesPitch = false`). Default `false`. */\n shiftPitch?: boolean;\n /** Offer a fullscreen button where the browser supports it. Default `true`. */\n fullscreen?: boolean;\n /** Start playing as soon as `src` is ready. Default `false`. */\n autoPlay?: boolean;\n /** Loop. Default `false`. */\n loop?: boolean;\n /** Start muted. Autoplay without a gesture needs this. Default `false`. */\n muted?: boolean;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the control row — a download button, a delete button. */\n actions?: ReactNode;\n /** Fired when playback reaches the end. */\n onEnded?: () => void;\n /** Fired when the element reports a decode/network error. */\n onError?: (error: unknown) => void;\n /** No `src` yet, or playback not allowed. */\n disabled?: boolean;\n}\n\nconst STRINGS = {\n \"pt-BR\": {\n play: \"Tocar\",\n pause: \"Pausar\",\n seek: \"Posição\",\n mute: \"Silenciar\",\n unmute: \"Ativar som\",\n volume: \"Volume\",\n rate: \"Velocidade\",\n enterFullscreen: \"Tela cheia\",\n exitFullscreen: \"Sair da tela cheia\",\n },\n en: {\n play: \"Play\",\n pause: \"Pause\",\n seek: \"Seek\",\n mute: \"Mute\",\n unmute: \"Unmute\",\n volume: \"Volume\",\n rate: \"Speed\",\n enterFullscreen: \"Full screen\",\n exitFullscreen: \"Exit full screen\",\n },\n} as const;\n\n/**\n * Label a rate the way a viewer reads it.\n *\n * @param rate The multiplier.\n * @param locale Which decimal separator to use.\n * @returns e.g. `\"1,5×\"` in pt-BR, `\"1.5×\"` in English.\n */\nfunction rateLabel(rate: number, locale: \"pt-BR\" | \"en\"): string {\n return `${rate.toLocaleString(locale === \"en\" ? \"en-US\" : \"pt-BR\")}×`;\n}\n\n/**\n * Playback transport for one video: play/pause, seek, volume, speed, fullscreen.\n *\n * Built on a real `<video>` element, and deliberately **not** an overlay: the\n * controls sit in a bar under the frame rather than on top of it. Controls drawn\n * over video have to earn their contrast against arbitrary pixels, which no\n * `--tempest-*` token can promise — every text token in this SDK is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction.\n *\n * The seek bar, the volume slider and the speed picker are bare `<input>` and\n * `<select>` elements rather than the SDK's `Slider` and `Select`: those are form\n * fields, with a label row and a value badge, and a transport wants neither. The\n * native elements keep the keyboard and screen-reader behaviour that matters here\n * for free.\n *\n * @example\n * const rec = useVideoRecorder(screen.stream);\n * {rec.recording && (\n * <VideoPlayer src={rec.recording.blob} durationMs={rec.recording.durationMs} />\n * )}\n */\nexport function VideoPlayer({\n src,\n durationMs,\n poster,\n tracks,\n aspectRatio = 16 / 9,\n rate,\n rates = DEFAULT_PLAYBACK_RATES,\n onRateChange,\n shiftPitch = false,\n fullscreen = true,\n autoPlay = false,\n loop = false,\n muted = false,\n locale = \"pt-BR\",\n actions,\n onEnded,\n onError,\n disabled = false,\n className,\n ...rest\n}: VideoPlayerProps) {\n const strings = STRINGS[locale];\n const shell = useRef<HTMLDivElement | null>(null);\n const video = useRef<HTMLVideoElement | null>(null);\n const probed = useRef(false);\n\n const [playing, setPlaying] = useState(false);\n const [currentMs, setCurrentMs] = useState(0);\n const [elementMs, setElementMs] = useState(Number.POSITIVE_INFINITY);\n const [ownRate, setOwnRate] = useState(rate ?? 1);\n const [volume, setVolume] = useState(1);\n const [silent, setSilent] = useState(muted);\n\n const immersive = useFullscreen(shell);\n const blobUrl = useObjectUrl(typeof src === \"string\" ? null : src);\n const url = typeof src === \"string\" ? src : blobUrl;\n\n const effectiveRate = rate ?? ownRate;\n const totalMs = Number.isFinite(durationMs) ? (durationMs as number) : elementMs;\n const seekable = Number.isFinite(totalMs) && totalMs > 0;\n\n useEffect(() => {\n setCurrentMs(0);\n setPlaying(false);\n setElementMs(Number.POSITIVE_INFINITY);\n probed.current = false;\n }, [url]);\n\n /**\n * Keep the rate on the element, and keep it across sources.\n *\n * `defaultPlaybackRate` is written alongside `playbackRate` because the load\n * algorithm copies the former onto the latter: writing only `playbackRate`\n * means the rate the viewer chose is silently lost on the next clip.\n */\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.defaultPlaybackRate = effectiveRate;\n node.playbackRate = effectiveRate;\n node.preservesPitch = !shiftPitch;\n }, [effectiveRate, shiftPitch, url]);\n\n useEffect(() => {\n const node = video.current;\n if (!node) return;\n node.volume = volume;\n node.muted = silent;\n }, [volume, silent, url]);\n\n /**\n * Coax a duration out of an element that reports `Infinity`.\n *\n * Seeking past the end forces the browser to demux to the last frame, after\n * which it knows the real length. It runs at most once per source, and only\n * when the caller gave us no `durationMs`.\n */\n const handleMetadata = (): void => {\n const node = video.current;\n if (!node) return;\n if (Number.isFinite(node.duration)) {\n setElementMs(node.duration * 1000);\n return;\n }\n if (probed.current || Number.isFinite(durationMs)) return;\n probed.current = true;\n const onProbed = (): void => {\n node.removeEventListener(\"timeupdate\", onProbed);\n if (Number.isFinite(node.duration)) setElementMs(node.duration * 1000);\n node.currentTime = 0;\n };\n node.addEventListener(\"timeupdate\", onProbed);\n node.currentTime = 1e101;\n };\n\n const toggle = (): void => {\n const node = video.current;\n if (!node || !url) return;\n if (node.paused) {\n void Promise.resolve(node.play())\n .then(() => setPlaying(true))\n .catch((error: unknown) => onError?.(error));\n return;\n }\n node.pause();\n setPlaying(false);\n };\n\n const seek = (ms: number): void => {\n const node = video.current;\n if (!node || !seekable) return;\n node.currentTime = ms / 1000;\n setCurrentMs(ms);\n };\n\n const changeRate = (next: number): void => {\n if (rate === undefined) setOwnRate(next);\n onRateChange?.(next);\n };\n\n return (\n <div\n ref={shell}\n className={cn(styles.player, immersive.isFullscreen && styles.immersive, className)}\n {...rest}\n >\n <div className={styles.frame} style={{ aspectRatio }}>\n <video\n ref={video}\n className={styles.video}\n src={url ?? undefined}\n poster={poster}\n loop={loop}\n autoPlay={autoPlay}\n playsInline\n preload=\"metadata\"\n onLoadedMetadata={handleMetadata}\n onDurationChange={handleMetadata}\n onTimeUpdate={() => {\n const node = video.current;\n if (node) setCurrentMs(node.currentTime * 1000);\n }}\n onPlay={() => setPlaying(true)}\n onPause={() => setPlaying(false)}\n onVolumeChange={() => {\n const node = video.current;\n if (!node) return;\n setVolume(node.volume);\n setSilent(node.muted);\n }}\n onEnded={() => {\n setPlaying(false);\n onEnded?.();\n }}\n onError={(event) => onError?.(event)}\n >\n {tracks?.map((track) => (\n <track\n key={`${track.kind ?? \"captions\"}-${track.srcLang}-${track.src}`}\n kind={track.kind ?? \"captions\"}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))}\n </video>\n </div>\n\n <div className={styles.chrome}>\n <button\n type=\"button\"\n className={styles.transport}\n onClick={toggle}\n disabled={disabled || !url}\n aria-label={playing ? strings.pause : strings.play}\n title={playing ? strings.pause : strings.play}\n >\n {playing ? <Pause size={16} aria-hidden /> : <Play size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.seek}\n min={0}\n max={seekable ? Math.round(totalMs) : 0}\n step={100}\n value={Math.min(Math.round(currentMs), seekable ? Math.round(totalMs) : 0)}\n onChange={(event) => seek(Number(event.target.value))}\n disabled={disabled || !seekable}\n aria-label={strings.seek}\n aria-valuetext={`${formatDuration(currentMs)} / ${formatDuration(totalMs)}`}\n />\n\n <span className={styles.time}>\n <span>{formatDuration(currentMs)}</span>\n <span aria-hidden=\"true\">/</span>\n <span>{formatDuration(totalMs)}</span>\n </span>\n\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => setSilent((was) => !was)}\n disabled={disabled}\n aria-label={silent ? strings.unmute : strings.mute}\n title={silent ? strings.unmute : strings.mute}\n >\n {silent ? <VolumeX size={16} aria-hidden /> : <Volume2 size={16} aria-hidden />}\n </button>\n\n <input\n type=\"range\"\n className={styles.volume}\n min={0}\n max={1}\n step={0.05}\n value={silent ? 0 : volume}\n onChange={(event) => {\n const next = Number(event.target.value);\n setVolume(next);\n setSilent(next === 0);\n }}\n disabled={disabled}\n aria-label={strings.volume}\n />\n\n {rates.length > 0 && (\n <select\n className={styles.rate}\n value={effectiveRate}\n onChange={(event) => changeRate(Number(event.target.value))}\n disabled={disabled}\n aria-label={strings.rate}\n title={strings.rate}\n >\n {rates.map((option) => (\n <option key={option} value={option}>\n {rateLabel(option, locale)}\n </option>\n ))}\n </select>\n )}\n\n {fullscreen && immersive.supported && (\n <button\n type=\"button\"\n className={styles.icon}\n onClick={() => {\n void immersive.toggle().catch((error: unknown) => onError?.(error));\n }}\n disabled={disabled}\n aria-label={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n title={\n immersive.isFullscreen\n ? strings.exitFullscreen\n : strings.enterFullscreen\n }\n >\n {immersive.isFullscreen ? (\n <Minimize size={16} aria-hidden />\n ) : (\n <Maximize size={16} aria-hidden />\n )}\n </button>\n )}\n\n {actions && <span className={styles.actions}>{actions}</span>}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e=`tempest_player_tP0ul`,t=`tempest_frame_ds-xN`,n=`tempest_video_Ejoer`,r=`tempest_chrome_LI2ZI`,i=`tempest_transport_waV9q`,a=`tempest_icon_jIwyC`,o=`tempest_seek_qB6l-`,s=`tempest_volume_R0Fr0`,c=`tempest_rate_Dluev`,l=`tempest_time_z9Tg1`,u=`tempest_actions_UxFoM`,d=`tempest_immersive_eSsR6`,f={player:e,frame:t,video:n,chrome:r,transport:i,icon:a,seek:o,volume:s,rate:c,time:l,actions:u,immersive:d};exports.actions=u,exports.chrome=r,exports.default=f,exports.frame=t,exports.icon=a,exports.immersive=d,exports.player=e,exports.rate=c,exports.seek=o,exports.time=l,exports.transport=i,exports.video=n,exports.volume=s;
2
+ //# sourceMappingURL=VideoPlayer.module.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoPlayer.module.cjs","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.module.css"],"sourcesContent":["/*\n * The controls are a bar under the frame, not an overlay.\n *\n * An overlay has to earn its contrast against arbitrary video pixels, and no\n * `--tempest-*` token can promise that: every text token here is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction. An app that\n * wants a cinema overlay can position its own chrome over the frame; what ships\n * is the one that is right in both themes.\n */\n.player {\n display: flex;\n flex-direction: column;\n min-width: 0;\n overflow: hidden;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n/*\n * The frame keeps its shape before the video reports one, so the control bar\n * does not jump down the page when metadata lands. Black rather than a surface\n * token: letterboxing is part of the picture, and a light bar around a dark\n * video reads as a rendering bug.\n */\n.frame {\n position: relative;\n width: 100%;\n background-color: #000000;\n}\n\n.video {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.chrome {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-top: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.transport {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.transport:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.icon {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.icon:hover:not(:disabled) {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n\n.icon:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * The track is drawn on the input itself rather than with a wrapper and an\n * overlay: the native element already carries the keyboard behaviour and the\n * value semantics, and the only thing missing is the look.\n */\n.seek {\n flex: 1 1 8rem;\n min-width: 3rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.volume {\n flex: 0 1 4.5rem;\n min-width: 2.5rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.seek:disabled,\n.volume:disabled {\n accent-color: var(--tempest-surface-3);\n cursor: not-allowed;\n}\n\n.rate {\n flex: 0 0 auto;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n}\n\n.rate:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.transport:focus-visible,\n.icon:focus-visible,\n.seek:focus-visible,\n.volume:focus-visible,\n.rate:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n border-radius: var(--tempest-radius-sm);\n}\n\n/*\n * Tabular figures, so the elapsed time does not shift the seek bar sideways as\n * the digits change — a proportional `1` is narrower than a `0`, and at ten\n * updates a second the whole row visibly jitters.\n */\n.time {\n display: inline-flex;\n flex: 0 0 auto;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n}\n\n.actions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-1);\n}\n\n/*\n * Fullscreen presents the shell, so the frame has to give up its aspect box and\n * take the height that is left over — otherwise a 16/9 box inside a 16/10\n * display leaves the bar floating in the middle of the screen.\n */\n.immersive {\n width: 100%;\n height: 100%;\n border: none;\n border-radius: 0;\n}\n\n.immersive .frame {\n flex: 1 1 auto;\n aspect-ratio: auto !important;\n min-height: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .transport,\n .icon {\n transition: none;\n }\n}\n\n/* Touch hit-slop — 44×44 without changing the visual footprint, as `Button` does. */\n@media (pointer: coarse) {\n .transport::after,\n .icon::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/*\n * On a phone the row cannot hold seven controls. The seek bar takes a line of its\n * own and the rest wraps under it, which keeps every target at full size instead\n * of shrinking them all below the touch minimum.\n */\n@media (max-width: 480px) {\n .chrome {\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2);\n }\n\n .seek {\n flex: 1 1 100%;\n order: -1;\n }\n\n /*\n * No volume slider on a phone. Measured at 390px in Chromium: the second row\n * came to exactly the container width, which pushed the fullscreen button\n * onto a third row — spent on a slider nobody drags on a phone, where the\n * hardware keys do that job. The mute button stays, because silencing fast\n * is the thing people actually want from a video in a feed.\n */\n .volume {\n display: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,19 @@
1
+ //#region src/components/VideoPlayer/VideoPlayer.module.css
2
+ var e = "tempest_player_tP0ul", t = "tempest_frame_ds-xN", n = "tempest_video_Ejoer", r = "tempest_chrome_LI2ZI", i = "tempest_transport_waV9q", a = "tempest_icon_jIwyC", o = "tempest_seek_qB6l-", s = "tempest_volume_R0Fr0", c = "tempest_rate_Dluev", l = "tempest_time_z9Tg1", u = "tempest_actions_UxFoM", d = "tempest_immersive_eSsR6", f = {
3
+ player: e,
4
+ frame: t,
5
+ video: n,
6
+ chrome: r,
7
+ transport: i,
8
+ icon: a,
9
+ seek: o,
10
+ volume: s,
11
+ rate: c,
12
+ time: l,
13
+ actions: u,
14
+ immersive: d
15
+ };
16
+ //#endregion
17
+ export { u as actions, r as chrome, f as default, t as frame, a as icon, d as immersive, e as player, c as rate, o as seek, l as time, i as transport, n as video, s as volume };
18
+
19
+ //# sourceMappingURL=VideoPlayer.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoPlayer.module.js","names":[],"sources":["../../../src/components/VideoPlayer/VideoPlayer.module.css"],"sourcesContent":["/*\n * The controls are a bar under the frame, not an overlay.\n *\n * An overlay has to earn its contrast against arbitrary video pixels, and no\n * `--tempest-*` token can promise that: every text token here is resolved\n * against a known surface, and the two contrast defects this repo shipped were\n * both a text token used over a surface it was never checked against. A bar with\n * its own surface inherits the theme and is legible by construction. An app that\n * wants a cinema overlay can position its own chrome over the frame; what ships\n * is the one that is right in both themes.\n */\n.player {\n display: flex;\n flex-direction: column;\n min-width: 0;\n overflow: hidden;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n/*\n * The frame keeps its shape before the video reports one, so the control bar\n * does not jump down the page when metadata lands. Black rather than a surface\n * token: letterboxing is part of the picture, and a light bar around a dark\n * video reads as a rendering bug.\n */\n.frame {\n position: relative;\n width: 100%;\n background-color: #000000;\n}\n\n.video {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.chrome {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--tempest-space-3);\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border-top: 1px solid var(--tempest-border);\n background-color: var(--tempest-surface);\n}\n\n.transport {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.transport:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.icon {\n position: relative;\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: transparent;\n color: var(--tempest-text-muted);\n cursor: pointer;\n transition: var(--tempest-transition-color);\n}\n\n.icon:hover:not(:disabled) {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n}\n\n.icon:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n/*\n * The track is drawn on the input itself rather than with a wrapper and an\n * overlay: the native element already carries the keyboard behaviour and the\n * value semantics, and the only thing missing is the look.\n */\n.seek {\n flex: 1 1 8rem;\n min-width: 3rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.volume {\n flex: 0 1 4.5rem;\n min-width: 2.5rem;\n height: 1.25rem;\n margin: 0;\n background: none;\n accent-color: var(--tempest-primary);\n cursor: pointer;\n}\n\n.seek:disabled,\n.volume:disabled {\n accent-color: var(--tempest-surface-3);\n cursor: not-allowed;\n}\n\n.rate {\n flex: 0 0 auto;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font-family: inherit;\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n}\n\n.rate:disabled {\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.transport:focus-visible,\n.icon:focus-visible,\n.seek:focus-visible,\n.volume:focus-visible,\n.rate:focus-visible {\n outline: var(--tempest-focus-ring-width) solid var(--tempest-focus-ring-color);\n outline-offset: 2px;\n border-radius: var(--tempest-radius-sm);\n}\n\n/*\n * Tabular figures, so the elapsed time does not shift the seek bar sideways as\n * the digits change — a proportional `1` is narrower than a `0`, and at ten\n * updates a second the whole row visibly jitters.\n */\n.time {\n display: inline-flex;\n flex: 0 0 auto;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-variant-numeric: tabular-nums;\n}\n\n.actions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: var(--tempest-space-1);\n}\n\n/*\n * Fullscreen presents the shell, so the frame has to give up its aspect box and\n * take the height that is left over — otherwise a 16/9 box inside a 16/10\n * display leaves the bar floating in the middle of the screen.\n */\n.immersive {\n width: 100%;\n height: 100%;\n border: none;\n border-radius: 0;\n}\n\n.immersive .frame {\n flex: 1 1 auto;\n aspect-ratio: auto !important;\n min-height: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .transport,\n .icon {\n transition: none;\n }\n}\n\n/* Touch hit-slop — 44×44 without changing the visual footprint, as `Button` does. */\n@media (pointer: coarse) {\n .transport::after,\n .icon::after {\n content: \"\";\n position: absolute;\n inset: -6px;\n }\n}\n\n/*\n * On a phone the row cannot hold seven controls. The seek bar takes a line of its\n * own and the rest wraps under it, which keeps every target at full size instead\n * of shrinking them all below the touch minimum.\n */\n@media (max-width: 480px) {\n .chrome {\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-2);\n }\n\n .seek {\n flex: 1 1 100%;\n order: -1;\n }\n\n /*\n * No volume slider on a phone. Measured at 390px in Chromium: the second row\n * came to exactly the container width, which pushed the fullscreen button\n * onto a third row — spent on a slider nobody drags on a phone, where the\n * hardware keys do that job. The mute button stays, because silencing fast\n * is the thing people actually want from a video in a feed.\n */\n .volume {\n display: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,2 @@
1
+ var e=[.5,1,1.5,2];exports.DEFAULT_PLAYBACK_RATES=e;
2
+ //# sourceMappingURL=playback-rates.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-rates.cjs","names":[],"sources":["../../../src/components/VideoPlayer/playback-rates.ts"],"sourcesContent":["/**\n * The speed presets the player offers, and why they stop at 2×.\n *\n * The spec lets a browser drop the audio once `playbackRate` leaves a range it\n * considers useful, and that limit varies by engine. These four sit inside every\n * range measured, so the default control never silences a clip by accident. A\n * caller who wants 4× can pass it — and should test it with sound on the\n * browsers they support before promising it.\n */\nexport const DEFAULT_PLAYBACK_RATES: readonly number[] = [0.5, 1, 1.5, 2];\n"],"mappings":"AASA,IAAa,EAA4C,CAAC,GAAK,EAAG,IAAK,CAAC"}
@@ -0,0 +1,11 @@
1
+ //#region src/components/VideoPlayer/playback-rates.ts
2
+ var e = [
3
+ .5,
4
+ 1,
5
+ 1.5,
6
+ 2
7
+ ];
8
+ //#endregion
9
+ export { e as DEFAULT_PLAYBACK_RATES };
10
+
11
+ //# sourceMappingURL=playback-rates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-rates.js","names":[],"sources":["../../../src/components/VideoPlayer/playback-rates.ts"],"sourcesContent":["/**\n * The speed presets the player offers, and why they stop at 2×.\n *\n * The spec lets a browser drop the audio once `playbackRate` leaves a range it\n * considers useful, and that limit varies by engine. These four sit inside every\n * range measured, so the default control never silences a clip by accident. A\n * caller who wants 4× can pass it — and should test it with sound on the\n * browsers they support before promising it.\n */\nexport const DEFAULT_PLAYBACK_RATES: readonly number[] = [0.5, 1, 1.5, 2];\n"],"mappings":";AASA,IAAa,IAA4C;CAAC;CAAK;CAAG;CAAK;AAAC"}
@@ -1,2 +1,2 @@
1
- let e=require("react");var t=[{code:`Space`,label:`Espaço`},{code:`ControlLeft`,label:`Ctrl esquerdo`},{code:`AltLeft`,label:`Alt esquerdo`},{code:`ShiftLeft`,label:`Shift esquerdo`},{code:`Backquote`,label:"Crase (`)"}],n=`Space`;function r(e){return t.find(t=>t.code===e)?.label??e}function i(e){if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;let t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`}function a({code:t=n,onDown:r,onUp:a,enabled:o=!0}){let s=(0,e.useRef)({onDown:r,onUp:a});(0,e.useEffect)(()=>{s.current={onDown:r,onUp:a}}),(0,e.useEffect)(()=>{if(!o||typeof window>`u`)return;let e=!1,n=()=>{e&&(e=!1,s.current.onUp())},r=n=>{n.code!==t||n.repeat||i(n.target)||(n.preventDefault(),!e&&(e=!0,s.current.onDown()))},a=e=>{e.code===t&&(i(e.target)||(e.preventDefault(),n()))};return window.addEventListener(`keydown`,r),window.addEventListener(`keyup`,a),window.addEventListener(`blur`,n),()=>{window.removeEventListener(`keydown`,r),window.removeEventListener(`keyup`,a),window.removeEventListener(`blur`,n),n()}},[t,o])}exports.DEFAULT_PUSH_TO_TALK_KEY=n,exports.PUSH_TO_TALK_KEYS=t,exports.pushToTalkKeyLabel=r,exports.usePushToTalk=a;
1
+ let e=require("react");var t=[{code:`Space`,label:`Espaço`},{code:`ControlLeft`,label:`Ctrl esquerdo`},{code:`AltLeft`,label:`Alt esquerdo`},{code:`ShiftLeft`,label:`Shift esquerdo`},{code:`Backquote`,label:"Crase (`)"}],n=`Space`;function r(e){return t.find(t=>t.code===e)?.label??e}function i(e){if(!(e instanceof HTMLElement))return!1;if(e.isContentEditable)return!0;let t=e.tagName;return t===`INPUT`||t===`TEXTAREA`||t===`SELECT`}function a({code:t=n,onDown:r,onUp:a,enabled:o=!0}){let s=(0,e.useRef)({onDown:r,onUp:a});(0,e.useEffect)(()=>{s.current={onDown:r,onUp:a}}),(0,e.useEffect)(()=>{if(!o||typeof window>`u`)return;let e=!1,n=()=>{e&&(e=!1,s.current.onUp())},r=n=>{n.code!==t||n.repeat||i(n.target)||(n.preventDefault(),!e&&(e=!0,s.current.onDown()))},a=r=>{r.code!==t||!e||(r.preventDefault(),n())};return window.addEventListener(`keydown`,r),window.addEventListener(`keyup`,a),window.addEventListener(`blur`,n),()=>{window.removeEventListener(`keydown`,r),window.removeEventListener(`keyup`,a),window.removeEventListener(`blur`,n),n()}},[t,o])}exports.DEFAULT_PUSH_TO_TALK_KEY=n,exports.PUSH_TO_TALK_KEYS=t,exports.pushToTalkKeyLabel=r,exports.usePushToTalk=a;
2
2
  //# sourceMappingURL=use-push-to-talk.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAEM,EAAe,GAA+B,CAC5C,EAAM,OAAS,IACf,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACrB,EAAQ,GACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}
1
+ {"version":3,"file":"use-push-to-talk.cjs","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":"uBAsBA,IAAa,EAA8C,CACvD,CAAE,KAAM,QAAS,MAAO,QAAS,EACjC,CAAE,KAAM,cAAe,MAAO,eAAgB,EAC9C,CAAE,KAAM,UAAW,MAAO,cAAe,EACzC,CAAE,KAAM,YAAa,MAAO,gBAAiB,EAC7C,CAAE,KAAM,YAAa,MAAO,WAAY,CAC5C,EAGa,EAA2B,QASxC,SAAgB,EAAmB,EAAsB,CACrD,OAAO,EAAkB,KAAM,GAAQ,EAAI,OAAS,CAAI,CAAC,EAAE,OAAS,CACxE,CAYA,SAAS,EAAe,EAAqC,CACzD,GAAI,EAAE,aAAkB,aAAc,MAAO,GAC7C,GAAI,EAAO,kBAAmB,MAAO,GACrC,IAAM,EAAM,EAAO,QACnB,OAAO,IAAQ,SAAW,IAAQ,YAAc,IAAQ,QAC5D,CAmDA,SAAgB,EAAc,CAC1B,OAAO,EACP,SACA,OACA,UAAU,IACiB,CAC3B,IAAM,GAAA,EAAY,EAAA,OAAA,CAAO,CAAE,SAAQ,MAAK,CAAC,GACzC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,QAAU,CAAE,SAAQ,MAAK,CACvC,CAAC,GAED,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,OAAO,OAAW,IAAa,OAC/C,IAAI,EAAO,GAEL,MAAsB,CACnB,IACL,EAAO,GACP,EAAU,QAAQ,KAAK,EAC3B,EAEM,EAAiB,GAA+B,CAC9C,EAAM,OAAS,GAAQ,EAAM,QAC7B,EAAe,EAAM,MAAM,IAC/B,EAAM,eAAe,EACjB,KACJ,EAAO,GACP,EAAU,QAAQ,OAAO,GAC7B,EAkBM,EAAe,GAA+B,CAC5C,EAAM,OAAS,GAAQ,CAAC,IAC5B,EAAM,eAAe,EACrB,EAAQ,EACZ,EAMA,OAJA,OAAO,iBAAiB,UAAW,CAAa,EAChD,OAAO,iBAAiB,QAAS,CAAW,EAC5C,OAAO,iBAAiB,OAAQ,CAAO,MAE1B,CACT,OAAO,oBAAoB,UAAW,CAAa,EACnD,OAAO,oBAAoB,QAAS,CAAW,EAC/C,OAAO,oBAAoB,OAAQ,CAAO,EAC1C,EAAQ,CACZ,CACJ,EAAG,CAAC,EAAM,CAAO,CAAC,CACtB"}
@@ -47,8 +47,8 @@ function o({ code: n = r, onDown: i, onUp: o, enabled: s = !0 }) {
47
47
  e && (e = !1, c.current.onUp());
48
48
  }, r = (t) => {
49
49
  t.code !== n || t.repeat || a(t.target) || (t.preventDefault(), !e && (e = !0, c.current.onDown()));
50
- }, i = (e) => {
51
- e.code === n && (a(e.target) || (e.preventDefault(), t()));
50
+ }, i = (r) => {
51
+ r.code !== n || !e || (r.preventDefault(), t());
52
52
  };
53
53
  return window.addEventListener("keydown", r), window.addEventListener("keyup", i), window.addEventListener("blur", t), () => {
54
54
  window.removeEventListener("keydown", r), window.removeEventListener("keyup", i), window.removeEventListener("blur", t), t();
@@ -1 +1 @@
1
- {"version":3,"file":"use-push-to-talk.js","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":";;AAsBA,IAAa,IAA8C;CACvD;EAAE,MAAM;EAAS,OAAO;CAAS;CACjC;EAAE,MAAM;EAAe,OAAO;CAAgB;CAC9C;EAAE,MAAM;EAAW,OAAO;CAAe;CACzC;EAAE,MAAM;EAAa,OAAO;CAAiB;CAC7C;EAAE,MAAM;EAAa,OAAO;CAAY;AAC5C,GAGa,IAA2B;AASxC,SAAgB,EAAmB,GAAsB;CACrD,OAAO,EAAkB,MAAM,MAAQ,EAAI,SAAS,CAAI,CAAC,EAAE,SAAS;AACxE;AAYA,SAAS,EAAe,GAAqC;CACzD,IAAI,EAAE,aAAkB,cAAc,OAAO;CAC7C,IAAI,EAAO,mBAAmB,OAAO;CACrC,IAAM,IAAM,EAAO;CACnB,OAAO,MAAQ,WAAW,MAAQ,cAAc,MAAQ;AAC5D;AAmDA,SAAgB,EAAc,EAC1B,UAAO,GACP,WACA,SACA,aAAU,MACiB;CAC3B,IAAM,IAAY,EAAO;EAAE;EAAQ;CAAK,CAAC;CAKzC,AAJA,QAAgB;EACZ,EAAU,UAAU;GAAE;GAAQ;EAAK;CACvC,CAAC,GAED,QAAgB;EACZ,IAAI,CAAC,KAAW,OAAO,SAAW,KAAa;EAC/C,IAAI,IAAO,IAEL,UAAsB;GACnB,MACL,IAAO,IACP,EAAU,QAAQ,KAAK;EAC3B,GAEM,KAAiB,MAA+B;GAC9C,EAAM,SAAS,KAAQ,EAAM,UAC7B,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACjB,OACJ,IAAO,IACP,EAAU,QAAQ,OAAO;EAC7B,GAEM,KAAe,MAA+B;GAC5C,EAAM,SAAS,MACf,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACrB,EAAQ;EACZ;EAMA,OAJA,OAAO,iBAAiB,WAAW,CAAa,GAChD,OAAO,iBAAiB,SAAS,CAAW,GAC5C,OAAO,iBAAiB,QAAQ,CAAO,SAE1B;GAIT,AAHA,OAAO,oBAAoB,WAAW,CAAa,GACnD,OAAO,oBAAoB,SAAS,CAAW,GAC/C,OAAO,oBAAoB,QAAQ,CAAO,GAC1C,EAAQ;EACZ;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC;AACtB"}
1
+ {"version":3,"file":"use-push-to-talk.js","names":[],"sources":["../../src/hooks/use-push-to-talk.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/** A key offered for push-to-talk, with the label a settings screen shows. */\nexport interface PushToTalkKey {\n /** `KeyboardEvent.code`, which is layout-independent. */\n code: string;\n /** What to show a person. `code` itself is not a label. */\n label: string;\n}\n\n/**\n * The keys worth offering for push-to-talk.\n *\n * Identified by `code` rather than `key` so a binding survives a layout change:\n * on an ABNT2 keyboard the `key` reported for the backquote position is not what\n * a US layout reports, while `Backquote` is the same physical key everywhere.\n *\n * Left-hand modifiers only. The right-hand ones sit under the hand that is\n * usually on the mouse, and a modifier that is also a shortcut prefix\n * (`ControlLeft` with a browser shortcut) is a deliberate trade the caller makes\n * by choosing it.\n */\nexport const PUSH_TO_TALK_KEYS: readonly PushToTalkKey[] = [\n { code: \"Space\", label: \"Espaço\" },\n { code: \"ControlLeft\", label: \"Ctrl esquerdo\" },\n { code: \"AltLeft\", label: \"Alt esquerdo\" },\n { code: \"ShiftLeft\", label: \"Shift esquerdo\" },\n { code: \"Backquote\", label: \"Crase (`)\" },\n];\n\n/** The key a caller gets when it does not choose one. */\nexport const DEFAULT_PUSH_TO_TALK_KEY = \"Space\";\n\n/**\n * The label to show for a `KeyboardEvent.code`.\n *\n * @param code - The bound key's code.\n * @returns Its label, or the code itself for a key this list does not name —\n * showing `\"F13\"` beats showing nothing.\n */\nexport function pushToTalkKeyLabel(code: string): string {\n return PUSH_TO_TALK_KEYS.find((key) => key.code === code)?.label ?? code;\n}\n\n/**\n * Whether a keystroke is meant for something the person is typing into.\n *\n * Without this a push-to-talk key bound to Space opens the microphone every time\n * somebody writes a message — and the space never reaches the text field,\n * because the handler preventDefault'd it.\n *\n * @param target - The event's target.\n * @returns `true` when the keystroke belongs to a field, not to the app.\n */\nfunction isTypingTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n if (target.isContentEditable) return true;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\";\n}\n\n/** Options for {@link usePushToTalk}. */\nexport interface UsePushToTalkOptions {\n /** `KeyboardEvent.code` to hold. Default {@link DEFAULT_PUSH_TO_TALK_KEY}. */\n code?: string;\n /** Called once when the key goes down. */\n onDown: () => void;\n /** Called when the key comes up, the window blurs, or the hook unmounts. */\n onUp: () => void;\n /**\n * Whether the binding is live. Default `true`.\n *\n * Turning it off releases first, so flipping a call from push-to-talk to an\n * open microphone while the key is held does not leave `onUp` unfired.\n */\n enabled?: boolean;\n}\n\n/**\n * Hold a key to transmit; release it to go silent again.\n *\n * Looks like `keydown`/`keyup` and is not, because three things go wrong:\n *\n * 1. **`blur` has to release.** Alt-tabbing away while holding the key means the\n * browser never sees the `keyup`, and the microphone stays open for as long as\n * the person is looking at another window — exactly what push-to-talk exists\n * to prevent.\n * 2. **Auto-repeat has to be ignored.** A held key repeats at the keyboard's rate,\n * and without the `repeat` check `onDown` fires on every one of them.\n * 3. **A text field has to win.** Space bound to push-to-talk with the focus in an\n * `<input>` means the space never reaches the message being written.\n *\n * Unmounting releases too, for the same reason `blur` does: the callback that\n * stops transmitting has to run even when the component holding it goes away\n * mid-press.\n *\n * The callbacks are read through a ref that a commit-time effect refreshes, so\n * passing inline arrows neither tears the listeners down on every render nor\n * writes to the ref during one — a render React discards would leave the ref\n * pointing at callbacks that never became the UI.\n *\n * @param options - See {@link UsePushToTalkOptions}.\n *\n * @example\n * usePushToTalk({\n * code: \"Space\",\n * onDown: () => setMicEnabled(true),\n * onUp: () => setMicEnabled(false),\n * });\n */\nexport function usePushToTalk({\n code = DEFAULT_PUSH_TO_TALK_KEY,\n onDown,\n onUp,\n enabled = true,\n}: UsePushToTalkOptions): void {\n const callbacks = useRef({ onDown, onUp });\n useEffect(() => {\n callbacks.current = { onDown, onUp };\n });\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n let held = false;\n\n const release = (): void => {\n if (!held) return;\n held = false;\n callbacks.current.onUp();\n };\n\n const handleKeyDown = (event: KeyboardEvent): void => {\n if (event.code !== code || event.repeat) return;\n if (isTypingTarget(event.target)) return;\n event.preventDefault();\n if (held) return;\n held = true;\n callbacks.current.onDown();\n };\n\n /**\n * Release on the way up, and never mind where the keystroke landed.\n *\n * The field guard belongs on the way **down** — it is what stops a\n * push-to-talk bound to Space from opening the microphone every time\n * somebody writes a message. On the way up it did the opposite of its\n * job: `keyup` is delivered to whatever is focused when the key rises,\n * not to what was focused when it fell, so clicking into an input while\n * still holding sent the release into the guard and left `held` true.\n * The microphone stayed open until the window blurred.\n *\n * `held` is the guard this needs. If we never opened the microphone\n * there is nothing to close, and `release()` already returns on that —\n * so the only thing left to decide is `preventDefault`, which is\n * honest only for a keystroke we actually acted on.\n */\n const handleKeyUp = (event: KeyboardEvent): void => {\n if (event.code !== code || !held) return;\n event.preventDefault();\n release();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n window.addEventListener(\"blur\", release);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n window.removeEventListener(\"blur\", release);\n release();\n };\n }, [code, enabled]);\n}\n"],"mappings":";;AAsBA,IAAa,IAA8C;CACvD;EAAE,MAAM;EAAS,OAAO;CAAS;CACjC;EAAE,MAAM;EAAe,OAAO;CAAgB;CAC9C;EAAE,MAAM;EAAW,OAAO;CAAe;CACzC;EAAE,MAAM;EAAa,OAAO;CAAiB;CAC7C;EAAE,MAAM;EAAa,OAAO;CAAY;AAC5C,GAGa,IAA2B;AASxC,SAAgB,EAAmB,GAAsB;CACrD,OAAO,EAAkB,MAAM,MAAQ,EAAI,SAAS,CAAI,CAAC,EAAE,SAAS;AACxE;AAYA,SAAS,EAAe,GAAqC;CACzD,IAAI,EAAE,aAAkB,cAAc,OAAO;CAC7C,IAAI,EAAO,mBAAmB,OAAO;CACrC,IAAM,IAAM,EAAO;CACnB,OAAO,MAAQ,WAAW,MAAQ,cAAc,MAAQ;AAC5D;AAmDA,SAAgB,EAAc,EAC1B,UAAO,GACP,WACA,SACA,aAAU,MACiB;CAC3B,IAAM,IAAY,EAAO;EAAE;EAAQ;CAAK,CAAC;CAKzC,AAJA,QAAgB;EACZ,EAAU,UAAU;GAAE;GAAQ;EAAK;CACvC,CAAC,GAED,QAAgB;EACZ,IAAI,CAAC,KAAW,OAAO,SAAW,KAAa;EAC/C,IAAI,IAAO,IAEL,UAAsB;GACnB,MACL,IAAO,IACP,EAAU,QAAQ,KAAK;EAC3B,GAEM,KAAiB,MAA+B;GAC9C,EAAM,SAAS,KAAQ,EAAM,UAC7B,EAAe,EAAM,MAAM,MAC/B,EAAM,eAAe,GACjB,OACJ,IAAO,IACP,EAAU,QAAQ,OAAO;EAC7B,GAkBM,KAAe,MAA+B;GAC5C,EAAM,SAAS,KAAQ,CAAC,MAC5B,EAAM,eAAe,GACrB,EAAQ;EACZ;EAMA,OAJA,OAAO,iBAAiB,WAAW,CAAa,GAChD,OAAO,iBAAiB,SAAS,CAAW,GAC5C,OAAO,iBAAiB,QAAQ,CAAO,SAE1B;GAIT,AAHA,OAAO,oBAAoB,WAAW,CAAa,GACnD,OAAO,oBAAoB,SAAS,CAAW,GAC/C,OAAO,oBAAoB,QAAQ,CAAO,GAC1C,EAAQ;EACZ;CACJ,GAAG,CAAC,GAAM,CAAO,CAAC;AACtB"}
@@ -1,2 +1,2 @@
1
- var e=class extends Error{constructor(e,t){super(e,t),this.name=`ImagingError`}},t=class extends e{constructor(e,t){super(e,t),this.name=`ImageDecodeError`}},n=class extends e{constructor(e,t){super(e,t),this.name=`ImageEncodeError`}},r=class extends e{constructor(e,t){super(e,t),this.name=`UnsupportedImageTypeError`}},i=class extends e{constructor(e,t){super(e,t),this.name=`ImagingUnavailableError`}};exports.ImageDecodeError=t,exports.ImageEncodeError=n,exports.ImagingError=e,exports.ImagingUnavailableError=i,exports.UnsupportedImageTypeError=r;
1
+ var e=class extends Error{constructor(e,t){super(e,t),this.name=`ImagingError`}},t=class extends e{constructor(e,t){super(e,t),this.name=`ImageDecodeError`}},n=class extends e{constructor(e,t){super(e,t),this.name=`ImageEncodeError`}},r=class extends e{constructor(e,t){super(e,t),this.name=`UnsupportedImageTypeError`}},i=class extends e{constructor(e,t){super(e,t),this.name=`FrameSeekError`}},a=class extends e{constructor(e,t){super(e,t),this.name=`ImagingUnavailableError`}};exports.FrameSeekError=i,exports.ImageDecodeError=t,exports.ImageEncodeError=n,exports.ImagingError=e,exports.ImagingUnavailableError=a,exports.UnsupportedImageTypeError=r;
2
2
  //# sourceMappingURL=exceptions.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"exceptions.cjs","names":[],"sources":["../../src/imaging/exceptions.ts"],"sourcesContent":["/**\n * Errors thrown by the imaging module.\n *\n * `name` is a literal string on every subclass: minifiers rename classes,\n * and a derived `new.target.name` ships as `error.name === \"t\"`.\n */\n\n/** Base class for every error this module throws. */\nexport class ImagingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingError\";\n }\n}\n\n/** The source could not be decoded into pixels. */\nexport class ImageDecodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageDecodeError\";\n }\n}\n\n/** The canvas could not produce encoded bytes. */\nexport class ImageEncodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageEncodeError\";\n }\n}\n\n/**\n * The browser cannot encode the requested format.\n *\n * Worth its own class because the failure is otherwise silent: asking a\n * canvas for an unsupported type does not throw — it hands back a PNG,\n * and an app that trusted the request ships 4 MB where it expected 300 KB.\n */\nexport class UnsupportedImageTypeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"UnsupportedImageTypeError\";\n }\n}\n\n/** The environment has no canvas to draw on. */\nexport class ImagingUnavailableError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingUnavailableError\";\n }\n}\n"],"mappings":"AAQA,IAAa,EAAb,cAAkC,KAAM,CACpC,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,cAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EASa,EAAb,cAA+C,CAAa,CACxD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,2BAChB,CACJ,EAGa,EAAb,cAA6C,CAAa,CACtD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,yBAChB,CACJ"}
1
+ {"version":3,"file":"exceptions.cjs","names":[],"sources":["../../src/imaging/exceptions.ts"],"sourcesContent":["/**\n * Errors thrown by the imaging module.\n *\n * `name` is a literal string on every subclass: minifiers rename classes,\n * and a derived `new.target.name` ships as `error.name === \"t\"`.\n */\n\n/** Base class for every error this module throws. */\nexport class ImagingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingError\";\n }\n}\n\n/** The source could not be decoded into pixels. */\nexport class ImageDecodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageDecodeError\";\n }\n}\n\n/** The canvas could not produce encoded bytes. */\nexport class ImageEncodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageEncodeError\";\n }\n}\n\n/**\n * The browser cannot encode the requested format.\n *\n * Worth its own class because the failure is otherwise silent: asking a\n * canvas for an unsupported type does not throw — it hands back a PNG,\n * and an app that trusted the request ships 4 MB where it expected 300 KB.\n */\nexport class UnsupportedImageTypeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"UnsupportedImageTypeError\";\n }\n}\n\n/**\n * The frame asked for never arrived.\n *\n * Its own class because the alternative is the failure this module refuses to\n * produce: a frame from the wrong instant. `seeked` firing does not mean the\n * frame for the new position is composited and readable, so a capture that\n * gave up waiting has to say so — a thumbnail of the neighbouring frame looks\n * exactly like a correct one, and nothing downstream can tell.\n */\nexport class FrameSeekError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"FrameSeekError\";\n }\n}\n\n/** The environment has no canvas to draw on. */\nexport class ImagingUnavailableError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingUnavailableError\";\n }\n}\n"],"mappings":"AAQA,IAAa,EAAb,cAAkC,KAAM,CACpC,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,cAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EASa,EAAb,cAA+C,CAAa,CACxD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,2BAChB,CACJ,EAWa,EAAb,cAAoC,CAAa,CAC7C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,gBAChB,CACJ,EAGa,EAAb,cAA6C,CAAa,CACtD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,yBAChB,CACJ"}