@ndla/ui 56.0.186-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 (78) hide show
  1. package/dist/panda.buildinfo.json +15 -5
  2. package/dist/styles.css +53 -13
  3. package/es/AudioPlayer/AudioElement.mjs +12 -0
  4. package/es/AudioPlayer/AudioElement.mjs.map +1 -0
  5. package/es/AudioPlayer/AudioPlayer.mjs +7 -2
  6. package/es/AudioPlayer/AudioPlayer.mjs.map +1 -1
  7. package/es/AudioPlayer/AudioProgress.mjs +54 -0
  8. package/es/AudioPlayer/AudioProgress.mjs.map +1 -0
  9. package/es/AudioPlayer/CompactAudioPlayer.mjs +111 -0
  10. package/es/AudioPlayer/CompactAudioPlayer.mjs.map +1 -0
  11. package/es/AudioPlayer/Controls.mjs +25 -110
  12. package/es/AudioPlayer/Controls.mjs.map +1 -1
  13. package/es/AudioPlayer/PlayButton.mjs +24 -0
  14. package/es/AudioPlayer/PlayButton.mjs.map +1 -0
  15. package/es/AudioPlayer/SpeechControl.mjs +5 -16
  16. package/es/AudioPlayer/SpeechControl.mjs.map +1 -1
  17. package/es/AudioPlayer/VolumeSlider.mjs +31 -0
  18. package/es/AudioPlayer/VolumeSlider.mjs.map +1 -0
  19. package/es/AudioPlayer/audioUtils.mjs +17 -0
  20. package/es/AudioPlayer/audioUtils.mjs.map +1 -0
  21. package/es/AudioPlayer/useAudioControls.mjs +55 -0
  22. package/es/AudioPlayer/useAudioControls.mjs.map +1 -0
  23. package/es/Embed/AudioEmbed.mjs +2 -5
  24. package/es/Embed/AudioEmbed.mjs.map +1 -1
  25. package/es/Gloss/Gloss.mjs +1 -2
  26. package/es/Gloss/Gloss.mjs.map +1 -1
  27. package/es/index.mjs +2 -1
  28. package/lib/AudioPlayer/AudioElement.d.ts +14 -0
  29. package/lib/AudioPlayer/AudioElement.js +13 -0
  30. package/lib/AudioPlayer/AudioElement.js.map +1 -0
  31. package/lib/AudioPlayer/AudioPlayer.d.ts +5 -4
  32. package/lib/AudioPlayer/AudioPlayer.js +7 -2
  33. package/lib/AudioPlayer/AudioPlayer.js.map +1 -1
  34. package/lib/AudioPlayer/AudioProgress.d.ts +16 -0
  35. package/lib/AudioPlayer/AudioProgress.js +55 -0
  36. package/lib/AudioPlayer/AudioProgress.js.map +1 -0
  37. package/lib/AudioPlayer/CompactAudioPlayer.d.ts +13 -0
  38. package/lib/AudioPlayer/CompactAudioPlayer.js +112 -0
  39. package/lib/AudioPlayer/CompactAudioPlayer.js.map +1 -0
  40. package/lib/AudioPlayer/Controls.d.ts +1 -0
  41. package/lib/AudioPlayer/Controls.js +25 -110
  42. package/lib/AudioPlayer/Controls.js.map +1 -1
  43. package/lib/AudioPlayer/PlayButton.d.ts +13 -0
  44. package/lib/AudioPlayer/PlayButton.js +25 -0
  45. package/lib/AudioPlayer/PlayButton.js.map +1 -0
  46. package/lib/AudioPlayer/SpeechControl.d.ts +1 -2
  47. package/lib/AudioPlayer/SpeechControl.js +5 -16
  48. package/lib/AudioPlayer/SpeechControl.js.map +1 -1
  49. package/lib/AudioPlayer/VolumeSlider.d.ts +14 -0
  50. package/lib/AudioPlayer/VolumeSlider.js +32 -0
  51. package/lib/AudioPlayer/VolumeSlider.js.map +1 -0
  52. package/lib/AudioPlayer/audioUtils.d.ts +8 -0
  53. package/lib/AudioPlayer/audioUtils.js +17 -0
  54. package/lib/AudioPlayer/audioUtils.js.map +1 -0
  55. package/lib/AudioPlayer/useAudioControls.d.ts +24 -0
  56. package/lib/AudioPlayer/useAudioControls.js +56 -0
  57. package/lib/AudioPlayer/useAudioControls.js.map +1 -0
  58. package/lib/Embed/AudioEmbed.js +2 -5
  59. package/lib/Embed/AudioEmbed.js.map +1 -1
  60. package/lib/Gloss/Gloss.js +1 -2
  61. package/lib/Gloss/Gloss.js.map +1 -1
  62. package/lib/index.d.ts +2 -0
  63. package/lib/index.js +2 -0
  64. package/package.json +2 -2
  65. package/src/AudioPlayer/AudioElement.tsx +20 -0
  66. package/src/AudioPlayer/{AudiPlayer.stories.tsx → AudioPlayer.stories.tsx} +10 -1
  67. package/src/AudioPlayer/AudioPlayer.tsx +12 -5
  68. package/src/AudioPlayer/AudioProgress.tsx +92 -0
  69. package/src/AudioPlayer/CompactAudioPlayer.tsx +124 -0
  70. package/src/AudioPlayer/Controls.tsx +36 -149
  71. package/src/AudioPlayer/PlayButton.tsx +24 -0
  72. package/src/AudioPlayer/SpeechControl.tsx +6 -19
  73. package/src/AudioPlayer/VolumeSlider.tsx +56 -0
  74. package/src/AudioPlayer/audioUtils.ts +15 -0
  75. package/src/AudioPlayer/useAudioControls.ts +80 -0
  76. package/src/Embed/AudioEmbed.tsx +3 -4
  77. package/src/Gloss/Gloss.tsx +1 -1
  78. package/src/index.ts +2 -0
