@telefonica/mistica 16.60.0 → 16.62.0-beta.1

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 (548) hide show
  1. package/css/mistica.css +1 -1
  2. package/dist/accordion.css-mistica.js +16 -16
  3. package/dist/align.css-mistica.js +2 -2
  4. package/dist/autocomplete.css-mistica.js +6 -6
  5. package/dist/avatar.css-mistica.js +3 -3
  6. package/dist/badge.css-mistica.js +7 -7
  7. package/dist/box.css-mistica.js +15 -15
  8. package/dist/boxed.css-mistica.js +31 -31
  9. package/dist/button-fixed-footer-layout.d.ts +2 -1
  10. package/dist/button-fixed-footer-layout.js +4 -3
  11. package/dist/button-group.css-mistica.js +10 -10
  12. package/dist/button-layout.css-mistica.js +21 -21
  13. package/dist/button.css-mistica.js +51 -51
  14. package/dist/callout.css-mistica.js +16 -16
  15. package/dist/card-internal.css-mistica.js +38 -38
  16. package/dist/carousel.css-mistica.js +18 -18
  17. package/dist/checkbox.css-mistica.js +21 -21
  18. package/dist/chip.css-mistica.js +30 -30
  19. package/dist/circle.css-mistica.js +2 -2
  20. package/dist/community/advanced-data-card.css-mistica.js +26 -26
  21. package/dist/community/blocks.css-mistica.js +3 -3
  22. package/dist/community/example-component.css-mistica.js +2 -2
  23. package/dist/counter.css-mistica.js +12 -12
  24. package/dist/cover-hero.css-mistica.js +16 -16
  25. package/dist/credit-card-number-field.css-mistica.js +6 -6
  26. package/dist/date-field.css-mistica.js +1 -1
  27. package/dist/date-time-picker.css-mistica.js +2 -2
  28. package/dist/dialog.css-mistica.js +15 -15
  29. package/dist/divider.css-mistica.js +5 -5
  30. package/dist/double-field.css-mistica.js +4 -4
  31. package/dist/drawer.css-mistica.js +15 -15
  32. package/dist/empty-state-card.css-mistica.js +4 -4
  33. package/dist/empty-state.css-mistica.js +14 -14
  34. package/dist/fade-in.css-mistica.js +1 -1
  35. package/dist/feedback.css-mistica.js +14 -14
  36. package/dist/file-upload.css-mistica.js +14 -14
  37. package/dist/fixed-footer-layout.css-mistica.js +12 -12
  38. package/dist/fixed-footer-layout.d.ts +6 -1
  39. package/dist/fixed-footer-layout.js +29 -28
  40. package/dist/form.css-mistica.js +2 -2
  41. package/dist/grid-layout.css-mistica.js +9 -9
  42. package/dist/grid.css-mistica.js +147 -147
  43. package/dist/header.css-mistica.js +5 -5
  44. package/dist/hero.css-mistica.js +11 -11
  45. package/dist/horizontal-scroll.css-mistica.js +3 -3
  46. package/dist/icon-button.css-mistica.js +66 -66
  47. package/dist/icons/icon-chevron.css-mistica.js +6 -6
  48. package/dist/icons/icon-error.css-mistica.js +3 -3
  49. package/dist/image.css-mistica.js +11 -11
  50. package/dist/image.js +8 -1
  51. package/dist/inline.css-mistica.js +16 -16
  52. package/dist/list.css-mistica.js +15 -15
  53. package/dist/loading-bar.css-mistica.js +5 -5
  54. package/dist/loading-screen.css-mistica.js +15 -15
  55. package/dist/logo.css-mistica.js +9 -9
  56. package/dist/menu.css-mistica.js +24 -24
  57. package/dist/mosaic.css-mistica.js +3 -3
  58. package/dist/navigation-bar.css-mistica.js +45 -45
  59. package/dist/navigation-breadcrumbs.css-mistica.js +5 -5
  60. package/dist/package-version.js +2 -2
  61. package/dist/pin-field.css-mistica.js +10 -10
  62. package/dist/popover.css-mistica.js +2 -2
  63. package/dist/progress-bar.css-mistica.js +12 -12
  64. package/dist/radio-button.css-mistica.js +33 -33
  65. package/dist/rating.css-mistica.js +12 -12
  66. package/dist/responsive-layout.css-mistica.js +20 -20
  67. package/dist/screen-reader-only.css-mistica.js +2 -2
  68. package/dist/select.css-mistica.js +29 -29
  69. package/dist/sheet-action-row.css-mistica.js +2 -2
  70. package/dist/sheet-common.css-mistica.js +16 -16
  71. package/dist/sheet-info.css-mistica.js +4 -4
  72. package/dist/skeletons.css-mistica.js +12 -12
  73. package/dist/skins/skin-contract.css-mistica.js +686 -686
  74. package/dist/skip-link.css-mistica.js +3 -3
  75. package/dist/slider.css-mistica.js +28 -28
  76. package/dist/snackbar.css-mistica.js +16 -16
  77. package/dist/spinner.css-mistica.js +5 -5
  78. package/dist/square.css-mistica.js +2 -2
  79. package/dist/stack.css-mistica.js +9 -9
  80. package/dist/stacking-group.css-mistica.js +2 -2
  81. package/dist/stepper.css-mistica.js +16 -16
  82. package/dist/switch-component.css-mistica.js +53 -53
  83. package/dist/table.css-mistica.js +24 -24
  84. package/dist/tabs.css-mistica.js +30 -30
  85. package/dist/tag.css-mistica.js +5 -5
  86. package/dist/text-field-base.css-mistica.js +30 -30
  87. package/dist/text-field-components.css-mistica.js +19 -19
  88. package/dist/text-link.css-mistica.js +10 -10
  89. package/dist/text.css-mistica.js +13 -13
  90. package/dist/theme-context.css-mistica.js +2 -2
  91. package/dist/timeline.css-mistica.js +18 -18
  92. package/dist/timer.css-mistica.js +13 -13
  93. package/dist/tooltip.css-mistica.js +12 -12
  94. package/dist/touchable.css-mistica.js +5 -5
  95. package/dist/utils/aspect-ratio-support.css-mistica.js +7 -7
  96. package/dist/video.css-mistica.js +2 -2
  97. package/dist/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +3 -3
  98. package/dist-es/accordion.css-mistica.js +7 -7
  99. package/dist-es/align.css-mistica.js +2 -2
  100. package/dist-es/autocomplete.css-mistica.js +2 -2
  101. package/dist-es/avatar.css-mistica.js +2 -2
  102. package/dist-es/badge.css-mistica.js +2 -2
  103. package/dist-es/box.css-mistica.js +15 -15
  104. package/dist-es/boxed.css-mistica.js +25 -25
  105. package/dist-es/button-fixed-footer-layout.js +12 -11
  106. package/dist-es/button-group.css-mistica.js +2 -2
  107. package/dist-es/button-layout.css-mistica.js +16 -16
  108. package/dist-es/button.css-mistica.js +38 -38
  109. package/dist-es/callout.css-mistica.js +12 -12
  110. package/dist-es/card-internal.css-mistica.js +20 -20
  111. package/dist-es/carousel.css-mistica.js +10 -10
  112. package/dist-es/checkbox.css-mistica.js +14 -14
  113. package/dist-es/chip.css-mistica.js +17 -17
  114. package/dist-es/circle.css-mistica.js +2 -2
  115. package/dist-es/community/advanced-data-card.css-mistica.js +7 -7
  116. package/dist-es/community/blocks.css-mistica.js +2 -2
  117. package/dist-es/community/example-component.css-mistica.js +2 -2
  118. package/dist-es/counter.css-mistica.js +2 -2
  119. package/dist-es/cover-hero.css-mistica.js +4 -4
  120. package/dist-es/credit-card-number-field.css-mistica.js +4 -4
  121. package/dist-es/date-field.css-mistica.js +1 -1
  122. package/dist-es/date-time-picker.css-mistica.js +2 -2
  123. package/dist-es/dialog.css-mistica.js +5 -5
  124. package/dist-es/divider.css-mistica.js +5 -5
  125. package/dist-es/double-field.css-mistica.js +4 -4
  126. package/dist-es/drawer.css-mistica.js +2 -2
  127. package/dist-es/empty-state-card.css-mistica.js +2 -2
  128. package/dist-es/empty-state.css-mistica.js +7 -7
  129. package/dist-es/fade-in.css-mistica.js +1 -1
  130. package/dist-es/feedback.css-mistica.js +2 -2
  131. package/dist-es/file-upload.css-mistica.js +8 -8
  132. package/dist-es/fixed-footer-layout.css-mistica.js +4 -4
  133. package/dist-es/fixed-footer-layout.js +57 -56
  134. package/dist-es/form.css-mistica.js +2 -2
  135. package/dist-es/grid-layout.css-mistica.js +4 -4
  136. package/dist-es/grid.css-mistica.js +127 -127
  137. package/dist-es/header.css-mistica.js +2 -2
  138. package/dist-es/hero.css-mistica.js +4 -4
  139. package/dist-es/horizontal-scroll.css-mistica.js +2 -2
  140. package/dist-es/icon-button.css-mistica.js +56 -56
  141. package/dist-es/icons/icon-chevron.css-mistica.js +4 -4
  142. package/dist-es/icons/icon-error.css-mistica.js +2 -2
  143. package/dist-es/image.css-mistica.js +4 -4
  144. package/dist-es/image.js +8 -1
  145. package/dist-es/inline.css-mistica.js +10 -10
  146. package/dist-es/list.css-mistica.js +2 -2
  147. package/dist-es/loading-bar.css-mistica.js +2 -2
  148. package/dist-es/loading-screen.css-mistica.js +5 -5
  149. package/dist-es/logo.css-mistica.js +7 -7
  150. package/dist-es/menu.css-mistica.js +15 -15
  151. package/dist-es/mosaic.css-mistica.js +2 -2
  152. package/dist-es/navigation-bar.css-mistica.js +20 -20
  153. package/dist-es/navigation-breadcrumbs.css-mistica.js +2 -2
  154. package/dist-es/package-version.js +2 -2
  155. package/dist-es/pin-field.css-mistica.js +2 -2
  156. package/dist-es/popover.css-mistica.js +2 -2
  157. package/dist-es/progress-bar.css-mistica.js +8 -8
  158. package/dist-es/radio-button.css-mistica.js +25 -25
  159. package/dist-es/rating.css-mistica.js +4 -4
  160. package/dist-es/responsive-layout.css-mistica.js +7 -7
  161. package/dist-es/screen-reader-only.css-mistica.js +2 -2
  162. package/dist-es/select.css-mistica.js +20 -20
  163. package/dist-es/sheet-action-row.css-mistica.js +2 -2
  164. package/dist-es/sheet-common.css-mistica.js +2 -2
  165. package/dist-es/sheet-info.css-mistica.js +2 -2
  166. package/dist-es/skeletons.css-mistica.js +8 -8
  167. package/dist-es/skins/skin-contract.css-mistica.js +686 -686
  168. package/dist-es/skip-link.css-mistica.js +2 -2
  169. package/dist-es/slider.css-mistica.js +20 -20
  170. package/dist-es/snackbar.css-mistica.js +5 -5
  171. package/dist-es/spinner.css-mistica.js +2 -2
  172. package/dist-es/square.css-mistica.js +2 -2
  173. package/dist-es/stack.css-mistica.js +7 -7
  174. package/dist-es/stacking-group.css-mistica.js +2 -2
  175. package/dist-es/stepper.css-mistica.js +4 -4
  176. package/dist-es/style.css +1 -1
  177. package/dist-es/switch-component.css-mistica.js +41 -41
  178. package/dist-es/table.css-mistica.js +11 -11
  179. package/dist-es/tabs.css-mistica.js +21 -21
  180. package/dist-es/tag.css-mistica.js +2 -2
  181. package/dist-es/text-field-base.css-mistica.js +17 -17
  182. package/dist-es/text-field-components.css-mistica.js +4 -4
  183. package/dist-es/text-link.css-mistica.js +9 -9
  184. package/dist-es/text.css-mistica.js +8 -8
  185. package/dist-es/theme-context.css-mistica.js +2 -2
  186. package/dist-es/timeline.css-mistica.js +11 -11
  187. package/dist-es/timer.css-mistica.js +7 -7
  188. package/dist-es/tooltip.css-mistica.js +3 -3
  189. package/dist-es/touchable.css-mistica.js +2 -2
  190. package/dist-es/utils/aspect-ratio-support.css-mistica.js +4 -4
  191. package/dist-es/video.css-mistica.js +2 -2
  192. package/dist-es/vivinho-loading-animation/vivinho-loading-animation.css-mistica.js +2 -2
  193. package/doc/components.md +19 -0
  194. package/doc/figma-mcp.md +136 -0
  195. package/doc/layout.md +20 -1
  196. package/doc/llms.md +18 -0
  197. package/doc/patterns.md +6 -17
  198. package/package.json +13 -1
  199. package/src/accordion.css.ts +121 -0
  200. package/src/accordion.tsx +366 -0
  201. package/src/align.css.ts +7 -0
  202. package/src/align.tsx +32 -0
  203. package/src/autocomplete.css.ts +62 -0
  204. package/src/autocomplete.tsx +239 -0
  205. package/src/avatar.css.ts +14 -0
  206. package/src/avatar.tsx +120 -0
  207. package/src/badge.css.ts +51 -0
  208. package/src/badge.tsx +79 -0
  209. package/src/box.css.ts +51 -0
  210. package/src/box.tsx +114 -0
  211. package/src/boxed.css.ts +132 -0
  212. package/src/boxed.tsx +153 -0
  213. package/src/button-fixed-footer-layout.tsx +65 -0
  214. package/src/button-group.css.ts +75 -0
  215. package/src/button-group.tsx +91 -0
  216. package/src/button-layout.css.ts +162 -0
  217. package/src/button-layout.tsx +91 -0
  218. package/src/button.css.ts +758 -0
  219. package/src/button.tsx +632 -0
  220. package/src/callout.css.ts +50 -0
  221. package/src/callout.tsx +147 -0
  222. package/src/card-cover.tsx +242 -0
  223. package/src/card-data.tsx +152 -0
  224. package/src/card-internal.css.ts +271 -0
  225. package/src/card-internal.tsx +1724 -0
  226. package/src/card-media.tsx +157 -0
  227. package/src/card-naked.tsx +63 -0
  228. package/src/carousel.css.ts +522 -0
  229. package/src/carousel.tsx +1300 -0
  230. package/src/checkbox.css.ts +94 -0
  231. package/src/checkbox.tsx +192 -0
  232. package/src/chip.css.ts +204 -0
  233. package/src/chip.tsx +191 -0
  234. package/src/circle.css.ts +14 -0
  235. package/src/circle.tsx +52 -0
  236. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-button-and-link-footer-image-false-1-snap.png +0 -0
  237. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-button-and-link-footer-image-true-1-snap.png +0 -0
  238. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-button-footer-image-false-1-snap.png +0 -0
  239. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-button-footer-image-true-1-snap.png +0 -0
  240. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-link-footer-image-false-1-snap.png +0 -0
  241. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-link-footer-image-true-1-snap.png +0 -0
  242. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-none-footer-image-false-1-snap.png +0 -0
  243. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-actions-none-footer-image-true-1-snap.png +0 -0
  244. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-extras-0-1-snap.png +0 -0
  245. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-extras-1-1-snap.png +0 -0
  246. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-extras-3-1-snap.png +0 -0
  247. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-extras-3-no-divider-1-snap.png +0 -0
  248. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-inside-carousel-1-snap.png +0 -0
  249. package/src/community/__screenshot_tests__/__image_snapshots__/advanced-data-card-screenshot-test-tsx-advanced-data-card-without-stacking-group-with-top-actions-and-too-long-title-1-snap.png +0 -0
  250. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-highlighted-value-block-1-snap.png +0 -0
  251. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-information-block-1-snap.png +0 -0
  252. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-progress-block-1-snap.png +0 -0
  253. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-progress-block-2-snap.png +0 -0
  254. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-row-block-1-snap.png +0 -0
  255. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-simple-block-1-snap.png +0 -0
  256. package/src/community/__screenshot_tests__/__image_snapshots__/blocks-screenshot-test-tsx-blocks-value-block-1-snap.png +0 -0
  257. package/src/community/__screenshot_tests__/advanced-data-card-screenshot-test.tsx +84 -0
  258. package/src/community/__screenshot_tests__/blocks-screenshot-test.tsx +72 -0
  259. package/src/community/__stories__/advanced-data-card-carousel-story.tsx +66 -0
  260. package/src/community/__stories__/advanced-data-card-story.tsx +158 -0
  261. package/src/community/__stories__/blocks-story.tsx +272 -0
  262. package/src/community/__stories__/example-component-story.tsx +15 -0
  263. package/src/community/__stories__/index-story.tsx +154 -0
  264. package/src/community/__type_tests__/advanced-data-card-type-test.tsx +40 -0
  265. package/src/community/advanced-data-card.css.ts +271 -0
  266. package/src/community/advanced-data-card.tsx +431 -0
  267. package/src/community/blocks.css.ts +12 -0
  268. package/src/community/blocks.tsx +290 -0
  269. package/src/community/example-component.css.ts +7 -0
  270. package/src/community/example-component.tsx +17 -0
  271. package/src/community/index.tsx +10 -0
  272. package/src/counter.css.ts +150 -0
  273. package/src/counter.tsx +215 -0
  274. package/src/cover-hero-media.tsx +39 -0
  275. package/src/cover-hero.css.ts +133 -0
  276. package/src/cover-hero.tsx +262 -0
  277. package/src/credit-card-expiration-field.tsx +187 -0
  278. package/src/credit-card-fields.tsx +56 -0
  279. package/src/credit-card-number-field.css.ts +47 -0
  280. package/src/credit-card-number-field.tsx +245 -0
  281. package/src/cvv-field.tsx +169 -0
  282. package/src/date-field.css.ts +14 -0
  283. package/src/date-field.tsx +130 -0
  284. package/src/date-time-field.tsx +141 -0
  285. package/src/date-time-picker.css.ts +126 -0
  286. package/src/date-time-picker.tsx +188 -0
  287. package/src/decimal-field.tsx +160 -0
  288. package/src/desktop-container-type-context.tsx +15 -0
  289. package/src/dialog-context.tsx +81 -0
  290. package/src/dialog.css.ts +155 -0
  291. package/src/dialog.tsx +423 -0
  292. package/src/divider.css.ts +10 -0
  293. package/src/divider.tsx +11 -0
  294. package/src/double-field.css.ts +33 -0
  295. package/src/double-field.tsx +71 -0
  296. package/src/drawer.css.ts +123 -0
  297. package/src/drawer.tsx +304 -0
  298. package/src/email-field.tsx +76 -0
  299. package/src/empty-state-card.css.ts +40 -0
  300. package/src/empty-state-card.tsx +92 -0
  301. package/src/empty-state.css.ts +119 -0
  302. package/src/empty-state.tsx +141 -0
  303. package/src/fade-in.css.ts +12 -0
  304. package/src/fade-in.tsx +40 -0
  305. package/src/feedback.css.ts +119 -0
  306. package/src/feedback.tsx +432 -0
  307. package/src/file-upload.css.ts +156 -0
  308. package/src/file-upload.tsx +612 -0
  309. package/src/fixed-footer-layout.css.ts +96 -0
  310. package/src/fixed-footer-layout.tsx +227 -0
  311. package/src/fixed-to-top.tsx +21 -0
  312. package/src/focus-trap.tsx +17 -0
  313. package/src/form-context.tsx +198 -0
  314. package/src/form.css.ts +5 -0
  315. package/src/form.tsx +287 -0
  316. package/src/grid-layout.css.ts +68 -0
  317. package/src/grid-layout.tsx +201 -0
  318. package/src/grid.css.ts +203 -0
  319. package/src/grid.tsx +241 -0
  320. package/src/header.css.ts +30 -0
  321. package/src/header.tsx +319 -0
  322. package/src/hero.css.ts +71 -0
  323. package/src/hero.tsx +318 -0
  324. package/src/hooks.tsx +313 -0
  325. package/src/horizontal-scroll.css.ts +43 -0
  326. package/src/horizontal-scroll.tsx +18 -0
  327. package/src/iban-field.tsx +218 -0
  328. package/src/icon-button.css.ts +561 -0
  329. package/src/icon-button.tsx +221 -0
  330. package/src/icons/__stories__/mistica-icons-story.tsx +192 -0
  331. package/src/icons/icon-amex.tsx +40 -0
  332. package/src/icons/icon-chevron.css.ts +23 -0
  333. package/src/icons/icon-chevron.tsx +150 -0
  334. package/src/icons/icon-cvv-amex.tsx +31 -0
  335. package/src/icons/icon-cvv-visa-mc.tsx +31 -0
  336. package/src/icons/icon-error.css.ts +27 -0
  337. package/src/icons/icon-error.tsx +207 -0
  338. package/src/icons/icon-info.tsx +86 -0
  339. package/src/icons/icon-mastercard.tsx +36 -0
  340. package/src/icons/icon-success-vivo-new.tsx +51 -0
  341. package/src/icons/icon-success-vivo.tsx +36 -0
  342. package/src/icons/icon-success.tsx +211 -0
  343. package/src/icons/icon-visa.tsx +32 -0
  344. package/src/image.css.ts +48 -0
  345. package/src/image.tsx +352 -0
  346. package/src/index.tsx +2466 -0
  347. package/src/inline.css.ts +131 -0
  348. package/src/inline.tsx +135 -0
  349. package/src/integer-field.tsx +93 -0
  350. package/src/list.css.ts +281 -0
  351. package/src/list.tsx +963 -0
  352. package/src/loading-bar.css.ts +69 -0
  353. package/src/loading-bar.tsx +25 -0
  354. package/src/loading-screen.css.ts +114 -0
  355. package/src/loading-screen.tsx +376 -0
  356. package/src/logo-blau-shell.tsx +30 -0
  357. package/src/logo-blau.tsx +60 -0
  358. package/src/logo-common.tsx +29 -0
  359. package/src/logo-esimflag-shell.tsx +30 -0
  360. package/src/logo-esimflag.tsx +56 -0
  361. package/src/logo-movistar-new-shell.tsx +30 -0
  362. package/src/logo-movistar-new.tsx +85 -0
  363. package/src/logo-movistar-shell.tsx +30 -0
  364. package/src/logo-movistar.tsx +63 -0
  365. package/src/logo-o2-new-shell.tsx +26 -0
  366. package/src/logo-o2-new.tsx +27 -0
  367. package/src/logo-o2-shell.tsx +26 -0
  368. package/src/logo-o2.tsx +27 -0
  369. package/src/logo-telefonica-shell.tsx +30 -0
  370. package/src/logo-telefonica.tsx +95 -0
  371. package/src/logo-tu-shell.tsx +26 -0
  372. package/src/logo-tu.tsx +28 -0
  373. package/src/logo-vivo-shell.tsx +30 -0
  374. package/src/logo-vivo.tsx +53 -0
  375. package/src/logo.css.ts +33 -0
  376. package/src/logo.tsx +313 -0
  377. package/src/master-detail-layout.tsx +28 -0
  378. package/src/maybe-dismissable.css.ts +37 -0
  379. package/src/maybe-dismissable.tsx +58 -0
  380. package/src/media-queries.css.ts +67 -0
  381. package/src/menu.css.ts +132 -0
  382. package/src/menu.tsx +468 -0
  383. package/src/meter.tsx +516 -0
  384. package/src/modal-context-provider.tsx +45 -0
  385. package/src/month-field.tsx +124 -0
  386. package/src/mosaic.css.ts +73 -0
  387. package/src/mosaic.tsx +205 -0
  388. package/src/navigation-bar.css.ts +558 -0
  389. package/src/navigation-bar.tsx +1637 -0
  390. package/src/navigation-breadcrumbs.css.ts +22 -0
  391. package/src/navigation-breadcrumbs.tsx +69 -0
  392. package/src/negative-box.tsx +15 -0
  393. package/src/nestable-context.tsx +139 -0
  394. package/src/overlay.tsx +86 -0
  395. package/src/overscroll-color-context.tsx +141 -0
  396. package/src/package-version.tsx +2 -0
  397. package/src/password-field.tsx +126 -0
  398. package/src/phone-number-field-lite.tsx +265 -0
  399. package/src/phone-number-field.tsx +171 -0
  400. package/src/pin-field.css.ts +90 -0
  401. package/src/pin-field.tsx +346 -0
  402. package/src/placeholder.tsx +41 -0
  403. package/src/popover.css.ts +8 -0
  404. package/src/popover.tsx +85 -0
  405. package/src/portal.tsx +43 -0
  406. package/src/progress-bar.css.ts +61 -0
  407. package/src/progress-bar.tsx +174 -0
  408. package/src/radio-button.css.ts +174 -0
  409. package/src/radio-button.tsx +322 -0
  410. package/src/rating.css.ts +128 -0
  411. package/src/rating.tsx +351 -0
  412. package/src/responsive-layout.css.ts +162 -0
  413. package/src/responsive-layout.tsx +106 -0
  414. package/src/screen-reader-only.css.ts +27 -0
  415. package/src/screen-reader-only.tsx +33 -0
  416. package/src/screen-size-context-provider.tsx +96 -0
  417. package/src/screen-size-context.tsx +23 -0
  418. package/src/search-field.tsx +126 -0
  419. package/src/select.css.ts +226 -0
  420. package/src/select.tsx +513 -0
  421. package/src/sheet-action-row.css.ts +33 -0
  422. package/src/sheet-actions-list.tsx +113 -0
  423. package/src/sheet-actions.tsx +95 -0
  424. package/src/sheet-common.css.ts +254 -0
  425. package/src/sheet-common.tsx +402 -0
  426. package/src/sheet-info.css.ts +19 -0
  427. package/src/sheet-info.tsx +127 -0
  428. package/src/sheet-native.tsx +189 -0
  429. package/src/sheet-radio-list.tsx +118 -0
  430. package/src/sheet-root.tsx +127 -0
  431. package/src/sheet-types.tsx +94 -0
  432. package/src/sheet-web.tsx +140 -0
  433. package/src/skeleton-base.tsx +38 -0
  434. package/src/skeletons.css.ts +56 -0
  435. package/src/skeletons.tsx +133 -0
  436. package/src/skins/blau.tsx +724 -0
  437. package/src/skins/constants.tsx +10 -0
  438. package/src/skins/defaults.tsx +104 -0
  439. package/src/skins/esimflag.tsx +728 -0
  440. package/src/skins/movistar-new.tsx +735 -0
  441. package/src/skins/movistar.tsx +740 -0
  442. package/src/skins/o2-new.tsx +731 -0
  443. package/src/skins/o2.tsx +727 -0
  444. package/src/skins/skin-contract.css.ts +380 -0
  445. package/src/skins/telefonica.tsx +768 -0
  446. package/src/skins/tu.tsx +741 -0
  447. package/src/skins/types/colors.tsx +286 -0
  448. package/src/skins/types/index.tsx +153 -0
  449. package/src/skins/utils.tsx +66 -0
  450. package/src/skins/vivo-new.tsx +745 -0
  451. package/src/skins/vivo.tsx +720 -0
  452. package/src/skip-link.css.ts +34 -0
  453. package/src/skip-link.tsx +52 -0
  454. package/src/slider.css.ts +181 -0
  455. package/src/slider.tsx +384 -0
  456. package/src/snackbar-context.tsx +98 -0
  457. package/src/snackbar-native.ts +37 -0
  458. package/src/snackbar.css.ts +176 -0
  459. package/src/snackbar.tsx +258 -0
  460. package/src/spinner.css.ts +66 -0
  461. package/src/spinner.tsx +136 -0
  462. package/src/sprinkles.css.ts +83 -0
  463. package/src/square.css.ts +15 -0
  464. package/src/square.tsx +55 -0
  465. package/src/stack.css.ts +44 -0
  466. package/src/stack.tsx +79 -0
  467. package/src/stacking-group.css.ts +15 -0
  468. package/src/stacking-group.tsx +82 -0
  469. package/src/stepper.css.ts +233 -0
  470. package/src/stepper.tsx +156 -0
  471. package/src/switch-component.css.ts +181 -0
  472. package/src/switch-component.tsx +187 -0
  473. package/src/tab-focus.tsx +68 -0
  474. package/src/table-actions-header.tsx +21 -0
  475. package/src/table-cell-text.tsx +35 -0
  476. package/src/table.css.ts +297 -0
  477. package/src/table.tsx +398 -0
  478. package/src/tabs.css.ts +212 -0
  479. package/src/tabs.tsx +263 -0
  480. package/src/tag.css.ts +42 -0
  481. package/src/tag.tsx +161 -0
  482. package/src/test-utils/environment/setup-ssr.tsx +10 -0
  483. package/src/test-utils/fail-test-on-console-error.tsx +22 -0
  484. package/src/test-utils/index.tsx +341 -0
  485. package/src/test-utils/setup-ssr-test-env.tsx +13 -0
  486. package/src/test-utils/ssr.tsx +197 -0
  487. package/src/text-field-base.css.ts +416 -0
  488. package/src/text-field-base.tsx +628 -0
  489. package/src/text-field-components.css.ts +165 -0
  490. package/src/text-field-components.tsx +230 -0
  491. package/src/text-field.tsx +118 -0
  492. package/src/text-link.css.ts +83 -0
  493. package/src/text-link.tsx +85 -0
  494. package/src/text-tokens.tsx +708 -0
  495. package/src/text.css.ts +60 -0
  496. package/src/text.tsx +516 -0
  497. package/src/theme-context-provider.tsx +370 -0
  498. package/src/theme-context.css.ts +3 -0
  499. package/src/theme-context.tsx +8 -0
  500. package/src/theme-variant-context.tsx +51 -0
  501. package/src/theme.tsx +184 -0
  502. package/src/time-field.tsx +99 -0
  503. package/src/timeline.css.ts +135 -0
  504. package/src/timeline.tsx +250 -0
  505. package/src/timer.css.ts +99 -0
  506. package/src/timer.tsx +420 -0
  507. package/src/title.tsx +119 -0
  508. package/src/tooltip-context-provider.tsx +57 -0
  509. package/src/tooltip.css.ts +106 -0
  510. package/src/tooltip.tsx +649 -0
  511. package/src/touchable.css.ts +56 -0
  512. package/src/touchable.tsx +355 -0
  513. package/src/types/font-face.d.ts +47 -0
  514. package/src/types/libs.d.ts +3 -0
  515. package/src/utils/__tests__/analytics-test.tsx +35 -0
  516. package/src/utils/__tests__/browser-test.tsx +28 -0
  517. package/src/utils/__tests__/dom-test.tsx +23 -0
  518. package/src/utils/__tests__/helpers-test.tsx +166 -0
  519. package/src/utils/analytics.tsx +28 -0
  520. package/src/utils/animation.tsx +201 -0
  521. package/src/utils/aspect-ratio-support.css.ts +28 -0
  522. package/src/utils/aspect-ratio-support.tsx +141 -0
  523. package/src/utils/browser.tsx +9 -0
  524. package/src/utils/color.tsx +46 -0
  525. package/src/utils/common.tsx +27 -0
  526. package/src/utils/credit-card.tsx +46 -0
  527. package/src/utils/css.tsx +25 -0
  528. package/src/utils/document-visibility.tsx +52 -0
  529. package/src/utils/dom.tsx +155 -0
  530. package/src/utils/environment.tsx +6 -0
  531. package/src/utils/headings.tsx +18 -0
  532. package/src/utils/helpers.tsx +182 -0
  533. package/src/utils/keys.tsx +8 -0
  534. package/src/utils/locale.tsx +27 -0
  535. package/src/utils/platform.tsx +94 -0
  536. package/src/utils/region-code.tsx +1 -0
  537. package/src/utils/renders-element.tsx +6 -0
  538. package/src/utils/time.tsx +22 -0
  539. package/src/utils/types.tsx +19 -0
  540. package/src/utils/utility-types.tsx +8 -0
  541. package/src/video.css.ts +11 -0
  542. package/src/video.tsx +355 -0
  543. package/src/vivinho-loading-animation/in-lottie.json +402 -0
  544. package/src/vivinho-loading-animation/index.tsx +90 -0
  545. package/src/vivinho-loading-animation/out-lottie.json +575 -0
  546. package/src/vivinho-loading-animation/pulse-lottie.json +551 -0
  547. package/src/vivinho-loading-animation/vivinho-loading-animation.css.ts +15 -0
  548. package/src/vivinho-loading-animation/wave-lottie.json +2829 -0
