@deriv-com/dds-marketing 0.2.0 → 1.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 (279) hide show
  1. package/.llm/components/accordion.md +8 -0
  2. package/.llm/components/accordionitem.md +8 -0
  3. package/.llm/components/aspectratio.md +8 -0
  4. package/.llm/components/badge.md +8 -0
  5. package/.llm/components/bottomsheet.md +8 -0
  6. package/.llm/components/breadcrumb.md +8 -0
  7. package/.llm/components/breadcrumbitem.md +8 -0
  8. package/.llm/components/button.md +46 -22
  9. package/.llm/components/buttonicon.md +14 -6
  10. package/.llm/components/card.md +15 -1
  11. package/.llm/components/cardbody.md +28 -2
  12. package/.llm/components/cardbutton.md +15 -1
  13. package/.llm/components/checkbox.md +8 -0
  14. package/.llm/components/ctabanner.md +186 -0
  15. package/.llm/components/ctabannerslider.md +204 -0
  16. package/.llm/components/divider.md +8 -0
  17. package/.llm/components/dropdownmenu.md +13 -5
  18. package/.llm/components/faqaccordion.md +217 -0
  19. package/.llm/components/featurebullets.md +254 -0
  20. package/.llm/components/featurecards.md +313 -0
  21. package/.llm/components/featurescroll.md +284 -0
  22. package/.llm/components/featuresplit.md +249 -0
  23. package/.llm/components/filterchip.md +8 -0
  24. package/.llm/components/footer.md +8 -0
  25. package/.llm/components/footergroup.md +8 -0
  26. package/.llm/components/grid.md +8 -0
  27. package/.llm/components/icon.md +8 -0
  28. package/.llm/components/landinghero.md +254 -0
  29. package/.llm/components/link.md +9 -1
  30. package/.llm/components/logocolumns.md +235 -0
  31. package/.llm/components/logorows.md +212 -0
  32. package/.llm/components/menuitem.md +8 -0
  33. package/.llm/components/navbar.md +79 -15
  34. package/.llm/components/navbaritem.md +9 -1
  35. package/.llm/components/navbarmenu.md +8 -0
  36. package/.llm/components/navbarmenugroup.md +8 -0
  37. package/.llm/components/navbarpanel.md +9 -1
  38. package/.llm/components/navbarswitcher.md +9 -1
  39. package/.llm/components/navbarswitcheroption.md +8 -0
  40. package/.llm/components/option.md +8 -0
  41. package/.llm/components/pageheader.md +231 -0
  42. package/.llm/components/pagination.md +26 -89
  43. package/.llm/components/paginationdots.md +215 -0
  44. package/.llm/components/producthero.md +248 -0
  45. package/.llm/components/removablechip.md +8 -0
  46. package/.llm/components/row.md +8 -0
  47. package/.llm/components/scroller.md +8 -0
  48. package/.llm/components/searchinput.md +8 -0
  49. package/.llm/components/segmentedcontrol.md +8 -0
  50. package/.llm/components/segmentedcontrolitem.md +8 -0
  51. package/.llm/components/select.md +8 -0
  52. package/.llm/components/selectchip.md +56 -15
  53. package/.llm/components/spinner.md +8 -0
  54. package/.llm/components/stack.md +8 -0
  55. package/.llm/components/statusdot.md +8 -0
  56. package/.llm/components/subdomainnavbar.md +8 -0
  57. package/.llm/components/tab.md +8 -0
  58. package/.llm/components/table.md +8 -0
  59. package/.llm/components/tablebody.md +8 -0
  60. package/.llm/components/tablecell.md +8 -0
  61. package/.llm/components/tablehead.md +8 -0
  62. package/.llm/components/tableheadercell.md +8 -0
  63. package/.llm/components/tablerow.md +8 -0
  64. package/.llm/components/tablist.md +8 -0
  65. package/.llm/components/testimonialrow.md +235 -0
  66. package/.llm/components/testimonialslider.md +254 -0
  67. package/.llm/components/textarea.md +8 -0
  68. package/.llm/components/textinput.md +8 -0
  69. package/.llm/components/tooltip.md +8 -0
  70. package/.llm/components/typography.md +8 -0
  71. package/.llm/digest/button.md +11 -6
  72. package/.llm/digest/buttonicon.md +2 -2
  73. package/.llm/digest/card.md +1 -0
  74. package/.llm/digest/cardbody.md +3 -1
  75. package/.llm/digest/cardbutton.md +1 -0
  76. package/.llm/digest/ctabanner.md +44 -0
  77. package/.llm/digest/ctabannerslider.md +45 -0
  78. package/.llm/digest/dropdownmenu.md +1 -1
  79. package/.llm/digest/faqaccordion.md +43 -0
  80. package/.llm/digest/featurebullets.md +52 -0
  81. package/.llm/digest/featurecards.md +49 -0
  82. package/.llm/digest/featurescroll.md +52 -0
  83. package/.llm/digest/featuresplit.md +54 -0
  84. package/.llm/digest/landinghero.md +53 -0
  85. package/.llm/digest/logocolumns.md +55 -0
  86. package/.llm/digest/logorows.md +48 -0
  87. package/.llm/digest/navbar.md +18 -3
  88. package/.llm/digest/pageheader.md +49 -0
  89. package/.llm/digest/pagination.md +6 -17
  90. package/.llm/digest/paginationdots.md +43 -0
  91. package/.llm/digest/producthero.md +53 -0
  92. package/.llm/digest/selectchip.md +6 -1
  93. package/.llm/digest/testimonialrow.md +51 -0
  94. package/.llm/digest/testimonialslider.md +51 -0
  95. package/.llm/index.md +24 -3
  96. package/.llm/rules.md +24 -0
  97. package/.llm/tokens.md +56 -12
  98. package/AGENTS.md +28 -3
  99. package/README.md +41 -21
  100. package/dist/components/banner-data.d.ts +38 -0
  101. package/dist/components/banner-data.js +15 -0
  102. package/dist/components/blocks/block-reveal.css +90 -0
  103. package/dist/components/blocks/cta-banner/CtaBanner.css +66 -0
  104. package/dist/components/blocks/cta-banner/CtaBanner.d.ts +22 -0
  105. package/dist/components/blocks/cta-banner/CtaBanner.js +31 -0
  106. package/dist/components/blocks/cta-banner/CtaBanner.types.d.ts +29 -0
  107. package/dist/components/blocks/cta-banner/CtaBanner.types.js +13 -0
  108. package/dist/components/blocks/cta-banner/cta-banner-impl.css +29 -0
  109. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.css +90 -0
  110. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.d.ts +39 -0
  111. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.js +60 -0
  112. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.types.d.ts +41 -0
  113. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.types.js +15 -0
  114. package/dist/components/blocks/cta-banner-slider/cta-banner-slider-impl.css +73 -0
  115. package/dist/components/blocks/faq-accordion/FaqAccordion.css +54 -0
  116. package/dist/components/blocks/faq-accordion/FaqAccordion.d.ts +20 -0
  117. package/dist/components/blocks/faq-accordion/FaqAccordion.js +28 -0
  118. package/dist/components/blocks/faq-accordion/FaqAccordion.types.d.ts +25 -0
  119. package/dist/components/blocks/faq-accordion/FaqAccordion.types.js +10 -0
  120. package/dist/components/blocks/faq-accordion/faq-accordion-impl.css +54 -0
  121. package/dist/components/blocks/feature-bullets/FeatureBullets.css +139 -0
  122. package/dist/components/blocks/feature-bullets/FeatureBullets.d.ts +31 -0
  123. package/dist/components/blocks/feature-bullets/FeatureBullets.js +55 -0
  124. package/dist/components/blocks/feature-bullets/FeatureBullets.types.d.ts +39 -0
  125. package/dist/components/blocks/feature-bullets/FeatureBullets.types.js +16 -0
  126. package/dist/components/blocks/feature-bullets/feature-bullets-impl.css +104 -0
  127. package/dist/components/blocks/feature-cards/FeatureCards.css +84 -0
  128. package/dist/components/blocks/feature-cards/FeatureCards.d.ts +24 -0
  129. package/dist/components/blocks/feature-cards/FeatureCards.js +66 -0
  130. package/dist/components/blocks/feature-cards/FeatureCards.types.d.ts +33 -0
  131. package/dist/components/blocks/feature-cards/FeatureCards.types.js +14 -0
  132. package/dist/components/blocks/feature-cards/feature-cards-impl.css +245 -0
  133. package/dist/components/blocks/feature-scroll/FeatureScroll.css +137 -0
  134. package/dist/components/blocks/feature-scroll/FeatureScroll.d.ts +39 -0
  135. package/dist/components/blocks/feature-scroll/FeatureScroll.js +167 -0
  136. package/dist/components/blocks/feature-scroll/FeatureScroll.types.d.ts +40 -0
  137. package/dist/components/blocks/feature-scroll/FeatureScroll.types.js +17 -0
  138. package/dist/components/blocks/feature-scroll/feature-scroll-impl.css +292 -0
  139. package/dist/components/blocks/feature-split/FeatureSplit.css +95 -0
  140. package/dist/components/blocks/feature-split/FeatureSplit.d.ts +30 -0
  141. package/dist/components/blocks/feature-split/FeatureSplit.js +38 -0
  142. package/dist/components/blocks/feature-split/FeatureSplit.types.d.ts +39 -0
  143. package/dist/components/blocks/feature-split/FeatureSplit.types.js +14 -0
  144. package/dist/components/blocks/feature-split/feature-split-impl.css +83 -0
  145. package/dist/components/blocks/landing-hero/LandingHero.css +85 -0
  146. package/dist/components/blocks/landing-hero/LandingHero.d.ts +41 -0
  147. package/dist/components/blocks/landing-hero/LandingHero.js +54 -0
  148. package/dist/components/blocks/landing-hero/LandingHero.types.d.ts +37 -0
  149. package/dist/components/blocks/landing-hero/LandingHero.types.js +15 -0
  150. package/dist/components/blocks/landing-hero/landing-hero-impl.css +355 -0
  151. package/dist/components/blocks/logo-columns/LogoColumns.css +161 -0
  152. package/dist/components/blocks/logo-columns/LogoColumns.d.ts +23 -0
  153. package/dist/components/blocks/logo-columns/LogoColumns.js +46 -0
  154. package/dist/components/blocks/logo-columns/LogoColumns.types.d.ts +46 -0
  155. package/dist/components/blocks/logo-columns/LogoColumns.types.js +18 -0
  156. package/dist/components/blocks/logo-columns/logo-columns-impl.css +188 -0
  157. package/dist/components/blocks/logo-rows/LogoRows.css +122 -0
  158. package/dist/components/blocks/logo-rows/LogoRows.d.ts +20 -0
  159. package/dist/components/blocks/logo-rows/LogoRows.js +43 -0
  160. package/dist/components/blocks/logo-rows/LogoRows.types.d.ts +36 -0
  161. package/dist/components/blocks/logo-rows/LogoRows.types.js +16 -0
  162. package/dist/components/blocks/logo-rows/logo-rows-impl.css +179 -0
  163. package/dist/components/blocks/page-header/PageHeader.css +79 -0
  164. package/dist/components/blocks/page-header/PageHeader.d.ts +34 -0
  165. package/dist/components/blocks/page-header/PageHeader.js +43 -0
  166. package/dist/components/blocks/page-header/PageHeader.types.d.ts +32 -0
  167. package/dist/components/blocks/page-header/PageHeader.types.js +14 -0
  168. package/dist/components/blocks/page-header/page-header-impl.css +176 -0
  169. package/dist/components/blocks/product-hero/ProductHero.css +86 -0
  170. package/dist/components/blocks/product-hero/ProductHero.d.ts +35 -0
  171. package/dist/components/blocks/product-hero/ProductHero.js +48 -0
  172. package/dist/components/blocks/product-hero/ProductHero.types.d.ts +37 -0
  173. package/dist/components/blocks/product-hero/ProductHero.types.js +15 -0
  174. package/dist/components/blocks/product-hero/product-hero-impl.css +354 -0
  175. package/dist/components/blocks/testimonial-row/TestimonialRow.css +245 -0
  176. package/dist/components/blocks/testimonial-row/TestimonialRow.d.ts +20 -0
  177. package/dist/components/blocks/testimonial-row/TestimonialRow.js +128 -0
  178. package/dist/components/blocks/testimonial-row/TestimonialRow.types.d.ts +54 -0
  179. package/dist/components/blocks/testimonial-row/TestimonialRow.types.js +26 -0
  180. package/dist/components/blocks/testimonial-row/testimonial-row-impl.css +195 -0
  181. package/dist/components/blocks/testimonial-slider/TestimonialSlider.css +181 -0
  182. package/dist/components/blocks/testimonial-slider/TestimonialSlider.d.ts +22 -0
  183. package/dist/components/blocks/testimonial-slider/TestimonialSlider.js +36 -0
  184. package/dist/components/blocks/testimonial-slider/TestimonialSlider.types.d.ts +49 -0
  185. package/dist/components/blocks/testimonial-slider/TestimonialSlider.types.js +21 -0
  186. package/dist/components/blocks/testimonial-slider/testimonial-slider-impl.css +202 -0
  187. package/dist/components/blocks/use-reveal.d.ts +46 -0
  188. package/dist/components/blocks/use-reveal.js +120 -0
  189. package/dist/components/blocks/use-rotation.d.ts +62 -0
  190. package/dist/components/blocks/use-rotation.js +176 -0
  191. package/dist/components/core/accordion-item/AccordionItem.css +6 -5
  192. package/dist/components/core/bottom-sheet/BottomSheet.css +1 -1
  193. package/dist/components/core/button/Button.css +63 -16
  194. package/dist/components/core/button/Button.d.ts +14 -12
  195. package/dist/components/core/button/Button.js +43 -14
  196. package/dist/components/core/button/Button.types.d.ts +9 -14
  197. package/dist/components/core/button/button-impl.css +25 -0
  198. package/dist/components/core/button-icon/ButtonIcon.css +31 -15
  199. package/dist/components/core/button-icon/ButtonIcon.d.ts +2 -2
  200. package/dist/components/core/button-icon/ButtonIcon.js +2 -2
  201. package/dist/components/core/button-icon/ButtonIcon.types.d.ts +4 -4
  202. package/dist/components/core/card/Card.css +13 -4
  203. package/dist/components/core/card/Card.types.d.ts +2 -1
  204. package/dist/components/core/card-body/CardBody.css +11 -6
  205. package/dist/components/core/card-body/CardBody.d.ts +1 -1
  206. package/dist/components/core/card-body/CardBody.js +2 -2
  207. package/dist/components/core/card-body/CardBody.types.d.ts +4 -1
  208. package/dist/components/core/card-body/CardBody.types.js +1 -0
  209. package/dist/components/core/card-button/CardButton.css +13 -4
  210. package/dist/components/core/card-button/CardButton.types.d.ts +2 -1
  211. package/dist/components/core/checkbox/Checkbox.css +8 -1
  212. package/dist/components/core/dropdown-menu/DropdownMenu.css +13 -12
  213. package/dist/components/core/dropdown-menu/DropdownMenu.d.ts +5 -3
  214. package/dist/components/core/dropdown-menu/DropdownMenu.js +5 -3
  215. package/dist/components/core/filter-chip/FilterChip.css +2 -1
  216. package/dist/components/core/footer/footer-impl.css +1 -1
  217. package/dist/components/core/footer-group/FooterGroup.css +8 -7
  218. package/dist/components/core/icon/Icon.css +1 -1
  219. package/dist/components/core/link/Link.css +1 -1
  220. package/dist/components/core/menu-item/MenuItem.css +1 -1
  221. package/dist/components/core/navbar/Navbar.css +7 -8
  222. package/dist/components/core/navbar/Navbar.d.ts +17 -1
  223. package/dist/components/core/navbar/Navbar.js +139 -12
  224. package/dist/components/core/navbar/Navbar.types.d.ts +23 -4
  225. package/dist/components/core/navbar/Navbar.types.js +1 -1
  226. package/dist/components/core/navbar/navbar-impl.css +10 -3
  227. package/dist/components/core/navbar/navbar-material.css +96 -0
  228. package/dist/components/core/navbar-item/NavbarItem.css +4 -4
  229. package/dist/components/core/navbar-menu/NavbarMenu.css +7 -5
  230. package/dist/components/core/navbar-menu/NavbarMenu.d.ts +1 -0
  231. package/dist/components/core/navbar-menu/NavbarMenu.js +1 -0
  232. package/dist/components/core/navbar-menu/navbar-menu-impl.css +7 -7
  233. package/dist/components/core/navbar-switcher/NavbarSwitcher.css +6 -5
  234. package/dist/components/core/navbar-switcher/NavbarSwitcher.d.ts +1 -0
  235. package/dist/components/core/navbar-switcher/NavbarSwitcher.js +2 -1
  236. package/dist/components/core/option/Option.css +1 -1
  237. package/dist/components/core/pagination/Pagination.css +1 -75
  238. package/dist/components/core/pagination/Pagination.d.ts +4 -4
  239. package/dist/components/core/pagination/Pagination.js +11 -51
  240. package/dist/components/core/pagination/Pagination.types.d.ts +5 -17
  241. package/dist/components/core/pagination/Pagination.types.js +1 -3
  242. package/dist/components/core/pagination-dots/PaginationDots.css +60 -0
  243. package/dist/components/core/pagination-dots/PaginationDots.d.ts +25 -0
  244. package/dist/components/core/pagination-dots/PaginationDots.js +104 -0
  245. package/dist/components/core/pagination-dots/PaginationDots.types.d.ts +38 -0
  246. package/dist/components/core/pagination-dots/PaginationDots.types.js +9 -0
  247. package/dist/components/core/removable-chip/RemovableChip.css +3 -3
  248. package/dist/components/core/search-input/SearchInput.css +1 -1
  249. package/dist/components/core/segmented-control/SegmentedControl.css +1 -1
  250. package/dist/components/core/segmented-control-item/SegmentedControlItem.css +1 -1
  251. package/dist/components/core/select/Select.css +2 -1
  252. package/dist/components/core/select-chip/SelectChip.css +14 -7
  253. package/dist/components/core/select-chip/SelectChip.d.ts +1 -1
  254. package/dist/components/core/select-chip/SelectChip.js +7 -4
  255. package/dist/components/core/select-chip/SelectChip.types.d.ts +7 -1
  256. package/dist/components/core/select-chip/SelectChip.types.js +1 -1
  257. package/dist/components/core/select-chip/select-chip-impl.css +14 -3
  258. package/dist/components/core/status-dot/StatusDot.css +1 -1
  259. package/dist/components/core/subdomain-navbar/SubdomainNavbar.css +3 -3
  260. package/dist/components/core/tab/Tab.css +1 -1
  261. package/dist/components/core/tab-list/TabList.css +1 -1
  262. package/dist/components/core/table/table-impl.css +36 -14
  263. package/dist/components/core/table-header-cell/TableHeaderCell.css +5 -5
  264. package/dist/components/core/text-area/TextArea.css +1 -1
  265. package/dist/components/core/text-input/TextInput.css +1 -1
  266. package/dist/components/core/tooltip/Tooltip.css +1 -1
  267. package/dist/components/core/typography/Typography.css +5 -5
  268. package/dist/components/index.d.ts +35 -0
  269. package/dist/components/index.js +30 -0
  270. package/dist/components/logo-data.d.ts +24 -0
  271. package/dist/components/logo-data.js +11 -0
  272. package/dist/components/scroll-data.d.ts +64 -0
  273. package/dist/components/scroll-data.js +17 -0
  274. package/dist/components/testimonial-row-data.d.ts +51 -0
  275. package/dist/components/testimonial-row-data.js +11 -0
  276. package/dist/components/testimonial-slider-data.d.ts +26 -0
  277. package/dist/components/testimonial-slider-data.js +10 -0
  278. package/dist/marketing.css +385 -14
  279. package/package.json +1 -6
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface FeatureCardsOwnProps {
3
+ /**
4
+ * `page` — The default, and the page's own white. A section that has nothing to separate itself from what is above it — most of them — and the ground a reader is already on
5
+ * `subtle` — A quiet step off the page, for a band that wants to read as its own section without changing the page's voice. The one to reach for first when two sections need telling apart. A light card — surface-light — turns white on it, color.bg.surface-card-light-raised, because its own gray.50 is the band's and would vanish into it. A dark card — accent-strong — is lifted to color.bg.surface-card-strong-raised, a step lighter in dark mode, where the band is within a shade of its gray.900. A plain surface card is raised off it the same way, to color.bg.surface-card-raised, because the recessed fill is the band's gray too. Painted as the band's tint laid over the page's own ground, so it is the same grey whatever is behind the section — in dark mode the tint alone took on whatever it was set against, and on a light surface it was light ink on white.
6
+ * `strong` — `color.bg.brand-strong`, the dark band. It stops the page rather than dividing it, so it is for a section a reader should arrive at — and one per page is usually one more than enough. The coloured cards keep their own grounds on it; a neutral card — surface or surface-raised — turns to Card's inverse fill and white ink, and a plain card takes the white ink alone, because their own would be dark ink on near-black; an eyebrow inside any of them takes the band's quieter ink
7
+ */
8
+ ground?: 'page' | 'subtle' | 'strong';
9
+ /** Always. What the cards are, in the fewest words that name them — "Why trade with Deriv", "Markets you can trade". */
10
+ title: string;
11
+ /** A line saying what the set has in common, when the title alone does not. */
12
+ description?: string;
13
+ /** A way on to the page the whole set belongs to — "See all markets". A standalone Link, under the line. */
14
+ link?: ReactNode;
15
+ /** Allowed: Card, CardButton */
16
+ children: ReactNode;
17
+ }
18
+ export type FeatureCardsProps = FeatureCardsOwnProps;
19
+ /** The class each part carries. Apply these — do not retype them. */
20
+ export declare const featureCardsParts: {
21
+ readonly root: "dds-featurecards";
22
+ readonly inner: "dds-featurecards__inner";
23
+ readonly header: "dds-featurecards__header";
24
+ readonly title: "dds-featurecards__title";
25
+ readonly description: "dds-featurecards__description";
26
+ readonly link: "dds-featurecards__link";
27
+ readonly grid: "dds-featurecards__grid";
28
+ readonly item: "dds-featurecards__item";
29
+ };
30
+ /** Generated so defaults cannot drift from the contract. */
31
+ export declare const featureCardsDefaults: {
32
+ readonly ground: "page";
33
+ };
@@ -0,0 +1,14 @@
1
+ // GENERATED from feature-cards.contract.json. Do not edit.
2
+ /** The class each part carries. Apply these — do not retype them. */
3
+ export const featureCardsParts = {
4
+ root: 'dds-featurecards',
5
+ inner: 'dds-featurecards__inner',
6
+ header: 'dds-featurecards__header',
7
+ title: 'dds-featurecards__title',
8
+ description: 'dds-featurecards__description',
9
+ link: 'dds-featurecards__link',
10
+ grid: 'dds-featurecards__grid',
11
+ item: 'dds-featurecards__item',
12
+ };
13
+ /** Generated so defaults cannot drift from the contract. */
14
+ export const featureCardsDefaults = { ground: 'page' };
@@ -0,0 +1,245 @@
1
+ /*
2
+ * Scoped to this package, like the generated sheet beside it.
3
+ */
4
+
5
+ /*
6
+ * The header starts on a phone and is centred once the band is wide enough to hold a line.
7
+ *
8
+ * PageHeader's rule and its reason: centred text starts every line at a different edge, which
9
+ * costs little across a wide measure and a great deal across a narrow one. The header and the
10
+ * line under it are capped narrower than the column, so `margin-inline: auto` is what centres
11
+ * those boxes rather than their words inside a box left at the start edge.
12
+ *
13
+ * `48rem` longhand because `var()` is not permitted in a container query condition, and it is
14
+ * the width the rest of this package folds at.
15
+ */
16
+ @container (min-width: 48rem) {
17
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__header {
18
+ align-items: center;
19
+ margin-inline: auto;
20
+ text-align: center;
21
+ }
22
+
23
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__description {
24
+ margin-inline: auto;
25
+ }
26
+
27
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__link {
28
+ justify-content: center;
29
+ }
30
+ }
31
+
32
+ /*
33
+ * One column on a phone. `minmax(0, 1fr)` rather than `1fr`, so a card holding a long unbroken
34
+ * word or a wide picture shrinks to the column instead of pushing the grid past the page.
35
+ */
36
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid {
37
+ grid-template-columns: minmax(0, 1fr);
38
+ }
39
+
40
+ /*
41
+ * How many cards share a row, read from the cards.
42
+ *
43
+ * Two from the fold, three at full width, four when there are exactly four — the reference
44
+ * design's rule, so a set of four is one row rather than a row of three and an orphan — and a pair
45
+ * as two columns. `:has()` asks the markup how many there are; the count is already there, and a
46
+ * prop restating it would be a second place for the two to disagree. 62rem is the reference's
47
+ * desktop step.
48
+ *
49
+ * Where `:has()` is not supported every count takes the three columns and a single card stays
50
+ * stacked: the set still reads, only less exactly.
51
+ */
52
+ @container (min-width: 48rem) {
53
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid {
54
+ grid-template-columns: repeat(2, minmax(0, 1fr));
55
+ }
56
+
57
+ /* One card takes the row. Above the three- and four-column rules below by specificity, not by
58
+ order — `:has()` counts the selector inside it — so it holds at every width from here up. */
59
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid:has(> :only-child) {
60
+ grid-template-columns: minmax(0, 1fr);
61
+ }
62
+
63
+ /*
64
+ * And a card alone lays its words and its picture side by side.
65
+ *
66
+ * A card the width of the section with its picture under the words is a strip of copy over a
67
+ * letterbox. The reference design's single panel is a split instead — words at the start, the
68
+ * picture at the end — and the block is what knows the card is alone, so the row is said here
69
+ * rather than asked of CardBody. Its parts keep their own order: a body whose picture leads
70
+ * (mediaPosition="above") leads with it here too. Only a body with its picture in the column;
71
+ * one whose copy sits over its picture is already the whole card and has nothing to split.
72
+ *
73
+ * The two halves share the width, and the words start at the top of theirs, level with the top
74
+ * of the picture, the way a card's words start at the top when they are stacked.
75
+ */
76
+ :where([data-dds-context="marketing"])
77
+ .dds-featurecards
78
+ .dds-featurecards__grid:has(> :only-child)
79
+ .dds-cardbody[data-over-media="false"] {
80
+ flex-direction: row;
81
+ }
82
+
83
+ :where([data-dds-context="marketing"])
84
+ .dds-featurecards
85
+ .dds-featurecards__grid:has(> :only-child)
86
+ .dds-cardbody[data-over-media="false"]
87
+ > :is(.dds-cardbody__content, .dds-cardbody__media-frame) {
88
+ flex: 1 1 0;
89
+ min-inline-size: 0;
90
+ }
91
+
92
+ /*
93
+ * The picture beside a single card's words is portrait, aspect.portrait.
94
+ *
95
+ * Half a wide card is a tall column, and a landscape picture in it is a letterbox with the words
96
+ * running past its lower edge. Portrait fills the half the way the reference's single panel does.
97
+ * `align-self: flex-start` so the ratio decides the frame's height rather than the row stretching
98
+ * it to the words; the picture inside covers, so whatever shape the source is, the frame is
99
+ * portrait — unless the page set CardBody's `aspect`, whose inline ratio is the page's to choose.
100
+ */
101
+ :where([data-dds-context="marketing"])
102
+ .dds-featurecards
103
+ .dds-featurecards__grid:has(> :only-child)
104
+ .dds-cardbody[data-over-media="false"]
105
+ > .dds-cardbody__media-frame {
106
+ align-self: flex-start;
107
+ aspect-ratio: var(--aspect-portrait);
108
+ /* Half the card when CardBody's own `aspect` is set: that writes `flex: none` and the page's
109
+ ratio inline, which win over the rule above, and a frame with no flex and no width sizes to
110
+ the picture's own pixels — eight hundred of them, past the card's edge. The width is what
111
+ holds it to its half; where the flex rule does apply, its zero basis overrides this. */
112
+ inline-size: 50%;
113
+ }
114
+ }
115
+
116
+ @container (min-width: 62rem) {
117
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid {
118
+ grid-template-columns: repeat(3, minmax(0, 1fr));
119
+ }
120
+
121
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid:has(> :nth-child(4):last-child) {
122
+ grid-template-columns: repeat(4, minmax(0, 1fr));
123
+ }
124
+
125
+ /* A pair as two columns of their own, rather than two of three with an empty third beside them —
126
+ the rule below then holds the pair to the width two columns take in a row of three. */
127
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid:has(> :nth-child(2):last-child) {
128
+ grid-template-columns: repeat(2, minmax(0, 1fr));
129
+ }
130
+
131
+ /*
132
+ * One or two cards hold to two thirds of the row, centred, rather than the whole of it.
133
+ *
134
+ * Two cards across the full width are each half as wide again as a card in a row of three, and
135
+ * one alone is a banner. Two thirds is exactly two of the three columns and the gutter between
136
+ * them, so a pair is the width a pair is in any other set, and a single card the width of that
137
+ * pair. Computed from the row's own width and gutter rather than a fixed measure, so it follows
138
+ * the grid wherever the grid is.
139
+ */
140
+ :where([data-dds-context="marketing"]) .dds-featurecards .dds-featurecards__grid:is(:has(> :only-child), :has(> :nth-child(2):last-child)) {
141
+ margin-inline: auto;
142
+ max-inline-size: calc((100% - 2 * var(--space-grid-gutter)) * 2 / 3 + var(--space-grid-gutter));
143
+ inline-size: 100%;
144
+ }
145
+ }
146
+
147
+ /*
148
+ * A light card turns white on the subtle band.
149
+ *
150
+ * surface-light is gray.50 in both themes so it stays light beside a dark card, and the subtle
151
+ * band is gray.50 too — so on that band the light card had no edge at all. The block knows its
152
+ * ground and the card does not, so the answer is here rather than in Card: the card keeps its
153
+ * variant, its ink and its hover (a lift, which this leaves alone), and only its fill moves to
154
+ * color.bg.surface-card-light-raised. White in light mode; in dark mode the band is a white tint
155
+ * over near-black rather than gray.50, so the role is the card's own gray.50 there and nothing
156
+ * changes — a white card on the dark band was brighter than every card beside it.
157
+ */
158
+ :where([data-dds-context="marketing"])
159
+ .dds-featurecards[data-ground="subtle"]
160
+ .dds-featurecards__item
161
+ > :is(.dds-card, .dds-cardbutton)[data-variant="surface-light"] {
162
+ background-color: var(--color-bg-surface-card-light-raised);
163
+ }
164
+
165
+ /*
166
+ * A neutral card turns round on the dark band.
167
+ *
168
+ * surface and surface-raised carry the page's dark ink and a fill made for a light page, which on
169
+ * color.bg.brand-strong is dark words on near-black. Card answers that with tone="inverse", and
170
+ * CardButton has no tone to answer with — so the block, which knows its ground, gives both the
171
+ * values Card's own inverse uses: the inverse surface, the raised veil, and white ink. The coloured
172
+ * cards — surface-light, accent, accent-strong — already paint their own ground and ink and are
173
+ * left as they are.
174
+ */
175
+ :where([data-dds-context="marketing"])
176
+ .dds-featurecards[data-ground="strong"]
177
+ .dds-featurecards__item
178
+ > :is(.dds-card, .dds-cardbutton)[data-variant="surface"] {
179
+ background-color: var(--color-bg-surface-inverse);
180
+ color: var(--color-fg-on-brand-strong);
181
+ }
182
+
183
+ :where([data-dds-context="marketing"])
184
+ .dds-featurecards[data-ground="strong"]
185
+ .dds-featurecards__item
186
+ > :is(.dds-card, .dds-cardbutton)[data-variant="surface-raised"] {
187
+ background-color: var(--color-bg-surface-raised-on-brand-strong);
188
+ color: var(--color-fg-on-brand-strong);
189
+ }
190
+
191
+ /*
192
+ * A plain card has no ground of its own, so on the dark band it is the band's words: white ink,
193
+ * the value Card's own plain-and-inverse takes. Without it a plain CardButton drew the page's
194
+ * near-black ink on the near-black band.
195
+ */
196
+ :where([data-dds-context="marketing"])
197
+ .dds-featurecards[data-ground="strong"]
198
+ .dds-featurecards__item
199
+ > :is(.dds-card, .dds-cardbutton)[data-variant="plain"] {
200
+ color: var(--color-fg-on-brand-strong);
201
+ }
202
+
203
+ /*
204
+ * And the eyebrow inside a card turned round. CardBody quietens its eyebrow by its own tone, which
205
+ * a card recoloured here does not change, so the eyebrow kept the page's muted grey on a dark card.
206
+ * It takes the band's own quieter ink, the value CardBody's inverse tone uses.
207
+ */
208
+ :where([data-dds-context="marketing"])
209
+ .dds-featurecards[data-ground="strong"]
210
+ .dds-featurecards__item
211
+ > :is(.dds-card, .dds-cardbutton):is([data-variant="surface"], [data-variant="surface-raised"], [data-variant="plain"])
212
+ .dds-cardbody__eyebrow {
213
+ color: var(--color-fg-muted-inverse);
214
+ }
215
+
216
+ /*
217
+ * And a dark card is lifted a step in dark mode.
218
+ *
219
+ * accent-strong is gray.900, and in dark mode the subtle band is a white tint over near-black that
220
+ * lands within a shade of it — the card blended into the band the way the light card did in light
221
+ * mode. color.bg.surface-card-strong-raised is the same gray.900 in light, where the dark card
222
+ * already stands off the band, and gray.800 in dark, a step up so it lifts off it. The ink is
223
+ * unchanged: white on either.
224
+ */
225
+ :where([data-dds-context="marketing"])
226
+ .dds-featurecards[data-ground="subtle"]
227
+ .dds-featurecards__item
228
+ > :is(.dds-card, .dds-cardbutton)[data-variant="accent-strong"] {
229
+ background-color: var(--color-bg-surface-card-strong-raised);
230
+ }
231
+
232
+ /*
233
+ * And a plain card is raised off it.
234
+ *
235
+ * surface is the recessed fill for a card on the page, color.bg.surface — the same gray the subtle
236
+ * band is, so on the band it vanished exactly as the light card did. It takes the raised fill,
237
+ * color.bg.surface-card-raised, which is what Card's surface-raised variant paints: white on the
238
+ * light band and the raised veil on the dark one, following the theme as surface does.
239
+ */
240
+ :where([data-dds-context="marketing"])
241
+ .dds-featurecards[data-ground="subtle"]
242
+ .dds-featurecards__item
243
+ > :is(.dds-card, .dds-cardbutton)[data-variant="surface"] {
244
+ background-color: var(--color-bg-surface-card-raised);
245
+ }
@@ -0,0 +1,137 @@
1
+ /* GENERATED from featurescroll.contract.json. Do not edit. */
2
+
3
+ :where([data-dds-context="marketing"]) .dds-featurescroll {
4
+ box-sizing: border-box;
5
+ container-type: inline-size;
6
+ display: flex;
7
+ inline-size: 100%;
8
+ justify-content: center;
9
+ }
10
+
11
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__inner {
12
+ box-sizing: border-box;
13
+ display: flex;
14
+ flex-direction: column;
15
+ inline-size: 100%;
16
+ min-inline-size: 0;
17
+ max-inline-size: var(--size-content-max);
18
+ padding-block: var(--space-section-default);
19
+ padding-inline: var(--space-inset-page);
20
+ row-gap: var(--space-heading-content);
21
+ }
22
+
23
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__heading {
24
+ margin-block: 0;
25
+ text-wrap: balance;
26
+ font-family: var(--font-h2-family);
27
+ font-size: var(--font-h2-size);
28
+ font-weight: var(--font-h2-weight);
29
+ letter-spacing: var(--font-h2-letter-spacing);
30
+ line-height: var(--font-h2-line-height);
31
+ max-inline-size: var(--size-measure-heading);
32
+ }
33
+
34
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__steps {
35
+ display: flex;
36
+ flex-direction: column;
37
+ min-inline-size: 0;
38
+ column-gap: var(--space-gap-3xl);
39
+ row-gap: var(--space-gap-2xl);
40
+ }
41
+
42
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__step {
43
+ box-sizing: border-box;
44
+ display: flex;
45
+ min-inline-size: 0;
46
+ row-gap: var(--space-gap-md);
47
+ }
48
+
49
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__content {
50
+ display: flex;
51
+ flex-direction: column;
52
+ justify-content: center;
53
+ min-inline-size: 0;
54
+ row-gap: var(--space-gap-none);
55
+ }
56
+
57
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__title {
58
+ margin-block: 0;
59
+ text-wrap: balance;
60
+ font-family: var(--font-h3-family);
61
+ font-size: var(--font-h3-size);
62
+ font-weight: var(--font-h3-weight);
63
+ letter-spacing: var(--font-h3-letter-spacing);
64
+ line-height: var(--font-h3-line-height);
65
+ margin-block-end: var(--space-heading-title-description);
66
+ }
67
+
68
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__description {
69
+ margin-block: 0;
70
+ font-family: var(--font-body-md-family);
71
+ font-size: var(--font-body-md-size);
72
+ font-weight: var(--font-body-md-weight);
73
+ letter-spacing: var(--font-body-md-letter-spacing);
74
+ line-height: var(--font-body-md-line-height);
75
+ margin-block-end: var(--space-heading-description-actions);
76
+ max-inline-size: var(--size-measure-description);
77
+ }
78
+
79
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__link {
80
+ display: flex;
81
+ margin-block-start: var(--space-gap-none);
82
+ }
83
+
84
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__media {
85
+ align-items: center;
86
+ display: flex;
87
+ justify-content: center;
88
+ min-inline-size: 0;
89
+ padding-block: var(--space-gap-lg);
90
+ transition-duration: var(--motion-duration-media-hover);
91
+ transition-timing-function: var(--motion-easing-media-hover);
92
+ }
93
+
94
+ :where([data-dds-context="marketing"]) .dds-featurescroll .dds-featurescroll__picture {
95
+ display: block;
96
+ object-fit: cover;
97
+ aspect-ratio: var(--aspect-square);
98
+ border-radius: var(--radius-container);
99
+ }
100
+
101
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-media-position="end"] .dds-featurescroll__content {
102
+ grid-column: words;
103
+ }
104
+
105
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-media-position="start"] .dds-featurescroll__content {
106
+ grid-column: picture;
107
+ }
108
+
109
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-media-position="end"] .dds-featurescroll__media {
110
+ grid-column: picture;
111
+ }
112
+
113
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-media-position="start"] .dds-featurescroll__media {
114
+ grid-column: words;
115
+ }
116
+
117
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-ground="page"] {
118
+ background-color: var(--color-bg-page);
119
+ color: var(--color-fg-default);
120
+ }
121
+
122
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-ground="strong"] {
123
+ background-color: var(--color-bg-brand-strong);
124
+ color: var(--color-fg-on-brand-strong);
125
+ }
126
+
127
+ :where([data-dds-context="marketing"]) .dds-featurescroll[data-ground="subtle"] {
128
+ background-color: var(--color-bg-page);
129
+ background-image: linear-gradient(var(--color-bg-band-subtle), var(--color-bg-band-subtle));
130
+ color: var(--color-fg-default);
131
+ }
132
+
133
+ @media (prefers-reduced-motion: reduce) {
134
+ :where([data-dds-context="marketing"]) .dds-featurescroll,
135
+ :where([data-dds-context="marketing"]) .dds-featurescroll * { transition: none !important; }
136
+ }
137
+
@@ -0,0 +1,39 @@
1
+ import './FeatureScroll.css';
2
+ import './feature-scroll-impl.css';
3
+ import '../block-reveal.css';
4
+ import { type FeatureScrollProps } from './FeatureScroll.types.js';
5
+ export type { FeatureScrollProps };
6
+ /**
7
+ * A section that gives a set of things one screen each, with the picture pinned beside the words.
8
+ *
9
+ * Two mechanisms, and which does what is the thing to understand here. Holding a picture in place
10
+ * while words move past it is `position: sticky`, and nothing in this file measures it. Knowing
11
+ * *which* picture belongs to the words a reader has reached is a question about where each title
12
+ * has got to, and no stylesheet can ask it — so that much is below, and only that much.
13
+ *
14
+ * The rule the reference design settles on: a stop's picture is the one showing once the *next*
15
+ * stop's title has risen to the line the pictures are pinned at. Read as "the last title at or
16
+ * above the line" plus one, clamped to the end. It means the square changes at the moment a reader
17
+ * reaches a new heading rather than at some fraction of the way through, which is the only moment
18
+ * they would notice either way.
19
+ *
20
+ * Throttled to one read per frame through `requestAnimationFrame`, on a passive listener, and the
21
+ * read itself is `getBoundingClientRect` on the titles — no layout is written back, so the loop
22
+ * cannot thrash. `useLayoutEffect` rather than `useEffect` so the first measurement lands before
23
+ * the browser paints; the effect otherwise ran after the first frame and the section opened on
24
+ * step one for that frame however far down the page a reader had already scrolled.
25
+ *
26
+ * It measures the section's own width rather than the window's, because the fold is a container
27
+ * query on the section and the two disagree wherever a frame stands in for a window. The fold is
28
+ * the stylesheet's own `48rem`, turned into pixels against the root font size each time it is
29
+ * asked: a fixed 768 agreed with it only while the root stayed at sixteen, and a page that set it
30
+ * smaller pinned the picture on the first stop for the width in between.
31
+ *
32
+ * The parts of a stop are in the order a phone reads them, which is the order they are written:
33
+ * name, picture, line, way onward. At full width each stop gives up its box — `display: contents`
34
+ * — and the grid places the words in one column and every picture in the other, stacked, so the
35
+ * change is a change in one square rather than one picture leaving and another arriving. The
36
+ * reference design ships two DOM trees to get that; one tree means a reader's assistive technology
37
+ * and anybody reading the markup meet each stop once.
38
+ */
39
+ export declare function FeatureScroll({ ground, title, steps, mediaPosition, }: FeatureScrollProps): import("react").JSX.Element;
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId, useLayoutEffect, useRef, useState } from 'react';
4
+ import './FeatureScroll.css';
5
+ import './feature-scroll-impl.css';
6
+ import '../block-reveal.css';
7
+ import { arriving, useReveal } from '../use-reveal.js';
8
+ import { Link } from '../../core/link/Link.js';
9
+ import { featureScrollDefaults, featureScrollParts, } from './FeatureScroll.types.js';
10
+ /**
11
+ * Where the fold is, in pixels — the stylesheet's `48rem`, read against the page's own root font
12
+ * size, so the two agree when a page or a reader has changed it. See `showing` below.
13
+ */
14
+ const FOLD_REM = 48;
15
+ const foldsAt = () => FOLD_REM * (Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16);
16
+ /**
17
+ * A section that gives a set of things one screen each, with the picture pinned beside the words.
18
+ *
19
+ * Two mechanisms, and which does what is the thing to understand here. Holding a picture in place
20
+ * while words move past it is `position: sticky`, and nothing in this file measures it. Knowing
21
+ * *which* picture belongs to the words a reader has reached is a question about where each title
22
+ * has got to, and no stylesheet can ask it — so that much is below, and only that much.
23
+ *
24
+ * The rule the reference design settles on: a stop's picture is the one showing once the *next*
25
+ * stop's title has risen to the line the pictures are pinned at. Read as "the last title at or
26
+ * above the line" plus one, clamped to the end. It means the square changes at the moment a reader
27
+ * reaches a new heading rather than at some fraction of the way through, which is the only moment
28
+ * they would notice either way.
29
+ *
30
+ * Throttled to one read per frame through `requestAnimationFrame`, on a passive listener, and the
31
+ * read itself is `getBoundingClientRect` on the titles — no layout is written back, so the loop
32
+ * cannot thrash. `useLayoutEffect` rather than `useEffect` so the first measurement lands before
33
+ * the browser paints; the effect otherwise ran after the first frame and the section opened on
34
+ * step one for that frame however far down the page a reader had already scrolled.
35
+ *
36
+ * It measures the section's own width rather than the window's, because the fold is a container
37
+ * query on the section and the two disagree wherever a frame stands in for a window. The fold is
38
+ * the stylesheet's own `48rem`, turned into pixels against the root font size each time it is
39
+ * asked: a fixed 768 agreed with it only while the root stayed at sixteen, and a page that set it
40
+ * smaller pinned the picture on the first stop for the width in between.
41
+ *
42
+ * The parts of a stop are in the order a phone reads them, which is the order they are written:
43
+ * name, picture, line, way onward. At full width each stop gives up its box — `display: contents`
44
+ * — and the grid places the words in one column and every picture in the other, stacked, so the
45
+ * change is a change in one square rather than one picture leaving and another arriving. The
46
+ * reference design ships two DOM trees to get that; one tree means a reader's assistive technology
47
+ * and anybody reading the markup meet each stop once.
48
+ */
49
+ export function FeatureScroll({ ground = featureScrollDefaults.ground, title, steps, mediaPosition = featureScrollDefaults.mediaPosition, }) {
50
+ const titleId = useId();
51
+ const [heading, revealed] = useReveal();
52
+ const root = useRef(null);
53
+ const titles = useRef([]);
54
+ const band = useRef(null);
55
+ const [showing, setShowing] = useState(0);
56
+ const count = steps.length;
57
+ useLayoutEffect(() => {
58
+ if (count === 0)
59
+ return;
60
+ /**
61
+ * The line the pictures are pinned at, read off the pinned band itself.
62
+ *
63
+ * Asked of the element rather than held here, because the offset is the stylesheet's — it is
64
+ * `size.bar-height`, so that a picture comes to rest under a navigation bar rather than behind
65
+ * one. A copy of the number in this file would be a second answer to drift from it, and the
66
+ * band already knows.
67
+ */
68
+ const line = () => {
69
+ const pinned = band.current;
70
+ if (pinned === null)
71
+ return 0;
72
+ const top = Number.parseFloat(getComputedStyle(pinned).insetBlockStart);
73
+ return Number.isFinite(top) ? top : 0;
74
+ };
75
+ let frame = 0;
76
+ const read = () => {
77
+ frame = 0;
78
+ const section = root.current;
79
+ if (section === null)
80
+ return;
81
+ // The fold is the section's own width, which is what the stylesheet asks. Below it every
82
+ // stop carries its own picture and there is nothing to choose between.
83
+ if (section.clientWidth < foldsAt())
84
+ return;
85
+ const at = line();
86
+ let reached = -1;
87
+ for (let i = 0; i < count; i += 1) {
88
+ const heading = titles.current[i];
89
+ if (heading === null || heading === undefined)
90
+ continue;
91
+ if (heading.getBoundingClientRect().top <= at)
92
+ reached = i;
93
+ }
94
+ const next = Math.min(count - 1, reached + 1);
95
+ setShowing((held) => (held === next ? held : next));
96
+ };
97
+ const schedule = () => {
98
+ if (frame !== 0)
99
+ return;
100
+ frame = window.requestAnimationFrame(read);
101
+ };
102
+ /**
103
+ * Heard on the way down, because a scroll does not come back up.
104
+ *
105
+ * A `scroll` event fired at an element does not bubble, so a listener on the window hears only
106
+ * the window's own scrolling. Wherever the section sits inside something that scrolls — a
107
+ * pane, a dialog, the workbench's own stage — the pictures simply never changed, while the
108
+ * same section on a page of its own worked perfectly. That is a difference no reader should be
109
+ * able to find, and the reference design has it too: it listens on the window alone.
110
+ *
111
+ * The capture phase is what catches them. Every scroll passes through the window on its way to
112
+ * its target whether or not it comes back, so one listener here answers for every scroller on
113
+ * the page, and the read itself is unchanged — `getBoundingClientRect` is measured against the
114
+ * viewport, which is the right frame however the section came to move within it.
115
+ *
116
+ * The flag has to be given to the removal as well: a capturing listener and a bubbling one are
117
+ * two different registrations, and taking off the wrong one leaves the other listening.
118
+ */
119
+ const heard = { passive: true, capture: true };
120
+ read();
121
+ window.addEventListener('scroll', schedule, heard);
122
+ window.addEventListener('resize', schedule, { passive: true });
123
+ return () => {
124
+ window.removeEventListener('scroll', schedule, { capture: true });
125
+ window.removeEventListener('resize', schedule);
126
+ if (frame !== 0)
127
+ window.cancelAnimationFrame(frame);
128
+ };
129
+ }, [count]);
130
+ return (_jsx("section", { className: featureScrollParts.root, "data-ground": ground, "data-media-position": mediaPosition, "aria-labelledby": titleId, ref: root, children: _jsxs("div", { className: featureScrollParts.inner, children: [_jsx("h2", { className: featureScrollParts.heading, ref: heading, "data-revealed": revealed, ...arriving(0), id: titleId, children: title }), _jsx("div", { className: featureScrollParts.steps, style: { '--dds-featurescroll-stops': count }, children: steps.map((step, index) => (
131
+ /* Keyed by the title. An index would key the set by position, so filtering a stop out
132
+ would renumber every stop under it and React would reconcile the wrong ones — and
133
+ these are the content, so two the same would be a duplicate stop rather than a key
134
+ collision worth tolerating. */
135
+ _jsx(Stop, { step: step, showing: index === showing, onTitle: (held) => {
136
+ titles.current[index] = held;
137
+ }, onBand: index === 0 ? band : undefined }, step.title))) })] }) }));
138
+ }
139
+ /**
140
+ * One stop, which arrives on its own below the fold and not at all above it.
141
+ *
142
+ * A component of its own for one reason: it holds a `useReveal` each, and a hook cannot be called
143
+ * inside a loop. Private to this file, because a stop is not a thing a page composes — the section
144
+ * takes its set as data, and this is how one record is drawn.
145
+ *
146
+ * Two arrivals rather than one, and the gap between them is the layout's rather than a number. The
147
+ * words and the picture each watch for themselves, and on a phone the picture sits under the line
148
+ * it belongs to — so it comes up a beat later because it reaches the viewport a beat later. A delay
149
+ * would have been a guess about how far apart they are; this is the distance itself.
150
+ *
151
+ * Above the fold neither moves, and the stylesheet says so. What moves there is the picture, which
152
+ * is pinned beside the words and exchanged as a reader passes each title — the section's own
153
+ * motion, and enough of it. It is also the one place a transform cannot go: the band is
154
+ * `position: sticky`, and the crossfade owns its opacity.
155
+ *
156
+ * Below the fold there is no pinning and nothing is exchanged: the stops are ordinary blocks in a
157
+ * column, so without this the only thing on a phone that moved was the section's heading — one
158
+ * flourish at the top and six screens of nothing after it.
159
+ */
160
+ function Stop({ step, showing, onTitle, onBand, }) {
161
+ const [words, wordsHere] = useReveal();
162
+ const [picture, pictureHere] = useReveal();
163
+ return (_jsxs("div", { className: featureScrollParts.step, children: [_jsxs("div", { className: featureScrollParts.content, "data-revealed": wordsHere, children: [_jsx("h3", { className: featureScrollParts.title, ...arriving(0), ref: (held) => {
164
+ words(held);
165
+ onTitle(held);
166
+ }, children: step.title }), _jsx("p", { className: featureScrollParts.description, ...arriving(1), children: step.description }), step.href !== undefined && step.linkLabel !== undefined ? (_jsx("div", { className: featureScrollParts.link, ...arriving(2), children: _jsx(Link, { href: step.href, children: step.linkLabel }) })) : null] }), _jsx("div", { className: featureScrollParts.media, "data-showing": showing, "data-revealed": pictureHere, "aria-hidden": "true", ref: onBand, children: _jsx("img", { className: featureScrollParts.picture, src: step.image, alt: "", ...arriving(0), ref: picture }) })] }));
167
+ }