@@ -81,6 +81,20 @@
81
81
  "direction]___[value:rtl]___[cond:& p:has(span[dir=\"rtl\"])",
82
82
  "alignSelf]___[value:flex-start",
83
83
  "marginInlineStart]___[value:3xsmall",
84
+ "borderRadius]___[value:0",
85
+ "width]___[value:4xsmall",
86
+ "height]___[value:4xsmall",
87
+ "background]___[value:unset",
88
+ "height]___[value:unset",
89
+ "paddingBlockEnd]___[value:0",
90
+ "boxShadow]___[value:xsmall",
91
+ "background]___[value:surface.brand.1.subtle",
92
+ "minWidth]___[value:4xlarge",
93
+ "flexShrink]___[value:0",
94
+ "textAlign]___[value:center",
95
+ "marginInlineStart]___[value:auto",
96
+ "textOverflow]___[value:ellipsis",
97
+ "whiteSpace]___[value:nowrap",
84
98
  "borderBottomRadius]___[value:xsmall",
85
99
  "justifyContent]___[value:center",
86
100
  "paddingInline]___[value:medium",
@@ -97,8 +111,6 @@
97
111
  "gridArea]___[value:track",
98
112
  "paddingInline]___[value:xsmall]___[cond:mobileDown",
99
113
  "minWidth]___[value:xxlarge",
100
- "flexShrink]___[value:0",
101
- "textAlign]___[value:center",
102
114
  "gridArea]___[value:volume",
103
115
  "paddingBlock]___[value:auto",
104
116
  "paddingInline]___[value:auto",
@@ -106,9 +118,9 @@
106
118
  "maxHeight]___[value:xxlarge",
107
119
  "flex]___[value:1]___[cond:& span",
108
120
  "gridArea]___[value:speed",
121
+ "paddingInline]___[value:small",
109
122
  "height]___[value:surface.3xsmall",
110
123
  "minWidth]___[value:small",
111
- "paddingInline]___[value:small",
112
124
  "alignItems]___[value:flex-start]___[cond:mobileDown",
113
125
  "justifyContent]___[value:center]___[cond:mobileDown",
114
126
  "flexDirection]___[value:column]___[cond:mobileDown",
@@ -316,7 +328,6 @@
316
328
  "maxHeight]___[value:none]___[cond:mobileWideDown<___>_open",
317
329
  "display]___[value:none]___[cond:mobileWide",
318
330
  "color]___[value:text.link",
319
- "whiteSpace]___[value:nowrap",
320
331
  "textDecoration]___[value:none]___[cond:_focusWithin",
