@ndla/ui 56.0.185-alpha.0 → 56.0.187-alpha.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 (113) hide show
  1. package/dist/panda.buildinfo.json +20 -27
  2. package/dist/styles.css +61 -140
  3. package/es/Article/ArticleByline.mjs +2 -1
  4. package/es/Article/ArticleByline.mjs.map +1 -1
  5. package/es/AudioPlayer/AudioElement.mjs +12 -0
  6. package/es/AudioPlayer/AudioElement.mjs.map +1 -0
  7. package/es/AudioPlayer/AudioPlayer.mjs +7 -2
  8. package/es/AudioPlayer/AudioPlayer.mjs.map +1 -1
  9. package/es/AudioPlayer/AudioProgress.mjs +54 -0
  10. package/es/AudioPlayer/AudioProgress.mjs.map +1 -0
  11. package/es/AudioPlayer/CompactAudioPlayer.mjs +111 -0
  12. package/es/AudioPlayer/CompactAudioPlayer.mjs.map +1 -0
  13. package/es/AudioPlayer/Controls.mjs +25 -110
  14. package/es/AudioPlayer/Controls.mjs.map +1 -1
  15. package/es/AudioPlayer/PlayButton.mjs +24 -0
  16. package/es/AudioPlayer/PlayButton.mjs.map +1 -0
  17. package/es/AudioPlayer/SpeechControl.mjs +5 -16
  18. package/es/AudioPlayer/SpeechControl.mjs.map +1 -1
  19. package/es/AudioPlayer/VolumeSlider.mjs +31 -0
  20. package/es/AudioPlayer/VolumeSlider.mjs.map +1 -0
  21. package/es/AudioPlayer/audioUtils.mjs +17 -0
  22. package/es/AudioPlayer/audioUtils.mjs.map +1 -0
  23. package/es/AudioPlayer/useAudioControls.mjs +55 -0
  24. package/es/AudioPlayer/useAudioControls.mjs.map +1 -0
  25. package/es/Breadcrumb/BreadcrumbItem.mjs +1 -2
  26. package/es/Breadcrumb/BreadcrumbItem.mjs.map +1 -1
  27. package/es/Embed/AudioEmbed.mjs +3 -7
  28. package/es/Embed/AudioEmbed.mjs.map +1 -1
  29. package/es/Embed/ExternalEmbed.mjs +13 -16
  30. package/es/Embed/ExternalEmbed.mjs.map +1 -1
  31. package/es/Embed/IframeEmbed.mjs +4 -5
  32. package/es/Embed/IframeEmbed.mjs.map +1 -1
  33. package/es/FactBox/FactBox.mjs +14 -38
  34. package/es/FactBox/FactBox.mjs.map +1 -1
  35. package/es/Gloss/Gloss.mjs +1 -2
  36. package/es/Gloss/Gloss.mjs.map +1 -1
  37. package/es/Grid/Grid.mjs +1 -2
  38. package/es/Grid/Grid.mjs.map +1 -1
  39. package/es/LinkBlock/LinkBlock.mjs +9 -2
  40. package/es/LinkBlock/LinkBlock.mjs.map +1 -1
  41. package/es/Pitch/Pitch.mjs +1 -2
  42. package/es/Pitch/Pitch.mjs.map +1 -1
  43. package/es/index.mjs +2 -1
  44. package/lib/Article/ArticleByline.js +2 -1
  45. package/lib/Article/ArticleByline.js.map +1 -1
  46. package/lib/AudioPlayer/AudioElement.d.ts +14 -0
  47. package/lib/AudioPlayer/AudioElement.js +13 -0
  48. package/lib/AudioPlayer/AudioElement.js.map +1 -0
  49. package/lib/AudioPlayer/AudioPlayer.d.ts +5 -4
  50. package/lib/AudioPlayer/AudioPlayer.js +7 -2
  51. package/lib/AudioPlayer/AudioPlayer.js.map +1 -1
  52. package/lib/AudioPlayer/AudioProgress.d.ts +16 -0
  53. package/lib/AudioPlayer/AudioProgress.js +55 -0
  54. package/lib/AudioPlayer/AudioProgress.js.map +1 -0
  55. package/lib/AudioPlayer/CompactAudioPlayer.d.ts +13 -0
  56. package/lib/AudioPlayer/CompactAudioPlayer.js +112 -0
  57. package/lib/AudioPlayer/CompactAudioPlayer.js.map +1 -0
  58. package/lib/AudioPlayer/Controls.d.ts +1 -0
  59. package/lib/AudioPlayer/Controls.js +25 -110
  60. package/lib/AudioPlayer/Controls.js.map +1 -1
  61. package/lib/AudioPlayer/PlayButton.d.ts +13 -0
  62. package/lib/AudioPlayer/PlayButton.js +25 -0
  63. package/lib/AudioPlayer/PlayButton.js.map +1 -0
  64. package/lib/AudioPlayer/SpeechControl.d.ts +1 -2
  65. package/lib/AudioPlayer/SpeechControl.js +5 -16
  66. package/lib/AudioPlayer/SpeechControl.js.map +1 -1
  67. package/lib/AudioPlayer/VolumeSlider.d.ts +14 -0
  68. package/lib/AudioPlayer/VolumeSlider.js +32 -0
  69. package/lib/AudioPlayer/VolumeSlider.js.map +1 -0
  70. package/lib/AudioPlayer/audioUtils.d.ts +8 -0
  71. package/lib/AudioPlayer/audioUtils.js +17 -0
  72. package/lib/AudioPlayer/audioUtils.js.map +1 -0
  73. package/lib/AudioPlayer/useAudioControls.d.ts +24 -0
  74. package/lib/AudioPlayer/useAudioControls.js +56 -0
  75. package/lib/AudioPlayer/useAudioControls.js.map +1 -0
  76. package/lib/Breadcrumb/BreadcrumbItem.js +1 -2
  77. package/lib/Breadcrumb/BreadcrumbItem.js.map +1 -1
  78. package/lib/Embed/AudioEmbed.js +3 -7
  79. package/lib/Embed/AudioEmbed.js.map +1 -1
  80. package/lib/Embed/ExternalEmbed.js +13 -16
  81. package/lib/Embed/ExternalEmbed.js.map +1 -1
  82. package/lib/Embed/IframeEmbed.js +4 -5
  83. package/lib/Embed/IframeEmbed.js.map +1 -1
  84. package/lib/FactBox/FactBox.js +13 -37
  85. package/lib/FactBox/FactBox.js.map +1 -1
  86. package/lib/Gloss/Gloss.js +1 -2
  87. package/lib/Gloss/Gloss.js.map +1 -1
  88. package/lib/Grid/Grid.js +1 -2
  89. package/lib/Grid/Grid.js.map +1 -1
  90. package/lib/LinkBlock/LinkBlock.js +9 -2
  91. package/lib/LinkBlock/LinkBlock.js.map +1 -1
  92. package/lib/Pitch/Pitch.js +1 -2
  93. package/lib/Pitch/Pitch.js.map +1 -1
  94. package/lib/index.d.ts +2 -0
  95. package/lib/index.js +2 -0
  96. package/package.json +10 -10
  97. package/src/Article/ArticleByline.tsx +5 -1
  98. package/src/AudioPlayer/AudioElement.tsx +20 -0
  99. package/src/AudioPlayer/{AudiPlayer.stories.tsx → AudioPlayer.stories.tsx} +10 -1
  100. package/src/AudioPlayer/AudioPlayer.tsx +12 -5
  101. package/src/AudioPlayer/AudioProgress.tsx +92 -0
  102. package/src/AudioPlayer/CompactAudioPlayer.tsx +124 -0
  103. package/src/AudioPlayer/Controls.tsx +36 -149
  104. package/src/AudioPlayer/PlayButton.tsx +24 -0
  105. package/src/AudioPlayer/SpeechControl.tsx +6 -19
  106. package/src/AudioPlayer/VolumeSlider.tsx +56 -0
  107. package/src/AudioPlayer/audioUtils.ts +15 -0
  108. package/src/AudioPlayer/useAudioControls.ts +80 -0
  109. package/src/Embed/AudioEmbed.tsx +3 -4
  110. package/src/FactBox/FactBox.tsx +13 -43
  111. package/src/Gloss/Gloss.tsx +1 -1
  112. package/src/LinkBlock/LinkBlock.tsx +5 -2
  113. package/src/index.ts +2 -0
