@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.
- package/dist/panda.buildinfo.json +20 -27
- package/dist/styles.css +61 -140
- package/es/Article/ArticleByline.mjs +2 -1
- package/es/Article/ArticleByline.mjs.map +1 -1
- package/es/AudioPlayer/AudioElement.mjs +12 -0
- package/es/AudioPlayer/AudioElement.mjs.map +1 -0
- package/es/AudioPlayer/AudioPlayer.mjs +7 -2
- package/es/AudioPlayer/AudioPlayer.mjs.map +1 -1
- package/es/AudioPlayer/AudioProgress.mjs +54 -0
- package/es/AudioPlayer/AudioProgress.mjs.map +1 -0
- package/es/AudioPlayer/CompactAudioPlayer.mjs +111 -0
- package/es/AudioPlayer/CompactAudioPlayer.mjs.map +1 -0
- package/es/AudioPlayer/Controls.mjs +25 -110
- package/es/AudioPlayer/Controls.mjs.map +1 -1
- package/es/AudioPlayer/PlayButton.mjs +24 -0
- package/es/AudioPlayer/PlayButton.mjs.map +1 -0
- package/es/AudioPlayer/SpeechControl.mjs +5 -16
- package/es/AudioPlayer/SpeechControl.mjs.map +1 -1
- package/es/AudioPlayer/VolumeSlider.mjs +31 -0
- package/es/AudioPlayer/VolumeSlider.mjs.map +1 -0
- package/es/AudioPlayer/audioUtils.mjs +17 -0
- package/es/AudioPlayer/audioUtils.mjs.map +1 -0
- package/es/AudioPlayer/useAudioControls.mjs +55 -0
- package/es/AudioPlayer/useAudioControls.mjs.map +1 -0
- package/es/Breadcrumb/BreadcrumbItem.mjs +1 -2
- package/es/Breadcrumb/BreadcrumbItem.mjs.map +1 -1
- package/es/Embed/AudioEmbed.mjs +3 -7
- package/es/Embed/AudioEmbed.mjs.map +1 -1
- package/es/Embed/ExternalEmbed.mjs +13 -16
- package/es/Embed/ExternalEmbed.mjs.map +1 -1
- package/es/Embed/IframeEmbed.mjs +4 -5
- package/es/Embed/IframeEmbed.mjs.map +1 -1
- package/es/FactBox/FactBox.mjs +14 -38
- package/es/FactBox/FactBox.mjs.map +1 -1
- package/es/Gloss/Gloss.mjs +1 -2
- package/es/Gloss/Gloss.mjs.map +1 -1
- package/es/Grid/Grid.mjs +1 -2
- package/es/Grid/Grid.mjs.map +1 -1
- package/es/LinkBlock/LinkBlock.mjs +9 -2
- package/es/LinkBlock/LinkBlock.mjs.map +1 -1
- package/es/Pitch/Pitch.mjs +1 -2
- package/es/Pitch/Pitch.mjs.map +1 -1
- package/es/index.mjs +2 -1
- package/lib/Article/ArticleByline.js +2 -1
- package/lib/Article/ArticleByline.js.map +1 -1
- package/lib/AudioPlayer/AudioElement.d.ts +14 -0
- package/lib/AudioPlayer/AudioElement.js +13 -0
- package/lib/AudioPlayer/AudioElement.js.map +1 -0
- package/lib/AudioPlayer/AudioPlayer.d.ts +5 -4
- package/lib/AudioPlayer/AudioPlayer.js +7 -2
- package/lib/AudioPlayer/AudioPlayer.js.map +1 -1
- package/lib/AudioPlayer/AudioProgress.d.ts +16 -0
- package/lib/AudioPlayer/AudioProgress.js +55 -0
- package/lib/AudioPlayer/AudioProgress.js.map +1 -0
- package/lib/AudioPlayer/CompactAudioPlayer.d.ts +13 -0
- package/lib/AudioPlayer/CompactAudioPlayer.js +112 -0
- package/lib/AudioPlayer/CompactAudioPlayer.js.map +1 -0
- package/lib/AudioPlayer/Controls.d.ts +1 -0
- package/lib/AudioPlayer/Controls.js +25 -110
- package/lib/AudioPlayer/Controls.js.map +1 -1
- package/lib/AudioPlayer/PlayButton.d.ts +13 -0
- package/lib/AudioPlayer/PlayButton.js +25 -0
- package/lib/AudioPlayer/PlayButton.js.map +1 -0
- package/lib/AudioPlayer/SpeechControl.d.ts +1 -2
- package/lib/AudioPlayer/SpeechControl.js +5 -16
- package/lib/AudioPlayer/SpeechControl.js.map +1 -1
- package/lib/AudioPlayer/VolumeSlider.d.ts +14 -0
- package/lib/AudioPlayer/VolumeSlider.js +32 -0
- package/lib/AudioPlayer/VolumeSlider.js.map +1 -0
- package/lib/AudioPlayer/audioUtils.d.ts +8 -0
- package/lib/AudioPlayer/audioUtils.js +17 -0
- package/lib/AudioPlayer/audioUtils.js.map +1 -0
- package/lib/AudioPlayer/useAudioControls.d.ts +24 -0
- package/lib/AudioPlayer/useAudioControls.js +56 -0
- package/lib/AudioPlayer/useAudioControls.js.map +1 -0
- package/lib/Breadcrumb/BreadcrumbItem.js +1 -2
- package/lib/Breadcrumb/BreadcrumbItem.js.map +1 -1
- package/lib/Embed/AudioEmbed.js +3 -7
- package/lib/Embed/AudioEmbed.js.map +1 -1
- package/lib/Embed/ExternalEmbed.js +13 -16
- package/lib/Embed/ExternalEmbed.js.map +1 -1
- package/lib/Embed/IframeEmbed.js +4 -5
- package/lib/Embed/IframeEmbed.js.map +1 -1
- package/lib/FactBox/FactBox.js +13 -37
- package/lib/FactBox/FactBox.js.map +1 -1
- package/lib/Gloss/Gloss.js +1 -2
- package/lib/Gloss/Gloss.js.map +1 -1
- package/lib/Grid/Grid.js +1 -2
- package/lib/Grid/Grid.js.map +1 -1
- package/lib/LinkBlock/LinkBlock.js +9 -2
- package/lib/LinkBlock/LinkBlock.js.map +1 -1
- package/lib/Pitch/Pitch.js +1 -2
- package/lib/Pitch/Pitch.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/package.json +10 -10
- package/src/Article/ArticleByline.tsx +5 -1
- package/src/AudioPlayer/AudioElement.tsx +20 -0
- package/src/AudioPlayer/{AudiPlayer.stories.tsx → AudioPlayer.stories.tsx} +10 -1
- package/src/AudioPlayer/AudioPlayer.tsx +12 -5
- package/src/AudioPlayer/AudioProgress.tsx +92 -0
- package/src/AudioPlayer/CompactAudioPlayer.tsx +124 -0
- package/src/AudioPlayer/Controls.tsx +36 -149
- package/src/AudioPlayer/PlayButton.tsx +24 -0
- package/src/AudioPlayer/SpeechControl.tsx +6 -19
- package/src/AudioPlayer/VolumeSlider.tsx +56 -0
- package/src/AudioPlayer/audioUtils.ts +15 -0
- package/src/AudioPlayer/useAudioControls.ts +80 -0
- package/src/Embed/AudioEmbed.tsx +3 -4
- package/src/FactBox/FactBox.tsx +13 -43
- package/src/Gloss/Gloss.tsx +1 -1
- package/src/LinkBlock/LinkBlock.tsx +5 -2
- package/src/index.ts +2 -0
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { Replay15Line, Forward15Line,
|
|
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
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
|
21
|
-
const {
|
|
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
|
-
<
|
|
25
|
+
<PlayButton variant="tertiary" onClick={togglePlay}>
|
|
39
26
|
<VolumeUpFill />
|
|
40
|
-
</
|
|
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
|
+
};
|
package/src/Embed/AudioEmbed.tsx
CHANGED
|
@@ -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
|
-
|
|
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}
|
package/src/FactBox/FactBox.tsx
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
182
|
-
</
|
|
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>
|
package/src/Gloss/Gloss.tsx
CHANGED
|
@@ -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}
|
|
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";
|