321
332
  "display]___[value:none]___[cond:mobileWideDown<___>_disabled",
322
333
  "textDecoration]___[value:underline]___[cond:& h3",
@@ -332,7 +343,6 @@
332
343
  "outline]___[value:0px",
333
344
  "boxShadow]___[value:none",
334
345
  "aspectRatio]___[value:16/9",
335
- "height]___[value:unset",
336
346
  "gridTemplateColumns]___[value:repeat(2, 1fr)",
337
347
  "gridTemplateColumns]___[value:1fr]___[cond:tabletDown",
338
348
  "marginBlockStart]___[value:xsmall",
package/dist/styles.css CHANGED
@@ -182,6 +182,14 @@
182
182
  padding: var(--spacing-xsmall);
183
183
  }
184
184
 
185
+ .bg_unset {
186
+ background: unset;
187
+ }
188
+
189
+ .bg_surface\.brand\.1\.subtle {
190
+ background: var(--colors-surface-brand-1-subtle);
191
+ }
192
+
185
193
  .grid-area_play {
186
194
  grid-area: play;
187
195
  }
@@ -306,6 +314,10 @@
306
314
  padding-inline: var(--spacing-xsmall);
307
315
  }
308
316
 
317
+ .bdr_0 {
318
+ border-radius: 0;
319
+ }
320
+
309
321
  .px_medium {
310
322
  padding-inline: var(--spacing-medium);
311
323
  }
@@ -455,13 +467,12 @@
455
467
  margin-inline-start: var(--spacing-3xsmall);
456
468
  }
457
469
 
458
- .bdr-b_xsmall {
459
- border-bottom-left-radius: var(--radii-xsmall);
460
- border-bottom-right-radius: var(--radii-xsmall);
470
+ .pbe_0 {
471
+ padding-block-end: 0;
461
472
  }
462
473
 
463
- .jc_center {
464
- justify-content: center;
474
+ .bx-sh_xsmall {
475
+ box-shadow: var(--shadows-xsmall);
465
476
  }
466
477
 
467
478
  .flex-sh_0 {
@@ -472,6 +483,27 @@
472
483
  text-align: center;
473
484
  }
474
485
 
486
+ .ms_auto {
487
+ margin-inline-start: auto;
488
+ }
489
+
490
+ .tov_ellipsis {
491
+ text-overflow: ellipsis;
492
+ }
493
+
494
+ .white-space_nowrap {
495
+ white-space: nowrap;
496
+ }
497
+
498
+ .bdr-b_xsmall {
499
+ border-bottom-left-radius: var(--radii-xsmall);
500
+ border-bottom-right-radius: var(--radii-xsmall);
501
+ }
502
+
503
+ .jc_center {
504
+ justify-content: center;
505
+ }
506
+
475
507
  .c_inherit {
476
508
  color: inherit;
477
509
  }
@@ -662,10 +694,6 @@
662
694
  color: var(--colors-text-link);
663
695
  }
664
696
 
665
- .white-space_nowrap {
666
- white-space: nowrap;
667
- }
668
-
669
697
  .c_icon\.strong {
670
698
  color: var(--colors-icon-strong);
671
699
  }
@@ -722,6 +750,22 @@
722
750
  max-width: var(--sizes-surface-xlarge);
723
751
  }
724
752
 
753
+ .w_4xsmall {
754
+ width: var(--sizes-4xsmall);
755
+ }
756
+
757
+ .h_4xsmall {
758
+ height: var(--sizes-4xsmall);
759
+ }
760
+
761
+ .h_unset {
762
+ height: unset;
763
+ }
764
+
765
+ .min-w_4xlarge {
766
+ min-width: var(--sizes-4xlarge);
767
+ }
768
+
725
769
  .min-w_xxlarge {
726
770
  min-width: var(--sizes-xxlarge);
727
771
  }
@@ -810,10 +854,6 @@
810
854
  width: var(--sizes-surface-3xsmall);
811
855
  }
812
856
 
813
- .h_unset {
814
- height: unset;
815
- }
816
-
817
857
  .before\:inset_0::before {
818
858
  inset: 0;
819
859
  }