@@ -0,0 +1,111 @@
1
+ import { AudioElement } from "./AudioElement.mjs";
2
+ import { formatTime } from "./audioUtils.mjs";
3
+ import { AudioProgress } from "./AudioProgress.mjs";
4
+ import { PlayButton } from "./PlayButton.mjs";
5
+ import { useAudioControls } from "./useAudioControls.mjs";
6
+ import { VolumeSlider } from "./VolumeSlider.mjs";
7
+ import { IconButton, PopoverContent, PopoverRoot, PopoverTitle, PopoverTrigger, Text } from "@ndla/primitives";
8
+ import { styled } from "@ndla/styled-system/jsx";
9
+ import { useTranslation } from "react-i18next";
10
+ import { VolumeUpFill } from "@ndla/icons";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ //#region src/AudioPlayer/CompactAudioPlayer.tsx
13
+ /**
14
+ * Copyright (c) 2026-present, NDLA.
15
+ *
16
+ * This source code is licensed under the GPLv3 license found in the
17
+ * LICENSE file in the root directory of this source tree.
18
+ *
19
+ */
20
+ const AudioContainer = styled("div", { base: {
21
+ display: "flex",
22
+ gap: "xxsmall",
23
+ flexDirection: "column",
24
+ padding: "xsmall",
25
+ paddingBlockEnd: "0",
26
+ borderRadius: "xsmall",
27
+ boxShadow: "xsmall",
28
+ background: "surface.brand.1.subtle"
29
+ } });
30
+ const ControlsContainer = styled("div", { base: {
31
+ display: "flex",
32
+ gap: "xsmall",
33
+ alignItems: "center"
34
+ } });
35
+ const StyledText = styled(Text, { base: {
36
+ minWidth: "4xlarge",
37
+ flexShrink: "0",
38
+ textAlign: "center"
39
+ } });
40
+ const StyledIconButton = styled(IconButton, { base: { marginInlineStart: "auto" } });
41
+ const EllipsedText = styled(Text, { base: {
42
+ overflow: "hidden",
43
+ textOverflow: "ellipsis",
44
+ whiteSpace: "nowrap"
45
+ } });
46
+ const CompactAudioPlayer = ({ src, title }) => {
47
+ const { t } = useTranslation();
48
+ const { audioRef, playing, togglePlay, currentTime, duration, handleSliderChange, volumeValue, handleVolumeSliderChange, onEnded, onHandleTime } = useAudioControls();
49
+ return /* @__PURE__ */ jsxs(AudioContainer, { children: [
50
+ /* @__PURE__ */ jsx(AudioElement, {
51
+ ref: audioRef,
52
+ src,
53
+ title,
54
+ onEnded,
55
+ onLoadedMetadata: onHandleTime,
56
+ onTimeUpdate: onHandleTime
57
+ }),
58
+ /* @__PURE__ */ jsxs(ControlsContainer, { children: [
59
+ /* @__PURE__ */ jsx(PlayButton, {
60
+ playing,
61
+ onClick: togglePlay
62
+ }),
63
+ /* @__PURE__ */ jsxs(StyledText, { children: [
64
+ /* @__PURE__ */ jsx(Text, {
65
+ textStyle: "label.medium",
66
+ asChild: true,
67
+ consumeCss: true,
68
+ children: /* @__PURE__ */ jsx("span", { children: formatTime(currentTime) })
69
+ }),
70
+ "/ ",
71
+ /* @__PURE__ */ jsx(Text, {
72
+ textStyle: "label.medium",
73
+ color: "text.subtle",
74
+ asChild: true,
75
+ consumeCss: true,
76
+ children: /* @__PURE__ */ jsx("span", { children: formatTime(duration) })
77
+ })
78
+ ] }),
79
+ /* @__PURE__ */ jsx(EllipsedText, {
80
+ textStyle: "title.medium",
81
+ children: title
82
+ }),
83
+ /* @__PURE__ */ jsxs(PopoverRoot, {
84
+ positioning: { placement: "top" },
85
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
86
+ asChild: true,
87
+ children: /* @__PURE__ */ jsx(StyledIconButton, {
88
+ variant: "tertiary",
89
+ children: /* @__PURE__ */ jsx(VolumeUpFill, {})
90
+ })
91
+ }), /* @__PURE__ */ jsxs(PopoverContent, { children: [/* @__PURE__ */ jsx(PopoverTitle, {
92
+ srOnly: true,
93
+ children: t("audio.controls.adjustVolume")
94
+ }), /* @__PURE__ */ jsx(VolumeSlider, {
95
+ value: volumeValue,
96
+ onValueChange: handleVolumeSliderChange
97
+ })] })]
98
+ })
99
+ ] }),
100
+ /* @__PURE__ */ jsx(AudioProgress, {
101
+ currentTime,
102
+ duration,
103
+ onValueChange: handleSliderChange,
104
+ variant: "simple"
105
+ })
106
+ ] });
107
+ };
108
+ //#endregion
109
+ export { CompactAudioPlayer };
110
+
111
+ //# sourceMappingURL=CompactAudioPlayer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CompactAudioPlayer.mjs","names":[],"sources":["../../src/AudioPlayer/CompactAudioPlayer.tsx"],"sourcesContent":["/**\n * Copyright (c) 2026-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { VolumeUpFill } from \"@ndla/icons\";\nimport { PopoverRoot, PopoverTrigger, IconButton, PopoverContent, Text, PopoverTitle } from \"@ndla/primitives\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { AudioElement } from \"./AudioElement\";\nimport { AudioProgress } from \"./AudioProgress\";\nimport { formatTime } from \"./audioUtils\";\nimport { PlayButton } from \"./PlayButton\";\nimport { useAudioControls } from \"./useAudioControls\";\nimport { VolumeSlider } from \"./VolumeSlider\";\n\nconst AudioContainer = styled(\"div\", {\n base: {\n display: \"flex\",\n gap: \"xxsmall\",\n flexDirection: \"column\",\n padding: \"xsmall\",\n paddingBlockEnd: \"0\",\n borderRadius: \"xsmall\",\n boxShadow: \"xsmall\",\n background: \"surface.brand.1.subtle\",\n },\n});\n\nconst ControlsContainer = styled(\"div\", {\n base: {\n display: \"flex\",\n gap: \"xsmall\",\n alignItems: \"center\",\n },\n});\n\ninterface Props {\n src: string;\n title: string;\n}\n\nconst StyledText = styled(Text, {\n base: {\n minWidth: \"4xlarge\",\n flexShrink: \"0\",\n textAlign: \"center\",\n },\n});\n\nconst StyledIconButton = styled(IconButton, {\n base: {\n marginInlineStart: \"auto\",\n },\n});\n\nconst EllipsedText = styled(Text, {\n base: {\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n },\n});\n\nexport const CompactAudioPlayer = ({ src, title }: Props) => {\n const { t } = useTranslation();\n const {\n audioRef,\n playing,\n togglePlay,\n currentTime,\n duration,\n handleSliderChange,\n volumeValue,\n handleVolumeSliderChange,\n onEnded,\n onHandleTime,\n } = useAudioControls();\n return (\n <AudioContainer>\n <AudioElement\n ref={audioRef}\n src={src}\n title={title}\n onEnded={onEnded}\n onLoadedMetadata={onHandleTime}\n onTimeUpdate={onHandleTime}\n />\n <ControlsContainer>\n <PlayButton playing={playing} onClick={togglePlay} />\n <StyledText>\n <Text textStyle=\"label.medium\" asChild consumeCss>\n <span>{formatTime(currentTime)}</span>\n </Text>\n {\"/ \"}\n <Text textStyle=\"label.medium\" color=\"text.subtle\" asChild consumeCss>\n <span>{formatTime(duration)}</span>\n </Text>\n </StyledText>\n <EllipsedText textStyle=\"title.medium\">{title}</EllipsedText>\n <PopoverRoot positioning={{ placement: \"top\" }}>\n <PopoverTrigger asChild>\n <StyledIconButton variant=\"tertiary\">\n <VolumeUpFill />\n </StyledIconButton>\n </PopoverTrigger>\n <PopoverContent>\n <PopoverTitle srOnly>{t(\"audio.controls.adjustVolume\")}</PopoverTitle>\n <VolumeSlider value={volumeValue} onValueChange={handleVolumeSliderChange} />\n </PopoverContent>\n </PopoverRoot>\n </ControlsContainer>\n <AudioProgress\n currentTime={currentTime}\n duration={duration}\n onValueChange={handleSliderChange}\n variant=\"simple\"\n />\n </AudioContainer>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAM,iBAAiB,OAAO,OAAO,EACnC,MAAM;CACJ,SAAS;CACT,KAAK;CACL,eAAe;CACf,SAAS;CACT,iBAAiB;CACjB,cAAc;CACd,WAAW;CACX,YAAY;CACb,EACF,CAAC;AAEF,MAAM,oBAAoB,OAAO,OAAO,EACtC,MAAM;CACJ,SAAS;CACT,KAAK;CACL,YAAY;CACb,EACF,CAAC;AAOF,MAAM,aAAa,OAAO,MAAM,EAC9B,MAAM;CACJ,UAAU;CACV,YAAY;CACZ,WAAW;CACZ,EACF,CAAC;AAEF,MAAM,mBAAmB,OAAO,YAAY,EAC1C,MAAM,EACJ,mBAAmB,QACpB,EACF,CAAC;AAEF,MAAM,eAAe,OAAO,MAAM,EAChC,MAAM;CACJ,UAAU;CACV,cAAc;CACd,YAAY;CACb,EACF,CAAC;AAEF,MAAa,sBAAsB,EAAE,KAAK,YAAmB;CAC3D,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,EACJ,UACA,SACA,YACA,aACA,UACA,oBACA,aACA,0BACA,SACA,iBACE,kBAAkB;AACtB,QACE,qBAAC,gBAAD,EAAA,UAAA;EACE,oBAAC,cAAD;GACE,KAAK;GACA;GACE;GACE;GACT,kBAAkB;GAClB,cAAc;GACd,CAAA;EACF,qBAAC,mBAAD,EAAA,UAAA;GACE,oBAAC,YAAD;IAAqB;IAAS,SAAS;IAAc,CAAA;GACrD,qBAAC,YAAD,EAAA,UAAA;IACE,oBAAC,MAAD;KAAM,WAAU;KAAe,SAAA;KAAQ,YAAA;eACrC,oBAAC,QAAD,EAAA,UAAO,WAAW,YAAY,EAAQ,CAAA;KACjC,CAAA;IACN;IACD,oBAAC,MAAD;KAAM,WAAU;KAAe,OAAM;KAAc,SAAA;KAAQ,YAAA;eACzD,oBAAC,QAAD,EAAA,UAAO,WAAW,SAAS,EAAQ,CAAA;KAC9B,CAAA;IACI,EAAA,CAAA;GACb,oBAAC,cAAD;IAAc,WAAU;cAAgB;IAAqB,CAAA;GAC7D,qBAAC,aAAD;IAAa,aAAa,EAAE,WAAW,OAAO;cAA9C,CACE,oBAAC,gBAAD;KAAgB,SAAA;eACd,oBAAC,kBAAD;MAAkB,SAAQ;gBACxB,oBAAC,cAAD,EAAgB,CAAA;MACC,CAAA;KACJ,CAAA,EACjB,qBAAC,gBAAD,EAAA,UAAA,CACE,oBAAC,cAAD;KAAc,QAAA;eAAQ,EAAE,8BAA8B;KAAgB,CAAA,EACtE,oBAAC,cAAD;KAAc,OAAO;KAAa,eAAe;KAA4B,CAAA,CAC9D,EAAA,CAAA,CACL;;GACI,EAAA,CAAA;EACpB,oBAAC,eAAD;GACe;GACH;GACV,eAAe;GACf,SAAQ;GACR,CAAA;EACa,EAAA,CAAA"}
@@ -1,8 +1,13 @@
1
- import { Button, FieldRoot, IconButton, PopoverContent, PopoverRoot, PopoverTrigger, SelectContent, SelectControl, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectRoot, SelectTrigger, SliderControl, SliderHiddenInput, SliderLabel, SliderRange, SliderRoot, SliderThumb, SliderTrack, Text } from "@ndla/primitives";
1
+ import { AudioElement } from "./AudioElement.mjs";
2
+ import { formatTime } from "./audioUtils.mjs";
3
+ import { AudioProgress } from "./AudioProgress.mjs";
4
+ import { PlayButton } from "./PlayButton.mjs";
5
+ import { useAudioControls } from "./useAudioControls.mjs";
6
+ import { VolumeSlider } from "./VolumeSlider.mjs";
7
+ import { Button, FieldRoot, IconButton, PopoverContent, PopoverRoot, PopoverTrigger, SelectContent, SelectControl, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectRoot, SelectTrigger, Text } from "@ndla/primitives";
2
8
  import { styled } from "@ndla/styled-system/jsx";
3
- import { useEffect, useRef, useState } from "react";
4
9
  import { useTranslation } from "react-i18next";
5
- import { CheckLine, Forward15Line, PauseLine, PlayFill, Replay15Line, VolumeUpFill } from "@ndla/icons";
10
+ import { CheckLine, Forward15Line, Replay15Line, VolumeUpFill } from "@ndla/icons";
6
11
  import { jsx, jsxs } from "react/jsx-runtime";
7
12
  import { createListCollection } from "@ark-ui/react";
8
13
  //#region src/AudioPlayer/Controls.tsx