@@ -0,0 +1,1724 @@
1
+ // spec: https://www.figma.com/design/tKdPOfcUALzVIh5oizFbm7
2
+ 'use client';
3
+ import * as React from 'react';
4
+ import * as styles from './card-internal.css';
5
+ import * as mediaStyles from './image.css';
6
+ import * as tokens from './text-tokens';
7
+ import {Text} from './text';
8
+ import {useInnerText, useTheme} from './hooks';
9
+ import {ThemeVariant, normalizeVariant, useThemeVariant} from './theme-variant-context';
10
+ import Tag from './tag';
11
+ import Stack from './stack';
12
+ import Image from './image';
13
+ import Video from './video';
14
+ import Inline from './inline';
15
+ import Spinner from './spinner';
16
+ import IconPlayFilled from './generated/mistica-icons/icon-play-filled';
17
+ import IconPauseFilled from './generated/mistica-icons/icon-pause-filled';
18
+ import IconCloseRegular from './generated/mistica-icons/icon-close-regular';
19
+ import {getPrefixedDataAttributes} from './utils/dom';
20
+ import {applyCssVars} from './utils/css';
21
+ import {InternalBoxed} from './boxed';
22
+ import {BaseTouchable} from './touchable';
23
+ import {AspectRatioContainer, aspectRatioToNumber} from './utils/aspect-ratio-support';
24
+ import {vars as skinVars} from './skins/skin-contract.css';
25
+ import {IconButton, ToggleIconButton} from './icon-button';
26
+ import {combineRefs} from './utils/common';
27
+ import {isRunningAcceptanceTest} from './utils/platform';
28
+ import classnames from 'classnames';
29
+ import ButtonGroup from './button-group';
30
+ import {isBiggerHeading} from './utils/headings';
31
+ import {applyAlpha} from './utils/color';
32
+
33
+ import type {
34
+ DataAttributes,
35
+ HeadingType,
36
+ IconProps,
37
+ RendersElement,
38
+ RendersNullableElement,
39
+ TrackingEvent,
40
+ } from './utils/types';
41
+ import type {ExclusifyUnion} from './utils/utility-types';
42
+ import type {ButtonLink, ButtonPrimary, ButtonSecondary} from './button';
43
+ import type {NonDeprecatedVariant, Variant} from './theme-variant-context';
44
+ import type {VideoElement, VideoSource, AspectRatio as VideoAspectRatio} from './video';
45
+ import type {AspectRatio as ImageAspectRatio} from './image';
46
+ import type {PressHandler, TouchableElement} from './touchable';
47
+
48
+ export type CardAspectRatio = '1:1' | '16:9' | '7:10' | '9:10' | 'auto' | number;
49
+ export type MediaAspectRatio = ImageAspectRatio | VideoAspectRatio | 'auto' | number;
50
+
51
+ export type CardType = 'data' | 'media' | 'cover' | 'naked';
52
+ export type CardSize = 'snap' | 'default' | 'display';
53
+ export type MediaPosition = 'top' | 'left' | 'right';
54
+
55
+ /** @deprecated use imageSrc, imageSrcSet, videoSrc and related props */
56
+ export type DeprecatedMediaProp = RendersElement<typeof Image> | RendersElement<typeof Video>;
57
+
58
+ export type SlotAlignment = 'content' | 'bottom' | 'space-between';
59
+
60
+ export type CardActionButtonPrimary = RendersNullableElement<typeof ButtonPrimary>;
61
+ export type CardActionButtonSecondary = RendersNullableElement<typeof ButtonSecondary>;
62
+ export type CardActionButtonLink = RendersNullableElement<typeof ButtonLink>;
63
+
64
+ type CardVideoProps = {
65
+ videoLoop?: boolean;
66
+ videoAutoPlay?: boolean;
67
+ videoDataAttributes?: DataAttributes;
68
+ };
69
+
70
+ type ContainerProps = {
71
+ type: CardType;
72
+ size: CardSize;
73
+ variant?: Variant;
74
+ width?: string | number;
75
+ height?: string | number;
76
+ /** Gradient overlay color for cover cards. If not set it uses the theme color */
77
+ gradientOverlayColor?: 'transparent' | string;
78
+ aspectRatio?: CardAspectRatio;
79
+ dataAttributes?: DataAttributes;
80
+ 'aria-label'?: React.AriaAttributes['aria-label'];
81
+ 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
82
+ 'aria-description'?: string; // W3C Editor's Draft for ARIA 1.3
83
+ 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
84
+ };
85
+
86
+ type ButtonsProps = {
87
+ buttonPrimary?: CardActionButtonPrimary;
88
+ buttonSecondary?: CardActionButtonSecondary;
89
+ buttonLink?: CardActionButtonLink;
90
+ };
91
+
92
+ type MediaProps = {
93
+ /** @deprecated use imageSrc, imageSrcSet, videoSrc and related props */
94
+ media?: DeprecatedMediaProp;
95
+ backgroundColor?: string;
96
+ imageSrc?: string;
97
+ imageSrcSet?: string;
98
+ imageAlt?: string;
99
+ imageFit?: 'fit' | 'fill' | 'fill-center';
100
+ videoSrc?: VideoSource;
101
+ videoRef?: React.RefObject<VideoElement>;
102
+ mediaPosition?: MediaPosition;
103
+ /** Ignored when mediaPosition !== 'top' */
104
+ mediaAspectRatio?: MediaAspectRatio;
105
+ /** Ignored when mediaPosition === 'top' */
106
+ mediaWidth?: string | number;
107
+ circledImage?: boolean;
108
+ } & CardVideoProps;
109
+
110
+ type TextContentProps = {
111
+ type: CardType;
112
+ headline?: string | RendersNullableElement<typeof Tag>;
113
+ pretitle?:
114
+ | string
115
+ | {
116
+ text: string;
117
+ 'aria-label'?: React.AriaAttributes['aria-label'];
118
+ };
119
+ pretitleAs?: HeadingType;
120
+ pretitleLinesMax?: number;
121
+ title?:
122
+ | string
123
+ | {
124
+ text: string;
125
+ 'aria-label'?: React.AriaAttributes['aria-label'];
126
+ };
127
+ titleAs?: HeadingType;
128
+ titleLinesMax?: number;
129
+ subtitle?: string;
130
+ subtitleLinesMax?: number;
131
+ description?: string;
132
+ descriptionLinesMax?: number;
133
+ };
134
+
135
+ type AssetProps = {
136
+ asset?: React.ReactElement;
137
+ };
138
+
139
+ type TopActionsProps = {
140
+ onClose?: () => void;
141
+ closeButtonLabel?: string;
142
+ topActions?: TopActionsArray;
143
+ videoAction?: CardAction;
144
+ };
145
+
146
+ type SlotProps = {
147
+ slot?: React.ReactNode;
148
+ slotAlignment?: SlotAlignment;
149
+ };
150
+
151
+ type FooterProps = {
152
+ showFooter?: boolean;
153
+ footerSlot?: React.ReactNode;
154
+ footerBackgroundColor?: string;
155
+ footerVariant?: 'default' | 'brand' | 'inverse';
156
+ footerDivider?: boolean;
157
+ };
158
+
159
+ type NoChildrenProps = {
160
+ children?: undefined;
161
+ };
162
+
163
+ type CardProps = ContainerProps &
164
+ MediaProps &
165
+ TextContentProps &
166
+ AssetProps &
167
+ ButtonsProps &
168
+ TopActionsProps &
169
+ SlotProps &
170
+ FooterProps &
171
+ NoChildrenProps;
172
+
173
+ type TouchableProps = {
174
+ trackingEvent?: TrackingEvent | ReadonlyArray<TrackingEvent>;
175
+ role?: string;
176
+ 'aria-current'?: React.AriaAttributes['aria-current'];
177
+ /**
178
+ * Aria label for the touchable element. If set the whole card will be announced as a link/button by screen readers.
179
+ */
180
+ touchableAriaLabel?: React.AriaAttributes['aria-label'];
181
+ /**
182
+ * When true, the touchable content of the card will be segregated for screen readers. Only one of those elements will be announced as link/button,
183
+ * according to the following logic:
184
+ * - If there is title or pretitle it will be the one between these two with the higher hierarchy (for example, if titleAs is 'h2' and pretitleAs is 'h3', the title will be the touchable element).
185
+ * - If those are not present it will be the next hierarchy element: headline, subtitle, ...
186
+ * This behaviour can be overriden with the touchableAriaLabel prop.
187
+ */
188
+ segregateTouchableContent?: boolean;
189
+ } & ExclusifyUnion<
190
+ | {
191
+ href: string | undefined;
192
+ newTab?: boolean;
193
+ loadOnTop?: boolean;
194
+ onNavigate?: () => void | Promise<void>;
195
+ }
196
+ | {
197
+ to: string | undefined;
198
+ newTab?: boolean;
199
+ fullPageOnWebView?: boolean;
200
+ replace?: boolean;
201
+ onNavigate?: () => void | Promise<void>;
202
+ }
203
+ | {onPress: PressHandler | undefined}
204
+ >;
205
+
206
+ type TouchableCard<T> = T & TouchableProps;
207
+ export type MaybeTouchableCard<T> = ExclusifyUnion<TouchableCard<T> | T>;
208
+
209
+ type PrivateContainerProps = {
210
+ children?: React.ReactNode;
211
+ };
212
+
213
+ const Container = React.forwardRef<HTMLDivElement, ContainerProps & MediaProps & PrivateContainerProps>(
214
+ (
215
+ {
216
+ type,
217
+ children,
218
+ width,
219
+ height,
220
+ aspectRatio,
221
+ 'aria-label': ariaLabel,
222
+ 'aria-labelledby': ariaLabelledby,
223
+ 'aria-description': ariaDescription,
224
+ 'aria-describedby': ariaDescribedby,
225
+ dataAttributes,
226
+ backgroundColor,
227
+ variant,
228
+ },
229
+ ref
230
+ ): JSX.Element => {
231
+ const aspectRatioValue = width && height ? undefined : aspectRatioToNumber(aspectRatio);
232
+ const aspectRatioStyle = aspectRatioValue
233
+ ? applyCssVars({[styles.vars.aspectRatio]: String(aspectRatioValue)})
234
+ : {};
235
+
236
+ const boxedBorderStyleOverride = backgroundColor ? 'none' : undefined;
237
+ const isNaked = type === 'naked';
238
+
239
+ return (
240
+ // aria-description should be vaild, but this eslint rule is complaining about it
241
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
242
+ <section
243
+ ref={ref}
244
+ aria-label={ariaLabel}
245
+ aria-labelledby={ariaLabelledby}
246
+ aria-description={ariaDescription}
247
+ aria-describedby={ariaDescribedby}
248
+ className={classnames(styles.container)}
249
+ {...getPrefixedDataAttributes(dataAttributes, 'InternalCard')}
250
+ style={{
251
+ width: width || '100%',
252
+ height: height || '100%',
253
+ position: 'relative',
254
+ ...aspectRatioStyle,
255
+ }}
256
+ >
257
+ <div
258
+ style={{
259
+ display: 'flex',
260
+ flexDirection: 'column',
261
+ width: '100%',
262
+ position: 'relative',
263
+ minHeight: '100%',
264
+ }}
265
+ >
266
+ <InternalBoxed
267
+ // Without setting the width here, the component fails to get the correct width in some cases
268
+ // even if we set the 100% width style in the boxed class
269
+ width="100%"
270
+ height="100%"
271
+ variant={variant}
272
+ className={classnames(styles.boxed)}
273
+ background={isNaked ? 'transparent' : backgroundColor}
274
+ borderRadius={isNaked ? 'none' : skinVars.borderRadii.container}
275
+ border={isNaked ? 'none' : boxedBorderStyleOverride}
276
+ overflow="visible"
277
+ >
278
+ {children}
279
+ </InternalBoxed>
280
+ </div>
281
+ </section>
282
+ );
283
+ }
284
+ );
285
+
286
+ type FillerProps = {
287
+ minHeight?: number;
288
+ };
289
+
290
+ const Filler = ({minHeight}: FillerProps) => (
291
+ <div
292
+ style={{
293
+ flexGrow: 1,
294
+ flexShrink: 0,
295
+ minHeight,
296
+ }}
297
+ />
298
+ );
299
+
300
+ type PrivateAssetProps = {
301
+ size: CardSize;
302
+ type: CardType;
303
+ absolute?: boolean;
304
+ };
305
+
306
+ const Asset = ({size, absolute, asset, type}: AssetProps & PrivateAssetProps): JSX.Element | null => {
307
+ if (!asset) {
308
+ return null;
309
+ }
310
+
311
+ const assetWithContainer = (
312
+ <div
313
+ data-testid="asset"
314
+ aria-hidden
315
+ style={applyCssVars({[mediaStyles.vars.mediaBorderRadius]: skinVars.borderRadii.mediaSmall})}
316
+ >
317
+ {asset}
318
+ </div>
319
+ );
320
+
321
+ if (absolute) {
322
+ return (
323
+ <div
324
+ style={{position: 'absolute', top: 0, left: 0}}
325
+ className={classnames(
326
+ styles.containerPaddingXVariants[size],
327
+ styles.containerPaddingTopVariants[size]
328
+ )}
329
+ >
330
+ {assetWithContainer}
331
+ </div>
332
+ );
333
+ }
334
+
335
+ return (
336
+ <div className={classnames({[styles.containerPaddingXVariants[size]]: type !== 'naked'})}>
337
+ {assetWithContainer}
338
+ </div>
339
+ );
340
+ };
341
+
342
+ export type BackgroundImageOrVideoProps = {
343
+ video?: React.ReactNode;
344
+ src?: string;
345
+ srcSet?: string;
346
+ backgroundVariant: Variant;
347
+ };
348
+
349
+ const BackgroundImageOrVideo = ({
350
+ video,
351
+ src,
352
+ srcSet,
353
+ backgroundVariant,
354
+ }: BackgroundImageOrVideoProps): JSX.Element => {
355
+ return (
356
+ <ThemeVariant variant={backgroundVariant}>
357
+ <div
358
+ className={styles.backgroundImageOrVideoContainer}
359
+ style={applyCssVars({
360
+ [mediaStyles.vars.mediaBorderRadius]: '0px',
361
+ borderRadius: skinVars.borderRadii.container,
362
+ overflow: 'hidden',
363
+ })}
364
+ >
365
+ {video ? (
366
+ video
367
+ ) : (
368
+ <Image
369
+ dataAttributes={{testid: 'image'}}
370
+ width="100%"
371
+ height="100%"
372
+ src={src || ''}
373
+ srcSet={srcSet}
374
+ alt=""
375
+ />
376
+ )}
377
+ </div>
378
+ </ThemeVariant>
379
+ );
380
+ };
381
+
382
+ type VideoState = 'loading' | 'playing' | 'paused' | 'error';
383
+
384
+ type VideoAction = 'play' | 'pause' | 'fail' | 'reset';
385
+
386
+ const transitions: Record<VideoState, Partial<Record<VideoAction, VideoState>>> = {
387
+ loading: {
388
+ play: 'playing',
389
+ pause: 'paused',
390
+ fail: 'error',
391
+ reset: 'loading',
392
+ },
393
+
394
+ playing: {
395
+ pause: 'paused',
396
+ reset: 'loading',
397
+ fail: 'error',
398
+ },
399
+
400
+ paused: {
401
+ play: 'playing',
402
+ reset: 'loading',
403
+ fail: 'error',
404
+ },
405
+
406
+ error: {
407
+ reset: 'loading',
408
+ },
409
+ };
410
+
411
+ const videoReducer = (state: VideoState, action: VideoAction): VideoState =>
412
+ transitions[state][action] || state;
413
+
414
+ export const CardActionSpinner = ({color}: IconProps): React.ReactElement => (
415
+ <Spinner color={color} size={16} delay="0" />
416
+ );
417
+
418
+ const CardActionPauseIcon = ({color}: IconProps) => <IconPauseFilled color={color} size={12} />;
419
+
420
+ const CardActionPlayIcon = ({color}: IconProps) => <IconPlayFilled color={color} size={12} />;
421
+
422
+ export const useVideoWithControls = ({
423
+ src,
424
+ poster,
425
+ ref: videoRef,
426
+ autoHeight,
427
+ loop,
428
+ autoPlay,
429
+ dataAttributes,
430
+ }: {
431
+ src?: VideoSource;
432
+ poster?: string;
433
+ ref?: React.RefObject<VideoElement>;
434
+ autoHeight?: boolean;
435
+ loop?: boolean;
436
+ autoPlay?: boolean;
437
+ dataAttributes?: DataAttributes;
438
+ }): {
439
+ video?: React.ReactNode;
440
+ videoAction?: CardAction;
441
+ } => {
442
+ const {texts, t} = useTheme();
443
+ const videoController = React.useRef<VideoElement>(null);
444
+ const initialLoadDoneRef = React.useRef(false);
445
+ const [videoStatus, dispatch] = React.useReducer(
446
+ videoReducer,
447
+ process.env.NODE_ENV === 'test' ? 'playing' : 'loading'
448
+ );
449
+
450
+ React.useEffect(() => {
451
+ initialLoadDoneRef.current = false;
452
+ videoController.current?.load();
453
+
454
+ return () => {
455
+ dispatch('reset');
456
+ };
457
+ }, [src]);
458
+
459
+ const video = React.useMemo(() => {
460
+ if (src === undefined) {
461
+ return undefined;
462
+ }
463
+
464
+ const handleLoadedData = () => {
465
+ // When autoPlay is false, the video loads but doesn't play.
466
+ // We need to transition to 'paused' state to show the play button instead of spinner.
467
+ // We use initialLoadDoneRef to ensure this only happens once per video source,
468
+ // because Chrome can fire onCanPlayThrough multiple times for network videos,
469
+ // which would revert the card to 'paused' state even while the video is playing.
470
+ if (autoPlay === false && !initialLoadDoneRef.current) {
471
+ initialLoadDoneRef.current = true;
472
+ dispatch('pause');
473
+ }
474
+ };
475
+
476
+ return (
477
+ <Video
478
+ ref={combineRefs(videoController, videoRef)}
479
+ src={src}
480
+ poster={poster}
481
+ width="100%"
482
+ height={autoHeight ? undefined : '100%'}
483
+ loop={loop}
484
+ autoPlay={autoPlay}
485
+ dataAttributes={dataAttributes}
486
+ onLoad={handleLoadedData}
487
+ onError={() => dispatch('fail')}
488
+ onPause={() => dispatch('pause')}
489
+ onPlay={() => dispatch('play')}
490
+ />
491
+ );
492
+ }, [videoRef, src, poster, autoHeight, loop, autoPlay, dataAttributes]);
493
+
494
+ const onVideoControlPress = () => {
495
+ const video = videoController.current;
496
+ if (!video) {
497
+ return;
498
+ }
499
+
500
+ if (videoStatus === 'playing') {
501
+ video.pause();
502
+ } else {
503
+ video.play().then(
504
+ () => dispatch('play'),
505
+ () => {}
506
+ );
507
+ }
508
+ };
509
+
510
+ if (videoStatus === 'error') {
511
+ return {video};
512
+ }
513
+
514
+ const isVideoLoading = videoStatus === 'loading';
515
+
516
+ const videoAction: CardAction | undefined = video
517
+ ? {
518
+ uncheckedProps: {
519
+ Icon:
520
+ isVideoLoading && !isRunningAcceptanceTest() ? CardActionSpinner : CardActionPauseIcon,
521
+ label: isVideoLoading ? '' : texts.pauseIconButtonLabel || t(tokens.pauseIconButtonLabel),
522
+ },
523
+ checkedProps: {
524
+ Icon: CardActionPlayIcon,
525
+ label: texts.playIconButtonLabel || t(tokens.playIconButtonLabel),
526
+ },
527
+ onChange: onVideoControlPress,
528
+ disabled: isRunningAcceptanceTest() ? false : isVideoLoading,
529
+ checked: videoStatus === 'paused',
530
+ }
531
+ : undefined;
532
+
533
+ return {
534
+ video,
535
+ videoAction,
536
+ };
537
+ };
538
+
539
+ type PrivateButtonsProps = {
540
+ size: CardSize;
541
+ };
542
+
543
+ const Buttons = ({
544
+ size,
545
+ buttonPrimary,
546
+ buttonSecondary,
547
+ buttonLink,
548
+ }: ButtonsProps & PrivateButtonsProps): JSX.Element => {
549
+ return (
550
+ <div className={styles.actionsContainerVariants[size]}>
551
+ <ButtonGroup primaryButton={buttonPrimary} secondaryButton={buttonSecondary} link={buttonLink} />
552
+ </div>
553
+ );
554
+ };
555
+
556
+ type BaseIconButtonAction = {
557
+ Icon: (props: IconProps) => JSX.Element;
558
+ label: string;
559
+ 'aria-description'?: string;
560
+ 'aria-describedby'?: string;
561
+ 'aria-current'?: React.AriaAttributes['aria-current'];
562
+ };
563
+
564
+ type IconButtonAction = BaseIconButtonAction &
565
+ ExclusifyUnion<
566
+ | {href: string; newTab?: boolean}
567
+ | {
568
+ to: string;
569
+ newTab?: boolean;
570
+ fullPageOnWebView?: boolean;
571
+ replace?: boolean;
572
+ }
573
+ | {onPress: () => void}
574
+ >;
575
+
576
+ type ToggleIconButtonAction = {
577
+ checkedProps: BaseIconButtonAction;
578
+ uncheckedProps: BaseIconButtonAction;
579
+ onChange?: (checked: boolean) => void | undefined | Promise<void>;
580
+ checked?: boolean;
581
+ defaultChecked?: boolean;
582
+ };
583
+
584
+ export type CardAction = {
585
+ disabled?: boolean;
586
+ trackingEvent?: TrackingEvent | ReadonlyArray<TrackingEvent>;
587
+ } & ExclusifyUnion<IconButtonAction | ToggleIconButtonAction>;
588
+
589
+ export type TopActionsArray = ReadonlyArray<CardAction | React.ReactElement>;
590
+
591
+ export const CardActionIconButton = (props: CardAction): JSX.Element => {
592
+ const variant = useThemeVariant();
593
+
594
+ if (props.Icon) {
595
+ return (
596
+ <IconButton
597
+ {...props}
598
+ small
599
+ aria-label={props.label}
600
+ type="neutral"
601
+ backgroundType="transparent"
602
+ />
603
+ );
604
+ }
605
+
606
+ const {checkedProps, uncheckedProps, ...rest} = props;
607
+ return (
608
+ <ToggleIconButton
609
+ small
610
+ {...rest}
611
+ checkedProps={{
612
+ ...checkedProps,
613
+ 'aria-label': props.checkedProps.label,
614
+ type: variant === 'media' ? 'neutral' : 'brand',
615
+ backgroundType: 'solid',
616
+ }}
617
+ uncheckedProps={{
618
+ ...uncheckedProps,
619
+ 'aria-label': props.uncheckedProps.label,
620
+ type: 'neutral',
621
+ backgroundType: 'transparent',
622
+ }}
623
+ />
624
+ );
625
+ };
626
+
627
+ type PrivateTopActionsProps = {
628
+ actions?: TopActionsArray;
629
+ testid?: string;
630
+ variant?: Variant;
631
+ containerStyles?: React.CSSProperties;
632
+ };
633
+
634
+ export const TopActions = ({
635
+ testid = 'topActions',
636
+ onClose,
637
+ closeButtonLabel,
638
+ actions: actionsProp,
639
+ variant,
640
+ containerStyles = {},
641
+ }: Omit<TopActionsProps, 'topActions'> & PrivateTopActionsProps): JSX.Element => {
642
+ const {texts, t} = useTheme();
643
+ const actions = actionsProp ? [...actionsProp] : [];
644
+
645
+ if (onClose) {
646
+ actions.push({
647
+ label: closeButtonLabel || texts.closeButtonLabel || t(tokens.closeButtonLabel),
648
+ onPress: onClose,
649
+ Icon: IconCloseRegular,
650
+ });
651
+ }
652
+
653
+ if (actions.length === 0) {
654
+ return <></>;
655
+ }
656
+
657
+ return (
658
+ <ThemeVariant variant={variant}>
659
+ <div className={styles.topActionsContainer} style={containerStyles} data-testid={testid}>
660
+ {actions.map((action, index) => {
661
+ if ('Icon' in action || 'checkedProps' in action) {
662
+ return <CardActionIconButton key={index} {...action} />;
663
+ }
664
+ return action;
665
+ })}
666
+ </div>
667
+ </ThemeVariant>
668
+ );
669
+ };
670
+
671
+ type MediaComponentProps = {
672
+ type: CardType;
673
+ size: CardSize;
674
+ asset?: React.ReactElement;
675
+ imageSrc?: string;
676
+ imageSrcSet?: string;
677
+ imageAlt?: string;
678
+ imageFit: 'fit' | 'fill' | 'fill-center';
679
+ video?: React.ReactNode;
680
+ mediaAspectRatio: MediaAspectRatio;
681
+ mediaPosition: MediaPosition;
682
+ mediaWidth: string | number;
683
+ videoAction?: CardAction;
684
+ circledImage?: boolean;
685
+ };
686
+
687
+ const Media = ({
688
+ type,
689
+ size,
690
+ asset,
691
+ imageSrc,
692
+ imageSrcSet,
693
+ imageFit,
694
+ imageAlt = '',
695
+ video,
696
+ mediaAspectRatio,
697
+ mediaPosition,
698
+ mediaWidth,
699
+ circledImage,
700
+ }: MediaComponentProps): JSX.Element => {
701
+ const aspectRatioAsNumber = aspectRatioToNumber(mediaAspectRatio);
702
+
703
+ const imageProps =
704
+ type === 'naked' && circledImage
705
+ ? {circular: true}
706
+ : {
707
+ width: '100%',
708
+ height: mediaPosition === 'top' ? (aspectRatioAsNumber === 0 ? undefined : '100%') : '100%',
709
+ };
710
+
711
+ const renderMedia = () => {
712
+ if (video) {
713
+ return video;
714
+ }
715
+ if (imageSrc !== undefined || imageSrcSet !== undefined) {
716
+ const isLeftOrRight = mediaPosition === 'left' || mediaPosition === 'right';
717
+ const imageFitProps = {
718
+ fit: {objectFit: 'contain', objectPosition: `bottom ${mediaPosition}`},
719
+ fill: {objectFit: 'cover', objectPosition: mediaPosition},
720
+ 'fill-center': {objectFit: 'cover', objectPosition: 'center'},
721
+ } as const;
722
+
723
+ return (
724
+ <Image
725
+ src={imageSrc || ''}
726
+ srcSet={imageSrcSet}
727
+ {...imageProps}
728
+ dataAttributes={{testid: 'image'}}
729
+ alt={imageAlt}
730
+ {...(isLeftOrRight ? imageFitProps[imageFit] : {})}
731
+ />
732
+ );
733
+ }
734
+ return null;
735
+ };
736
+
737
+ const mediaElement = renderMedia();
738
+
739
+ if (!mediaElement) {
740
+ return <></>;
741
+ }
742
+
743
+ const commonContainerStyles = {
744
+ // overrides media border radius
745
+ ...(type === 'naked' ? undefined : applyCssVars({[mediaStyles.vars.mediaBorderRadius]: '0px'})),
746
+ };
747
+
748
+ if (mediaPosition === 'top') {
749
+ // using AspectRatioContainer because the <video> element flashes with the poster image size while loading
750
+ return (
751
+ <>
752
+ <AspectRatioContainer aspectRatio={aspectRatioAsNumber} style={commonContainerStyles}>
753
+ {mediaElement}
754
+ </AspectRatioContainer>
755
+ <Asset absolute size={size} asset={asset} type={type} />
756
+ </>
757
+ );
758
+ }
759
+
760
+ // in left/right media position, mediaAspectRatio is ignored
761
+ return (
762
+ <>
763
+ <div
764
+ style={{
765
+ ...commonContainerStyles,
766
+ width: mediaWidth,
767
+ flexShrink: 0,
768
+ flexGrow: 0,
769
+ height: '100%',
770
+ position: 'relative',
771
+ }}
772
+ >
773
+ {mediaElement}
774
+ </div>
775
+ {mediaPosition !== 'right' && <Asset absolute size={size} asset={asset} type={type} />}
776
+ </>
777
+ );
778
+ };
779
+
780
+ type PrivateFooterProps = {
781
+ type: CardType;
782
+ size: CardSize;
783
+ variant: NonDeprecatedVariant;
784
+ hasBackgroundImageOrVideo?: boolean;
785
+ externalVariant: NonDeprecatedVariant;
786
+ overlayColor: string;
787
+ };
788
+
789
+ const Footer = ({
790
+ type,
791
+ size,
792
+ variant,
793
+ footerSlot,
794
+ buttonPrimary,
795
+ buttonSecondary,
796
+ buttonLink,
797
+ hasBackgroundImageOrVideo,
798
+ footerVariant,
799
+ footerBackgroundColor,
800
+ footerDivider,
801
+ externalVariant,
802
+ overlayColor,
803
+ }: FooterProps & ButtonsProps & PrivateFooterProps): JSX.Element => {
804
+ const hasButtons = !!(buttonPrimary || buttonSecondary || buttonLink);
805
+ const has3Buttons = !!(buttonPrimary && buttonSecondary && buttonLink);
806
+ const dividerColor = {
807
+ default: skinVars.colors.divider,
808
+ brand: skinVars.colors.dividerBrand,
809
+ negative: skinVars.colors.dividerNegative,
810
+ media: skinVars.colors.dividerBrand,
811
+ alternative: skinVars.colors.divider,
812
+ }[variant];
813
+ const isNaked = type === 'naked';
814
+ const backgroundColor =
815
+ footerBackgroundColor ||
816
+ (footerVariant && footerVariant !== variant
817
+ ? footerVariant === 'default'
818
+ ? skinVars.colors.backgroundContainer
819
+ : externalVariant === 'brand' || externalVariant === 'media' || externalVariant === 'negative'
820
+ ? skinVars.colors.backgroundContainerBrandOverBrand
821
+ : skinVars.colors.backgroundContainerBrand
822
+ : undefined);
823
+ const withDivider = footerDivider ?? !backgroundColor;
824
+
825
+ return (
826
+ <ThemeVariant variant={footerVariant || variant}>
827
+ <Filler />
828
+ <div
829
+ style={{
830
+ background: backgroundColor || (hasBackgroundImageOrVideo ? overlayColor : undefined),
831
+ position: 'relative',
832
+ backdropFilter: hasBackgroundImageOrVideo ? 'blur(12px)' : undefined,
833
+ borderBottomLeftRadius: isNaked ? 0 : skinVars.borderRadii.container,
834
+ borderBottomRightRadius: isNaked ? 0 : skinVars.borderRadii.container,
835
+ }}
836
+ >
837
+ <div
838
+ // The divider is outside the footer because it has a conditional right margin
839
+ style={{
840
+ borderTop: withDivider ? `1px solid ${dividerColor}` : undefined,
841
+ marginRight: isNaked ? 16 : 0,
842
+ }}
843
+ />
844
+ <div
845
+ data-testid="footer"
846
+ className={classnames({
847
+ [styles.containerPaddingXVariants[size]]: !isNaked,
848
+ [styles.containerPaddingBottomVariants[size]]: !isNaked,
849
+ })}
850
+ style={{
851
+ paddingTop: 16,
852
+ paddingBottom: isNaked ? 0 : undefined,
853
+ paddingRight: isNaked ? 16 : undefined,
854
+ }}
855
+ >
856
+ <Stack space={16}>
857
+ {footerSlot}
858
+ {hasButtons &&
859
+ // @FIXME: We should use the ButtonGroup component
860
+ // https://jira.tid.es/browse/WEB-2278
861
+ // https://www.figma.com/design/koROdh3HpEPG2O8jG52Emh/%F0%9F%94%B8-Buttons-Specs?node-id=4337-1606&t=HtImvar8DMbivDqC-0
862
+ (has3Buttons ? (
863
+ <Stack space={16}>
864
+ <Inline space="between" alignItems="center">
865
+ {buttonPrimary}
866
+ {buttonSecondary}
867
+ </Inline>
868
+ <div
869
+ // bleed workaround
870
+ style={{marginLeft: -12}}
871
+ >
872
+ {buttonLink}
873
+ </div>
874
+ </Stack>
875
+ ) : (
876
+ <Inline space="between" alignItems="center">
877
+ {buttonPrimary}
878
+ {buttonSecondary}
879
+ {buttonLink}
880
+ </Inline>
881
+ ))}
882
+ </Stack>
883
+ </div>
884
+ </div>
885
+ </ThemeVariant>
886
+ );
887
+ };
888
+
889
+ type PrivateTextContentProps = {
890
+ size: CardSize;
891
+ variant?: Variant;
892
+ withTextShadow?: boolean;
893
+ headlineRef?: (element: HTMLHeadingElement | null) => void;
894
+ hasCustomBackground: boolean;
895
+ touchableRef?: React.RefObject<TouchableElement | null>;
896
+ touchableProps?: TouchableProps;
897
+ };
898
+
899
+ const TextContent = ({
900
+ type,
901
+ hasCustomBackground,
902
+ headlineRef,
903
+ touchableRef,
904
+ touchableProps,
905
+ size,
906
+ variant,
907
+ headline,
908
+ title: titleProp,
909
+ titleAs = 'h3',
910
+ titleLinesMax,
911
+ pretitle: pretitleProp,
912
+ pretitleAs,
913
+ pretitleLinesMax,
914
+ subtitle,
915
+ subtitleLinesMax,
916
+ description,
917
+ descriptionLinesMax,
918
+ withTextShadow,
919
+ }: TextContentProps & PrivateTextContentProps): JSX.Element => {
920
+ const {textPresets, colorValues} = useTheme();
921
+ const externalVariant = useThemeVariant();
922
+
923
+ const title = typeof titleProp === 'string' ? {text: titleProp} : titleProp;
924
+ const pretitle = typeof pretitleProp === 'string' ? {text: pretitleProp} : pretitleProp;
925
+
926
+ const commonProps = {
927
+ hyphens: 'auto',
928
+ } as const;
929
+
930
+ const colorVariants = {
931
+ default: {
932
+ pretitle: colorValues.textPrimary,
933
+ title: colorValues.textPrimary,
934
+ subtitle: colorValues.textPrimary,
935
+ description: colorValues.textSecondary,
936
+ },
937
+ brand: {
938
+ pretitle: colorValues.textPrimaryBrand,
939
+ title: colorValues.textPrimaryBrand,
940
+ subtitle: colorValues.textPrimaryBrand,
941
+ description: colorValues.textSecondaryBrand,
942
+ },
943
+ negative: {
944
+ pretitle: colorValues.textPrimaryNegative,
945
+ title: colorValues.textPrimaryNegative,
946
+ subtitle: colorValues.textPrimaryNegative,
947
+ description: colorValues.textSecondaryNegative,
948
+ },
949
+ media: {
950
+ pretitle: colorValues.textPrimaryMedia,
951
+ title: colorValues.textPrimaryMedia,
952
+ subtitle: colorValues.textPrimaryMedia,
953
+ description:
954
+ type === 'cover' && hasCustomBackground
955
+ ? colorValues.textPrimaryMedia
956
+ : colorValues.textSecondaryMedia,
957
+ },
958
+ } as const;
959
+
960
+ const textVariants = {
961
+ snap: {
962
+ pretitle: {
963
+ mobileSize: textPresets.cardPretitleSnap.size.mobile,
964
+ desktopSize: textPresets.cardPretitleSnap.size.desktop,
965
+ mobileLineHeight: textPresets.cardPretitleSnap.lineHeight.mobile,
966
+ desktopLineHeight: textPresets.cardPretitleSnap.lineHeight.desktop,
967
+ weight: 'regular',
968
+ },
969
+ title: {
970
+ mobileSize: textPresets.cardTitleSnap.size.mobile,
971
+ desktopSize: textPresets.cardTitleSnap.size.desktop,
972
+ mobileLineHeight: textPresets.cardTitleSnap.lineHeight.mobile,
973
+ desktopLineHeight: textPresets.cardTitleSnap.lineHeight.desktop,
974
+ weight: textPresets.cardTitle.weight,
975
+ },
976
+ subtitle: {
977
+ mobileSize: textPresets.cardSubtitleSnap.size.mobile,
978
+ desktopSize: textPresets.cardSubtitleSnap.size.desktop,
979
+ mobileLineHeight: textPresets.cardSubtitleSnap.lineHeight.mobile,
980
+ desktopLineHeight: textPresets.cardSubtitleSnap.lineHeight.desktop,
981
+ weight: 'regular',
982
+ },
983
+ description: {
984
+ mobileSize: textPresets.cardDescriptionSnap.size.mobile,
985
+ desktopSize: textPresets.cardDescriptionSnap.size.desktop,
986
+ mobileLineHeight: textPresets.cardDescriptionSnap.lineHeight.mobile,
987
+ desktopLineHeight: textPresets.cardDescriptionSnap.lineHeight.desktop,
988
+ weight: 'regular',
989
+ },
990
+ },
991
+ default: {
992
+ pretitle: {
993
+ mobileSize: textPresets.cardPretitleDefault.size.mobile,
994
+ desktopSize: textPresets.cardPretitleDefault.size.desktop,
995
+ mobileLineHeight: textPresets.cardPretitleDefault.lineHeight.mobile,
996
+ desktopLineHeight: textPresets.cardPretitleDefault.lineHeight.desktop,
997
+ weight: 'regular',
998
+ },
999
+ title: {
1000
+ mobileSize: textPresets.cardTitleDefault.size.mobile,
1001
+ desktopSize: textPresets.cardTitleDefault.size.desktop,
1002
+ mobileLineHeight: textPresets.cardTitleDefault.lineHeight.mobile,
1003
+ desktopLineHeight: textPresets.cardTitleDefault.lineHeight.desktop,
1004
+ weight: textPresets.cardTitle.weight,
1005
+ },
1006
+ subtitle: {
1007
+ mobileSize: textPresets.cardSubtitleDefault.size.mobile,
1008
+ desktopSize: textPresets.cardSubtitleDefault.size.desktop,
1009
+ mobileLineHeight: textPresets.cardSubtitleDefault.lineHeight.mobile,
1010
+ desktopLineHeight: textPresets.cardSubtitleDefault.lineHeight.desktop,
1011
+ weight: 'regular',
1012
+ },
1013
+ description: {
1014
+ mobileSize: textPresets.cardDescriptionDefault.size.mobile,
1015
+ desktopSize: textPresets.cardDescriptionDefault.size.desktop,
1016
+ mobileLineHeight: textPresets.cardDescriptionDefault.lineHeight.mobile,
1017
+ desktopLineHeight: textPresets.cardDescriptionDefault.lineHeight.desktop,
1018
+ weight: 'regular',
1019
+ },
1020
+ },
1021
+ display: {
1022
+ pretitle: {
1023
+ mobileSize: textPresets.text2.size.mobile,
1024
+ desktopSize: textPresets.text2.size.desktop,
1025
+ mobileLineHeight: textPresets.text2.lineHeight.mobile,
1026
+ desktopLineHeight: textPresets.text2.lineHeight.desktop,
1027
+ weight: 'regular',
1028
+ },
1029
+ title: {
1030
+ mobileSize: textPresets.text6.size.mobile,
1031
+ desktopSize: textPresets.text6.size.desktop,
1032
+ mobileLineHeight: textPresets.text6.lineHeight.mobile,
1033
+ desktopLineHeight: textPresets.text6.lineHeight.desktop,
1034
+ weight: textPresets.text6.weight,
1035
+ },
1036
+ subtitle: {
1037
+ mobileSize: textPresets.text2.size.mobile,
1038
+ desktopSize: textPresets.text2.size.desktop,
1039
+ mobileLineHeight: textPresets.text2.lineHeight.mobile,
1040
+ desktopLineHeight: textPresets.text2.lineHeight.desktop,
1041
+ weight: 'regular',
1042
+ },
1043
+ description: {
1044
+ mobileSize: textPresets.text3.size.mobile,
1045
+ desktopSize: textPresets.text3.size.desktop,
1046
+ mobileLineHeight: textPresets.text3.lineHeight.mobile,
1047
+ desktopLineHeight: textPresets.text3.lineHeight.desktop,
1048
+ weight: 'regular',
1049
+ },
1050
+ },
1051
+ } as const;
1052
+
1053
+ const colors =
1054
+ colorVariants[variant as keyof typeof colorVariants] ||
1055
+ colorVariants[externalVariant as keyof typeof colorVariants] ||
1056
+ colorVariants.default;
1057
+ const textVariant = textVariants[size] || textVariants.default;
1058
+ const textShadowStyle = withTextShadow ? '0 0 15px rgba(0, 0, 0, 0.4)' : undefined;
1059
+
1060
+ const touchableTarget = touchableProps
1061
+ ? (() => {
1062
+ if (title?.text && pretitle?.text) {
1063
+ return isBiggerHeading(titleAs, pretitleAs) ? 'title' : 'pretitle';
1064
+ }
1065
+ if (title?.text) return 'title';
1066
+ if (pretitle?.text) return 'pretitle';
1067
+ if (headline) return 'headline';
1068
+ if (subtitle) return 'subtitle';
1069
+ if (description) return 'description';
1070
+ return null;
1071
+ })()
1072
+ : null;
1073
+
1074
+ const renderText = (
1075
+ children: React.ReactNode,
1076
+ containerProps: Record<string, unknown>,
1077
+ isTouchableTarget: boolean
1078
+ ) => {
1079
+ if (!children) {
1080
+ return null;
1081
+ }
1082
+ if (!isTouchableTarget) {
1083
+ return <div {...containerProps}>{children}</div>;
1084
+ }
1085
+
1086
+ return (
1087
+ <div {...containerProps}>
1088
+ <BaseTouchable
1089
+ maybe
1090
+ {...touchableProps}
1091
+ ref={touchableRef}
1092
+ className={classnames(styles.touchable, styles.stretchedLink)}
1093
+ >
1094
+ {children}
1095
+ </BaseTouchable>
1096
+ </div>
1097
+ );
1098
+ };
1099
+
1100
+ const headlineElement = renderText(
1101
+ headline && typeof headline === 'string' ? <Tag type="promo">{headline}</Tag> : headline,
1102
+ // Read order 2. Visual order 1
1103
+ {style: {order: 1, paddingBottom: 8}, 'data-testid': 'headline', ref: headlineRef},
1104
+ touchableTarget === 'headline'
1105
+ );
1106
+
1107
+ const pretitleElement = renderText(
1108
+ pretitle?.text && (
1109
+ <Text
1110
+ {...commonProps}
1111
+ {...textVariant.pretitle}
1112
+ as={pretitleAs || 'p'}
1113
+ truncate={pretitleLinesMax}
1114
+ color={colors.pretitle}
1115
+ textShadow={textShadowStyle}
1116
+ aria-label={pretitle?.['aria-label']}
1117
+ >
1118
+ {pretitle.text}
1119
+ </Text>
1120
+ ),
1121
+ {style: {paddingBottom: 4, order: 2}, 'data-testid': 'pretitle'},
1122
+ touchableTarget === 'pretitle'
1123
+ );
1124
+
1125
+ const titleElement = renderText(
1126
+ title?.text && (
1127
+ <Text
1128
+ {...commonProps}
1129
+ {...textVariant.title}
1130
+ as={titleAs}
1131
+ truncate={titleLinesMax}
1132
+ color={colors.title}
1133
+ textShadow={textShadowStyle}
1134
+ aria-label={title?.['aria-label']}
1135
+ >
1136
+ {title.text}
1137
+ </Text>
1138
+ ),
1139
+ // Read order: 1 or 3. Visual order 3
1140
+ {style: {paddingBottom: 4, order: 3}, 'data-testid': 'title'},
1141
+ touchableTarget === 'title'
1142
+ );
1143
+
1144
+ const subtitleElement = renderText(
1145
+ subtitle && (
1146
+ <Text
1147
+ {...commonProps}
1148
+ {...textVariant.subtitle}
1149
+ as="p"
1150
+ truncate={subtitleLinesMax}
1151
+ color={colors.subtitle}
1152
+ textShadow={textShadowStyle}
1153
+ >
1154
+ {subtitle}
1155
+ </Text>
1156
+ ),
1157
+ // Read order: 4. Visual order 4
1158
+ {style: {paddingBottom: 0, order: 4}, 'data-testid': 'subtitle'},
1159
+ touchableTarget === 'subtitle'
1160
+ );
1161
+
1162
+ const descriptionElement = renderText(
1163
+ description && (
1164
+ <Text
1165
+ {...commonProps}
1166
+ {...textVariant.description}
1167
+ as="p"
1168
+ truncate={descriptionLinesMax}
1169
+ color={colors.description}
1170
+ textShadow={textShadowStyle}
1171
+ >
1172
+ {description}
1173
+ </Text>
1174
+ ),
1175
+ // Read order: 5. Visual order 5
1176
+ {style: {paddingTop: 4, order: 5}, 'data-testid': 'description'},
1177
+ touchableTarget === 'description'
1178
+ );
1179
+
1180
+ const [title1, title2] =
1181
+ title && isBiggerHeading(titleAs, pretitleAs)
1182
+ ? [titleElement, pretitleElement]
1183
+ : [pretitleElement, titleElement];
1184
+
1185
+ return (
1186
+ <div style={{display: 'flex', flexDirection: 'column'}}>
1187
+ {title1}
1188
+ {headlineElement}
1189
+ {title2}
1190
+ {subtitleElement}
1191
+ {descriptionElement}
1192
+ </div>
1193
+ );
1194
+ };
1195
+
1196
+ type CardTouchableProps = {
1197
+ children: React.ReactNode;
1198
+ isTouchable: boolean;
1199
+ touchableAriaLabel?: string;
1200
+ ariaLabeledByProp?: string;
1201
+ ariaDescriptionProp?: string;
1202
+ ariaDescribedByProp?: string;
1203
+ touchableProps: TouchableProps;
1204
+ segregateTouchableContent: boolean;
1205
+ hasTouchableInContent: boolean;
1206
+ overlayClassname: string;
1207
+ contentStyle: React.CSSProperties;
1208
+ contentRadiusStyle: React.CSSProperties;
1209
+ };
1210
+
1211
+ const CardTouchable = ({
1212
+ children,
1213
+ isTouchable,
1214
+ touchableAriaLabel,
1215
+ ariaLabeledByProp,
1216
+ ariaDescriptionProp,
1217
+ ariaDescribedByProp,
1218
+ touchableProps,
1219
+ segregateTouchableContent,
1220
+ hasTouchableInContent,
1221
+ overlayClassname,
1222
+ contentStyle,
1223
+ contentRadiusStyle,
1224
+ }: CardTouchableProps) => {
1225
+ const isOverlayInsideContent = isTouchable && (!segregateTouchableContent || hasTouchableInContent);
1226
+
1227
+ const content = (
1228
+ /**
1229
+ * role="text" makes VoiceOver read the whole div as a single text block. This is needed
1230
+ * for VoiceOver rectangle to cover the whole card when using aria-label in <a> elements,
1231
+ * otherwise it only renders a small rectangle in the begining of the <a> element.
1232
+ * This workaround is only needed for <a> not for <button> (ask safari developers why)
1233
+ */
1234
+ <div
1235
+ role={
1236
+ !segregateTouchableContent && (touchableProps.href || touchableProps.to) ? 'text' : undefined
1237
+ }
1238
+ className={styles.touchableContent}
1239
+ style={{...contentStyle, ...contentRadiusStyle}}
1240
+ >
1241
+ {isOverlayInsideContent && <div className={overlayClassname} />}
1242
+ {children}
1243
+ </div>
1244
+ );
1245
+
1246
+ if (isTouchable && segregateTouchableContent) {
1247
+ return hasTouchableInContent ? (
1248
+ <div className={classnames(styles.touchable, styles.touchableContainer)}>{content}</div>
1249
+ ) : (
1250
+ <div style={{position: 'relative'}}>
1251
+ <BaseTouchable
1252
+ aria-label={touchableAriaLabel}
1253
+ maybe
1254
+ {...touchableProps}
1255
+ className={classnames(
1256
+ styles.touchable,
1257
+ styles.touchableContainer,
1258
+ styles.stretchedTouchable
1259
+ )}
1260
+ >
1261
+ <div className={classnames(overlayClassname)} style={contentRadiusStyle} />
1262
+ </BaseTouchable>
1263
+ {content}
1264
+ </div>
1265
+ );
1266
+ }
1267
+
1268
+ return (
1269
+ <BaseTouchable
1270
+ maybe
1271
+ aria-label={isTouchable ? touchableAriaLabel : undefined}
1272
+ aria-labelledby={isTouchable ? ariaLabeledByProp : undefined}
1273
+ aria-description={isTouchable ? ariaDescriptionProp : undefined}
1274
+ aria-describedby={isTouchable ? ariaDescribedByProp : undefined}
1275
+ className={classnames(styles.touchable, styles.touchableContainer)}
1276
+ {...touchableProps}
1277
+ >
1278
+ {content}
1279
+ </BaseTouchable>
1280
+ );
1281
+ };
1282
+
1283
+ const SKIN_OVERLAY_COLORS = [
1284
+ skinVars.colors.cardContentOverlay as string,
1285
+ skinVars.colors.cardFooterOverlay as string,
1286
+ ];
1287
+
1288
+ const RGBA_REGEX = /rgba\([^,]+,\s*[^,]+,\s*[^,]+,\s*([^)]+)\)/g;
1289
+
1290
+ const replaceRgbaWithColor = (stringWithRgbaColors: string, newColor: string): string => {
1291
+ return stringWithRgbaColors.replace(RGBA_REGEX, (_, alpha) => applyAlpha(newColor, parseFloat(alpha)));
1292
+ };
1293
+
1294
+ export const InternalCard = React.forwardRef<HTMLDivElement, MaybeTouchableCard<CardProps>>(
1295
+ (
1296
+ {
1297
+ type,
1298
+ size,
1299
+ backgroundColor: backgroundColorProp,
1300
+ imageSrc,
1301
+ imageSrcSet,
1302
+ imageAlt = '',
1303
+ imageFit = 'fill-center',
1304
+ videoSrc,
1305
+ videoRef,
1306
+ media,
1307
+ mediaAspectRatio: mediaAspectRatioProp = 'auto',
1308
+ mediaPosition: mediaPositionProp = 'top',
1309
+ mediaWidth = 150,
1310
+ circledImage,
1311
+ asset,
1312
+ headline,
1313
+ title,
1314
+ titleAs = 'h3',
1315
+ titleLinesMax,
1316
+ pretitle,
1317
+ pretitleAs,
1318
+ pretitleLinesMax,
1319
+ subtitle,
1320
+ subtitleLinesMax,
1321
+ description,
1322
+ descriptionLinesMax,
1323
+ dataAttributes,
1324
+ variant: variantProp,
1325
+ width,
1326
+ height,
1327
+ aspectRatio,
1328
+ slot,
1329
+ slotAlignment = 'content',
1330
+ buttonPrimary,
1331
+ buttonSecondary,
1332
+ buttonLink,
1333
+ showFooter: showFooterProp,
1334
+ footerBackgroundColor,
1335
+ footerVariant,
1336
+ footerSlot,
1337
+ footerDivider,
1338
+ topActions,
1339
+ onClose,
1340
+ closeButtonLabel,
1341
+ 'aria-label': ariaLabelProp,
1342
+ 'aria-labelledby': ariaLabeledByProp,
1343
+ 'aria-description': ariaDescriptionProp,
1344
+ 'aria-describedby': ariaDescribedByProp,
1345
+ gradientOverlayColor,
1346
+ videoLoop,
1347
+ videoAutoPlay,
1348
+ videoDataAttributes,
1349
+ segregateTouchableContent = false,
1350
+ touchableAriaLabel: touchableAriaLabelProp,
1351
+ ...touchableProps
1352
+ },
1353
+ ref
1354
+ ): JSX.Element => {
1355
+ const {text: slotText, ref: slotRef} = useInnerText();
1356
+ const {text: headlineText, ref: headlineRef} = useInnerText();
1357
+ const touchableContentRef = React.useRef<TouchableElement>(null);
1358
+ const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress);
1359
+ const hasTouchableInContent = !!(
1360
+ segregateTouchableContent &&
1361
+ !touchableAriaLabelProp &&
1362
+ (title || pretitle || headline || subtitle || description)
1363
+ );
1364
+ const hasButtons = !!(buttonPrimary || buttonSecondary || buttonLink);
1365
+ const {colorValues} = useTheme();
1366
+
1367
+ // In this context "media" refers to the image or video that is placed inside the card, not the background
1368
+ const typeAllowsMedia = type === 'media' || type === 'naked';
1369
+ const hasMediaImage = typeAllowsMedia && (imageSrc !== undefined || imageSrcSet !== undefined);
1370
+ const hasMediaVideo = typeAllowsMedia && videoSrc !== undefined;
1371
+ const hasMediaSources = hasMediaImage || hasMediaVideo;
1372
+ const hasDeprecatedMedia = typeAllowsMedia && !!media && !hasMediaSources;
1373
+ const hasMedia = hasMediaSources || hasDeprecatedMedia;
1374
+ const isNaked = type === 'naked';
1375
+
1376
+ // If no media is provided, we use the "top" media position to simplify logic
1377
+ const mediaPosition = hasMedia ? mediaPositionProp : 'top';
1378
+ // Override mediaAspectRatio for naked cards with circled image
1379
+ const mediaAspectRatio = type === 'naked' && circledImage ? 1 : mediaAspectRatioProp;
1380
+
1381
+ // We consider any string (including empty string) as an image/video source. If not valid a fallback image will be used.
1382
+ const hasBackgroundImage = type === 'cover' && (imageSrc !== undefined || imageSrcSet !== undefined);
1383
+ const hasBackgroundVideo = type === 'cover' && videoSrc !== undefined;
1384
+ const hasCustomBackground = !!backgroundColorProp || hasBackgroundImage || hasBackgroundVideo;
1385
+ const hasBackgroundImageOrVideo = hasBackgroundImage || hasBackgroundVideo;
1386
+
1387
+ const shouldShowVideo = hasMediaVideo || hasBackgroundVideo;
1388
+ const {video, videoAction} = useVideoWithControls({
1389
+ src: shouldShowVideo ? videoSrc : undefined,
1390
+ poster: imageSrc,
1391
+ ref: videoRef,
1392
+ autoHeight:
1393
+ type === 'cover' || mediaPosition !== 'top'
1394
+ ? false
1395
+ : aspectRatioToNumber(mediaAspectRatio) === 0,
1396
+ loop: videoLoop,
1397
+ autoPlay: videoAutoPlay,
1398
+ dataAttributes: videoDataAttributes,
1399
+ });
1400
+
1401
+ const externalVariant = useThemeVariant();
1402
+ const backgroundVariant = variantProp ? normalizeVariant(variantProp) : externalVariant;
1403
+ const variant =
1404
+ (variantProp && normalizeVariant(variantProp)) ||
1405
+ (type === 'cover' && hasCustomBackground ? 'media' : 'default');
1406
+
1407
+ const overlayStyle =
1408
+ variant === 'brand' ? styles.touchableCardOverlayInverse : styles.touchableCardOverlay;
1409
+
1410
+ // If the card has actions and an onClose handler, the footer will always be shown
1411
+ // If the footer has no content, it will not be shown
1412
+ const shouldShowFooter =
1413
+ (showFooterProp && (hasButtons || !!footerSlot)) || (hasButtons && isTouchable);
1414
+
1415
+ const showButtonsInBody = !shouldShowFooter && hasButtons;
1416
+
1417
+ const topActionsLengthWithoutVideo = (topActions?.length || 0) + (onClose ? 1 : 0);
1418
+ const topActionsLength = videoAction
1419
+ ? topActionsLengthWithoutVideo + 1
1420
+ : topActionsLengthWithoutVideo;
1421
+
1422
+ const hasAssetInContent = asset && !(hasMedia && mediaPosition === 'left');
1423
+ const shouldAddContentSpacingForTopActions =
1424
+ type !== 'cover' && topActionsLength > 0 && !hasAssetInContent && !headline;
1425
+
1426
+ // See asset spacing config in spec
1427
+ const isAssetConfigA = type === 'cover' || (type === 'data' && size === 'display');
1428
+
1429
+ const borderRadius = skinVars.borderRadii.container;
1430
+
1431
+ const backgroundColor =
1432
+ hasBackgroundImage || hasBackgroundVideo
1433
+ ? 'transparent'
1434
+ : backgroundColorProp ||
1435
+ (variant === 'media'
1436
+ ? externalVariant === 'brand' ||
1437
+ externalVariant === 'media' ||
1438
+ externalVariant === 'negative'
1439
+ ? skinVars.colors.backgroundContainerBrandOverBrand
1440
+ : skinVars.colors.backgroundBrand
1441
+ : variant === 'alternative'
1442
+ ? skinVars.colors.backgroundAlternative
1443
+ : undefined);
1444
+
1445
+ const touchableAriaLabel =
1446
+ touchableAriaLabelProp ||
1447
+ ariaLabelProp ||
1448
+ (ariaLabeledByProp
1449
+ ? undefined
1450
+ : (title && isBiggerHeading(titleAs, pretitleAs)
1451
+ ? [title, imageAlt, headlineText, pretitle, subtitle, description, slotText]
1452
+ : [pretitle, headlineText, title, imageAlt, subtitle, description, slotText]
1453
+ )
1454
+ .filter(Boolean)
1455
+ .join(' '));
1456
+
1457
+ const calcOverlayBackgrounds = () => {
1458
+ if (gradientOverlayColor === 'transparent') {
1459
+ return ['transparent', 'transparent'];
1460
+ }
1461
+ if (gradientOverlayColor) {
1462
+ return [
1463
+ replaceRgbaWithColor(colorValues.cardContentOverlay, gradientOverlayColor),
1464
+ replaceRgbaWithColor(colorValues.cardFooterOverlay, gradientOverlayColor),
1465
+ ];
1466
+ } else {
1467
+ return SKIN_OVERLAY_COLORS;
1468
+ }
1469
+ };
1470
+
1471
+ const [contentOverlayBackground, footerOverlayBackground] = calcOverlayBackgrounds();
1472
+
1473
+ return (
1474
+ <Container
1475
+ aria-label={isTouchable ? undefined : ariaLabelProp}
1476
+ aria-labelledby={isTouchable ? undefined : ariaLabeledByProp}
1477
+ aria-description={isTouchable ? undefined : ariaDescriptionProp}
1478
+ aria-describedby={isTouchable ? undefined : ariaDescribedByProp}
1479
+ type={type}
1480
+ size={size}
1481
+ dataAttributes={dataAttributes}
1482
+ ref={ref}
1483
+ variant={variant}
1484
+ width={width}
1485
+ height={height}
1486
+ aspectRatio={aspectRatio}
1487
+ backgroundColor={backgroundColor}
1488
+ >
1489
+ {hasBackgroundImageOrVideo && (
1490
+ <BackgroundImageOrVideo
1491
+ video={video}
1492
+ src={imageSrc}
1493
+ srcSet={imageSrcSet}
1494
+ backgroundVariant={backgroundVariant}
1495
+ />
1496
+ )}
1497
+
1498
+ {videoAction && (
1499
+ // The video action is placed first in the card reading order, so it is placed outside the other topActions container
1500
+ <div data-testid="videoAction">
1501
+ <TopActions
1502
+ testid="videoAction"
1503
+ variant="media"
1504
+ actions={[videoAction]}
1505
+ containerStyles={{
1506
+ position: 'absolute',
1507
+ top: 16,
1508
+ left:
1509
+ mediaPosition === 'left'
1510
+ ? typeof mediaWidth === 'number'
1511
+ ? `calc(${mediaWidth}px - 48px)`
1512
+ : `calc(${mediaWidth} - 48px)`
1513
+ : 'auto',
1514
+ right:
1515
+ mediaPosition !== 'left'
1516
+ ? topActionsLengthWithoutVideo * 48 + 16
1517
+ : 'auto',
1518
+ }}
1519
+ />
1520
+ </div>
1521
+ )}
1522
+
1523
+ <CardTouchable
1524
+ isTouchable={isTouchable}
1525
+ touchableAriaLabel={touchableAriaLabel}
1526
+ ariaLabeledByProp={ariaLabeledByProp}
1527
+ ariaDescriptionProp={ariaDescriptionProp}
1528
+ ariaDescribedByProp={ariaDescribedByProp}
1529
+ touchableProps={touchableProps}
1530
+ segregateTouchableContent={segregateTouchableContent}
1531
+ hasTouchableInContent={hasTouchableInContent}
1532
+ overlayClassname={overlayStyle}
1533
+ contentStyle={{
1534
+ flexDirection:
1535
+ mediaPosition === 'top'
1536
+ ? 'column'
1537
+ : mediaPosition === 'left'
1538
+ ? 'row'
1539
+ : 'row-reverse',
1540
+ }}
1541
+ contentRadiusStyle={{
1542
+ borderTopLeftRadius: isNaked && !hasMedia ? 0 : `calc(${borderRadius} - 1px)`,
1543
+ borderTopRightRadius: isNaked && !hasMedia ? 0 : `calc(${borderRadius} - 1px)`,
1544
+ borderBottomLeftRadius:
1545
+ shouldShowFooter || isNaked ? 0 : `calc(${borderRadius} - 1px)`,
1546
+ borderBottomRightRadius:
1547
+ shouldShowFooter || isNaked ? 0 : `calc(${borderRadius} - 1px)`,
1548
+ }}
1549
+ >
1550
+ {hasDeprecatedMedia && (
1551
+ <div
1552
+ style={{
1553
+ // for some reason, this width is required to pass headless screenshot tests
1554
+ // otherwise, it gets 0px width and the media is not visible
1555
+ width: '100%',
1556
+ ...(type === 'naked'
1557
+ ? undefined
1558
+ : applyCssVars({[mediaStyles.vars.mediaBorderRadius]: '0px'})),
1559
+ }}
1560
+ >
1561
+ {media}
1562
+ </div>
1563
+ )}
1564
+ {hasDeprecatedMedia && <Asset absolute size={size} asset={asset} type={type} />}
1565
+
1566
+ {hasMedia && (
1567
+ <Media
1568
+ type={type}
1569
+ size={size}
1570
+ mediaAspectRatio={mediaAspectRatio}
1571
+ mediaPosition={mediaPosition}
1572
+ asset={asset}
1573
+ video={video}
1574
+ imageFit={imageFit}
1575
+ imageSrc={imageSrc}
1576
+ imageSrcSet={imageSrcSet}
1577
+ imageAlt={imageAlt}
1578
+ mediaWidth={mediaWidth}
1579
+ circledImage={circledImage}
1580
+ />
1581
+ )}
1582
+ <div
1583
+ aria-hidden={isTouchable && !segregateTouchableContent}
1584
+ data-testid="body"
1585
+ className={classnames(styles.touchable, {
1586
+ [styles.containerPaddingTopVariants[size]]:
1587
+ !!asset && type !== 'naked' && (!hasMedia || mediaPosition === 'right'),
1588
+ })}
1589
+ >
1590
+ {(!hasMedia || mediaPosition === 'right') && (
1591
+ <Asset size={size} asset={asset} type={type} />
1592
+ )}
1593
+ {isAssetConfigA && (
1594
+ <Filler
1595
+ minHeight={type === 'cover' && topActionsLength > 0 && !asset ? 48 + 8 : 0}
1596
+ />
1597
+ )}
1598
+ <div
1599
+ className={classnames(
1600
+ styles.containerPaddingXVariants[size],
1601
+ styles.containerPaddingBottomVariants[size],
1602
+ styles.containerPaddingTopVariants[size]
1603
+ )}
1604
+ style={{
1605
+ display: 'flex',
1606
+ flexDirection: 'column',
1607
+ height: isAssetConfigA ? undefined : '100%',
1608
+ background: hasBackgroundImageOrVideo ? contentOverlayBackground : undefined,
1609
+ // padding overrides for specific cases
1610
+ paddingTop:
1611
+ isAssetConfigA && hasBackgroundImageOrVideo
1612
+ ? 40
1613
+ : asset
1614
+ ? 16
1615
+ : isNaked && mediaPosition !== 'top'
1616
+ ? 16
1617
+ : isNaked && !hasMedia
1618
+ ? 0
1619
+ : undefined,
1620
+ paddingLeft:
1621
+ isNaked && (mediaPosition !== 'left' || !hasMedia) ? 0 : undefined,
1622
+ paddingRight: isNaked && mediaPosition !== 'right' ? 16 : undefined,
1623
+ paddingBottom: shouldShowFooter ? 16 : isNaked ? 0 : undefined,
1624
+ borderBottomLeftRadius: shouldShowFooter ? 0 : borderRadius,
1625
+ borderBottomRightRadius: shouldShowFooter ? 0 : borderRadius,
1626
+ }}
1627
+ >
1628
+ <div className={styles.contentContainer}>
1629
+ <div className={styles.textContent}>
1630
+ <TextContent
1631
+ type={type}
1632
+ hasCustomBackground={hasCustomBackground}
1633
+ headlineRef={headlineRef}
1634
+ variant={variant}
1635
+ size={size}
1636
+ headline={headline}
1637
+ pretitle={pretitle}
1638
+ pretitleAs={pretitleAs}
1639
+ pretitleLinesMax={pretitleLinesMax}
1640
+ title={title}
1641
+ titleAs={titleAs}
1642
+ titleLinesMax={titleLinesMax}
1643
+ subtitle={subtitle}
1644
+ subtitleLinesMax={subtitleLinesMax}
1645
+ description={description}
1646
+ descriptionLinesMax={descriptionLinesMax}
1647
+ withTextShadow={hasBackgroundImageOrVideo}
1648
+ {...(hasTouchableInContent
1649
+ ? {touchableRef: touchableContentRef, touchableProps}
1650
+ : {})}
1651
+ />
1652
+ </div>
1653
+ {shouldAddContentSpacingForTopActions && (
1654
+ <div
1655
+ style={{
1656
+ flexShrink: 0,
1657
+ flexGrow: 0,
1658
+ width:
1659
+ topActionsLengthWithoutVideo * 48 -
1660
+ // required space depends on the card padding
1661
+ (type === 'naked' ? 0 : size === 'display' ? 24 : 16) -
1662
+ //
1663
+ 8,
1664
+ }}
1665
+ />
1666
+ )}
1667
+ </div>
1668
+ {!isAssetConfigA && slotAlignment === 'bottom' && <Filler />}
1669
+ {slot && (
1670
+ <div
1671
+ ref={slotRef}
1672
+ data-testid="slot"
1673
+ className={classnames(
1674
+ slotAlignment === 'space-between' && styles.slotContainerSpaceBetween
1675
+ )}
1676
+ >
1677
+ {slot}
1678
+ </div>
1679
+ )}
1680
+ {!isAssetConfigA && slotAlignment === 'content' && showButtonsInBody && (
1681
+ <Filler />
1682
+ )}
1683
+ {showButtonsInBody && (
1684
+ <Buttons
1685
+ size={size}
1686
+ buttonPrimary={buttonPrimary}
1687
+ buttonSecondary={buttonSecondary}
1688
+ buttonLink={buttonLink}
1689
+ />
1690
+ )}
1691
+ </div>
1692
+ </div>
1693
+ </CardTouchable>
1694
+ {shouldShowFooter && (
1695
+ <Footer
1696
+ type={type}
1697
+ variant={variant}
1698
+ footerVariant={footerVariant}
1699
+ footerBackgroundColor={footerBackgroundColor}
1700
+ footerDivider={footerDivider}
1701
+ size={size}
1702
+ footerSlot={footerSlot}
1703
+ buttonPrimary={buttonPrimary}
1704
+ buttonSecondary={buttonSecondary}
1705
+ buttonLink={buttonLink}
1706
+ hasBackgroundImageOrVideo={hasBackgroundImageOrVideo}
1707
+ externalVariant={externalVariant}
1708
+ overlayColor={footerOverlayBackground}
1709
+ />
1710
+ )}
1711
+ <TopActions
1712
+ onClose={onClose}
1713
+ closeButtonLabel={closeButtonLabel}
1714
+ actions={topActions}
1715
+ variant={
1716
+ hasBackgroundImageOrVideo || (hasMedia && mediaPosition !== 'left')
1717
+ ? 'media'
1718
+ : variant
1719
+ }
1720
+ />
1721
+ </Container>
1722
+ );
1723
+ }
1724
+ );