@@ -0,0 +1,12 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region src/AudioPlayer/AudioElement.tsx
3
+ const AudioElement = (props) => {
4
+ return /* @__PURE__ */ jsx("audio", {
5
+ preload: "metadata",
6
+ ...props
7
+ });
8
+ };
9
+ //#endregion
10
+ export { AudioElement };
11
+
12
+ //# sourceMappingURL=AudioElement.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioElement.mjs","names":[],"sources":["../../src/AudioPlayer/AudioElement.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 { ComponentProps } from \"react\";\n\ninterface Props extends ComponentProps<\"audio\"> {\n src: string;\n title: string;\n}\n\nexport const AudioElement = (props: Props) => {\n // TODO: We should tie this up to the textual description somehow\n // oxlint-disable-next-line jsx-a11y/media-has-caption\n return <audio preload=\"metadata\" {...props} />;\n};\n"],"mappings":";;AAeA,MAAa,gBAAgB,UAAiB;AAG5C,QAAO,oBAAC,SAAD;EAAO,SAAQ;EAAW,GAAI;EAAS,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { CompactAudioPlayer } from "./CompactAudioPlayer.mjs";
1
2
  import { Controls } from "./Controls.mjs";
2
3
  import { SpeechControl } from "./SpeechControl.mjs";
3
4
  import { Button, Heading, Text } from "@ndla/primitives";
