@financial-times/cp-content-pipeline-ui 6.15.6-beta.0 → 7.0.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 (137) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/components/BigNumber/index.d.ts +4 -1
  3. package/lib/components/BigNumber/index.js +1 -1
  4. package/lib/components/BigNumber/index.js.map +1 -1
  5. package/lib/components/Body/index.test.js +12 -1
  6. package/lib/components/Body/index.test.js.map +1 -1
  7. package/lib/components/Byline/index.js +2 -2
  8. package/lib/components/Byline/index.js.map +1 -1
  9. package/lib/components/Clip/components/ClipTag.d.ts +3 -3
  10. package/lib/components/Clip/components/ClipTag.js.map +1 -1
  11. package/lib/components/Clip/components/ClosedCaptions.d.ts +2 -2
  12. package/lib/components/Clip/components/VideoDescription.d.ts +2 -2
  13. package/lib/components/Clip/components/VideoDescription.js.map +1 -1
  14. package/lib/components/Clip/template/component.d.ts +9 -4
  15. package/lib/components/Clip/template/component.js +19 -14
  16. package/lib/components/Clip/template/component.js.map +1 -1
  17. package/lib/components/Clip/test/fixtures.js +114 -77
  18. package/lib/components/Clip/test/fixtures.js.map +1 -1
  19. package/lib/components/Clip/test/snapshot.spec.js +110 -61
  20. package/lib/components/Clip/test/snapshot.spec.js.map +1 -1
  21. package/lib/components/Flourish/index.d.ts +4 -12
  22. package/lib/components/Flourish/index.js +1 -1
  23. package/lib/components/Flourish/index.js.map +1 -1
  24. package/lib/components/Flourish/test/snapshot.spec.js +48 -6
  25. package/lib/components/Flourish/test/snapshot.spec.js.map +1 -1
  26. package/lib/components/Heading/index.d.ts +2 -1
  27. package/lib/components/Heading/index.js +2 -2
  28. package/lib/components/Heading/index.js.map +1 -1
  29. package/lib/components/ImageOverrideWrappers/index.d.ts +4 -0
  30. package/lib/components/ImageOverrideWrappers/index.js +44 -0
  31. package/lib/components/ImageOverrideWrappers/index.js.map +1 -0
  32. package/lib/components/ImageSet/index.d.ts +12 -8
  33. package/lib/components/ImageSet/index.js +13 -8
  34. package/lib/components/ImageSet/index.js.map +1 -1
  35. package/lib/components/Layout/index.d.ts +3 -13
  36. package/lib/components/Layout/index.js +1 -1
  37. package/lib/components/Layout/index.js.map +1 -1
  38. package/lib/components/MainImage/index.js +2 -2
  39. package/lib/components/MainImage/index.js.map +1 -1
  40. package/lib/components/Paragraph/index.d.ts +4 -1
  41. package/lib/components/Paragraph/index.js.map +1 -1
  42. package/lib/components/Pullquote/index.d.ts +4 -1
  43. package/lib/components/Pullquote/index.js +7 -6
  44. package/lib/components/Pullquote/index.js.map +1 -1
  45. package/lib/components/Recommended/index.d.ts +5 -8
  46. package/lib/components/Recommended/index.js +1 -6
  47. package/lib/components/Recommended/index.js.map +1 -1
  48. package/lib/components/RichText/BasicComponents.d.ts +12 -11
  49. package/lib/components/RichText/BasicComponents.js +2 -2
  50. package/lib/components/RichText/BasicComponents.js.map +1 -1
  51. package/lib/components/RichText/index.d.ts +10 -3
  52. package/lib/components/RichText/index.js +8 -2
  53. package/lib/components/RichText/index.js.map +1 -1
  54. package/lib/components/Scrollytelling/ScrollyImage.d.ts +6 -3
  55. package/lib/components/Scrollytelling/ScrollyImage.js +2 -2
  56. package/lib/components/Scrollytelling/ScrollyImage.js.map +1 -1
  57. package/lib/components/Scrollytelling/index.d.ts +14 -6
  58. package/lib/components/Scrollytelling/index.js +3 -3
  59. package/lib/components/Scrollytelling/index.js.map +1 -1
  60. package/lib/components/Table/TableBody.d.ts +4 -1
  61. package/lib/components/Table/TableBody.js +1 -1
  62. package/lib/components/Table/TableBody.js.map +1 -1
  63. package/lib/components/Table/TableCell.d.ts +4 -4
  64. package/lib/components/Table/TableCell.js +2 -2
  65. package/lib/components/Table/TableCell.js.map +1 -1
  66. package/lib/components/Table/index.d.ts +4 -1
  67. package/lib/components/Table/index.js +1 -1
  68. package/lib/components/Table/index.js.map +1 -1
  69. package/lib/components/Topper/Picture.d.ts +1 -1
  70. package/lib/components/Topper/Picture.js +6 -3
  71. package/lib/components/Topper/Picture.js.map +1 -1
  72. package/lib/components/Topper/index.js +7 -1
  73. package/lib/components/Topper/index.js.map +1 -1
  74. package/lib/components/Tweet/index.d.ts +4 -2
  75. package/lib/components/Tweet/index.js +1 -1
  76. package/lib/components/Tweet/index.js.map +1 -1
  77. package/lib/components/Video/index.d.ts +4 -2
  78. package/lib/components/Video/index.js +1 -1
  79. package/lib/components/Video/index.js.map +1 -1
  80. package/lib/components/Workarounds.d.ts +88 -0
  81. package/lib/components/Workarounds.js +3 -0
  82. package/lib/components/Workarounds.js.map +1 -0
  83. package/lib/components/YoutubeVideo/index.d.ts +5 -2
  84. package/lib/components/YoutubeVideo/index.js +1 -1
  85. package/lib/components/YoutubeVideo/index.js.map +1 -1
  86. package/lib/components/types.d.ts +5 -0
  87. package/lib/components/types.js +3 -0
  88. package/lib/components/types.js.map +1 -0
  89. package/lib/context.d.ts +14 -0
  90. package/lib/context.js +2 -1
  91. package/lib/context.js.map +1 -1
  92. package/lib/index.d.ts +1 -1
  93. package/lib/index.js +2 -1
  94. package/lib/index.js.map +1 -1
  95. package/lib/stories/Clip.stories.d.ts +39 -243
  96. package/lib/stories/Clip.stories.js +320 -307
  97. package/lib/stories/Clip.stories.js.map +1 -1
  98. package/package.json +2 -1
  99. package/src/components/BigNumber/index.tsx +6 -3
  100. package/src/components/Body/__snapshots__/index.test.tsx.snap +2 -1
  101. package/src/components/Body/index.test.tsx +12 -1
  102. package/src/components/Byline/index.tsx +9 -4
  103. package/src/components/Clip/components/ClipTag.tsx +3 -6
  104. package/src/components/Clip/components/ClosedCaptions.tsx +2 -2
  105. package/src/components/Clip/components/VideoDescription.tsx +2 -2
  106. package/src/components/Clip/template/component.tsx +39 -41
  107. package/src/components/Clip/test/__snapshots__/snapshot.spec.tsx.snap +4 -8
  108. package/src/components/Clip/test/fixtures.ts +127 -90
  109. package/src/components/Clip/test/snapshot.spec.tsx +123 -102
  110. package/src/components/Flourish/index.tsx +10 -21
  111. package/src/components/Flourish/test/snapshot.spec.tsx +48 -36
  112. package/src/components/Heading/index.tsx +6 -5
  113. package/src/components/ImageOverrideWrappers/index.tsx +34 -0
  114. package/src/components/ImageSet/index.tsx +75 -55
  115. package/src/components/Layout/index.tsx +6 -18
  116. package/src/components/MainImage/index.tsx +10 -8
  117. package/src/components/Paragraph/index.tsx +4 -1
  118. package/src/components/Pullquote/index.tsx +15 -8
  119. package/src/components/Recommended/index.tsx +11 -11
  120. package/src/components/RichText/BasicComponents.tsx +22 -20
  121. package/src/components/RichText/index.tsx +35 -12
  122. package/src/components/Scrollytelling/ScrollyImage.tsx +10 -7
  123. package/src/components/Scrollytelling/index.tsx +19 -9
  124. package/src/components/Table/TableBody.tsx +7 -3
  125. package/src/components/Table/TableCell.tsx +9 -7
  126. package/src/components/Table/index.tsx +12 -9
  127. package/src/components/Topper/Picture.tsx +17 -12
  128. package/src/components/Topper/index.tsx +7 -4
  129. package/src/components/Tweet/index.tsx +4 -2
  130. package/src/components/Video/index.tsx +4 -6
  131. package/src/components/Workarounds.ts +188 -0
  132. package/src/components/YoutubeVideo/index.tsx +5 -4
  133. package/src/components/types.ts +6 -0
  134. package/src/context.ts +22 -1
  135. package/src/index.ts +4 -1
  136. package/src/stories/Clip.stories.tsx +357 -341
  137. package/tsconfig.tsbuildinfo +1 -1