@@ -36,7 +41,7 @@ const ControlsWrapper = styled("div", { base: {
36
41
  },
37
42
  mobileWideDown: { columnGap: "3xsmall" }
38
43
  } });
39
- const PlayButton = styled(IconButton, { base: { gridArea: "play" } });
44
+ const StyledPlayButton = styled(PlayButton, { base: { gridArea: "play" } });
40
45
  const Forward15SecButton = styled(IconButton, { base: { gridArea: "forwards" } });
41
46
  const Back15SecButton = styled(IconButton, { base: { gridArea: "backwards" } });
42
47
  const ProgressWrapper = styled("div", { base: {
@@ -64,16 +69,7 @@ const SpeedButton = styled(Button, { base: {
64
69
  "& span": { flex: "1" }
65
70
  } });
66
71
  const StyledSelectRoot = styled(SelectRoot, { base: { gridArea: "speed" } });
67
- const StyledSliderControl = styled(SliderControl, { base: {
68
- height: "surface.3xsmall",
69
- minWidth: "small"
70
- } });
71
72
  const StyledPopoverContent = styled(PopoverContent, { base: { paddingInline: "small" } });
72
- const formatTime = (seconds) => {
73
- const minutes = Math.floor(seconds / 60);
74
- const currentSeconds = seconds % 60;
75
- return `${minutes}:${currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds}`;
76
- };
77
73
  const speedValues = createListCollection({ items: [
78
74
  "0.5",
79
75
  "0.75",
@@ -85,68 +81,14 @@ const speedValues = createListCollection({ items: [
85
81
  ] });
86
82
  const Controls = ({ src, title }) => {
87
83
  const { t } = useTranslation();
88
- const [speedValue, setSpeedValue] = useState(1);
89
- const [volumeValue, setVolumeValue] = useState(100);
90
- const [currentTime, setCurrentTime] = useState(0);
91
- const [duration, setDuration] = useState(0);
92
- const [playing, setPlaying] = useState(false);
93
- const audioRef = useRef(null);
94
- useEffect(() => {
95
- if (audioRef.current) {
96
- const audioElement = audioRef.current;
97
- const handleTimeUpdate = () => {
98
- const { currentTime, duration } = audioElement;
99
- setCurrentTime(Math.round(currentTime));
100
- setDuration(Math.round(duration));
101
- };
102
- const handleLoadedMetaData = () => {
103
- const { currentTime, duration } = audioElement;
104
- setCurrentTime(Math.round(currentTime));
105
- setDuration(Math.round(duration));
106
- };
107
- const handleTimeEnded = () => {
108
- setPlaying(false);
109
- };
110
- audioElement.addEventListener("timeupdate", handleTimeUpdate);
111
- audioElement.addEventListener("loadedmetadata", handleLoadedMetaData);
112
- audioElement.addEventListener("ended", handleTimeEnded);
113
- return () => {
114
- audioElement.removeEventListener("timeupdate", handleTimeUpdate);
115
- audioElement.removeEventListener("loadedmetadata", handleLoadedMetaData);
116
- audioElement.removeEventListener("ended", handleTimeEnded);
117
- };
118
- }
119
- }, []);
120
- const togglePlay = () => {
121
- if (audioRef.current) {
122
- const audioElement = audioRef.current;
123
- if (!playing) audioElement.play();
124
- else audioElement.pause();
125
- setPlaying(!playing);
126
- }
127
- };
128
- const onPlaybackRateChange = (rate) => {
129
- setSpeedValue(rate);
130
- if (audioRef.current) audioRef.current.playbackRate = rate;
131
- };
132
- const onSeekSeconds = (seconds) => {
133
- if (audioRef.current) audioRef.current.currentTime += seconds;
134
- };
135
- const handleSliderChange = (details) => {
136
- const newValue = details.value[0];
137
- if (audioRef.current && newValue != null && !isNaN(newValue)) audioRef.current.currentTime = details.value[0];
138
- };
139
- const handleVolumeSliderChange = (details) => {
140
- if (audioRef.current) {
141
- audioRef.current.volume = details.value[0] / 100;
142
- setVolumeValue(details.value[0]);
143
- }
144
- };
145
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("audio", {
146
- ref: audioRef,
84
+ const { audioRef, onEnded, onHandleTime, onSeekSeconds, playing, togglePlay, handleSliderChange, handleVolumeSliderChange, currentTime, duration, speedValue, onPlaybackRateChange, volumeValue } = useAudioControls();
85
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(AudioElement, {
147
86
  src,
148
87
  title,
149
- preload: "metadata"
88
+ ref: audioRef,
89
+ onEnded,
90
+ onLoadedMetadata: onHandleTime,
91
+ onTimeUpdate: onHandleTime
150
92
  }), /* @__PURE__ */ jsxs(ControlsWrapper, { children: [
151
93
  /* @__PURE__ */ jsx(Back15SecButton, {
152
94
  variant: "tertiary",
@@ -155,11 +97,9 @@ const Controls = ({ src, title }) => {
155
97
  onClick: () => onSeekSeconds(-15),
156
98
  children: /* @__PURE__ */ jsx(Replay15Line, {})
157
99
  }),
158
- /* @__PURE__ */ jsx(PlayButton, {
159
- "aria-label": t(playing ? t("audio.pause") : t("audio.play")),
160
- variant: "primary",
161
- onClick: togglePlay,
162
- children: playing ? /* @__PURE__ */ jsx(PauseLine, {}) : /* @__PURE__ */ jsx(PlayFill, {})
100
+ /* @__PURE__ */ jsx(StyledPlayButton, {
101
+ playing,
102
+ onClick: togglePlay
163
103
  }),
164
104
  /* @__PURE__ */ jsx(Forward15SecButton, {
165
105
  variant: "tertiary",
@@ -175,23 +115,10 @@ const Controls = ({ src, title }) => {
175
115
  consumeCss: true,
176
116
  children: /* @__PURE__ */ jsx("div", { children: formatTime(currentTime) })
177
117
  }),
178
- /* @__PURE__ */ jsxs(SliderRoot, {
179
- value: [currentTime],
180
- defaultValue: [0],
181
- step: 1,
182
- max: duration,
183
- onValueChange: handleSliderChange,
184
- getAriaValueText: (value) => t("audio.valueText", {
185
- start: formatTime(Math.round(value.value)),
186
- end: formatTime(Math.round(duration))
187
- }),
188
- children: [/* @__PURE__ */ jsx(SliderLabel, {
189
- srOnly: true,
190
- children: t("audio.progressBar")
191
- }), /* @__PURE__ */ jsxs(SliderControl, { children: [/* @__PURE__ */ jsx(SliderTrack, { children: /* @__PURE__ */ jsx(SliderRange, {}) }), /* @__PURE__ */ jsx(SliderThumb, {
192
- index: 0,
193
- children: /* @__PURE__ */ jsx(SliderHiddenInput, {})
194
- })] })]
118
+ /* @__PURE__ */ jsx(AudioProgress, {
119
+ currentTime,
120
+ duration,
121
+ onValueChange: handleSliderChange
195
122
  }),
196
123
  /* @__PURE__ */ jsx(StyledText, {
197
124
  textStyle: "label.medium",
@@ -234,21 +161,9 @@ const Controls = ({ src, title }) => {
234
161
  "aria-label": t("audio.controls.adjustVolume"),
235
162
  children: /* @__PURE__ */ jsx(VolumeUpFill, {})
236
163
  })
237
- }), /* @__PURE__ */ jsx(StyledPopoverContent, { children: /* @__PURE__ */ jsxs(SliderRoot, {
238
- orientation: "vertical",
239
- value: [volumeValue],
240
- min: 0,
241
- max: 100,
242
- defaultValue: [100],
243
- step: 1,
244
- onValueChange: handleVolumeSliderChange,
245
- children: [/* @__PURE__ */ jsx(SliderLabel, {
246
- srOnly: true,
247
- children: t("audio.controls.adjustVolume")
248
- }), /* @__PURE__ */ jsxs(StyledSliderControl, { children: [/* @__PURE__ */ jsx(SliderTrack, { children: /* @__PURE__ */ jsx(SliderRange, {}) }), /* @__PURE__ */ jsx(SliderThumb, {
249
- index: 0,
250
- children: /* @__PURE__ */ jsx(SliderHiddenInput, {})
251
- })] })]
164
+ }), /* @__PURE__ */ jsx(StyledPopoverContent, { children: /* @__PURE__ */ jsx(VolumeSlider, {
165
+ value: volumeValue,
166
+ onValueChange: handleVolumeSliderChange
252
167
  }) })]
253
168
  })
254
169
  ] })] });
@@ -1 +1 @@
1
- {"version":3,"file":"Controls.mjs","names":[],"sources":["../../src/AudioPlayer/Controls.tsx"],"sourcesContent":["/**\n * Copyright (c) 2021-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { type SliderValueChangeDetails, createListCollection } from \"@ark-ui/react\";\nimport { Replay15Line, Forward15Line, PlayFill, PauseLine, VolumeUpFill, CheckLine } from \"@ndla/icons\";\nimport {\n Button,\n FieldRoot,\n IconButton,\n PopoverContent,\n PopoverRoot,\n PopoverTrigger,\n SelectContent,\n SelectControl,\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n SelectLabel,\n SelectRoot,\n SelectTrigger,\n SliderControl,\n SliderHiddenInput,\n SliderLabel,\n SliderRange,\n SliderRoot,\n SliderThumb,\n SliderTrack,\n Text,\n} from \"@ndla/primitives\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nconst ControlsWrapper = styled(\"div\", {\n base: {\n borderBlockStart: \"1px solid\",\n borderColor: \"stroke.default\",\n borderBottomRadius: \"xsmall\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: \"background.default\",\n gap: \"xsmall\",\n paddingBlock: \"xsmall\",\n paddingInline: \"medium\",\n tabletWideDown: {\n display: \"grid\",\n paddingBlock: \"xsmall\",\n paddingInline: \"xsmall\",\n gridTemplateColumns: \"1fr repeat(5, auto) 1fr\",\n gridTemplateAreas: `\n \"track track track track track track track\"\n \". speed backwards play forwards volume .\"\n`,\n },\n mobileWideDown: {\n columnGap: \"3xsmall\",\n },\n },\n});\n\nconst PlayButton = styled(IconButton, {\n base: {\n gridArea: \"play\",\n },\n});\n\nconst Forward15SecButton = styled(IconButton, {\n base: {\n gridArea: \"forwards\",\n },\n});\n\nconst Back15SecButton = styled(IconButton, {\n base: {\n gridArea: \"backwards\",\n },\n});\n\nconst ProgressWrapper = styled(\"div\", {\n base: {\n flex: \"1\",\n display: \"flex\",\n alignItems: \"center\",\n gap: \"xxsmall\",\n gridArea: \"track\",\n paddingBlock: \"xsmall\",\n mobileDown: {\n paddingInline: \"xsmall\",\n },\n },\n});\n\nconst StyledText = styled(Text, {\n base: {\n minWidth: \"xxlarge\",\n flexShrink: \"0\",\n textAlign: \"center\",\n },\n});\n\nconst VolumeButton = styled(IconButton, {\n base: {\n gridArea: \"volume\",\n },\n});\n\nconst SpeedButton = styled(Button, {\n base: {\n paddingBlock: \"auto\",\n paddingInline: \"auto\",\n maxWidth: \"xxlarge\",\n maxHeight: \"xxlarge\",\n minWidth: \"xxlarge\",\n minHeight: \"xxlarge\",\n \"& span\": {\n flex: \"1\",\n },\n },\n});\n\nconst StyledSelectRoot = styled(SelectRoot<string>, {\n base: {\n gridArea: \"speed\",\n },\n});\n\nconst StyledSliderControl = styled(SliderControl, {\n base: {\n height: \"surface.3xsmall\",\n minWidth: \"small\",\n },\n});\n\nconst StyledPopoverContent = styled(PopoverContent, {\n base: {\n paddingInline: \"small\",\n },\n});\n\nconst formatTime = (seconds: number) => {\n const minutes = Math.floor(seconds / 60);\n const currentSeconds = seconds % 60;\n\n const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;\n return `${minutes}:${formattedSeconds}`;\n};\n\nconst speedValues = createListCollection({ items: [\"0.5\", \"0.75\", \"1\", \"1.25\", \"1.5\", \"1.75\", \"2\"] });\n\ninterface Props {\n src: string;\n title: string;\n}\n\nexport const Controls = ({ src, title }: Props) => {\n const { t } = useTranslation();\n const [speedValue, setSpeedValue] = useState(1);\n const [volumeValue, setVolumeValue] = useState(100);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [playing, setPlaying] = useState(false);\n const audioRef = useRef<HTMLAudioElement>(null);\n\n useEffect(() => {\n if (audioRef.current) {\n const audioElement = audioRef.current;\n const handleTimeUpdate = () => {\n const { currentTime, duration } = audioElement;\n setCurrentTime(Math.round(currentTime));\n setDuration(Math.round(duration));\n };\n\n const handleLoadedMetaData = () => {\n const { currentTime, duration } = audioElement;\n setCurrentTime(Math.round(currentTime));\n setDuration(Math.round(duration));\n };\n\n const handleTimeEnded = () => {\n setPlaying(false);\n };\n\n audioElement.addEventListener(\"timeupdate\", handleTimeUpdate);\n audioElement.addEventListener(\"loadedmetadata\", handleLoadedMetaData);\n audioElement.addEventListener(\"ended\", handleTimeEnded);\n return () => {\n audioElement.removeEventListener(\"timeupdate\", handleTimeUpdate);\n audioElement.removeEventListener(\"loadedmetadata\", handleLoadedMetaData);\n audioElement.removeEventListener(\"ended\", handleTimeEnded);\n };\n }\n }, []);\n\n const togglePlay = () => {\n if (audioRef.current) {\n const audioElement = audioRef.current;\n if (!playing) {\n audioElement.play();\n } else {\n audioElement.pause();\n }\n setPlaying(!playing);\n }\n };\n\n const onPlaybackRateChange = (rate: number) => {\n setSpeedValue(rate);\n if (audioRef.current) {\n audioRef.current.playbackRate = rate;\n }\n };\n\n const onSeekSeconds = (seconds: number) => {\n if (audioRef.current) {\n audioRef.current.currentTime += seconds;\n }\n };\n\n const handleSliderChange = (details: SliderValueChangeDetails) => {\n const newValue = details.value[0];\n if (audioRef.current && newValue != null && !isNaN(newValue)) {\n audioRef.current.currentTime = details.value[0];\n }\n };\n\n const handleVolumeSliderChange = (details: SliderValueChangeDetails) => {\n if (audioRef.current) {\n audioRef.current.volume = details.value[0] / 100;\n setVolumeValue(details.value[0]);\n }\n };\n\n return (\n <div>\n {/* TODO: We should tie this up to the textual description somehow */}\n {/* oxlint-disable-next-line jsx-a11y/media-has-caption */}\n <audio ref={audioRef} src={src} title={title} preload=\"metadata\" />\n <ControlsWrapper>\n <Back15SecButton\n variant=\"tertiary\"\n title={t(\"audio.controls.rewind15sec\")}\n aria-label={t(\"audio.controls.rewind15sec\")}\n onClick={() => onSeekSeconds(-15)}\n >\n <Replay15Line />\n </Back15SecButton>\n <PlayButton aria-label={t(playing ? t(\"audio.pause\") : t(\"audio.play\"))} variant=\"primary\" onClick={togglePlay}>\n {playing ? <PauseLine /> : <PlayFill />}\n </PlayButton>\n <Forward15SecButton\n variant=\"tertiary\"\n title={t(\"audio.controls.forward15sec\")}\n aria-label={t(\"audio.controls.forward15sec\")}\n onClick={() => onSeekSeconds(15)}\n >\n <Forward15Line />\n </Forward15SecButton>\n <ProgressWrapper>\n <StyledText textStyle=\"label.medium\" asChild consumeCss>\n <div>{formatTime(currentTime)}</div>\n </StyledText>\n <SliderRoot\n value={[currentTime]}\n defaultValue={[0]}\n step={1}\n max={duration}\n onValueChange={handleSliderChange}\n getAriaValueText={(value) =>\n t(\"audio.valueText\", {\n start: formatTime(Math.round(value.value)),\n end: formatTime(Math.round(duration)),\n })\n }\n >\n <SliderLabel srOnly>{t(\"audio.progressBar\")}</SliderLabel>\n <SliderControl>\n <SliderTrack>\n <SliderRange />\n </SliderTrack>\n <SliderThumb index={0}>\n <SliderHiddenInput />\n </SliderThumb>\n </SliderControl>\n </SliderRoot>\n <StyledText textStyle=\"label.medium\" asChild consumeCss>\n <div>-{formatTime(Math.round(duration - currentTime))}</div>\n </StyledText>\n </ProgressWrapper>\n <FieldRoot>\n <StyledSelectRoot\n collection={speedValues}\n value={[speedValue.toString()]}\n onValueChange={(details) => onPlaybackRateChange(parseFloat(details.value[0]))}\n positioning={{ placement: \"top\" }}\n >\n <SelectLabel srOnly>{t(\"audio.controls.selectSpeed\")}</SelectLabel>\n <SelectControl>\n <SelectTrigger asChild>\n <SpeedButton\n variant=\"tertiary\"\n title={t(\"audio.controls.selectSpeed\")}\n aria-label={t(\"audio.controls.selectSpeed\")}\n >\n <span>{`${speedValue}x`}</span>\n </SpeedButton>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {speedValues.items.map((speed) => (\n <SelectItem key={speed} item={speed}>\n <SelectItemText>{speed}x</SelectItemText>\n <SelectItemIndicator>\n <CheckLine />\n </SelectItemIndicator>\n </SelectItem>\n ))}\n </SelectContent>\n </StyledSelectRoot>\n </FieldRoot>\n <PopoverRoot positioning={{ placement: \"top\" }}>\n <PopoverTrigger asChild>\n <VolumeButton variant=\"tertiary\" aria-label={t(\"audio.controls.adjustVolume\")}>\n <VolumeUpFill />\n </VolumeButton>\n </PopoverTrigger>\n <StyledPopoverContent>\n <SliderRoot\n orientation=\"vertical\"\n value={[volumeValue]}\n min={0}\n max={100}\n defaultValue={[100]}\n step={1}\n onValueChange={handleVolumeSliderChange}\n >\n <SliderLabel srOnly>{t(\"audio.controls.adjustVolume\")}</SliderLabel>\n <StyledSliderControl>\n <SliderTrack>\n <SliderRange />\n </SliderTrack>\n <SliderThumb index={0}>\n <SliderHiddenInput />\n </SliderThumb>\n </StyledSliderControl>\n </SliderRoot>\n </StyledPopoverContent>\n </PopoverRoot>\n </ControlsWrapper>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;AAsCA,MAAM,kBAAkB,OAAO,OAAO,EACpC,MAAM;CACJ,kBAAkB;CAClB,aAAa;CACb,oBAAoB;CACpB,SAAS;CACT,YAAY;CACZ,gBAAgB;CAChB,YAAY;CACZ,KAAK;CACL,cAAc;CACd,eAAe;CACf,gBAAgB;EACd,SAAS;EACT,cAAc;EACd,eAAe;EACf,qBAAqB;EACrB,mBAAmB;;;;EAIpB;CACD,gBAAgB,EACd,WAAW,WACZ;CACF,EACF,CAAC;AAEF,MAAM,aAAa,OAAO,YAAY,EACpC,MAAM,EACJ,UAAU,QACX,EACF,CAAC;AAEF,MAAM,qBAAqB,OAAO,YAAY,EAC5C,MAAM,EACJ,UAAU,YACX,EACF,CAAC;AAEF,MAAM,kBAAkB,OAAO,YAAY,EACzC,MAAM,EACJ,UAAU,aACX,EACF,CAAC;AAEF,MAAM,kBAAkB,OAAO,OAAO,EACpC,MAAM;CACJ,MAAM;CACN,SAAS;CACT,YAAY;CACZ,KAAK;CACL,UAAU;CACV,cAAc;CACd,YAAY,EACV,eAAe,UAChB;CACF,EACF,CAAC;AAEF,MAAM,aAAa,OAAO,MAAM,EAC9B,MAAM;CACJ,UAAU;CACV,YAAY;CACZ,WAAW;CACZ,EACF,CAAC;AAEF,MAAM,eAAe,OAAO,YAAY,EACtC,MAAM,EACJ,UAAU,UACX,EACF,CAAC;AAEF,MAAM,cAAc,OAAO,QAAQ,EACjC,MAAM;CACJ,cAAc;CACd,eAAe;CACf,UAAU;CACV,WAAW;CACX,UAAU;CACV,WAAW;CACX,UAAU,EACR,MAAM,KACP;CACF,EACF,CAAC;AAEF,MAAM,mBAAmB,OAAO,YAAoB,EAClD,MAAM,EACJ,UAAU,SACX,EACF,CAAC;AAEF,MAAM,sBAAsB,OAAO,eAAe,EAChD,MAAM;CACJ,QAAQ;CACR,UAAU;CACX,EACF,CAAC;AAEF,MAAM,uBAAuB,OAAO,gBAAgB,EAClD,MAAM,EACJ,eAAe,SAChB,EACF,CAAC;AAEF,MAAM,cAAc,YAAoB;CACtC,MAAM,UAAU,KAAK,MAAM,UAAU,GAAG;CACxC,MAAM,iBAAiB,UAAU;AAGjC,QAAO,GAAG,QAAQ,GADO,iBAAiB,KAAK,IAAI,mBAAmB;;AAIxE,MAAM,cAAc,qBAAqB,EAAE,OAAO;CAAC;CAAO;CAAQ;CAAK;CAAQ;CAAO;CAAQ;CAAI,EAAE,CAAC;AAOrG,MAAa,YAAY,EAAE,KAAK,YAAmB;CACjD,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,CAAC,YAAY,iBAAiB,SAAS,EAAE;CAC/C,MAAM,CAAC,aAAa,kBAAkB,SAAS,IAAI;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;CACjD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAC3C,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,WAAW,OAAyB,KAAK;AAE/C,iBAAgB;AACd,MAAI,SAAS,SAAS;GACpB,MAAM,eAAe,SAAS;GAC9B,MAAM,yBAAyB;IAC7B,MAAM,EAAE,aAAa,aAAa;AAClC,mBAAe,KAAK,MAAM,YAAY,CAAC;AACvC,gBAAY,KAAK,MAAM,SAAS,CAAC;;GAGnC,MAAM,6BAA6B;IACjC,MAAM,EAAE,aAAa,aAAa;AAClC,mBAAe,KAAK,MAAM,YAAY,CAAC;AACvC,gBAAY,KAAK,MAAM,SAAS,CAAC;;GAGnC,MAAM,wBAAwB;AAC5B,eAAW,MAAM;;AAGnB,gBAAa,iBAAiB,cAAc,iBAAiB;AAC7D,gBAAa,iBAAiB,kBAAkB,qBAAqB;AACrE,gBAAa,iBAAiB,SAAS,gBAAgB;AACvD,gBAAa;AACX,iBAAa,oBAAoB,cAAc,iBAAiB;AAChE,iBAAa,oBAAoB,kBAAkB,qBAAqB;AACxE,iBAAa,oBAAoB,SAAS,gBAAgB;;;IAG7D,EAAE,CAAC;CAEN,MAAM,mBAAmB;AACvB,MAAI,SAAS,SAAS;GACpB,MAAM,eAAe,SAAS;AAC9B,OAAI,CAAC,QACH,cAAa,MAAM;OAEnB,cAAa,OAAO;AAEtB,cAAW,CAAC,QAAQ;;;CAIxB,MAAM,wBAAwB,SAAiB;AAC7C,gBAAc,KAAK;AACnB,MAAI,SAAS,QACX,UAAS,QAAQ,eAAe;;CAIpC,MAAM,iBAAiB,YAAoB;AACzC,MAAI,SAAS,QACX,UAAS,QAAQ,eAAe;;CAIpC,MAAM,sBAAsB,YAAsC;EAChE,MAAM,WAAW,QAAQ,MAAM;AAC/B,MAAI,SAAS,WAAW,YAAY,QAAQ,CAAC,MAAM,SAAS,CAC1D,UAAS,QAAQ,cAAc,QAAQ,MAAM;;CAIjD,MAAM,4BAA4B,YAAsC;AACtE,MAAI,SAAS,SAAS;AACpB,YAAS,QAAQ,SAAS,QAAQ,MAAM,KAAK;AAC7C,kBAAe,QAAQ,MAAM,GAAG;;;AAIpC,QACE,qBAAC,OAAD,EAAA,UAAA,CAGE,oBAAC,SAAD;EAAO,KAAK;EAAe;EAAY;EAAO,SAAQ;EAAa,CAAA,EACnE,qBAAC,iBAAD,EAAA,UAAA;EACE,oBAAC,iBAAD;GACE,SAAQ;GACR,OAAO,EAAE,6BAA6B;GACtC,cAAY,EAAE,6BAA6B;GAC3C,eAAe,cAAc,IAAI;aAEjC,oBAAC,cAAD,EAAgB,CAAA;GACA,CAAA;EAClB,oBAAC,YAAD;GAAY,cAAY,EAAE,UAAU,EAAE,cAAc,GAAG,EAAE,aAAa,CAAC;GAAE,SAAQ;GAAU,SAAS;aACjG,UAAU,oBAAC,WAAD,EAAa,CAAA,GAAG,oBAAC,UAAD,EAAY,CAAA;GAC5B,CAAA;EACb,oBAAC,oBAAD;GACE,SAAQ;GACR,OAAO,EAAE,8BAA8B;GACvC,cAAY,EAAE,8BAA8B;GAC5C,eAAe,cAAc,GAAG;aAEhC,oBAAC,eAAD,EAAiB,CAAA;GACE,CAAA;EACrB,qBAAC,iBAAD,EAAA,UAAA;GACE,oBAAC,YAAD;IAAY,WAAU;IAAe,SAAA;IAAQ,YAAA;cAC3C,oBAAC,OAAD,EAAA,UAAM,WAAW,YAAY,EAAO,CAAA;IACzB,CAAA;GACb,qBAAC,YAAD;IACE,OAAO,CAAC,YAAY;IACpB,cAAc,CAAC,EAAE;IACjB,MAAM;IACN,KAAK;IACL,eAAe;IACf,mBAAmB,UACjB,EAAE,mBAAmB;KACnB,OAAO,WAAW,KAAK,MAAM,MAAM,MAAM,CAAC;KAC1C,KAAK,WAAW,KAAK,MAAM,SAAS,CAAC;KACtC,CAAC;cAVN,CAaE,oBAAC,aAAD;KAAa,QAAA;eAAQ,EAAE,oBAAoB;KAAe,CAAA,EAC1D,qBAAC,eAAD,EAAA,UAAA,CACE,oBAAC,aAAD,EAAA,UACE,oBAAC,aAAD,EAAe,CAAA,EACH,CAAA,EACd,oBAAC,aAAD;KAAa,OAAO;eAClB,oBAAC,mBAAD,EAAqB,CAAA;KACT,CAAA,CACA,EAAA,CAAA,CACL;;GACb,oBAAC,YAAD;IAAY,WAAU;IAAe,SAAA;IAAQ,YAAA;cAC3C,qBAAC,OAAD,EAAA,UAAA,CAAK,KAAE,WAAW,KAAK,MAAM,WAAW,YAAY,CAAC,CAAO,EAAA,CAAA;IACjD,CAAA;GACG,EAAA,CAAA;EAClB,oBAAC,WAAD,EAAA,UACE,qBAAC,kBAAD;GACE,YAAY;GACZ,OAAO,CAAC,WAAW,UAAU,CAAC;GAC9B,gBAAgB,YAAY,qBAAqB,WAAW,QAAQ,MAAM,GAAG,CAAC;GAC9E,aAAa,EAAE,WAAW,OAAO;aAJnC;IAME,oBAAC,aAAD;KAAa,QAAA;eAAQ,EAAE,6BAA6B;KAAe,CAAA;IACnE,oBAAC,eAAD,EAAA,UACE,oBAAC,eAAD;KAAe,SAAA;eACb,oBAAC,aAAD;MACE,SAAQ;MACR,OAAO,EAAE,6BAA6B;MACtC,cAAY,EAAE,6BAA6B;gBAE3C,oBAAC,QAAD,EAAA,UAAO,GAAG,WAAW,IAAU,CAAA;MACnB,CAAA;KACA,CAAA,EACF,CAAA;IAChB,oBAAC,eAAD,EAAA,UACG,YAAY,MAAM,KAAK,UACtB,qBAAC,YAAD;KAAwB,MAAM;eAA9B,CACE,qBAAC,gBAAD,EAAA,UAAA,CAAiB,OAAM,IAAkB,EAAA,CAAA,EACzC,oBAAC,qBAAD,EAAA,UACE,oBAAC,WAAD,EAAa,CAAA,EACO,CAAA,CACX;OALI,MAKJ,CACb,EACY,CAAA;IACC;MACT,CAAA;EACZ,qBAAC,aAAD;GAAa,aAAa,EAAE,WAAW,OAAO;aAA9C,CACE,oBAAC,gBAAD;IAAgB,SAAA;cACd,oBAAC,cAAD;KAAc,SAAQ;KAAW,cAAY,EAAE,8BAA8B;eAC3E,oBAAC,cAAD,EAAgB,CAAA;KACH,CAAA;IACA,CAAA,EACjB,oBAAC,sBAAD,EAAA,UACE,qBAAC,YAAD;IACE,aAAY;IACZ,OAAO,CAAC,YAAY;IACpB,KAAK;IACL,KAAK;IACL,cAAc,CAAC,IAAI;IACnB,MAAM;IACN,eAAe;cAPjB,CASE,oBAAC,aAAD;KAAa,QAAA;eAAQ,EAAE,8BAA8B;KAAe,CAAA,EACpE,qBAAC,qBAAD,EAAA,UAAA,CACE,oBAAC,aAAD,EAAA,UACE,oBAAC,aAAD,EAAe,CAAA,EACH,CAAA,EACd,oBAAC,aAAD;KAAa,OAAO;eAClB,oBAAC,mBAAD,EAAqB,CAAA;KACT,CAAA,CACM,EAAA,CAAA,CACX;OACQ,CAAA,CACX;;EACE,EAAA,CAAA,CACd,EAAA,CAAA"}
1
+ {"version":3,"file":"Controls.mjs","names":[],"sources":["../../src/AudioPlayer/Controls.tsx"],"sourcesContent":["/**\n * Copyright (c) 2021-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { createListCollection } from \"@ark-ui/react\";\nimport { Replay15Line, Forward15Line, VolumeUpFill, CheckLine } from \"@ndla/icons\";\nimport {\n Button,\n FieldRoot,\n IconButton,\n PopoverContent,\n PopoverRoot,\n PopoverTrigger,\n SelectContent,\n SelectControl,\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n SelectLabel,\n SelectRoot,\n SelectTrigger,\n Text,\n} from \"@ndla/primitives\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { AudioElement } from \"./AudioElement\";\nimport { AudioProgress } from \"./AudioProgress\";\nimport { formatTime } from \"./audioUtils\";\nimport { PlayButton } from \"./PlayButton\";\nimport { useAudioControls } from \"./useAudioControls\";\nimport { VolumeSlider } from \"./VolumeSlider\";\n\nconst ControlsWrapper = styled(\"div\", {\n base: {\n borderBlockStart: \"1px solid\",\n borderColor: \"stroke.default\",\n borderBottomRadius: \"xsmall\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: \"background.default\",\n gap: \"xsmall\",\n paddingBlock: \"xsmall\",\n paddingInline: \"medium\",\n tabletWideDown: {\n display: \"grid\",\n paddingBlock: \"xsmall\",\n paddingInline: \"xsmall\",\n gridTemplateColumns: \"1fr repeat(5, auto) 1fr\",\n gridTemplateAreas: `\n \"track track track track track track track\"\n \". speed backwards play forwards volume .\"\n`,\n },\n mobileWideDown: {\n columnGap: \"3xsmall\",\n },\n },\n});\n\nconst StyledPlayButton = styled(PlayButton, {\n base: {\n gridArea: \"play\",\n },\n});\n\nconst Forward15SecButton = styled(IconButton, {\n base: {\n gridArea: \"forwards\",\n },\n});\n\nconst Back15SecButton = styled(IconButton, {\n base: {\n gridArea: \"backwards\",\n },\n});\n\nconst ProgressWrapper = styled(\"div\", {\n base: {\n flex: \"1\",\n display: \"flex\",\n alignItems: \"center\",\n gap: \"xxsmall\",\n gridArea: \"track\",\n paddingBlock: \"xsmall\",\n mobileDown: {\n paddingInline: \"xsmall\",\n },\n },\n});\n\nconst StyledText = styled(Text, {\n base: {\n minWidth: \"xxlarge\",\n flexShrink: \"0\",\n textAlign: \"center\",\n },\n});\n\nconst VolumeButton = styled(IconButton, {\n base: {\n gridArea: \"volume\",\n },\n});\n\nconst SpeedButton = styled(Button, {\n base: {\n paddingBlock: \"auto\",\n paddingInline: \"auto\",\n maxWidth: \"xxlarge\",\n maxHeight: \"xxlarge\",\n minWidth: \"xxlarge\",\n minHeight: \"xxlarge\",\n \"& span\": {\n flex: \"1\",\n },\n },\n});\n\nconst StyledSelectRoot = styled(SelectRoot<string>, {\n base: {\n gridArea: \"speed\",\n },\n});\n\nconst StyledPopoverContent = styled(PopoverContent, {\n base: {\n paddingInline: \"small\",\n },\n});\n\nconst speedValues = createListCollection({ items: [\"0.5\", \"0.75\", \"1\", \"1.25\", \"1.5\", \"1.75\", \"2\"] });\n\ninterface Props {\n src: string;\n title: string;\n variant?: \"full\" | \"simplified\";\n}\n\nexport const Controls = ({ src, title }: Props) => {\n const { t } = useTranslation();\n const {\n audioRef,\n onEnded,\n onHandleTime,\n onSeekSeconds,\n playing,\n togglePlay,\n handleSliderChange,\n handleVolumeSliderChange,\n currentTime,\n duration,\n speedValue,\n onPlaybackRateChange,\n volumeValue,\n } = useAudioControls();\n\n return (\n <div>\n <AudioElement\n src={src}\n title={title}\n ref={audioRef}\n onEnded={onEnded}\n onLoadedMetadata={onHandleTime}\n onTimeUpdate={onHandleTime}\n />\n <ControlsWrapper>\n <Back15SecButton\n variant=\"tertiary\"\n title={t(\"audio.controls.rewind15sec\")}\n aria-label={t(\"audio.controls.rewind15sec\")}\n onClick={() => onSeekSeconds(-15)}\n >\n <Replay15Line />\n </Back15SecButton>\n <StyledPlayButton playing={playing} onClick={togglePlay} />\n <Forward15SecButton\n variant=\"tertiary\"\n title={t(\"audio.controls.forward15sec\")}\n aria-label={t(\"audio.controls.forward15sec\")}\n onClick={() => onSeekSeconds(15)}\n >\n <Forward15Line />\n </Forward15SecButton>\n <ProgressWrapper>\n <StyledText textStyle=\"label.medium\" asChild consumeCss>\n <div>{formatTime(currentTime)}</div>\n </StyledText>\n <AudioProgress currentTime={currentTime} duration={duration} onValueChange={handleSliderChange} />\n <StyledText textStyle=\"label.medium\" asChild consumeCss>\n <div>-{formatTime(Math.round(duration - currentTime))}</div>\n </StyledText>\n </ProgressWrapper>\n <FieldRoot>\n <StyledSelectRoot\n collection={speedValues}\n value={[speedValue.toString()]}\n onValueChange={(details) => onPlaybackRateChange(parseFloat(details.value[0]))}\n positioning={{ placement: \"top\" }}\n >\n <SelectLabel srOnly>{t(\"audio.controls.selectSpeed\")}</SelectLabel>\n <SelectControl>\n <SelectTrigger asChild>\n <SpeedButton\n variant=\"tertiary\"\n title={t(\"audio.controls.selectSpeed\")}\n aria-label={t(\"audio.controls.selectSpeed\")}\n >\n <span>{`${speedValue}x`}</span>\n </SpeedButton>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {speedValues.items.map((speed) => (\n <SelectItem key={speed} item={speed}>\n <SelectItemText>{speed}x</SelectItemText>\n <SelectItemIndicator>\n <CheckLine />\n </SelectItemIndicator>\n </SelectItem>\n ))}\n </SelectContent>\n </StyledSelectRoot>\n </FieldRoot>\n <PopoverRoot positioning={{ placement: \"top\" }}>\n <PopoverTrigger asChild>\n <VolumeButton variant=\"tertiary\" aria-label={t(\"audio.controls.adjustVolume\")}>\n <VolumeUpFill />\n </VolumeButton>\n </PopoverTrigger>\n <StyledPopoverContent>\n <VolumeSlider value={volumeValue} onValueChange={handleVolumeSliderChange} />\n </StyledPopoverContent>\n </PopoverRoot>\n </ControlsWrapper>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { IconButton } from "@ndla/primitives";
2
+ import { useTranslation } from "react-i18next";
3
+ import { PauseLine, PlayFill } from "@ndla/icons";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/AudioPlayer/PlayButton.tsx
6
+ /**
7
+ * Copyright (c) 2026-present, NDLA.
8
+ *
9
+ * This source code is licensed under the GPLv3 license found in the
10
+ * LICENSE file in the root directory of this source tree.
11
+ *
12
+ */
13
+ const PlayButton = ({ playing, children, ...rest }) => {
14
+ const { t } = useTranslation();
15
+ return /* @__PURE__ */ jsx(IconButton, {
16
+ "aria-label": playing ? t("audio.pause") : t("audio.play"),
17
+ ...rest,
18
+ children: children ?? (playing ? /* @__PURE__ */ jsx(PauseLine, {}) : /* @__PURE__ */ jsx(PlayFill, {}))
19
+ });
20
+ };
21
+ //#endregion
22
+ export { PlayButton };
23
+
24
+ //# sourceMappingURL=PlayButton.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlayButton.mjs","names":[],"sources":["../../src/AudioPlayer/PlayButton.tsx"],"sourcesContent":["/**\n * Copyright (c) 2026-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { PauseLine, PlayFill } from \"@ndla/icons\";\nimport { IconButton, type IconButtonProps } from \"@ndla/primitives\";\nimport { useTranslation } from \"react-i18next\";\n\ninterface Props extends IconButtonProps {\n playing?: boolean;\n}\n\nexport const PlayButton = ({ playing, children, ...rest }: Props) => {\n const { t } = useTranslation();\n return (\n <IconButton aria-label={playing ? t(\"audio.pause\") : t(\"audio.play\")} {...rest}>\n {children ?? (playing ? <PauseLine /> : <PlayFill />)}\n </IconButton>\n );\n};\n"],"mappings":";;;;;;;;;;;;AAgBA,MAAa,cAAc,EAAE,SAAS,UAAU,GAAG,WAAkB;CACnE,MAAM,EAAE,MAAM,gBAAgB;AAC9B,QACE,oBAAC,YAAD;EAAY,cAAY,UAAU,EAAE,cAAc,GAAG,EAAE,aAAa;EAAE,GAAI;YACvE,aAAa,UAAU,oBAAC,WAAD,EAAa,CAAA,GAAG,oBAAC,UAAD,EAAY,CAAA;EACzC,CAAA"}
@@ -1,6 +1,5 @@
1
- import { IconButton } from "@ndla/primitives";
2
- import { useRef } from "react";
3
- import { useTranslation } from "react-i18next";
1
+ import { PlayButton } from "./PlayButton.mjs";
2
+ import { useAudioControls } from "./useAudioControls.mjs";
4
3
  import { VolumeUpFill } from "@ndla/icons";
5
4
  import { jsx, jsxs } from "react/jsx-runtime";
6
5
  //#region src/AudioPlayer/SpeechControl.tsx
@@ -11,16 +10,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
11
10
  * LICENSE file in the root directory of this source tree.
12
11
  *
13
12
  */
14
- const SpeechControl = ({ src, title, type = "audio" }) => {
15
- const { t } = useTranslation();
16
- const audioRef = useRef(null);
17
- const togglePlay = () => {
18
- if (audioRef.current) {
19
- const audioElement = audioRef.current;
20
- if (audioElement.paused) audioElement.play();
21
- else audioElement.pause();
22
- }
23
- };
13
+ const SpeechControl = ({ src, title }) => {
14
+ const { audioRef, togglePlay } = useAudioControls();
24
15
  return /* @__PURE__ */ jsxs("div", {
25
16
  "data-embed-type": "speech",
26
17
  children: [/* @__PURE__ */ jsx("audio", {
@@ -28,10 +19,8 @@ const SpeechControl = ({ src, title, type = "audio" }) => {
28
19
  src,
29
20
  title,
30
21
  preload: "metadata"
31
- }), /* @__PURE__ */ jsx(IconButton, {
22
+ }), /* @__PURE__ */ jsx(PlayButton, {
32
23
  variant: "tertiary",
33
- "aria-label": t(`${type}.play`),
34
- title: t(`${type}.play`),
35
24
  onClick: togglePlay,
36
25
  children: /* @__PURE__ */ jsx(VolumeUpFill, {})
37
26
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"SpeechControl.mjs","names":[],"sources":["../../src/AudioPlayer/SpeechControl.tsx"],"sourcesContent":["/**\n * Copyright (c) 2021-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { VolumeUpFill } from \"@ndla/icons\";\nimport { IconButton } from \"@ndla/primitives\";\nimport { useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\ntype Props = {\n src: string;\n title: string;\n type?: \"gloss\" | \"audio\";\n};\n\nexport const SpeechControl = ({ src, title, type = \"audio\" }: Props) => {\n const { t } = useTranslation();\n const audioRef = useRef<HTMLAudioElement>(null);\n\n const togglePlay = () => {\n if (audioRef.current) {\n const audioElement = audioRef.current;\n if (audioElement.paused) {\n audioElement.play();\n } else {\n audioElement.pause();\n }\n }\n };\n return (\n <div data-embed-type=\"speech\">\n {/* oxlint-disable-next-line jsx-a11y/media-has-caption */}\n <audio ref={audioRef} src={src} title={title} preload=\"metadata\" />\n <IconButton variant=\"tertiary\" aria-label={t(`${type}.play`)} title={t(`${type}.play`)} onClick={togglePlay}>\n <VolumeUpFill />\n </IconButton>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;AAmBA,MAAa,iBAAiB,EAAE,KAAK,OAAO,OAAO,cAAqB;CACtE,MAAM,EAAE,MAAM,gBAAgB;CAC9B,MAAM,WAAW,OAAyB,KAAK;CAE/C,MAAM,mBAAmB;AACvB,MAAI,SAAS,SAAS;GACpB,MAAM,eAAe,SAAS;AAC9B,OAAI,aAAa,OACf,cAAa,MAAM;OAEnB,cAAa,OAAO;;;AAI1B,QACE,qBAAC,OAAD;EAAK,mBAAgB;YAArB,CAEE,oBAAC,SAAD;GAAO,KAAK;GAAe;GAAY;GAAO,SAAQ;GAAa,CAAA,EACnE,oBAAC,YAAD;GAAY,SAAQ;GAAW,cAAY,EAAE,GAAG,KAAK,OAAO;GAAE,OAAO,EAAE,GAAG,KAAK,OAAO;GAAE,SAAS;aAC/F,oBAAC,cAAD,EAAgB,CAAA;GACL,CAAA,CACT"}
1
+ {"version":3,"file":"SpeechControl.mjs","names":[],"sources":["../../src/AudioPlayer/SpeechControl.tsx"],"sourcesContent":["/**\n * Copyright (c) 2021-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { VolumeUpFill } from \"@ndla/icons\";\nimport { PlayButton } from \"./PlayButton\";\nimport { useAudioControls } from \"./useAudioControls\";\n\ntype Props = {\n src: string;\n title: string;\n};\n\nexport const SpeechControl = ({ src, title }: Props) => {\n const { audioRef, togglePlay } = useAudioControls();\n\n return (\n <div data-embed-type=\"speech\">\n {/* oxlint-disable-next-line jsx-a11y/media-has-caption */}\n <audio ref={audioRef} src={src} title={title} preload=\"metadata\" />\n <PlayButton variant=\"tertiary\" onClick={togglePlay}>\n <VolumeUpFill />\n </PlayButton>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;AAiBA,MAAa,iBAAiB,EAAE,KAAK,YAAmB;CACtD,MAAM,EAAE,UAAU,eAAe,kBAAkB;AAEnD,QACE,qBAAC,OAAD;EAAK,mBAAgB;YAArB,CAEE,oBAAC,SAAD;GAAO,KAAK;GAAe;GAAY;GAAO,SAAQ;GAAa,CAAA,EACnE,oBAAC,YAAD;GAAY,SAAQ;GAAW,SAAS;aACtC,oBAAC,cAAD,EAAgB,CAAA;GACL,CAAA,CACT"}
@@ -0,0 +1,31 @@
1
+ import { SliderControl, SliderHiddenInput, SliderLabel, SliderRange, SliderRoot, SliderThumb, SliderTrack } from "@ndla/primitives";
2
+ import { styled } from "@ndla/styled-system/jsx";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { t } from "i18next";
5
+ //#region src/AudioPlayer/VolumeSlider.tsx
6
+ const StyledSliderControl = styled(SliderControl, { base: {
7
+ height: "surface.3xsmall",
8
+ minWidth: "small"
9
+ } });
10
+ const VolumeSlider = ({ value, onValueChange }) => {
11
+ return /* @__PURE__ */ jsxs(SliderRoot, {
12
+ orientation: "vertical",
13
+ value: [value],
14
+ min: 0,
15
+ max: 100,
16
+ defaultValue: [100],
17
+ step: 1,
18
+ onValueChange,
19
+ children: [/* @__PURE__ */ jsx(SliderLabel, {
20
+ srOnly: true,
21
+ children: t("audio.controls.adjustVolume")
22
+ }), /* @__PURE__ */ jsxs(StyledSliderControl, { children: [/* @__PURE__ */ jsx(SliderTrack, { children: /* @__PURE__ */ jsx(SliderRange, {}) }), /* @__PURE__ */ jsx(SliderThumb, {
23
+ index: 0,
24
+ children: /* @__PURE__ */ jsx(SliderHiddenInput, {})
25
+ })] })]
26
+ });
27
+ };
28
+ //#endregion
29
+ export { VolumeSlider };
30
+
31
+ //# sourceMappingURL=VolumeSlider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VolumeSlider.mjs","names":[],"sources":["../../src/AudioPlayer/VolumeSlider.tsx"],"sourcesContent":["/**\n * Copyright (c) 2026-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type { SliderValueChangeDetails } from \"@ark-ui/react\";\nimport {\n SliderControl,\n SliderRoot,\n SliderLabel,\n SliderTrack,\n SliderRange,\n SliderHiddenInput,\n SliderThumb,\n} from \"@ndla/primitives\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { t } from \"i18next\";\n\nconst StyledSliderControl = styled(SliderControl, {\n base: {\n height: \"surface.3xsmall\",\n minWidth: \"small\",\n },\n});\n\ninterface Props {\n value: number;\n onValueChange: (value: SliderValueChangeDetails) => void;\n}\n\nexport const VolumeSlider = ({ value, onValueChange }: Props) => {\n return (\n <SliderRoot\n orientation=\"vertical\"\n value={[value]}\n min={0}\n max={100}\n defaultValue={[100]}\n step={1}\n onValueChange={onValueChange}\n >\n <SliderLabel srOnly>{t(\"audio.controls.adjustVolume\")}</SliderLabel>\n <StyledSliderControl>\n <SliderTrack>\n <SliderRange />\n </SliderTrack>\n <SliderThumb index={0}>\n <SliderHiddenInput />\n </SliderThumb>\n </StyledSliderControl>\n </SliderRoot>\n );\n};\n"],"mappings":";;;;;AAqBA,MAAM,sBAAsB,OAAO,eAAe,EAChD,MAAM;CACJ,QAAQ;CACR,UAAU;CACX,EACF,CAAC;AAOF,MAAa,gBAAgB,EAAE,OAAO,oBAA2B;AAC/D,QACE,qBAAC,YAAD;EACE,aAAY;EACZ,OAAO,CAAC,MAAM;EACd,KAAK;EACL,KAAK;EACL,cAAc,CAAC,IAAI;EACnB,MAAM;EACS;YAPjB,CASE,oBAAC,aAAD;GAAa,QAAA;aAAQ,EAAE,8BAA8B;GAAe,CAAA,EACpE,qBAAC,qBAAD,EAAA,UAAA,CACE,oBAAC,aAAD,EAAA,UACE,oBAAC,aAAD,EAAe,CAAA,EACH,CAAA,EACd,oBAAC,aAAD;GAAa,OAAO;aAClB,oBAAC,mBAAD,EAAqB,CAAA;GACT,CAAA,CACM,EAAA,CAAA,CACX"}
@@ -0,0 +1,17 @@
1
+ //#region src/AudioPlayer/audioUtils.ts
2
+ /**
3
+ * Copyright (c) 2026-present, NDLA.
4
+ *
5
+ * This source code is licensed under the GPLv3 license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ */
9
+ const formatTime = (seconds) => {
10
+ const minutes = Math.floor(seconds / 60);
11
+ const currentSeconds = seconds % 60;
12
+ return `${minutes}:${currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds}`;
13
+ };
14
+ //#endregion
15
+ export { formatTime };
16
+
17
+ //# sourceMappingURL=audioUtils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioUtils.mjs","names":[],"sources":["../../src/AudioPlayer/audioUtils.ts"],"sourcesContent":["/**\n * Copyright (c) 2026-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport const formatTime = (seconds: number) => {\n const minutes = Math.floor(seconds / 60);\n const currentSeconds = seconds % 60;\n\n const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;\n return `${minutes}:${formattedSeconds}`;\n};\n"],"mappings":";;;;;;;;AAQA,MAAa,cAAc,YAAoB;CAC7C,MAAM,UAAU,KAAK,MAAM,UAAU,GAAG;CACxC,MAAM,iBAAiB,UAAU;AAGjC,QAAO,GAAG,QAAQ,GADO,iBAAiB,KAAK,IAAI,mBAAmB"}
@@ -0,0 +1,55 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ //#region src/AudioPlayer/useAudioControls.ts
3
+ const useAudioControls = () => {
4
+ const [speedValue, setSpeedValue] = useState(1);
5
+ const [volumeValue, setVolumeValue] = useState(100);
6
+ const [currentTime, setCurrentTime] = useState(0);
7
+ const [duration, setDuration] = useState(0);
8
+ const [playing, setPlaying] = useState(false);
9
+ const audioRef = useRef(null);
10
+ const togglePlay = useCallback(() => {
11
+ if (!audioRef.current) return;
12
+ if (audioRef.current.paused) audioRef.current.play();
13
+ else audioRef.current.pause();
14
+ setPlaying((p) => !p);
15
+ }, []);
16
+ const onPlaybackRateChange = useCallback((rate) => {
17
+ setSpeedValue(rate);
18
+ if (audioRef.current) audioRef.current.playbackRate = rate;
19
+ }, []);
20
+ const onSeekSeconds = useCallback((seconds) => {
21
+ if (audioRef.current) audioRef.current.currentTime += seconds;
22
+ }, []);
23
+ const handleSliderChange = useCallback((details) => {
24
+ const newValue = details.value[0];
25
+ if (audioRef.current && newValue != null && !isNaN(newValue)) audioRef.current.currentTime = details.value[0];
26
+ }, []);
27
+ return {
28
+ togglePlay,
29
+ onPlaybackRateChange,
30
+ onSeekSeconds,
31
+ handleVolumeSliderChange: useCallback((details) => {
32
+ if (audioRef.current) {
33
+ audioRef.current.volume = details.value[0] / 100;
34
+ setVolumeValue(details.value[0]);
35
+ }
36
+ }, []),
37
+ handleSliderChange,
38
+ onEnded: useCallback(() => setPlaying(false), []),
39
+ onHandleTime: useCallback((meta) => {
40
+ const target = meta.currentTarget;
41
+ setCurrentTime(Math.round(target.currentTime));
42
+ setDuration(Math.round(target.duration));
43
+ }, []),
44
+ speedValue,
45
+ volumeValue,
46
+ currentTime,
47
+ duration,
48
+ playing,
49
+ audioRef
50
+ };
51
+ };
52
+ //#endregion
53
+ export { useAudioControls };
54
+
55
+ //# sourceMappingURL=useAudioControls.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAudioControls.mjs","names":[],"sources":["../../src/AudioPlayer/useAudioControls.ts"],"sourcesContent":["/**\n * Copyright (c) 2026-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport type { SliderValueChangeDetails } from \"@ark-ui/react\";\nimport { useCallback, useRef, useState, type ReactEventHandler } from \"react\";\n\nexport const useAudioControls = () => {\n const [speedValue, setSpeedValue] = useState(1);\n const [volumeValue, setVolumeValue] = useState(100);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [playing, setPlaying] = useState(false);\n const audioRef = useRef<HTMLAudioElement>(null);\n\n const togglePlay = useCallback(() => {\n if (!audioRef.current) return;\n if (audioRef.current.paused) {\n audioRef.current.play();\n } else {\n audioRef.current.pause();\n }\n setPlaying((p) => !p);\n }, []);\n\n const onPlaybackRateChange = useCallback((rate: number) => {\n setSpeedValue(rate);\n if (audioRef.current) {\n audioRef.current.playbackRate = rate;\n }\n }, []);\n\n const onSeekSeconds = useCallback((seconds: number) => {\n if (audioRef.current) {\n audioRef.current.currentTime += seconds;\n }\n }, []);\n\n const handleSliderChange = useCallback((details: SliderValueChangeDetails) => {\n const newValue = details.value[0];\n if (audioRef.current && newValue != null && !isNaN(newValue)) {\n audioRef.current.currentTime = details.value[0];\n }\n }, []);\n\n const handleVolumeSliderChange = useCallback((details: SliderValueChangeDetails) => {\n if (audioRef.current) {\n audioRef.current.volume = details.value[0] / 100;\n setVolumeValue(details.value[0]);\n }\n }, []);\n\n const onEnded = useCallback(() => setPlaying(false), []);\n\n const onHandleTime: ReactEventHandler<HTMLAudioElement> = useCallback((meta) => {\n const target = meta.currentTarget;\n setCurrentTime(Math.round(target.currentTime));\n setDuration(Math.round(target.duration));\n }, []);\n\n return {\n togglePlay,\n onPlaybackRateChange,\n onSeekSeconds,\n handleVolumeSliderChange,\n handleSliderChange,\n onEnded,\n onHandleTime,\n speedValue,\n volumeValue,\n currentTime,\n duration,\n playing,\n audioRef,\n };\n};\n"],"mappings":";;AAWA,MAAa,yBAAyB;CACpC,MAAM,CAAC,YAAY,iBAAiB,SAAS,EAAE;CAC/C,MAAM,CAAC,aAAa,kBAAkB,SAAS,IAAI;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,EAAE;CACjD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAC3C,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,WAAW,OAAyB,KAAK;CAE/C,MAAM,aAAa,kBAAkB;AACnC,MAAI,CAAC,SAAS,QAAS;AACvB,MAAI,SAAS,QAAQ,OACnB,UAAS,QAAQ,MAAM;MAEvB,UAAS,QAAQ,OAAO;AAE1B,cAAY,MAAM,CAAC,EAAE;IACpB,EAAE,CAAC;CAEN,MAAM,uBAAuB,aAAa,SAAiB;AACzD,gBAAc,KAAK;AACnB,MAAI,SAAS,QACX,UAAS,QAAQ,eAAe;IAEjC,EAAE,CAAC;CAEN,MAAM,gBAAgB,aAAa,YAAoB;AACrD,MAAI,SAAS,QACX,UAAS,QAAQ,eAAe;IAEjC,EAAE,CAAC;CAEN,MAAM,qBAAqB,aAAa,YAAsC;EAC5E,MAAM,WAAW,QAAQ,MAAM;AAC/B,MAAI,SAAS,WAAW,YAAY,QAAQ,CAAC,MAAM,SAAS,CAC1D,UAAS,QAAQ,cAAc,QAAQ,MAAM;IAE9C,EAAE,CAAC;AAiBN,QAAO;EACL;EACA;EACA;EACA,0BAnB+B,aAAa,YAAsC;AAClF,OAAI,SAAS,SAAS;AACpB,aAAS,QAAQ,SAAS,QAAQ,MAAM,KAAK;AAC7C,mBAAe,QAAQ,MAAM,GAAG;;KAEjC,EAAE,CAAC;EAeJ;EACA,SAdc,kBAAkB,WAAW,MAAM,EAAE,EAAE,CAAC;EAetD,cAbwD,aAAa,SAAS;GAC9E,MAAM,SAAS,KAAK;AACpB,kBAAe,KAAK,MAAM,OAAO,YAAY,CAAC;AAC9C,eAAY,KAAK,MAAM,OAAO,SAAS,CAAC;KACvC,EAAE,CAAC;EAUJ;EACA;EACA;EACA;EACA;EACA;EACD"}
@@ -18,9 +18,8 @@ const StyledListItem = styled("li", { base: {
18
18
  "& a": { _visited: { color: "inherit" } }
19
19
  } });
20
20
  const BreadcrumbItem = ({ renderItem, renderSeparator, item, totalCount }) => {
21
- const isLast = item.index === totalCount - 1;
22
21
  return /* @__PURE__ */ jsxs(StyledListItem, {
23
- "aria-current": isLast ? "page" : void 0,
22
+ "aria-current": item.index === totalCount - 1 ? "page" : void 0,
24
23
  children: [renderItem(item, totalCount), renderSeparator(item, totalCount)]
25
24
  });
26
25
  };
@@ -1 +1 @@
1
- {"version":3,"file":"BreadcrumbItem.mjs","names":[],"sources":["../../src/Breadcrumb/BreadcrumbItem.tsx"],"sourcesContent":["/**\n * Copyright (c) 2016-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { type ReactNode } from \"react\";\n\nexport interface SimpleBreadcrumbItem {\n to: string | Partial<Location>;\n name: ReactNode;\n}\n\nexport interface IndexedBreadcrumbItem extends SimpleBreadcrumbItem {\n index: number;\n}\n\nexport interface BreadcrumbRenderProps {\n item: IndexedBreadcrumbItem;\n totalCount: number;\n}\n\nconst StyledListItem = styled(\"li\", {\n base: {\n display: \"flex\",\n color: \"inherit\",\n gap: \"3xsmall\",\n alignItems: \"flex-end\",\n tabletDown: {\n display: \"block\",\n },\n \"& a\": {\n _visited: {\n color: \"inherit\",\n },\n },\n },\n});\n\ninterface Props {\n item: IndexedBreadcrumbItem;\n autoCollapse?: boolean;\n totalCount: number;\n renderItem: (item: IndexedBreadcrumbItem, totalCount: number) => ReactNode;\n renderSeparator: (item: IndexedBreadcrumbItem, totalCount: number) => ReactNode;\n}\n\nexport const BreadcrumbItem = ({ renderItem, renderSeparator, item, totalCount }: Props) => {\n const isLast = item.index === totalCount - 1;\n return (\n <StyledListItem aria-current={isLast ? \"page\" : undefined}>\n {renderItem(item, totalCount)}\n {renderSeparator(item, totalCount)}\n </StyledListItem>\n );\n};\n"],"mappings":";;;;;;;;;;;AAyBA,MAAM,iBAAiB,OAAO,MAAM,EAClC,MAAM;CACJ,SAAS;CACT,OAAO;CACP,KAAK;CACL,YAAY;CACZ,YAAY,EACV,SAAS,SACV;CACD,OAAO,EACL,UAAU,EACR,OAAO,WACR,EACF;CACF,EACF,CAAC;AAUF,MAAa,kBAAkB,EAAE,YAAY,iBAAiB,MAAM,iBAAwB;CAC1F,MAAM,SAAS,KAAK,UAAU,aAAa;AAC3C,QACE,qBAAC,gBAAD;EAAgB,gBAAc,SAAS,SAAS,KAAA;YAAhD,CACG,WAAW,MAAM,WAAW,EAC5B,gBAAgB,MAAM,WAAW,CACnB"}
1
+ {"version":3,"file":"BreadcrumbItem.mjs","names":[],"sources":["../../src/Breadcrumb/BreadcrumbItem.tsx"],"sourcesContent":["/**\n * Copyright (c) 2016-present, NDLA.\n *\n * This source code is licensed under the GPLv3 license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { type ReactNode } from \"react\";\n\nexport interface SimpleBreadcrumbItem {\n to: string | Partial<Location>;\n name: ReactNode;\n}\n\nexport interface IndexedBreadcrumbItem extends SimpleBreadcrumbItem {\n index: number;\n}\n\nexport interface BreadcrumbRenderProps {\n item: IndexedBreadcrumbItem;\n totalCount: number;\n}\n\nconst StyledListItem = styled(\"li\", {\n base: {\n display: \"flex\",\n color: \"inherit\",\n gap: \"3xsmall\",\n alignItems: \"flex-end\",\n tabletDown: {\n display: \"block\",\n },\n \"& a\": {\n _visited: {\n color: \"inherit\",\n },\n },\n },\n});\n\ninterface Props {\n item: IndexedBreadcrumbItem;\n autoCollapse?: boolean;\n totalCount: number;\n renderItem: (item: IndexedBreadcrumbItem, totalCount: number) => ReactNode;\n renderSeparator: (item: IndexedBreadcrumbItem, totalCount: number) => ReactNode;\n}\n\nexport const BreadcrumbItem = ({ renderItem, renderSeparator, item, totalCount }: Props) => {\n const isLast = item.index === totalCount - 1;\n return (\n <StyledListItem aria-current={isLast ? \"page\" : undefined}>\n {renderItem(item, totalCount)}\n {renderSeparator(item, totalCount)}\n </StyledListItem>\n );\n};\n"],"mappings":";;;;;;;;;;;AAyBA,MAAM,iBAAiB,OAAO,MAAM,EAClC,MAAM;CACJ,SAAS;CACT,OAAO;CACP,KAAK;CACL,YAAY;CACZ,YAAY,EACV,SAAS,SACV;CACD,OAAO,EACL,UAAU,EACR,OAAO,WACR,EACF;CACF,EACF,CAAC;AAUF,MAAa,kBAAkB,EAAE,YAAY,iBAAiB,MAAM,iBAAwB;AAE1F,QACE,qBAAC,gBAAD;EAAgB,gBAFH,KAAK,UAAU,aAAa,IAEF,SAAS,KAAA;YAAhD,CACG,WAAW,MAAM,WAAW,EAC5B,gBAAgB,MAAM,WAAW,CACnB"}
@@ -18,11 +18,7 @@ const AudioEmbed = ({ embed, lang }) => {
18
18
  const type = embed.embedData.type === "standard" ? "audio" : "podcast";
19
19
  if (embed.status === "error") return /* @__PURE__ */ jsx(EmbedErrorPlaceholder, { type });
20
20
  const { data, embedData } = embed;
21
- if (embedData.type === "minimal") return /* @__PURE__ */ jsx(AudioPlayer, {
22
- speech: true,
23
- src: data.audioFile.url,
24
- title: data.title.title
25
- });
21
+ const variant = embedData.type === "podcast" ? "standard" : embedData.type;
26
22
  const subtitle = data.series ? {
27
23
  title: data.series.title.title,
28
24
  url: `/podkast/${data.series.id}`
@@ -32,12 +28,12 @@ const AudioEmbed = ({ embed, lang }) => {
32
28
  url: coverPhoto.url,
33
29
  alt: coverPhoto.altText
34
30
  };
35
- const licenseProps = licenseAttributes(data.copyright.license.license, lang, embedData.url);
36
31
  return /* @__PURE__ */ jsxs(StyledFigure, {
37
32
  lang,
38
33
  "data-embed-type": type,
39
- ...licenseProps,
34
+ ...licenseAttributes(data.copyright.license.license, lang, embedData.url),
40
35
  children: [/* @__PURE__ */ jsx(AudioPlayer, {
36
+ variant,
41
37
  description: data.podcastMeta?.introduction ?? "",
42
38
  img,
43
39
  src: data.audioFile.url,