@@ -92,13 +93,17 @@ const TextVersionText = styled("div", { base: {
92
93
  const TextVersionButton = styled(Button, { base: { alignSelf: "flex-start" } });
93
94
  const ShowMoreButton = styled(Button, { base: { marginInlineStart: "3xsmall" } });
94
95
  const DESCRIPTION_MAX_LENGTH = 200;
95
- const AudioPlayer = ({ src, title, subtitle, speech, description, img, textVersion }) => {
96
+ const AudioPlayer = ({ src, title, subtitle, variant = "standard", description, img, textVersion }) => {
96
97
  const { t } = useTranslation();
97
98
  const [showTextVersion, setShowTextVersion] = useState(false);
98
99
  const [showFullDescription, setShowFullDescription] = useState(false);
99
100
  const truncatedDescription = useMemo(() => description?.slice(0, DESCRIPTION_MAX_LENGTH), [description]);
100
101
  const textDescriptionId = useId();
101
- if (speech) return /* @__PURE__ */ jsx(SpeechControl, {
102
+ if (variant === "minimal") return /* @__PURE__ */ jsx(SpeechControl, {
103
+ src,
104
+ title
105
+ });
106
+ if (variant === "compact") return /* @__PURE__ */ jsx(CompactAudioPlayer, {
102
107
  src,
103
108
  title
104
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"AudioPlayer.mjs","names":[],"sources":["../../src/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * Copyright (c) 2017-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 { Heading, Text, Button } from \"@ndla/primitives\";\nimport { SafeLink } from \"@ndla/safelink\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { type ReactNode, useId, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Controls } from \"./Controls\";\nimport { SpeechControl } from \"./SpeechControl\";\n\n// TODO: Could the audio metadata be more tightly coupled to the audio player?\n\nconst AudioPlayerWrapper = styled(\"div\", {\n base: {\n border: \"1px solid\",\n borderColor: \"stroke.default\",\n borderRadius: \"xsmall\",\n boxShadow: \"full\",\n marginBlockEnd: \"4xsmall\",\n overflow: \"hidden\",\n },\n});\n\nconst InfoWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n tabletWideDown: {\n display: \"block\",\n },\n },\n});\n\nconst ImageWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n alignItems: \"center\",\n flex: \"1 0 auto\",\n width: \"surface.4xsmall\",\n height: \"surface.4xsmall\",\n overflow: \"hidden\",\n \"& img\": {\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n },\n desktop: {\n width: \"260px\",\n height: \"260px\",\n },\n tabletWideDown: {\n maxHeight: \"surface.small\",\n maxWidth: \"100%\",\n width: \"100%\",\n height: \"auto\",\n },\n },\n});\n\nconst TextWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n alignItems: \"flex-start\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n padding: \"xsmall\",\n width: \"100%\",\n \"&[data-has-image='true']\": {\n tablet: {\n paddingBlock: \"xsmall\",\n paddingInline: \"medium\",\n },\n },\n },\n});\n\nconst TitleWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n fontFamily: \"sans\",\n tabletWide: {\n width: \"100%\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n },\n },\n});\n\nconst TextVersionWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n borderBlockStart: \"1px solid\",\n borderColor: \"stroke.default\",\n paddingBlock: \"medium\",\n paddingInline: \"xsmall\",\n tablet: {\n paddingInline: \"medium\",\n },\n },\n});\n\nconst TextVersionText = styled(\"div\", {\n base: {\n maxWidth: \"surface.xlarge\",\n \"& span > *\": {\n whiteSpace: \"pre-wrap\",\n },\n \"& p:not(:first-child):not(:last-child)\": {\n marginBlock: \"small\",\n },\n '& p[data-align=\"center\"]': {\n textAlign: \"center\",\n },\n '& p:has(span[dir=\"rtl\"])': {\n direction: \"rtl\",\n },\n },\n});\n\nconst TextVersionButton = styled(Button, {\n base: {\n alignSelf: \"flex-start\",\n },\n});\n\nconst ShowMoreButton = styled(Button, {\n base: {\n marginInlineStart: \"3xsmall\",\n },\n});\n\nconst DESCRIPTION_MAX_LENGTH = 200;\n\ntype Props = {\n src: string;\n title: string;\n subtitle?: {\n title: string;\n url?: string;\n };\n speech?: boolean;\n description?: string;\n textVersion?: ReactNode;\n img?: {\n url: string;\n alt: string;\n };\n};\n\nexport const AudioPlayer = ({ src, title, subtitle, speech, description, img, textVersion }: Props) => {\n const { t } = useTranslation();\n const [showTextVersion, setShowTextVersion] = useState(false);\n const [showFullDescription, setShowFullDescription] = useState(false);\n const truncatedDescription = useMemo(() => description?.slice(0, DESCRIPTION_MAX_LENGTH), [description]);\n const textDescriptionId = useId();\n\n if (speech) {\n return <SpeechControl src={src} title={title} />;\n }\n\n const toggleTextVersion = () => {\n setShowTextVersion((curr) => !curr);\n };\n\n const textVersionButton = (\n <TextVersionButton\n variant=\"secondary\"\n aria-expanded={showTextVersion}\n aria-controls={textDescriptionId}\n size=\"small\"\n onClick={toggleTextVersion}\n >\n {t(showTextVersion ? \"audio.textVersion.close\" : \"audio.textVersion.heading\")}\n </TextVersionButton>\n );\n\n return (\n <AudioPlayerWrapper>\n <InfoWrapper>\n {!!img && (\n <ImageWrapper>\n <img src={img.url} alt={img.alt} />\n </ImageWrapper>\n )}\n <TextWrapper data-has-image={!!img}>\n <TitleWrapper>\n <div>\n {subtitle?.url ? <SafeLink to={subtitle.url}>{subtitle.title}</SafeLink> : subtitle?.title}\n <Heading asChild consumeCss textStyle=\"title.large\">\n <h3>{title}</h3>\n </Heading>\n </div>\n {!!textVersion && !img && textVersionButton}\n </TitleWrapper>\n {!!description && (\n <Text textStyle=\"body.medium\">\n {showFullDescription || description.length < DESCRIPTION_MAX_LENGTH\n ? description\n : `${truncatedDescription}...`}\n {description.length > DESCRIPTION_MAX_LENGTH && (\n <ShowMoreButton variant=\"link\" onClick={() => setShowFullDescription((p) => !p)}>\n {t(`audio.${showFullDescription ? \"readLessDescriptionLabel\" : \"readMoreDescriptionLabel\"}`)}\n </ShowMoreButton>\n )}\n </Text>\n )}\n {!!textVersion && !!img && textVersionButton}\n </TextWrapper>\n </InfoWrapper>\n <Controls src={src} title={title} />\n {!!textVersion && (\n <TextVersionWrapper id={textDescriptionId} hidden={!showTextVersion}>\n <Heading asChild textStyle=\"title.medium\" consumeCss>\n <h4>{t(\"audio.textVersion.heading\")}</h4>\n </Heading>\n <TextVersionText>{textVersion}</TextVersionText>\n </TextVersionWrapper>\n )}\n </AudioPlayerWrapper>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioPlayer.mjs","names":[],"sources":["../../src/AudioPlayer/AudioPlayer.tsx"],"sourcesContent":["/**\n * Copyright (c) 2017-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 { Heading, Text, Button } from \"@ndla/primitives\";\nimport { SafeLink } from \"@ndla/safelink\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { type ReactNode, useId, useMemo, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { CompactAudioPlayer } from \"./CompactAudioPlayer\";\nimport { Controls } from \"./Controls\";\nimport { SpeechControl } from \"./SpeechControl\";\n\n// TODO: Could the audio metadata be more tightly coupled to the audio player?\n\nconst AudioPlayerWrapper = styled(\"div\", {\n base: {\n border: \"1px solid\",\n borderColor: \"stroke.default\",\n borderRadius: \"xsmall\",\n boxShadow: \"full\",\n marginBlockEnd: \"4xsmall\",\n overflow: \"hidden\",\n },\n});\n\nconst InfoWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n tabletWideDown: {\n display: \"block\",\n },\n },\n});\n\nconst ImageWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n alignItems: \"center\",\n flex: \"1 0 auto\",\n width: \"surface.4xsmall\",\n height: \"surface.4xsmall\",\n overflow: \"hidden\",\n \"& img\": {\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n },\n desktop: {\n width: \"260px\",\n height: \"260px\",\n },\n tabletWideDown: {\n maxHeight: \"surface.small\",\n maxWidth: \"100%\",\n width: \"100%\",\n height: \"auto\",\n },\n },\n});\n\nconst TextWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n alignItems: \"flex-start\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n padding: \"xsmall\",\n width: \"100%\",\n \"&[data-has-image='true']\": {\n tablet: {\n paddingBlock: \"xsmall\",\n paddingInline: \"medium\",\n },\n },\n },\n});\n\nconst TitleWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n fontFamily: \"sans\",\n tabletWide: {\n width: \"100%\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n },\n },\n});\n\nconst TextVersionWrapper = styled(\"div\", {\n base: {\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"xsmall\",\n borderBlockStart: \"1px solid\",\n borderColor: \"stroke.default\",\n paddingBlock: \"medium\",\n paddingInline: \"xsmall\",\n tablet: {\n paddingInline: \"medium\",\n },\n },\n});\n\nconst TextVersionText = styled(\"div\", {\n base: {\n maxWidth: \"surface.xlarge\",\n \"& span > *\": {\n whiteSpace: \"pre-wrap\",\n },\n \"& p:not(:first-child):not(:last-child)\": {\n marginBlock: \"small\",\n },\n '& p[data-align=\"center\"]': {\n textAlign: \"center\",\n },\n '& p:has(span[dir=\"rtl\"])': {\n direction: \"rtl\",\n },\n },\n});\n\nconst TextVersionButton = styled(Button, {\n base: {\n alignSelf: \"flex-start\",\n },\n});\n\nconst ShowMoreButton = styled(Button, {\n base: {\n marginInlineStart: \"3xsmall\",\n },\n});\n\nconst DESCRIPTION_MAX_LENGTH = 200;\n\nexport type AudioPlayerVariant = \"standard\" | \"minimal\" | \"compact\";\n\ninterface Props {\n src: string;\n title: string;\n subtitle?: {\n title: string;\n url?: string;\n };\n variant?: AudioPlayerVariant;\n description?: string;\n textVersion?: ReactNode;\n img?: {\n url: string;\n alt: string;\n };\n}\n\nexport const AudioPlayer = ({ src, title, subtitle, variant = \"standard\", description, img, textVersion }: Props) => {\n const { t } = useTranslation();\n const [showTextVersion, setShowTextVersion] = useState(false);\n const [showFullDescription, setShowFullDescription] = useState(false);\n const truncatedDescription = useMemo(() => description?.slice(0, DESCRIPTION_MAX_LENGTH), [description]);\n const textDescriptionId = useId();\n\n if (variant === \"minimal\") {\n return <SpeechControl src={src} title={title} />;\n }\n\n if (variant === \"compact\") {\n return <CompactAudioPlayer src={src} title={title} />;\n }\n\n const toggleTextVersion = () => {\n setShowTextVersion((curr) => !curr);\n };\n\n const textVersionButton = (\n <TextVersionButton\n variant=\"secondary\"\n aria-expanded={showTextVersion}\n aria-controls={textDescriptionId}\n size=\"small\"\n onClick={toggleTextVersion}\n >\n {t(showTextVersion ? \"audio.textVersion.close\" : \"audio.textVersion.heading\")}\n </TextVersionButton>\n );\n\n return (\n <AudioPlayerWrapper>\n <InfoWrapper>\n {!!img && (\n <ImageWrapper>\n <img src={img.url} alt={img.alt} />\n </ImageWrapper>\n )}\n <TextWrapper data-has-image={!!img}>\n <TitleWrapper>\n <div>\n {subtitle?.url ? <SafeLink to={subtitle.url}>{subtitle.title}</SafeLink> : subtitle?.title}\n <Heading asChild consumeCss textStyle=\"title.large\">\n <h3>{title}</h3>\n </Heading>\n </div>\n {!!textVersion && !img && textVersionButton}\n </TitleWrapper>\n {!!description && (\n <Text textStyle=\"body.medium\">\n {showFullDescription || description.length < DESCRIPTION_MAX_LENGTH\n ? description\n : `${truncatedDescription}...`}\n {description.length > DESCRIPTION_MAX_LENGTH && (\n <ShowMoreButton variant=\"link\" onClick={() => setShowFullDescription((p) => !p)}>\n {t(`audio.${showFullDescription ? \"readLessDescriptionLabel\" : \"readMoreDescriptionLabel\"}`)}\n </ShowMoreButton>\n )}\n </Text>\n )}\n {!!textVersion && !!img && textVersionButton}\n </TextWrapper>\n </InfoWrapper>\n <Controls src={src} title={title} />\n {!!textVersion && (\n <TextVersionWrapper id={textDescriptionId} hidden={!showTextVersion}>\n <Heading asChild textStyle=\"title.medium\" consumeCss>\n <h4>{t(\"audio.textVersion.heading\")}</h4>\n </Heading>\n <TextVersionText>{textVersion}</TextVersionText>\n </TextVersionWrapper>\n )}\n </AudioPlayerWrapper>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { formatTime } from "./audioUtils.mjs";
2
+ import { SliderControl, SliderHiddenInput, SliderLabel, SliderRange, SliderRoot, SliderThumb, SliderTrack } from "@ndla/primitives";
3
+ import { styled } from "@ndla/styled-system/jsx";
4
+ import { useTranslation } from "react-i18next";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/AudioPlayer/AudioProgress.tsx
7
+ const StyledSliderThumb = styled(SliderThumb, { variants: { variant: {
8
+ standard: {},
9
+ simple: {
10
+ borderRadius: "0",
11
+ width: "4xsmall",
12
+ height: "4xsmall"
13
+ }
14
+ } } });
15
+ const StyledSliderTrack = styled(SliderTrack, { variants: { variant: {
16
+ standard: {},
17
+ simple: { background: "unset" }
18
+ } } });
19
+ const StyledSliderControl = styled(SliderControl, { variants: { variant: {
20
+ standard: {},
21
+ simple: { height: "unset" }
22
+ } } });
23
+ const AudioProgress = ({ currentTime, duration, onValueChange, variant }) => {
24
+ const { t } = useTranslation();
25
+ return /* @__PURE__ */ jsxs(SliderRoot, {
26
+ value: [currentTime],
27
+ defaultValue: [0],
28
+ step: 1,
29
+ max: duration,
30
+ onValueChange,
31
+ getAriaValueText: (value) => t("audio.valueText", {
32
+ start: formatTime(Math.round(value.value)),
33
+ end: formatTime(Math.round(duration))
34
+ }),
35
+ children: [/* @__PURE__ */ jsx(SliderLabel, {
36
+ srOnly: true,
37
+ children: t("audio.progressBar")
38
+ }), /* @__PURE__ */ jsxs(StyledSliderControl, {
39
+ variant,
40
+ children: [/* @__PURE__ */ jsx(StyledSliderTrack, {
41
+ variant,
42
+ children: /* @__PURE__ */ jsx(SliderRange, {})
43
+ }), /* @__PURE__ */ jsx(StyledSliderThumb, {
44
+ index: 0,
45
+ variant,
46
+ children: /* @__PURE__ */ jsx(SliderHiddenInput, {})
47
+ })]
48
+ })]
49
+ });
50
+ };
51
+ //#endregion
52
+ export { AudioProgress };
53
+
54
+ //# sourceMappingURL=AudioProgress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AudioProgress.mjs","names":[],"sources":["../../src/AudioPlayer/AudioProgress.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 SliderRoot,\n SliderLabel,\n SliderControl,\n SliderTrack,\n SliderRange,\n SliderThumb,\n SliderHiddenInput,\n} from \"@ndla/primitives\";\nimport { styled } from \"@ndla/styled-system/jsx\";\nimport { useTranslation } from \"react-i18next\";\nimport { formatTime } from \"./audioUtils\";\n\ninterface Props {\n currentTime: number;\n duration: number;\n onValueChange: (details: SliderValueChangeDetails) => void;\n variant?: \"simple\" | \"standard\";\n}\n\nconst StyledSliderThumb = styled(SliderThumb, {\n variants: {\n variant: {\n standard: {},\n simple: {\n borderRadius: \"0\",\n width: \"4xsmall\",\n height: \"4xsmall\",\n },\n },\n },\n});\n\nconst StyledSliderTrack = styled(SliderTrack, {\n variants: {\n variant: {\n standard: {},\n simple: {\n background: \"unset\",\n },\n },\n },\n});\n\nconst StyledSliderControl = styled(SliderControl, {\n variants: {\n variant: {\n standard: {},\n simple: {\n height: \"unset\",\n },\n },\n },\n});\n\nexport const AudioProgress = ({ currentTime, duration, onValueChange, variant }: Props) => {\n const { t } = useTranslation();\n return (\n <SliderRoot\n value={[currentTime]}\n defaultValue={[0]}\n step={1}\n max={duration}\n onValueChange={onValueChange}\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 <StyledSliderControl variant={variant}>\n <StyledSliderTrack variant={variant}>\n <SliderRange />\n </StyledSliderTrack>\n <StyledSliderThumb index={0} variant={variant}>\n <SliderHiddenInput />\n </StyledSliderThumb>\n </StyledSliderControl>\n </SliderRoot>\n );\n};\n"],"mappings":";;;;;;AA6BA,MAAM,oBAAoB,OAAO,aAAa,EAC5C,UAAU,EACR,SAAS;CACP,UAAU,EAAE;CACZ,QAAQ;EACN,cAAc;EACd,OAAO;EACP,QAAQ;EACT;CACF,EACF,EACF,CAAC;AAEF,MAAM,oBAAoB,OAAO,aAAa,EAC5C,UAAU,EACR,SAAS;CACP,UAAU,EAAE;CACZ,QAAQ,EACN,YAAY,SACb;CACF,EACF,EACF,CAAC;AAEF,MAAM,sBAAsB,OAAO,eAAe,EAChD,UAAU,EACR,SAAS;CACP,UAAU,EAAE;CACZ,QAAQ,EACN,QAAQ,SACT;CACF,EACF,EACF,CAAC;AAEF,MAAa,iBAAiB,EAAE,aAAa,UAAU,eAAe,cAAqB;CACzF,MAAM,EAAE,MAAM,gBAAgB;AAC9B,QACE,qBAAC,YAAD;EACE,OAAO,CAAC,YAAY;EACpB,cAAc,CAAC,EAAE;EACjB,MAAM;EACN,KAAK;EACU;EACf,mBAAmB,UACjB,EAAE,mBAAmB;GACnB,OAAO,WAAW,KAAK,MAAM,MAAM,MAAM,CAAC;GAC1C,KAAK,WAAW,KAAK,MAAM,SAAS,CAAC;GACtC,CAAC;YAVN,CAaE,oBAAC,aAAD;GAAa,QAAA;aAAQ,EAAE,oBAAoB;GAAe,CAAA,EAC1D,qBAAC,qBAAD;GAA8B;aAA9B,CACE,oBAAC,mBAAD;IAA4B;cAC1B,oBAAC,aAAD,EAAe,CAAA;IACG,CAAA,EACpB,oBAAC,mBAAD;IAAmB,OAAO;IAAY;cACpC,oBAAC,mBAAD,EAAqB,CAAA;IACH,CAAA,CACA;KACX"}
@@ -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
  ] })] });