@@ -34,8 +34,12 @@ import {
34
34
  TableRow,
35
35
  } from './BasicComponents'
36
36
 
37
- import type { StructuredContentFragment } from '@financial-times/cp-content-pipeline-client'
37
+ import type {
38
+ StructuredContentFragment,
39
+ ReferenceMapping,
40
+ } from '@financial-times/cp-content-pipeline-client'
38
41
  import { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
42
+ import { mapNodeToReference } from '@financial-times/cp-content-pipeline-schema/lib/resolvers/content-tree/references'
39
43
  import TableBody from '../Table/TableBody'
40
44
  import { TableCell } from '../Table/TableCell'
41
45
  import {
@@ -46,17 +50,33 @@ import {
46
50
  } from '../Scrollytelling'
47
51
  import { ScrollyImage } from '../Scrollytelling/ScrollyImage'
48
52
  import YoutubeVideo from '../YoutubeVideo'
53
+ import { ContentProps } from '../types'
49
54
 
50
55
  import RichTextContext from './context'
51
56
 
52
- export type RichTextComponentMapRecord = Partial<
53
- Record<
54
- ContentTreeWorkarounds.AnyNode['type'] | 'fallback',
55
- // using any here, no easy way to make a type that works for all the potential renderers
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- JSXElementConstructor<any>
57
+ // A helper type that takes a content tree type identifier and maps it to the
58
+ // object that is returned in an accompanying reference
59
+ type mapComponentToReference<T extends ContentTreeWorkarounds.AnyNode['type']> =
60
+ T extends keyof typeof mapNodeToReference
61
+ ? (typeof mapNodeToReference)[T] extends keyof ReferenceMapping
62
+ ? ReferenceMapping[(typeof mapNodeToReference)[T]]
63
+ : never
64
+ : Record<string, never>
65
+
66
+ // Partial record of content tree nodes and their associated reference types
67
+ export type RichTextComponentMapRecord = {
68
+ [T in
69
+ | ContentTreeWorkarounds.AnyNode
70
+ | { type: 'fallback' } as T['type']]?: JSXElementConstructor<
71
+ React.PropsWithChildren<
72
+ T['type'] extends 'fallback'
73
+ ? unknown
74
+ : ContentProps<
75
+ T & mapComponentToReference<Exclude<T['type'], 'fallback'>>
76
+ >
77
+ >
58
78
  >
59
- >
79
+ }
60
80
 
61
81
  export const ComponentsContext = createContext<
62
82
  RichTextComponentMapRecord | undefined
@@ -165,16 +185,19 @@ const RichTextChild: React.FC<RichTextChildProps> = ({
165
185
  .filter(Boolean)
166
186
  : []
167
187
 
168
- const reference =
188
+ const reference:
189
+ | StructuredContentFragment['references'][number]
190
+ | Record<string, never> =
169
191
  typeof node.data?.referenceIndex === 'number'
170
- ? references[node.data?.referenceIndex]
192
+ ? references[node.data?.referenceIndex] ?? {}
171
193
  : {}
172
194
 
173
195
  return (
174
196
  <RichTextContext.Provider value={RichText}>
175
197
  <Component
176
- {...node}
177
- {...reference}
198
+ // HACK:20240808:IM TypeScript fails to unify the content types here
199
+ // and I'm not sure why :(
200
+ content={{ ...node, ...reference } as never}
178
201
  key={parentIndex}
179
202
  parentIndex={parentIndex}
180
203
  >
@@ -1,8 +1,8 @@
1
- import { ContentTree } from '@financial-times/content-tree'
2
- import { ScrollyImageFragment } from '@financial-times/cp-content-pipeline-client'
3
1
  import React, { useContext } from 'react'
4
2
  import { BreakpointGetter, FallbackImage, Sources } from '../ImageSet'
5
3
  import { ScrollyIDContext } from '.'
4
+ import { ContentProps } from '../types'
5
+ import type * as ComponentWorkarounds from '../Workarounds'
6
6
 
7
7
  const scrollyGetBreakpoints: BreakpointGetter = (image) => {
8
8
  switch (image.format) {
@@ -25,9 +25,12 @@ const scrollyGetBreakpoints: BreakpointGetter = (image) => {
25
25
  }
26
26
  }
27
27
 
28
- export const ScrollyImage: React.FC<
29
- ContentTree.ScrollyImage & ScrollyImageFragment
30
- > = ({ picture }) => {
28
+ interface ScrollyImageProps
29
+ extends ContentProps<ComponentWorkarounds.ScrollyImage> {}
30
+
31
+ export const ScrollyImage: React.FC<ScrollyImageProps> = ({
32
+ content: { picture },
33
+ }) => {
31
34
  const scrollyId = useContext(ScrollyIDContext)
32
35
 
33
36
  if (!picture?.images) {
@@ -47,7 +50,7 @@ export const ScrollyImage: React.FC<
47
50
  getBreakpoints={scrollyGetBreakpoints}
48
51
  />
49
52
  <FallbackImage
50
- image={picture.fallbackImage}
53
+ content={{ image: picture.fallbackImage }}
51
54
  imageType={picture.imageType}
52
55
  className="n-scrollytelling__image"
53
56
  inSourceSet={true}
@@ -61,4 +64,4 @@ export const ScrollyImage: React.FC<
61
64
  )}
62
65
  </figure>
63
66
  )
64
- }
67
+ }
@@ -3,14 +3,16 @@ import type { ContentTree } from '@financial-times/content-tree'
3
3
  import classnames from 'classnames'
4
4
  import type scrollytelling from '@financial-times/n-scrollytelling-image/server'
5
5
  import { ParagraphContext } from '../Paragraph'
6
+ import { ContentProps } from '../types'
6
7
 
7
8
  export const ScrollyIDContext = createContext<string | null>(null)
8
9
 
10
+ interface ScrollyBlockProps
11
+ extends ContentProps<ContentTree.transit.ScrollyBlock> {}
12
+
9
13
  export const ScrollyBlock: React.FC<
10
- React.PropsWithChildren<Omit<ContentTree.ScrollyBlock, 'children'>> & {
11
- parentIndex: number
12
- }
13
- > = ({ children, theme, parentIndex }) => (
14
+ React.PropsWithChildren<ScrollyBlockProps>
15
+ > = ({ content: { theme }, children, parentIndex }) => (
14
16
  <div
15
17
  className="n-content-layout n-content-layout--scrollytelling"
16
18
  data-layout-name="auto"
@@ -42,9 +44,12 @@ type ScrollySectionOptions = Pick<
42
44
  const ScrollySectionOptionsContext =
43
45
  createContext<ScrollySectionOptions | null>(null)
44
46
 
47
+ interface ScrollySectionProps
48
+ extends ContentProps<ContentTree.transit.ScrollySection> {}
49
+
45
50
  export const ScrollySection: React.FC<
46
- React.PropsWithChildren<ContentTree.ScrollySection>
47
- > = ({ children, display, noBox, position, transition }) => (
51
+ React.PropsWithChildren<ScrollySectionProps>
52
+ > = ({ content: { display, noBox, position, transition }, children }) => (
48
53
  <ScrollySectionOptionsContext.Provider
49
54
  value={{ display, noBox, position, transition }}
50
55
  >
@@ -55,8 +60,10 @@ export const ScrollySection: React.FC<
55
60
  type ScrollytellingDisplaySuffix =
56
61
  scrollytelling.ScrollytellingOptionsMap['display'][keyof scrollytelling.ScrollytellingOptionsMap['display']]
57
62
 
63
+ interface ScrollyCopyProps extends ContentProps<ContentTree.ScrollyCopy> {}
64
+
58
65
  export const ScrollyCopy: React.FC<
59
- React.PropsWithChildren<ContentTree.ScrollyCopy>
66
+ React.PropsWithChildren<ScrollyCopyProps>
60
67
  > = ({ children }) => {
61
68
  const { display, noBox, position, transition } = useContext(
62
69
  ScrollySectionOptionsContext
@@ -100,9 +107,12 @@ export const ScrollyCopy: React.FC<
100
107
  )
101
108
  }
102
109
 
110
+ interface ScrollyHeadingProps
111
+ extends ContentProps<ContentTree.ScrollyHeading> {}
112
+
103
113
  export const ScrollyHeading: React.FC<
104
- React.PropsWithChildren<ContentTree.ScrollyHeading>
105
- > = ({ children, level }) => {
114
+ React.PropsWithChildren<ScrollyHeadingProps>
115
+ > = ({ content: { level }, children }) => {
106
116
  const classNames = classnames(
107
117
  'n-scrollytelling__overlay-text',
108
118
  `n-scrollytelling__overlay-text--text-style-${level}`
@@ -1,9 +1,13 @@
1
1
  import { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
2
2
  import React from 'react'
3
+ import { ContentProps } from '../types'
3
4
 
4
- const TableBody: React.FC<
5
- React.PropsWithChildren<ContentTreeWorkarounds.TableBody>
6
- > = ({ children }) => {
5
+ interface TableBodyProps
6
+ extends ContentProps<ContentTreeWorkarounds.TableBody> {}
7
+
8
+ const TableBody: React.FC<React.PropsWithChildren<TableBodyProps>> = ({
9
+ children,
10
+ }) => {
7
11
  const [headerRow, ...bodyRows] = React.Children.toArray(children)
8
12
 
9
13
  return (
@@ -2,20 +2,22 @@ import React, { useContext } from 'react'
2
2
  import type { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
3
3
  import { ColumnSettingsContext } from '.'
4
4
  import classNames from 'classnames'
5
+ import { ContentProps } from '../types'
5
6
 
6
- type TableCellProps =
7
- React.PropsWithChildren<ContentTreeWorkarounds.TableCell> & {
8
- parentIndex: number
9
- }
10
- export const TableCell: React.FC<TableCellProps> = (props) => {
7
+ interface TableCellProps
8
+ extends ContentProps<ContentTreeWorkarounds.TableCell> {}
9
+
10
+ export const TableCell: React.FC<React.PropsWithChildren<TableCellProps>> = (
11
+ props
12
+ ) => {
11
13
  const allColumnSettings = useContext(ColumnSettingsContext)
12
14
  const columnSettings: ContentTreeWorkarounds.TableColumnSettings =
13
- allColumnSettings[props.parentIndex] ?? {
15
+ (props.parentIndex ? allColumnSettings[props.parentIndex] : undefined) ?? {
14
16
  hideOnMobile: false,
15
17
  sortable: false,
16
18
  }
17
19
 
18
- return props.heading ? (
20
+ return props.content.heading ? (
19
21
  <th
20
22
  className={classNames({
21
23
  'n-content-body__table-cell--hide-mobile': columnSettings.hideOnMobile,
@@ -1,21 +1,24 @@
1
1
  import type { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
2
2
  import React, { createContext } from 'react'
3
3
  import classNames from 'classnames'
4
+ import { ContentProps } from '../types'
4
5
 
5
6
  export const ColumnSettingsContext = createContext<
6
7
  ContentTreeWorkarounds.TableColumnSettings[]
7
8
  >([])
8
9
 
9
- const Table: React.FC<
10
- React.PropsWithChildren<ContentTreeWorkarounds.Table>
11
- > = ({
10
+ interface TableProps extends ContentProps<ContentTreeWorkarounds.Table> {}
11
+
12
+ const Table: React.FC<React.PropsWithChildren<TableProps>> = ({
12
13
  children,
13
- compact,
14
- layoutWidth,
15
- stripes,
16
- responsiveStyle,
17
- columnSettings,
18
- collapseAfterHowManyRows,
14
+ content: {
15
+ compact,
16
+ layoutWidth,
17
+ stripes,
18
+ responsiveStyle,
19
+ columnSettings,
20
+ collapseAfterHowManyRows,
21
+ },
19
22
  }) => {
20
23
  return (
21
24
  <ColumnSettingsContext.Provider value={columnSettings}>
@@ -3,6 +3,8 @@ import React from 'react'
3
3
  import type { TopperFragment } from '@financial-times/cp-content-pipeline-client'
4
4
 
5
5
  import { AliasedBreakpoints, BreakpointGetter, Sources } from '../ImageSet'
6
+ import { PictureOverrideWrapper } from '../ImageOverrideWrappers'
7
+ import { ContentTree } from '@financial-times/content-tree'
6
8
 
7
9
  const topperGetBreakpointsMap: Partial<
8
10
  Record<TopperFragment['__typename'], BreakpointGetter>
@@ -64,12 +66,13 @@ const topperGetBreakpointsMap: Partial<
64
66
  },
65
67
  }
66
68
 
67
- type PictureProps = {
69
+ export type PictureProps = {
68
70
  topper: TopperFragment
69
71
  alt?: string
70
72
  }
71
73
 
72
- const Picture: React.FC<PictureProps> = ({ topper, alt = '' }) => {
74
+ const Picture: React.FC<PictureProps> = (props) => {
75
+ const { topper, alt = '' } = props
73
76
  if (!('images' in topper) || !topper.fallbackImage) {
74
77
  return null
75
78
  }
@@ -77,17 +80,19 @@ const Picture: React.FC<PictureProps> = ({ topper, alt = '' }) => {
77
80
  return (
78
81
  <figure className="o-topper__visual">
79
82
  <picture className="o-topper__picture" key="responsive-images">
80
- <Sources
81
- images={topper.images}
82
- getBreakpoints={topperGetBreakpointsMap[topper.__typename]}
83
- />
83
+ <PictureOverrideWrapper {...props}>
84
+ <Sources
85
+ images={topper.images as readonly ContentTree.Image[]}
86
+ getBreakpoints={topperGetBreakpointsMap[topper.__typename]}
87
+ />
84
88
 
85
- <img
86
- alt={alt}
87
- className="o-topper__image"
88
- src={topper.fallbackImage.sourceSet[0]?.url}
89
- key="fallback-image"
90
- />
89
+ <img
90
+ alt={alt}
91
+ className="o-topper__image"
92
+ src={topper.fallbackImage.sourceSet[0]?.url}
93
+ key="fallback-image"
94
+ />
95
+ </PictureOverrideWrapper>
91
96
  </picture>
92
97
  {topper.fallbackImage.caption ||
93
98
  topper.__typename === 'DeepPortraitTopper' ||
@@ -147,10 +147,13 @@ const Topper: React.FC<TopperProps> = ({
147
147
  />
148
148
  {isLiveBlog && isFlourish ? (
149
149
  <Flourish
150
- id={topper.leadFlourish.id || ''}
151
- flourishType={topper.leadFlourish.type || ''}
152
- description={topper.leadFlourish.description || ''}
153
- fallbackImage={topper.leadFlourish.fallbackImage || ''}
150
+ content={{
151
+ type: 'flourish',
152
+ id: topper.leadFlourish.id ?? '',
153
+ flourishType: topper.leadFlourish.type ?? '',
154
+ description: topper.leadFlourish.description ?? '',
155
+ fallbackImage: topper.leadFlourish.fallbackImage,
156
+ }}
154
157
  iFrame={true}
155
158
  inArticleBody={false}
156
159
  />
@@ -1,8 +1,10 @@
1
1
  import React from 'react'
2
2
  import type { ContentTree } from '@financial-times/content-tree'
3
- import { TweetFragment } from '@financial-times/cp-content-pipeline-client'
3
+ import { ContentProps } from '../types'
4
4
 
5
- const Tweet: React.FC<ContentTree.Tweet & TweetFragment> = (tweet) => {
5
+ interface TweetProps extends ContentProps<ContentTree.Tweet> {}
6
+
7
+ const Tweet: React.FC<TweetProps> = ({ content: tweet }) => {
6
8
  if (tweet.html) {
7
9
  return (
8
10
  <div
@@ -1,12 +1,10 @@
1
1
  import React from 'react'
2
- import { VideoFragment } from '@financial-times/cp-content-pipeline-client'
2
+ import { ContentProps } from '../types'
3
3
  import { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
4
4
 
5
- const Video: React.FC<VideoFragment & ContentTreeWorkarounds.Video> = ({
6
- id,
7
- embedded,
8
- title,
9
- }) => {
5
+ interface VideoProps extends ContentProps<ContentTreeWorkarounds.Video> {}
6
+
7
+ const Video: React.FC<VideoProps> = ({ content: { id, embedded, title } }) => {
10
8
  return (
11
9
  <div
12
10
  className="n-content-video n-content-video--internal"
@@ -0,0 +1,188 @@
1
+ import type { ContentTree } from '@financial-times/content-tree'
2
+ import {
3
+ ConceptFragment,
4
+ TranscriptFragment,
5
+ } from '@financial-times/cp-content-pipeline-client'
6
+ import type { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
7
+ import type {
8
+ LiteralToPrimitiveDeep,
9
+ SetFieldType,
10
+ SetOptional,
11
+ Simplify,
12
+ } from 'type-fest'
13
+
14
+ // Analogous to the Content Tree workarounds we use in the schema package,
15
+ // these are workarounds for Content Tree types passed to components. These
16
+ // differ insofar as they also include the reference type for nodes, i.e., they
17
+ // are the ContentTree.full types to schema's ContentTree.transit types.
18
+ // Similarly, we should aim to reduce these divergences by either adding them
19
+ // to Content Tree or finding alternative ways of resolving them in our API.
20
+ // Unlike the schema's workarounds, I've opted for a more declarative, verbose
21
+ // approach to listing the differences between the workarounds and the original
22
+ // types, with the hope that the differences can be fixed more incrementally.
23
+
24
+ /**
25
+ * Helper type to extend partial/undefined fields to also be null. Optional
26
+ * fields in the Content Tree are represented by a partial field, but are
27
+ * represented by null (abstracted by the Maybe type) in the content pipeline.
28
+ */
29
+ type PartialToMaybe<T> = Simplify<{
30
+ [K in keyof T]: Extract<T[K], undefined> extends never ? T[K] : T[K] | null
31
+ }>
32
+ /**
33
+ * Helper type to apply PartialToMaybe mapper type to each object field
34
+ * recursively.
35
+ */
36
+ type PartialToMaybeDeep<T> = T extends object
37
+ ? PartialToMaybe<{
38
+ [K in keyof T]: PartialToMaybeDeep<T[K]>
39
+ }>
40
+ : T
41
+ /**
42
+ * Helper type to convert arrays to readonly arrays. Content Tree arrays are
43
+ * mutable but the arrays we return from our API are expected to be readonly. We
44
+ * should explore which assumption is correct and match that in the other
45
+ * declaration file.
46
+ */
47
+ type ReadonlyArrays<T> = {
48
+ [K in keyof T]: ReadonlyArray<T[K]>
49
+ }
50
+ // define conditional type separately so that it distributes the mapping over
51
+ // union types (e.g., to avoid removing the undefined from a union)
52
+ type ReadonlyArray<T> = T extends Array<infer Item> ? readonly Item[] : T
53
+
54
+ export type TeaserConcept = PartialToMaybe<
55
+ ReadonlyArrays<
56
+ SetOptional<
57
+ ContentTree.TeaserConcept,
58
+ 'apiUrl' | 'directType' | 'predicate' | 'prefLabel' | 'type' | 'types'
59
+ >
60
+ >
61
+ >
62
+
63
+ export type Image = PartialToMaybe<
64
+ ReadonlyArrays<
65
+ LiteralToPrimitiveDeep<
66
+ SetOptional<ContentTree.Image, 'width' | 'height' | 'sourceSet'>
67
+ >
68
+ >
69
+ >
70
+
71
+ export type Teaser = PartialToMaybeDeep<
72
+ LiteralToPrimitiveDeep<
73
+ SetOptional<
74
+ SetFieldType<
75
+ SetFieldType<
76
+ ContentTree.Teaser,
77
+ 'metaLink' | 'metaAltLink',
78
+ TeaserConcept
79
+ >,
80
+ 'image',
81
+ Omit<Image, 'id' | 'format'>
82
+ >,
83
+ 'type' | 'metaLink' | 'metaAltLink' | 'indicators' | 'image'
84
+ >
85
+ >
86
+ >
87
+
88
+ export type Recommended = PartialToMaybe<
89
+ ContentTreeWorkarounds.Recommended & {
90
+ teaser?: Teaser
91
+ }
92
+ >
93
+
94
+ export type ImageSetPicture = PartialToMaybe<
95
+ ReadonlyArrays<
96
+ SetOptional<
97
+ SetFieldType<
98
+ SetFieldType<ContentTree.ImageSetPicture, 'fallbackImage', Image>,
99
+ 'images',
100
+ Image[]
101
+ >,
102
+ 'caption' | 'credit'
103
+ >
104
+ >
105
+ >
106
+
107
+ export type ImageSet = PartialToMaybe<
108
+ SetOptional<
109
+ SetFieldType<
110
+ ContentTree.ImageSet | ContentTree.LayoutImage,
111
+ 'picture',
112
+ ImageSetPicture
113
+ >,
114
+ 'picture'
115
+ >
116
+ >
117
+
118
+ export type ScrollyImage = PartialToMaybe<
119
+ SetOptional<
120
+ SetFieldType<ContentTree.ScrollyImage, 'picture', ImageSetPicture>,
121
+ 'picture'
122
+ >
123
+ >
124
+
125
+ type ClipFormat = 'standard-inline' | 'mobile'
126
+ interface ClipSource {
127
+ audioCodec?: string
128
+ binaryUrl: string
129
+ duration?: number
130
+ mediaType: string
131
+ pixelHeight?: number
132
+ pixelWidth?: number
133
+ videoCodec?: string
134
+ }
135
+ export type Clip = PartialToMaybeDeep<
136
+ ReadonlyArrays<{
137
+ format?: ClipFormat
138
+ dataSource: ClipSource[]
139
+ poster?: string
140
+ }>
141
+ >
142
+ interface ClipCaption {
143
+ mediaType?: string
144
+ url?: string
145
+ }
146
+ type ClipTranscript = TranscriptFragment
147
+ export type ClipAccessibility = PartialToMaybeDeep<
148
+ ReadonlyArrays<{
149
+ captions?: ClipCaption[]
150
+ transcript?: ClipTranscript
151
+ }>
152
+ >
153
+ interface ClipSetReferences {
154
+ id: string
155
+ type: string
156
+ accessibility?: ClipAccessibility
157
+ noAudio?: boolean
158
+ caption?: string
159
+ credits?: string
160
+ description?: string
161
+ displayTitle?: string
162
+ contentWarning?: string[]
163
+ source?: string
164
+ subtitle?: string
165
+ publishedDate?: string
166
+ clips?: Clip[]
167
+ }
168
+
169
+ export type ClipSet = PartialToMaybeDeep<
170
+ ReadonlyArrays<
171
+ (ContentTreeWorkarounds.OldClip | ContentTreeWorkarounds.ClipSet) &
172
+ ClipSetReferences
173
+ >
174
+ >
175
+
176
+ export type RawImage = ContentTreeWorkarounds.RawImage & {
177
+ image: Image
178
+ }
179
+
180
+ export type MainImageRaw = ContentTreeWorkarounds.MainImageRaw & {
181
+ image: Image
182
+ }
183
+
184
+ export type Author = PartialToMaybe<
185
+ ContentTreeWorkarounds.Author & {
186
+ concept?: ConceptFragment
187
+ }
188
+ >
@@ -1,9 +1,10 @@
1
1
  import React from 'react'
2
- import { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema'
2
+ import { ContentTree } from '@financial-times/content-tree'
3
+ import { ContentProps } from '../types'
3
4
 
4
- const YoutubeVideo: React.FC<ContentTreeWorkarounds.YoutubeVideo> = ({
5
- url,
6
- }) => {
5
+ interface YoutubeVideoProps extends ContentProps<ContentTree.YoutubeVideo> {}
6
+
7
+ const YoutubeVideo: React.FC<YoutubeVideoProps> = ({ content: { url } }) => {
7
8
  try {
8
9
  const parsed = new URL(url)
9
10
  const id = parsed.searchParams.get('v')
@@ -0,0 +1,6 @@
1
+ import { ContentTree } from '@financial-times/content-tree'
2
+
3
+ export interface ContentProps<T extends ContentTree.Node> {
4
+ content: T
5
+ parentIndex?: number
6
+ }
package/src/context.ts CHANGED
@@ -1,4 +1,13 @@
1
- import { createContext } from 'react'
1
+ import { createContext, PropsWithChildren } from 'react'
2
+
3
+ import type { FallbackImageProps, ImageSetProps } from './components/ImageSet'
4
+ import type { PictureProps } from './components/Topper/Picture'
5
+
6
+ export type PresentationOverrideTypes = {
7
+ FallbackImage: React.FC<PropsWithChildren<FallbackImageProps>>
8
+ ImageSet: React.FC<PropsWithChildren<ImageSetProps>>
9
+ Picture: React.FC<PropsWithChildren<PictureProps>>
10
+ }
2
11
 
3
12
  // HACK:20240618:IM we're still not at the point where all components behave
4
13
  // correctly across all consumers. this context allows consumers to
@@ -9,3 +18,15 @@ export interface PresentationFlags {
9
18
  reduceFullBleedImages?: boolean
10
19
  }
11
20
  export const PresentationFlagsContext = createContext<PresentationFlags>({})
21
+
22
+ // COMPLEX:20240815:RB similarly to PresentationFlags, some consumers need
23
+ // to customise the presentation and behaviour of some components. This should be
24
+ // replaced by more transparent support in the consumer with the aim of removing
25
+ // this context.
26
+ export interface PresentationOverrides {
27
+ FallbackImage?: PresentationOverrideTypes['FallbackImage']
28
+ ImageSet?: PresentationOverrideTypes['ImageSet']
29
+ Picture?: PresentationOverrideTypes['Picture']
30
+ }
31
+ export const PresentationOverridesContext =
32
+ createContext<PresentationOverrides>({})
package/src/index.ts CHANGED
@@ -34,4 +34,7 @@ export {
34
34
  Strikethrough,
35
35
  Cite,
36
36
  } from './components/RichText/BasicComponents'
37
- export { PresentationFlagsContext } from './context'
37
+ export {
38
+ PresentationFlagsContext,
39
+ PresentationOverridesContext,
40
+ } from './context'