@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
@@ -6,8 +6,8 @@
6
6
  *
7
7
  */
8
8
 
9
- import { type SliderValueChangeDetails, createListCollection } from "@ark-ui/react";
10
- import { Replay15Line, Forward15Line, PlayFill, PauseLine, VolumeUpFill, CheckLine } from "@ndla/icons";
9
+ import { createListCollection } from "@ark-ui/react";
10
+ import { Replay15Line, Forward15Line, VolumeUpFill, CheckLine } from "@ndla/icons";
11
11
  import {
12
12
  Button,
13
13
  FieldRoot,
@@ -23,18 +23,16 @@ import {
23
23
  SelectLabel,
24
24
  SelectRoot,
25
25
  SelectTrigger,
26
- SliderControl,
27
- SliderHiddenInput,
28
- SliderLabel,
29
- SliderRange,
30
- SliderRoot,
31
- SliderThumb,
32
- SliderTrack,
33
26
  Text,
34
27
  } from "@ndla/primitives";
35
28
  import { styled } from "@ndla/styled-system/jsx";
36
- import { useEffect, useRef, useState } from "react";
37
29
  import { useTranslation } from "react-i18next";
30
+ import { AudioElement } from "./AudioElement";
31
+ import { AudioProgress } from "./AudioProgress";
32
+ import { formatTime } from "./audioUtils";
33
+ import { PlayButton } from "./PlayButton";
34
+ import { useAudioControls } from "./useAudioControls";
35
+ import { VolumeSlider } from "./VolumeSlider";
38
36
 
39
37
  const ControlsWrapper = styled("div", {
40
38
  base: {
@@ -64,7 +62,7 @@ const ControlsWrapper = styled("div", {
64
62
  },
65
63
  });
66
64
 
67
- const PlayButton = styled(IconButton, {
65
+ const StyledPlayButton = styled(PlayButton, {
68
66
  base: {
69
67
  gridArea: "play",
70
68
  },
@@ -130,117 +128,48 @@ const StyledSelectRoot = styled(SelectRoot<string>, {
130
128
  },
131
129
  });
132
130
 
133
- const StyledSliderControl = styled(SliderControl, {
134
- base: {
135
- height: "surface.3xsmall",
136
- minWidth: "small",
137
- },
138
- });
139
-
140
131
  const StyledPopoverContent = styled(PopoverContent, {
141
132
  base: {
142
133
  paddingInline: "small",
143
134
  },
144
135
  });
145
136
 
146
- const formatTime = (seconds: number) => {
147
- const minutes = Math.floor(seconds / 60);
148
- const currentSeconds = seconds % 60;
149
-
150
- const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;
151
- return `${minutes}:${formattedSeconds}`;
152
- };
153
-
154
137
  const speedValues = createListCollection({ items: ["0.5", "0.75", "1", "1.25", "1.5", "1.75", "2"] });
155
138
 
156
139
  interface Props {
157
140
  src: string;
158
141
  title: string;
142
+ variant?: "full" | "simplified";
159
143
  }
160
144
 
161
145
  export const Controls = ({ src, title }: Props) => {
162
146
  const { t } = useTranslation();
163
- const [speedValue, setSpeedValue] = useState(1);
164
- const [volumeValue, setVolumeValue] = useState(100);
165
- const [currentTime, setCurrentTime] = useState(0);
166
- const [duration, setDuration] = useState(0);
167
- const [playing, setPlaying] = useState(false);
168
- const audioRef = useRef<HTMLAudioElement>(null);
169
-
170
- useEffect(() => {
171
- if (audioRef.current) {
172
- const audioElement = audioRef.current;
173
- const handleTimeUpdate = () => {
174
- const { currentTime, duration } = audioElement;
175
- setCurrentTime(Math.round(currentTime));
176
- setDuration(Math.round(duration));
177
- };
178
-
179
- const handleLoadedMetaData = () => {
180
- const { currentTime, duration } = audioElement;
181
- setCurrentTime(Math.round(currentTime));
182
- setDuration(Math.round(duration));
183
- };
184
-
185
- const handleTimeEnded = () => {
186
- setPlaying(false);
187
- };
188
-
189
- audioElement.addEventListener("timeupdate", handleTimeUpdate);
190
- audioElement.addEventListener("loadedmetadata", handleLoadedMetaData);
191
- audioElement.addEventListener("ended", handleTimeEnded);
192
- return () => {
193
- audioElement.removeEventListener("timeupdate", handleTimeUpdate);
194
- audioElement.removeEventListener("loadedmetadata", handleLoadedMetaData);
195
- audioElement.removeEventListener("ended", handleTimeEnded);
196
- };
197
- }
198
- }, []);
199
-
200
- const togglePlay = () => {
201
- if (audioRef.current) {
202
- const audioElement = audioRef.current;
203
- if (!playing) {
204
- audioElement.play();
205
- } else {
206
- audioElement.pause();
207
- }
208
- setPlaying(!playing);
209
- }
210
- };
211
-
212
- const onPlaybackRateChange = (rate: number) => {
213
- setSpeedValue(rate);
214
- if (audioRef.current) {
215
- audioRef.current.playbackRate = rate;
216
- }
217
- };
218
-
219
- const onSeekSeconds = (seconds: number) => {
220
- if (audioRef.current) {
221
- audioRef.current.currentTime += seconds;
222
- }
223
- };
224
-
225
- const handleSliderChange = (details: SliderValueChangeDetails) => {
226
- const newValue = details.value[0];
227
- if (audioRef.current && newValue != null && !isNaN(newValue)) {
228
- audioRef.current.currentTime = details.value[0];
229
- }
230
- };
231
-
232
- const handleVolumeSliderChange = (details: SliderValueChangeDetails) => {
233
- if (audioRef.current) {
234
- audioRef.current.volume = details.value[0] / 100;
235
- setVolumeValue(details.value[0]);
236
- }
237
- };
147
+ const {
148
+ audioRef,
149
+ onEnded,
150
+ onHandleTime,
151
+ onSeekSeconds,
152
+ playing,
153
+ togglePlay,
154
+ handleSliderChange,
155
+ handleVolumeSliderChange,
156
+ currentTime,
157
+ duration,
158
+ speedValue,
159
+ onPlaybackRateChange,
160
+ volumeValue,
161
+ } = useAudioControls();
238
162
 
239
163
  return (
240
164
  <div>
241
- {/* TODO: We should tie this up to the textual description somehow */}
242
- {/* oxlint-disable-next-line jsx-a11y/media-has-caption */}
243
- <audio ref={audioRef} src={src} title={title} preload="metadata" />
165
+ <AudioElement
166
+ src={src}
167
+ title={title}
168
+ ref={audioRef}
169
+ onEnded={onEnded}
170
+ onLoadedMetadata={onHandleTime}
171
+ onTimeUpdate={onHandleTime}
172
+ />
244
173
  <ControlsWrapper>
245
174
  <Back15SecButton
246
175
  variant="tertiary"
@@ -250,9 +179,7 @@ export const Controls = ({ src, title }: Props) => {
250
179
  >
251
180
  <Replay15Line />
252
181
  </Back15SecButton>
253
- <PlayButton aria-label={t(playing ? t("audio.pause") : t("audio.play"))} variant="primary" onClick={togglePlay}>
254
- {playing ? <PauseLine /> : <PlayFill />}
255
- </PlayButton>
182
+ <StyledPlayButton playing={playing} onClick={togglePlay} />
256
183
  <Forward15SecButton
257
184
  variant="tertiary"
258
185
  title={t("audio.controls.forward15sec")}
@@ -265,29 +192,7 @@ export const Controls = ({ src, title }: Props) => {
265
192
  <StyledText textStyle="label.medium" asChild consumeCss>
266
193
  <div>{formatTime(currentTime)}</div>
267
194
  </StyledText>
268
- <SliderRoot
269
- value={[currentTime]}
270
- defaultValue={[0]}
271
- step={1}
272
- max={duration}
273
- onValueChange={handleSliderChange}
274
- getAriaValueText={(value) =>
275
- t("audio.valueText", {
276
- start: formatTime(Math.round(value.value)),
277
- end: formatTime(Math.round(duration)),
278
- })
279
- }
280
- >
281
- <SliderLabel srOnly>{t("audio.progressBar")}</SliderLabel>
282
- <SliderControl>
283
- <SliderTrack>
284
- <SliderRange />
285
- </SliderTrack>
286
- <SliderThumb index={0}>
287
- <SliderHiddenInput />
288
- </SliderThumb>
289
- </SliderControl>
290
- </SliderRoot>
195
+ <AudioProgress currentTime={currentTime} duration={duration} onValueChange={handleSliderChange} />
291
196
  <StyledText textStyle="label.medium" asChild consumeCss>
292
197
  <div>-{formatTime(Math.round(duration - currentTime))}</div>
293
198
  </StyledText>
@@ -330,25 +235,7 @@ export const Controls = ({ src, title }: Props) => {
330
235
  </VolumeButton>
331
236
  </PopoverTrigger>
332
237
  <StyledPopoverContent>
333
- <SliderRoot
334
- orientation="vertical"
335
- value={[volumeValue]}
336
- min={0}
337
- max={100}
338
- defaultValue={[100]}
339
- step={1}
340
- onValueChange={handleVolumeSliderChange}
341
- >
342
- <SliderLabel srOnly>{t("audio.controls.adjustVolume")}</SliderLabel>
343
- <StyledSliderControl>
344
- <SliderTrack>
345
- <SliderRange />
346
- </SliderTrack>
347
- <SliderThumb index={0}>
348
- <SliderHiddenInput />
349
- </SliderThumb>
350
- </StyledSliderControl>
351
- </SliderRoot>
238
+ <VolumeSlider value={volumeValue} onValueChange={handleVolumeSliderChange} />
352
239
  </StyledPopoverContent>
353
240
  </PopoverRoot>
354
241
  </ControlsWrapper>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright (c) 2026-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import { PauseLine, PlayFill } from "@ndla/icons";
10
+ import { IconButton, type IconButtonProps } from "@ndla/primitives";
11
+ import { useTranslation } from "react-i18next";
12
+
13
+ interface Props extends IconButtonProps {
14
+ playing?: boolean;
15
+ }
16
+
17
+ export const PlayButton = ({ playing, children, ...rest }: Props) => {
18
+ const { t } = useTranslation();
19
+ return (
20
+ <IconButton aria-label={playing ? t("audio.pause") : t("audio.play")} {...rest}>
21
+ {children ?? (playing ? <PauseLine /> : <PlayFill />)}
22
+ </IconButton>
23
+ );
24
+ };
@@ -7,37 +7,24 @@
7
7
  */
8
8
 
9
9
  import { VolumeUpFill } from "@ndla/icons";
10
- import { IconButton } from "@ndla/primitives";
11
- import { useRef } from "react";
12
- import { useTranslation } from "react-i18next";
10
+ import { PlayButton } from "./PlayButton";
11
+ import { useAudioControls } from "./useAudioControls";
13
12
 
14
13
  type Props = {
15
14
  src: string;
16
15
  title: string;
17
- type?: "gloss" | "audio";
18
16
  };
19
17
 
20
- export const SpeechControl = ({ src, title, type = "audio" }: Props) => {
21
- const { t } = useTranslation();
22
- const audioRef = useRef<HTMLAudioElement>(null);
18
+ export const SpeechControl = ({ src, title }: Props) => {
19
+ const { audioRef, togglePlay } = useAudioControls();
23
20
 
24
- const togglePlay = () => {
25
- if (audioRef.current) {
26
- const audioElement = audioRef.current;
27
- if (audioElement.paused) {
28
- audioElement.play();
29
- } else {
30
- audioElement.pause();
31
- }
32
- }
33
- };
34
21
  return (
35
22
  <div data-embed-type="speech">
36
23
  {/* oxlint-disable-next-line jsx-a11y/media-has-caption */}
37
24
  <audio ref={audioRef} src={src} title={title} preload="metadata" />
38
- <IconButton variant="tertiary" aria-label={t(`${type}.play`)} title={t(`${type}.play`)} onClick={togglePlay}>
25
+ <PlayButton variant="tertiary" onClick={togglePlay}>
39
26
  <VolumeUpFill />
40
- </IconButton>
27
+ </PlayButton>
41
28
  </div>
42
29
  );
43
30
  };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Copyright (c) 2026-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type { SliderValueChangeDetails } from "@ark-ui/react";
10
+ import {
11
+ SliderControl,
12
+ SliderRoot,
13
+ SliderLabel,
14
+ SliderTrack,
15
+ SliderRange,
16
+ SliderHiddenInput,
17
+ SliderThumb,
18
+ } from "@ndla/primitives";
19
+ import { styled } from "@ndla/styled-system/jsx";
20
+ import { t } from "i18next";
21
+
22
+ const StyledSliderControl = styled(SliderControl, {
23
+ base: {
24
+ height: "surface.3xsmall",
25
+ minWidth: "small",
26
+ },
27
+ });
28
+
29
+ interface Props {
30
+ value: number;
31
+ onValueChange: (value: SliderValueChangeDetails) => void;
32
+ }
33
+
34
+ export const VolumeSlider = ({ value, onValueChange }: Props) => {
35
+ return (
36
+ <SliderRoot
37
+ orientation="vertical"
38
+ value={[value]}
39
+ min={0}
40
+ max={100}
41
+ defaultValue={[100]}
42
+ step={1}
43
+ onValueChange={onValueChange}
44
+ >
45
+ <SliderLabel srOnly>{t("audio.controls.adjustVolume")}</SliderLabel>
46
+ <StyledSliderControl>
47
+ <SliderTrack>
48
+ <SliderRange />
49
+ </SliderTrack>
50
+ <SliderThumb index={0}>
51
+ <SliderHiddenInput />
52
+ </SliderThumb>
53
+ </StyledSliderControl>
54
+ </SliderRoot>
55
+ );
56
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Copyright (c) 2026-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ export const formatTime = (seconds: number) => {
10
+ const minutes = Math.floor(seconds / 60);
11
+ const currentSeconds = seconds % 60;
12
+
13
+ const formattedSeconds = currentSeconds < 10 ? `0${currentSeconds}` : currentSeconds;
14
+ return `${minutes}:${formattedSeconds}`;
15
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2026-present, NDLA.
3
+ *
4
+ * This source code is licensed under the GPLv3 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type { SliderValueChangeDetails } from "@ark-ui/react";
10
+ import { useCallback, useRef, useState, type ReactEventHandler } from "react";
11
+
12
+ export const useAudioControls = () => {
13
+ const [speedValue, setSpeedValue] = useState(1);
14
+ const [volumeValue, setVolumeValue] = useState(100);
15
+ const [currentTime, setCurrentTime] = useState(0);
16
+ const [duration, setDuration] = useState(0);
17
+ const [playing, setPlaying] = useState(false);
18
+ const audioRef = useRef<HTMLAudioElement>(null);
19
+
20
+ const togglePlay = useCallback(() => {
21
+ if (!audioRef.current) return;
22
+ if (audioRef.current.paused) {
23
+ audioRef.current.play();
24
+ } else {
25
+ audioRef.current.pause();
26
+ }
27
+ setPlaying((p) => !p);
28
+ }, []);
29
+
30
+ const onPlaybackRateChange = useCallback((rate: number) => {
31
+ setSpeedValue(rate);
32
+ if (audioRef.current) {
33
+ audioRef.current.playbackRate = rate;
34
+ }
35
+ }, []);
36
+
37
+ const onSeekSeconds = useCallback((seconds: number) => {
38
+ if (audioRef.current) {
39
+ audioRef.current.currentTime += seconds;
40
+ }
41
+ }, []);
42
+
43
+ const handleSliderChange = useCallback((details: SliderValueChangeDetails) => {
44
+ const newValue = details.value[0];
45
+ if (audioRef.current && newValue != null && !isNaN(newValue)) {
46
+ audioRef.current.currentTime = details.value[0];
47
+ }
48
+ }, []);
49
+
50
+ const handleVolumeSliderChange = useCallback((details: SliderValueChangeDetails) => {
51
+ if (audioRef.current) {
52
+ audioRef.current.volume = details.value[0] / 100;
53
+ setVolumeValue(details.value[0]);
54
+ }
55
+ }, []);
56
+
57
+ const onEnded = useCallback(() => setPlaying(false), []);
58
+
59
+ const onHandleTime: ReactEventHandler<HTMLAudioElement> = useCallback((meta) => {
60
+ const target = meta.currentTarget;
61
+ setCurrentTime(Math.round(target.currentTime));
62
+ setDuration(Math.round(target.duration));
63
+ }, []);
64
+
65
+ return {
66
+ togglePlay,
67
+ onPlaybackRateChange,
68
+ onSeekSeconds,
69
+ handleVolumeSliderChange,
70
+ handleSliderChange,
71
+ onEnded,
72
+ onHandleTime,
73
+ speedValue,
74
+ volumeValue,
75
+ currentTime,
76
+ duration,
77
+ playing,
78
+ audioRef,
79
+ };
80
+ };
@@ -9,7 +9,7 @@
9
9
  import { Figure } from "@ndla/primitives";
10
10
  import { styled } from "@ndla/styled-system/jsx";
11
11
  import type { AudioMetaData } from "@ndla/types-embed";
12
- import { AudioPlayer } from "../AudioPlayer/AudioPlayer";
12
+ import { AudioPlayer, type AudioPlayerVariant } from "../AudioPlayer/AudioPlayer";
13
13
  import { EmbedByline } from "../LicenseByline/EmbedByline";
14
14
  import { licenseAttributes } from "../utils/licenseAttributes";
15
15
  import { EmbedErrorPlaceholder } from "./EmbedErrorPlaceholder";
@@ -40,9 +40,7 @@ export const AudioEmbed = ({ embed, lang }: Props) => {
40
40
 
41
41
  const { data, embedData } = embed;
42
42
 
43
- if (embedData.type === "minimal") {
44
- return <AudioPlayer speech src={data.audioFile.url} title={data.title.title} />;
45
- }
43
+ const variant = embedData.type === "podcast" ? "standard" : (embedData.type as AudioPlayerVariant);
46
44
 
47
45
  const subtitle = data.series ? { title: data.series.title.title, url: `/podkast/${data.series.id}` } : undefined;
48
46
 
@@ -55,6 +53,7 @@ export const AudioEmbed = ({ embed, lang }: Props) => {
55
53
  return (
56
54
  <StyledFigure lang={lang} data-embed-type={type} {...licenseProps}>
57
55
  <AudioPlayer
56
+ variant={variant}
58
57
  description={data.podcastMeta?.introduction ?? ""}
59
58
  img={img}
60
59
  src={data.audioFile.url}
@@ -6,8 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { ArrowDownShortLine } from "@ndla/icons";
10
- import { IconButton } from "@ndla/primitives";
9
+ import { Button } from "@ndla/primitives";
11
10
  import { styled } from "@ndla/styled-system/jsx";
12
11
  import React, {
13
12
  type ComponentProps,
@@ -45,11 +44,10 @@ const StyledAside = styled("aside", {
45
44
  _open: {
46
45
  gridTemplateRows: "1fr",
47
46
  },
48
- _closed: {
49
- _print: {
50
- overflow: "visible",
51
- maxHeight: "500vh",
52
- },
47
+ _print: {
48
+ gridTemplateRows: "1fr",
49
+ overflow: "visible",
50
+ maxHeight: "500vh",
53
51
  },
54
52
  "& > div": {
55
53
  minHeight: "surface.3xsmall",
@@ -60,6 +58,9 @@ const StyledAside = styled("aside", {
60
58
  true: {
61
59
  "& > div": {
62
60
  overflow: "hidden",
61
+ _print: {
62
+ overflow: "visible",
63
+ },
63
64
  },
64
65
  },
65
66
  },
@@ -74,51 +75,20 @@ const StyledContent = styled("div", {
74
75
  "& :first-child": {
75
76
  marginBlockStart: "0",
76
77
  },
77
- _after: {
78
- content: '""',
79
- textAlign: "center",
80
- position: "absolute",
81
- inset: "0",
82
- transitionProperty: "opacity",
83
- transitionDuration: "slow",
84
- transitionTimingFunction: "ease-in-out",
85
- gradientFrom: "surface.default/20",
86
- gradientTo: "surface.default/95",
87
- backgroundGradient: "to-b",
88
- opacity: "1",
89
- zIndex: "base",
90
- pointerEvents: "none",
91
- },
92
78
  _print: {
93
79
  overflow: "visible",
94
- _after: {
95
- display: "none",
96
- },
97
80
  },
98
81
  _open: {
99
82
  paddingBlockEnd: "xsmall",
100
- _after: {
101
- opacity: "0",
102
- },
103
83
  },
104
84
  },
105
85
  });
106
86
 
107
- const StyledIconButton = styled(IconButton, {
87
+ const StyledButton = styled(Button, {
108
88
  base: {
109
89
  position: "absolute",
110
90
  bottom: "-medium",
111
91
  zIndex: "base",
112
- "& svg": {
113
- transitionProperty: "transform",
114
- transitionTimingFunction: "ease-in-out",
115
- transitionDuration: "fast",
116
- },
117
- _open: {
118
- "& svg": {
119
- transform: "rotate(180deg)",
120
- },
121
- },
122
92
  _print: {
123
93
  display: "none",
124
94
  },
@@ -170,16 +140,16 @@ export const FactBox = forwardRef<HTMLElement, Props>(
170
140
  }
171
141
  }}
172
142
  >
173
- <StyledIconButton
143
+ <StyledButton
174
144
  data-state={state}
175
145
  onClick={onClick}
176
146
  contentEditable={false}
177
147
  aria-expanded={state === "open"}
148
+ variant="secondary"
178
149
  aria-controls={contentId}
179
- aria-label={t(`factbox.${state === "open" ? "close" : "open"}`)}
180
150
  >
181
- <ArrowDownShortLine />
182
- </StyledIconButton>
151
+ {t(`factbox.${state === "open" ? "showLess" : "showMore"}`)}
152
+ </StyledButton>
183
153
  <StyledContent id={contentId} data-state={state} aria-hidden={state === "closed"} {...inertAttribute}>
184
154
  {children}
185
155
  </StyledContent>
@@ -162,7 +162,7 @@ export const Gloss = ({ title, glossData, audio, exampleIds, exampleLangs, varia
162
162
  </Text>
163
163
  )}
164
164
  </TextWrapper>
165
- {!!audio?.src && <SpeechControl src={audio.src} title={audio.title} type="gloss" />}
165
+ {!!audio?.src && <SpeechControl src={audio.src} title={audio.title} />}
166
166
  </Container>
167
167
  <StyledContainer>
168
168
  <Text textStyle="label.medium" asChild consumeCss>
@@ -11,8 +11,10 @@ import { Heading } from "@ndla/primitives";
11
11
  import { SafeLink } from "@ndla/safelink";
12
12
  import { styled } from "@ndla/styled-system/jsx";
13
13
  import type { LinkBlockEmbedData } from "@ndla/types-embed";
14
+ import { toIntlLanguage } from "@ndla/util";
14
15
  import parse from "html-react-parser";
15
16
  import { useMemo } from "react";
17
+ import { useTranslation } from "react-i18next";
16
18
  import { getPossiblyRelativeUrl } from "../utils/relativeUrl";
17
19
 
18
20
  const InfoWrapper = styled("div", {
@@ -73,16 +75,17 @@ interface Props extends Omit<LinkBlockEmbedData, "resource"> {
73
75
  }
74
76
 
75
77
  export const LinkBlock = ({ title, articleLanguage, date, url, path }: Props) => {
78
+ const { i18n } = useTranslation();
76
79
  const href = getPossiblyRelativeUrl(url, path);
77
80
  const formattedDate = useMemo(() => {
78
81
  if (!date) return null;
79
- return new Intl.DateTimeFormat(articleLanguage, {
82
+ return new Intl.DateTimeFormat(toIntlLanguage(articleLanguage ?? i18n.language), {
80
83
  timeZone: "CET",
81
84
  day: "2-digit",
82
85
  month: "long",
83
86
  year: "numeric",
84
87
  }).format(new Date(date));
85
- }, [date, articleLanguage]);
88
+ }, [date, articleLanguage, i18n.language]);
86
89
  return (
87
90
  <StyledSafeLink to={href} data-embed-type="link-block">
88
91
  <InfoWrapper>
package/src/index.ts CHANGED
@@ -57,6 +57,8 @@ export { FactBox } from "./FactBox/FactBox";
57
57
  export { ResourceBox } from "./ResourceBox/ResourceBox";
58
58
 
59
59
  export { AudioPlayer } from "./AudioPlayer/AudioPlayer";
60
+ export type { AudioPlayerVariant } from "./AudioPlayer/AudioPlayer";
61
+ export { CompactAudioPlayer } from "./AudioPlayer/CompactAudioPlayer";
60
62
 
61
63
  export { constants } from "./model";
62
64
  export { contentTypes, contentTypeMapping, resourceEmbedTypeMapping } from "./model/ContentType";