@deriv-com/dds-marketing 1.0.0 → 1.0.2

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 (131) hide show
  1. package/.llm/components/accordion.md +1 -1
  2. package/.llm/components/accordionitem.md +1 -1
  3. package/.llm/components/aspectratio.md +1 -1
  4. package/.llm/components/background.md +371 -0
  5. package/.llm/components/badge.md +1 -1
  6. package/.llm/components/bottomsheet.md +1 -1
  7. package/.llm/components/breadcrumb.md +1 -1
  8. package/.llm/components/breadcrumbitem.md +1 -1
  9. package/.llm/components/button.md +10 -6
  10. package/.llm/components/buttonicon.md +3 -3
  11. package/.llm/components/card.md +1 -1
  12. package/.llm/components/cardbody.md +1 -1
  13. package/.llm/components/cardbutton.md +1 -1
  14. package/.llm/components/checkbox.md +1 -1
  15. package/.llm/components/ctabanner.md +26 -1
  16. package/.llm/components/ctabannerslider.md +22 -1
  17. package/.llm/components/divider.md +1 -1
  18. package/.llm/components/dropdownmenu.md +2 -2
  19. package/.llm/components/faqaccordion.md +1 -1
  20. package/.llm/components/featurebullets.md +1 -1
  21. package/.llm/components/featurecards.md +1 -1
  22. package/.llm/components/featurescroll.md +1 -1
  23. package/.llm/components/featuresplit.md +1 -1
  24. package/.llm/components/filterchip.md +1 -1
  25. package/.llm/components/footer.md +1 -1
  26. package/.llm/components/footergroup.md +1 -1
  27. package/.llm/components/grid.md +1 -1
  28. package/.llm/components/icon.md +13 -3
  29. package/.llm/components/illustrationcard.md +236 -0
  30. package/.llm/components/illustrationscene.md +233 -0
  31. package/.llm/components/landinghero.md +25 -1
  32. package/.llm/components/link.md +4 -4
  33. package/.llm/components/logocolumns.md +1 -1
  34. package/.llm/components/logorows.md +1 -1
  35. package/.llm/components/menuitem.md +1 -1
  36. package/.llm/components/navbar.md +1 -1
  37. package/.llm/components/navbaritem.md +1 -1
  38. package/.llm/components/navbarmenu.md +1 -1
  39. package/.llm/components/navbarmenugroup.md +1 -1
  40. package/.llm/components/navbarpanel.md +1 -1
  41. package/.llm/components/navbarswitcher.md +1 -1
  42. package/.llm/components/navbarswitcheroption.md +1 -1
  43. package/.llm/components/option.md +1 -1
  44. package/.llm/components/pageheader.md +22 -1
  45. package/.llm/components/pagination.md +1 -1
  46. package/.llm/components/paginationdots.md +1 -1
  47. package/.llm/components/producthero.md +1 -1
  48. package/.llm/components/removablechip.md +1 -1
  49. package/.llm/components/row.md +1 -1
  50. package/.llm/components/scroller.md +1 -1
  51. package/.llm/components/searchinput.md +1 -1
  52. package/.llm/components/segmentedcontrol.md +1 -1
  53. package/.llm/components/segmentedcontrolitem.md +1 -1
  54. package/.llm/components/select.md +1 -1
  55. package/.llm/components/selectchip.md +1 -1
  56. package/.llm/components/spinner.md +1 -1
  57. package/.llm/components/stack.md +1 -1
  58. package/.llm/components/statusdot.md +1 -1
  59. package/.llm/components/subdomainnavbar.md +1 -1
  60. package/.llm/components/tab.md +3 -3
  61. package/.llm/components/table.md +1 -1
  62. package/.llm/components/tablebody.md +1 -1
  63. package/.llm/components/tablecell.md +1 -1
  64. package/.llm/components/tablehead.md +1 -1
  65. package/.llm/components/tableheadercell.md +1 -1
  66. package/.llm/components/tablerow.md +1 -1
  67. package/.llm/components/tablist.md +1 -1
  68. package/.llm/components/testimonialrow.md +1 -1
  69. package/.llm/components/testimonialslider.md +1 -1
  70. package/.llm/components/textarea.md +1 -1
  71. package/.llm/components/textinput.md +1 -1
  72. package/.llm/components/tooltip.md +1 -1
  73. package/.llm/components/typography.md +1 -1
  74. package/.llm/digest/background.md +52 -0
  75. package/.llm/digest/ctabanner.md +4 -0
  76. package/.llm/digest/ctabannerslider.md +3 -0
  77. package/.llm/digest/illustrationcard.md +46 -0
  78. package/.llm/digest/illustrationscene.md +51 -0
  79. package/.llm/digest/landinghero.md +7 -0
  80. package/.llm/digest/pageheader.md +3 -0
  81. package/.llm/index.md +7 -0
  82. package/.llm/rules.md +20 -10
  83. package/.llm/tokens.md +11 -0
  84. package/AGENTS.md +7 -5
  85. package/dist/components/blocks/cta-banner/CtaBanner.d.ts +1 -1
  86. package/dist/components/blocks/cta-banner/CtaBanner.js +3 -2
  87. package/dist/components/blocks/cta-banner/CtaBanner.types.d.ts +5 -0
  88. package/dist/components/blocks/cta-banner/cta-banner-impl.css +8 -0
  89. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.d.ts +1 -1
  90. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.js +5 -4
  91. package/dist/components/blocks/cta-banner-slider/CtaBannerSlider.types.d.ts +4 -0
  92. package/dist/components/blocks/cta-banner-slider/cta-banner-slider-impl.css +8 -0
  93. package/dist/components/blocks/landing-hero/LandingHero.d.ts +1 -1
  94. package/dist/components/blocks/landing-hero/LandingHero.js +4 -3
  95. package/dist/components/blocks/landing-hero/LandingHero.types.d.ts +7 -3
  96. package/dist/components/blocks/page-header/PageHeader.d.ts +1 -1
  97. package/dist/components/blocks/page-header/PageHeader.js +3 -2
  98. package/dist/components/blocks/page-header/PageHeader.types.d.ts +4 -0
  99. package/dist/components/blocks/page-header/page-header-impl.css +8 -0
  100. package/dist/components/core/background/Background.css +69 -0
  101. package/dist/components/core/background/Background.d.ts +21 -0
  102. package/dist/components/core/background/Background.js +36 -0
  103. package/dist/components/core/background/Background.types.d.ts +36 -0
  104. package/dist/components/core/background/Background.types.js +9 -0
  105. package/dist/components/core/background/background-impl.css +427 -0
  106. package/dist/components/core/background/halftone.d.ts +123 -0
  107. package/dist/components/core/background/halftone.js +682 -0
  108. package/dist/components/core/background/use-halftone.d.ts +15 -0
  109. package/dist/components/core/background/use-halftone.js +27 -0
  110. package/dist/components/core/background/use-offscreen.d.ts +10 -0
  111. package/dist/components/core/background/use-offscreen.js +30 -0
  112. package/dist/components/core/background/use-spotlight.d.ts +13 -0
  113. package/dist/components/core/background/use-spotlight.js +65 -0
  114. package/dist/components/core/illustration-card/IllustrationCard.css +40 -0
  115. package/dist/components/core/illustration-card/IllustrationCard.d.ts +22 -0
  116. package/dist/components/core/illustration-card/IllustrationCard.js +82 -0
  117. package/dist/components/core/illustration-card/IllustrationCard.types.d.ts +23 -0
  118. package/dist/components/core/illustration-card/IllustrationCard.types.js +8 -0
  119. package/dist/components/core/illustration-card/illustration-card-impl.css +27 -0
  120. package/dist/components/core/illustration-scene/IllustrationScene.css +44 -0
  121. package/dist/components/core/illustration-scene/IllustrationScene.d.ts +14 -0
  122. package/dist/components/core/illustration-scene/IllustrationScene.js +16 -0
  123. package/dist/components/core/illustration-scene/IllustrationScene.types.d.ts +33 -0
  124. package/dist/components/core/illustration-scene/IllustrationScene.types.js +8 -0
  125. package/dist/components/core/illustration-scene/illustration-scene-impl.css +40 -0
  126. package/dist/components/core/link/Link.js +5 -5
  127. package/dist/components/core/tab/Tab.types.d.ts +1 -1
  128. package/dist/components/index.d.ts +6 -0
  129. package/dist/components/index.js +6 -0
  130. package/dist/marketing.css +24 -0
  131. package/package.json +1 -1
package/.llm/rules.md CHANGED
@@ -25,7 +25,7 @@ Guidance only — cannot be checked automatically.
25
25
 
26
26
  ### Never nest one interactive element inside another
27
27
 
28
- Nested interactive elements produce ambiguous hit targets and an unreachable inner control for keyboard and screen-reader users. This is why the rule cannot live on any single component — it constrains Button, Link, Input and Switch simultaneously.
28
+ Nested interactive elements produce ambiguous hit targets and an unreachable inner control for keyboard and screen-reader users. This is why the rule cannot live on any single component — it constrains Button, Link, CardButton and TextInput simultaneously.
29
29
 
30
30
  Guidance only — cannot be checked automatically.
31
31
 
@@ -109,21 +109,25 @@ Guidance only — cannot be checked automatically.
109
109
 
110
110
  ## Component rules
111
111
 
112
- ### Destructive actions require a confirmation step (`Button`) *(applies to something not built yet)*
112
+ ### Light weight needs size to survive, and the largest step wants it (`Icon`)
113
113
 
114
- An irreversible action taken by mis-click has no recovery path. The confirmation must name what will be lost, not merely ask whether the user is sure.
114
+ Quill ships every system glyph three times — light, regular and solid — as separate components chosen at the import site. Icon has no weight prop and cannot have one, so nothing in this system can observe the choice, let alone enforce it.
115
+
116
+ Size settles the light weight, from both ends. At xs its thinnest strokes fall below a pixel and drop out, leaving a shape that reads as broken rather than as light, so light belongs at md and above. At the top of the ramp it is the better weight rather than merely an allowed one: a glyph carries its stroke weight through any scale, so a regular glyph drawn for 24px renders its strokes at twice that width at xl, and reads as heavy rather than confident. A marketing page puts glyphs at that size — a callout, a highlighted block — where a product screen rarely does, which is why this half is Marketing's own. At rest, regular from sm through lg, and light at xl.
117
+
118
+ What size does not settle is regular against solid. That is a question about what the glyph means, and icon-weight-within-a-set answers it. This rule previously claimed size decided all three, and named steps above xl that the ramp no longer has — the first is the fault Product's version of this rule was corrected for.
115
119
 
116
120
  Guidance only — cannot be checked automatically.
117
121
 
118
- ### Solid weight means selected (`Icon`) *(applies to something not built yet)*
122
+ ### In a set of icons, weight marks the current one (`Icon`)
119
123
 
120
- Solid is not an alternative style, it is what a selected icon looks like. A tab or a navigation item renders regular at rest and swaps to solid when it becomes the current one, so the weight change is what carries the selection. The swap changes weight only — the same glyph at the same size, so the shape stays recognisable and just fills in. Marketing has fewer selected states than Product does, which is the reason to hold the meaning rather than to relax it: a solid glyph used decoratively here teaches a reader that solid means nothing, and they carry that into the product.
124
+ Quill ships every system glyph three times — light, regular and solid — as separate components chosen at the import site, and Icon has no weight prop, so nothing here can observe the choice.
121
125
 
122
- Guidance only — cannot be checked automatically.
126
+ Within a set of sibling icons — a row of tabs, a segmented control — the weight is what says which one is current: regular at rest, solid once selected, the same glyph at the same size so the shape stays recognisable and simply fills in. That is the one cue for selection that does not depend on colour, and it survives only while nothing else in the set is solid. Reaching for solid inside a set because it looks better spends it.
123
127
 
124
- ### Match the glyph weight to the icon size (`Icon`) *(applies to something not built yet)*
128
+ Outside a set there is nothing to distinguish, and solid claims nothing about selection. The arrow after a standalone link, a mark beside a heading, an icon in a button — none of these sit among siblings competing to be current, and a filled shape is recognised faster than an outlined one, which is the reason to show a glyph rather than words.
125
129
 
126
- Quill ships every system glyph three times — light, regular and solid — as separate components chosen at the import site. Icon has no weight prop and cannot have one, so nothing in this system can observe the choice, let alone enforce it. Size is what should decide it: a glyph carries its stroke weight through any scale, so a regular-weight glyph drawn for 24px renders its strokes at over three times that width when placed at display size, and reads as heavy rather than confident. At rest, use regular from sm through lg, and light at xl, 2xl and display — selection is the one thing that overrides this, and it only ever moves regular to solid.
130
+ This replaces a rule that said solid means selected everywhere. Marketing has more lone glyphs than Product and fewer sets, so the broad version cost it most: Link's solid arrow had to be recorded as an exception to it, when the arrow was never the case the rule protects.
127
131
 
128
132
  Guidance only — cannot be checked automatically.
129
133
 
@@ -139,8 +143,14 @@ Guidance rather than a count. The bar is drawn with a second copy of an action i
139
143
 
140
144
  Guidance only — cannot be checked automatically.
141
145
 
142
- ### At most one primary button per view (`Button`)
146
+ ### At most one primary button in a group of actions (`Button`)
147
+
148
+ Two brand fills side by side ask the visitor to choose between two things that look equally urgent, and nothing tells them which one the page wants. So a group of actions — a hero's pair, a banner's pair, the buttons in a navigation bar — leads with at most one primary, and whatever sits beside it is secondary.
149
+
150
+ The limit belongs to the group, not the page. A landing page asks for the same thing more than once on purpose: the sign-up in the bar, again under the opening claim, again in a banner halfway down and in the closing band. Each of those is one ask repeated where the reader has arrived, not a second ask competing with the first, and a rule that counted primaries across the page would forbid the thing a marketing page is built to do. This rule came from Product as one primary per view, and there it is right: a view is one screen with one task, and two primaries on it really are two tasks. On a page that scrolls through several sections the view is the wrong unit. The group is the one a reader compares.
151
+
152
+ The group can lose its primary without a second one appearing. Inverse on the ordinary page ground carries roughly six times the contrast of the brand, so the primary beside it stops reading as the primary — the same fault, reached from the other side.
143
153
 
144
- Two primary actions compete for attention; the user cannot tell which one the system wants them to take.
154
+ A navigation bar is the group with the tightest limit, and navbar-actions-are-a-pair states it: two buttons at most, one of each.
145
155
 
146
156
  Machine-checkable — `dds lint` will enforce this once built.
package/.llm/tokens.md CHANGED
@@ -45,6 +45,7 @@ one value at every width.
45
45
  - `--color-bg-surface-card-raised` — A card lifting off the ground behind it, e.g. a Card or CardButton on a color.bg.subtle band. White in light, white at six in dark, so a dark card tints the band instead of sitting on it as grey. Menus and popovers take color.bg.surface-raised.
46
46
  - `--color-bg-surface-card-strong` — The dark fill of a card weightier than its neighbours, e.g. an accent-strong Card or CardButton. gray.900 in both modes, a step lighter than color.bg.brand-strong so it reads as a card, not a band.
47
47
  - `--color-bg-surface-card-strong-raised` — The strong card on a subtle band. gray.900 in light mode, the strong card's own fill, which already stands off the gray.50 band; gray.800 in dark, where the band is a near-black tint the strong card would blend into. Pair with color.fg.on-brand-strong.
48
+ - `--color-bg-surface-glass` — A pane of glass over whatever is behind it, e.g. an IllustrationCard on a band. Light glass in light mode, a faint frosted white in dark, with a blur behind it in both; ink is color.fg.default.
48
49
  - `--color-bg-surface-inverse` — The quieter of the two surfaces on a dark ground, for a panel rather than a pill. color.bg.surface-raised-inverse is the stronger lift and is right on a control the size of a navbar's; at that weight a card-sized box reads as a pale patch rather than a surface.
49
50
  - `--color-bg-surface-modal` — A surface that covers the page behind a scrim — dialogs, drawers, bottom sheets. The topmost plane there is, so it stays white in light where color.bg.surface is recessed. For one that floats without dimming what is under it use color.bg.surface-raised.
50
51
  - `--color-bg-surface-raised` — A surface that lifts off the one behind it, e.g. a dropdown, a popover, a card on a color.bg.subtle band. A card on the page itself takes color.bg.surface, which reads as recessed against it instead.
@@ -66,6 +67,8 @@ one value at every width.
66
67
  - `--color-border-focus-inverse` — The focus ring on a ground that runs against the mode. color.border.focus is near-black, which is invisible on a dark bar — and a focus ring that cannot be seen is the one affordance a keyboard reader has nothing to fall back on.
67
68
  - `--color-border-focus-on-brand-strong` — The focus ring on color.bg.brand-strong, e.g. a navigation bar's controls on a dark hero, a footer's links. White in both modes, because the band does not change. For a surface that flips with the mode use color.border.focus-inverse.
68
69
  - `--color-border-info` — Informational accent border, e.g. info banner edge, highlighted section.
70
+ - `--color-border-pattern` — The resting lines of a decorative rule grid on the page's ground. A step above color.border.subtle, which a hairline across a whole band thins to nothing; well below the default border, so the grid reads as texture.
71
+ - `--color-border-pattern-inverse` — The resting lines of a decorative rule grid on a dark or coral ground. White at a quarter in both modes, as those grounds are; a step above color.border.subtle-inverse, which a hairline grid thins to nothing.
69
72
  - `--color-border-strong` — Emphasised border for active or prominent elements, e.g. active input, selected card edge.
70
73
  - `--color-border-subtle` — A quiet separator, e.g. dividers, table rules. Alpha-based — works on any surface.
71
74
  - `--color-border-subtle-inverse` — A quiet rule on a dark ground. The counterpart to color.border.subtle, which is black at 5% and invisible on one.
@@ -127,6 +130,7 @@ one value at every width.
127
130
 
128
131
  ## size
129
132
 
133
+ - `--size-aurora-spotlight` — How far the pool of light trailing the pointer across an aurora reaches, as a radius. Small enough to read as light the pointer carries rather than a wash over the band.
130
134
  - `--size-avatar` — A person's picture beside their words — the author of a testimonial. Forty-eight, round: large enough to recognise a face, small enough to stay a signature under the quote rather than a portrait above it.
131
135
  - `--size-backdrop-blur` — How far a see-through surface blurs what is behind it, e.g. the navigation bar over the page it scrolls across. Enough that the words behind it stop reading as words, so the bar's own stay legible.
132
136
  - `--size-band-compact` — The least height an opening band takes when it is not a screen tall — a company, legal or support page, where a full viewport would be a wall before the first sentence. Deep enough that the band reads as a band rather than a gap the title fell into.
@@ -143,6 +147,8 @@ one value at every width.
143
147
  - `--size-grid-track-lg` — For a cell holding a picture as well as words, or a pricing plan with a list under it. Three fit the page's measure and four do not, which is what makes this the floor for a grid meant to read as three across.
144
148
  - `--size-grid-track-md` — The default track floor, for a feature card: a heading and a line or two beneath it. Four of these fit the page's measure, so a grid asking for three has room to spare and drops to two on a tablet rather than at the first sign of pressure.
145
149
  - `--size-grid-track-sm` — The narrowest a grid track goes before the grid drops a column. For a cell holding a logo, an icon and a word, or one short claim — six of them clear the page's measure with the page gutter still on both sides.
150
+ - `--size-halftone-dot` — The widest a halftone dot grows, at the brightest point of the glow. Six against size.halftone.pitch's twenty, so even the largest dots stand well apart and the pattern reads as a fine screen of light rather than a field of discs.
151
+ - `--size-halftone-pitch` — How far apart a halftone background's dots sit, centre to centre. A pitch rather than a gap, as size.rule-grid.pitch is: how often a decorative mark recurs. Twenty, against a six-pixel dot: a fine screen that reads as a tone across a band.
146
152
  - `--size-heading-group-max` — How wide an eyebrow, a title and a description may run as a block. Independent or centred groups take this; one that has to sit on the grid takes eight columns instead, which is a span rather than a measure.
147
153
  - `--size-logo-columns-height` — How much of a LogoColumns wall shows at once, from the fold. Tall enough that a column reads as running on past both edges; half of it on a phone.
148
154
  - `--size-logo-rows-tile` — The width of a tile on a LogoRows, from the fold; three quarters of it on a phone. Wide enough for a wordmark, narrow enough that a row shows several at once.
@@ -156,7 +162,10 @@ one value at every width.
156
162
  - `--size-menu-panel-min` — The shortest a section's menu panel gets, whatever it holds. A bar's sections rarely have the same number of destinations, and panels that each took their own height would change size as the reader moved along the row.
157
163
  - `--size-menu-max` — The tallest a menu's surface gets before its commands scroll. Product's measure, and counted in rows: five whole and part of a sixth at either size. Neither lands on a row boundary — a ceiling cutting clean at the last full row looks like the whole set.
158
164
  - `--size-review-card-min` — The shortest a TestimonialRow card gets. A floor, not a height: a one-line review keeps a card's shape, and a longer one still grows it.
165
+ - `--size-rings-pitch` — How far apart the rings on a band sit, counted out from the middle of its foot. A pitch, as size.rule-grid.pitch is; tighter than the grid's, because rings spaced that wide read as a target rather than a ripple.
166
+ - `--size-rings-spotlight` — How far the pointer's light reaches on the rings: the radius within which they brighten around the pointer and fade to the resting rings. The rule grid's reach, so the two answer the pointer alike.
159
167
  - `--size-rule-grid-pitch` — How often the rule grid repeats on a band that paints one. A pitch rather than a gap: it is how far apart a decorative line recurs, not the space between two things a reader is comparing.
168
+ - `--size-rule-grid-spotlight` — How far the pointer's light reaches on a rule grid: the radius within which the lines brighten around the pointer and fade to the resting grid. A little over a square either way: light under the pointer, not a lit band.
160
169
  - `--size-sheet-handle-height` — The thickness of that handle. At the smallest step the scale has, because a grab bar is a hint that the sheet moves rather than a part of what it holds.
161
170
  - `--size-sheet-handle-width` — The width of the grab handle at the top of a BottomSheet. Wide enough to take hold of, narrow enough to read as a grip rather than as a rule across the sheet.
162
171
  - `--size-sheet-max` — The widest a BottomSheet gets before it stops growing and centres. A sheet is a small-screen pattern, and one running the width of a desktop is a dialog that arrived from the wrong edge.
@@ -349,6 +358,7 @@ one value at every width.
349
358
 
350
359
  - `--motion-distance-drift` — How far decoration travels against the scroll while its section passes through the view, one way or the other. Far enough to be seen moving as the reader scrolls past, so it reads as depth.
351
360
  - `--motion-duration-dramatic` — Editorial hero entrances for visual impact, e.g. hero headline revealing, landing page title appearing. Not for scroll reveals — use motion.duration.reveal.
361
+ - `--motion-duration-drift` — One way of a slow drift that runs while something is on screen — an aurora wandering along a band's foot and back. Ten seconds, so the movement is felt rather than watched.
352
362
  - `--motion-duration-dwell` — How long a rotating message holds before the next one moves in, e.g. a CTA banner's slides. Long enough to read a headline and a line under it; the reference site's 2000 moved before a reader reached the button.
353
363
  - `--motion-duration-fast` — Direct user actions that should feel instant, e.g. hover, press, toggle.
354
364
  - `--motion-duration-instant` — A change the reader has already moved past — a tooltip leaving once the pointer is elsewhere. Not a short duration but no duration: animating a dismissal keeps a thing on screen after the reason for it has gone.
@@ -360,6 +370,7 @@ one value at every width.
360
370
  - `--motion-duration-slow` — Large UI transitions, e.g. accordions expanding, page transitions, interactive demos.
361
371
  - `--motion-duration-spinner` — One full rotation of a spinner. Fast enough to read as working, slow enough that the stroke does not blur into a solid ring.
362
372
  - `--motion-duration-stagger` — The step between one part of a section arriving and the next. A gap rather than a length: five parts at this step trail the first by less than half a second, so a block still arrives as one thing rather than assembling itself while a reader waits.
373
+ - `--motion-duration-trail` — How long something takes to catch up with a pointer it trails — a background's glow following the pointer across a band. Half a second, so the light drifts after the hand rather than sticking to it: a trail is felt, a cursor is tracked.
363
374
  - `--motion-easing-enter` — UI elements arriving on screen — decelerates into place, e.g. dropdown opening, tooltip appearing.
364
375
  - `--motion-easing-exit` — UI elements leaving the screen — accelerates away, e.g. menu closing, notification dismissing.
365
376
  - `--motion-easing-media-hover` — Art moving inside a frame that holds still. Symmetric, unlike easing.standard: a hover runs forward on enter and backward on leave, so a one-way curve that settles hard reads as a snap going one way and a drift the other.
package/AGENTS.md CHANGED
@@ -58,13 +58,12 @@ a component you give `onChange` or `onClick` to goes in a file of your own that
58
58
  - **A section that paints a ground sets the ink to go with it**
59
59
  - **A section's title is centred over stacked content, starts beside content in columns, and starts on a phone**
60
60
  - **Anything a pointer has to hit is at least 24px in both directions**
61
- - **At most one primary button per view**
62
- - **Destructive actions require a confirmation step**
61
+ - **At most one primary button in a group of actions**
63
62
  - **Every gap inside a section names the relationship it expresses, and the same relationship keeps its measure when it turns on its side**
64
63
  - **Never nest one interactive element inside another**
65
64
  - **On a phone a section's actions stack and each fills the width, including when there is only one**
66
- - **Match the glyph weight to the icon size** *(guidance)*
67
- - **Solid weight means selected** *(guidance)*
65
+ - **In a set of icons, weight marks the current one** *(guidance)*
66
+ - **Light weight needs size to survive, and the largest step wants it** *(guidance)*
68
67
 
69
68
  Reasons and the cases each one turns on: `.llm/rules.md`.
70
69
 
@@ -81,6 +80,7 @@ screen is built in is not something to reach for while building a control.
81
80
 
82
81
  - **Accordion** — Questions a reader asks near a decision
83
82
  - **AccordionItem** — One row inside an Accordion
83
+ - **Background** — A band that has to stand apart from the page around it with more than a change of fill
84
84
  - **Badge** — One of a set is the one the page wants read first
85
85
  - **BottomSheet** — A task needs the screen's full width and the reader's undivided attention
86
86
  - **Breadcrumb** — The page sits inside a section hierarchy, and the levels above it are places the visitor can navigate to
@@ -97,6 +97,8 @@ screen is built in is not something to reach for while building a control.
97
97
  - **Footer** — A marketing page needs its closing band
98
98
  - **FooterGroup** — A set of a footer's destinations belong under one word
99
99
  - **Icon** — A glyph stands beside words to speed recognition
100
+ - **IllustrationCard** — Product information laid over a photograph or a piece of art
101
+ - **IllustrationScene** — Product information shown over a photograph
100
102
  - **Link** — A term inside running copy has a page of its own
101
103
  - **MenuItem** — A row in a menu of destinations
102
104
  - **Navbar** — A marketing page needs its top-level navigation
@@ -168,7 +170,7 @@ saying what to mount and where.
168
170
  ## Reading order
169
171
 
170
172
  1. This file.
171
- 2. `.llm/rules.md` before writing anything — 1,500 tokens, and it is where the constraints
173
+ 2. `.llm/rules.md` before writing anything — 5,200 tokens, and it is where the constraints
172
174
  that no single component can state live.
173
175
  3. `.llm/digest/<name>.md` for every component you are using — **all of it, not the part
174
176
  you were looking for**. Around 1,400 tokens each: what it is for, what to reach for
@@ -19,4 +19,4 @@ export type { CtaBannerProps };
19
19
  * It arrives the way every block does, and the way CtaBannerSlider's slides do: block-reveal.css
20
20
  * brings the headline, the line and the actions up a step apart once the band is in view.
21
21
  */
22
- export declare function CtaBanner({ title, headingLevel, description, actions, }: CtaBannerProps): import("react").JSX.Element;
22
+ export declare function CtaBanner({ title, headingLevel, description, actions, background, }: CtaBannerProps): import("react").JSX.Element;
@@ -6,6 +6,7 @@ import './cta-banner-impl.css';
6
6
  import '../block-reveal.css';
7
7
  import { arriving, useReveal } from '../use-reveal.js';
8
8
  import { given } from '../../given.js';
9
+ import { Background } from '../../core/background/Background.js';
9
10
  import { ctaBannerDefaults, ctaBannerParts } from './CtaBanner.types.js';
10
11
  /**
11
12
  * A full-bleed coral band carrying one promotion and the action it asks for.
@@ -23,9 +24,9 @@ import { ctaBannerDefaults, ctaBannerParts } from './CtaBanner.types.js';
23
24
  * It arrives the way every block does, and the way CtaBannerSlider's slides do: block-reveal.css
24
25
  * brings the headline, the line and the actions up a step apart once the band is in view.
25
26
  */
26
- export function CtaBanner({ title, headingLevel = ctaBannerDefaults.headingLevel, description, actions, }) {
27
+ export function CtaBanner({ title, headingLevel = ctaBannerDefaults.headingLevel, description, actions, background, }) {
27
28
  const titleId = useId();
28
29
  const [band, revealed] = useReveal();
29
30
  const Title = headingLevel === 'h3' ? 'h3' : 'h2';
30
- return (_jsx("section", { className: ctaBannerParts.root, "aria-labelledby": titleId, children: _jsx("div", { className: ctaBannerParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: ctaBannerParts.content, children: [_jsxs("div", { className: ctaBannerParts.text, children: [_jsx(Title, { className: ctaBannerParts.title, id: titleId, ...arriving(0), children: title }), given(description) ? (_jsx("p", { className: ctaBannerParts.description, ...arriving(1), children: description })) : null] }), actions ? (_jsx("div", { className: ctaBannerParts.actions, ...arriving(2), children: actions })) : null] }) }) }));
31
+ return (_jsxs("section", { className: ctaBannerParts.root, "data-background": background, "aria-labelledby": titleId, children: [background === undefined ? null : (_jsx(Background, { tone: "brand", pattern: background, children: null })), _jsx("div", { className: ctaBannerParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: ctaBannerParts.content, children: [_jsxs("div", { className: ctaBannerParts.text, children: [_jsx(Title, { className: ctaBannerParts.title, id: titleId, ...arriving(0), children: title }), given(description) ? (_jsx("p", { className: ctaBannerParts.description, ...arriving(1), children: description })) : null] }), actions ? (_jsx("div", { className: ctaBannerParts.actions, ...arriving(2), children: actions })) : null] }) })] }));
31
32
  }
@@ -11,6 +11,11 @@ export interface CtaBannerOwnProps {
11
11
  description?: string;
12
12
  /** The one action the message asks for, or two at most. The coral is the banner's ground, so the primary is Button variant="inverse" tone="brand" — a primary Button is coral on coral, and the tone keeps it near-black in both themes — and a second is Button variant="secondary" tone="brand" — a white tint with a white label, fixed in both themes like the band. */
13
13
  actions?: ReactNode;
14
+ /**
15
+ * `halftone` — A halftone drawn behind the band's content: white dots on the band's own coral, following the pointer over the band. Drawn by Background, placed as a layer under the content, so the band's own layout is untouched.
16
+ * `grid` — A fine rule grid behind the band's content: white hairlines on the coral, fading in towards the foot, brightening in a soft circle around the pointer. Drawn by Background as a layer under the band's own content, by the stylesheet alone — quieter than the halftone, and nothing runs at rest
17
+ */
18
+ background?: 'halftone' | 'grid';
14
19
  }
15
20
  export type CtaBannerProps = CtaBannerOwnProps;
16
21
  /** The class each part carries. Apply these — do not retype them. */
@@ -27,3 +27,11 @@
27
27
  justify-content: center;
28
28
  }
29
29
  }
30
+
31
+ /*
32
+ * The banner's column, lifted over the pattern layer. It is not positioned otherwise — the banner
33
+ * has no other layer to clear — so it is lifted only where the pattern is drawn.
34
+ */
35
+ :where([data-dds-context="marketing"]) .dds-ctabanner[data-background] .dds-ctabanner__inner {
36
+ position: relative;
37
+ }
@@ -36,4 +36,4 @@ export type { CtaBannerSliderProps };
36
36
  * the same rise, a step apart. The slide leaving drops to waiting at once, as that sheet has every
37
37
  * part do, so the outgoing words never travel.
38
38
  */
39
- export declare function CtaBannerSlider({ slides, headingLevel, autoplay, label, controlsLabel, slideLabel, }: CtaBannerSliderProps): import("react").JSX.Element | null;
39
+ export declare function CtaBannerSlider({ slides, headingLevel, autoplay, label, controlsLabel, slideLabel, background, }: CtaBannerSliderProps): import("react").JSX.Element | null;
@@ -4,6 +4,7 @@ import './CtaBannerSlider.css';
4
4
  import './cta-banner-slider-impl.css';
5
5
  import '../block-reveal.css';
6
6
  import { arriving, useReveal } from '../use-reveal.js';
7
+ import { Background } from '../../core/background/Background.js';
7
8
  import { useRotation } from '../use-rotation.js';
8
9
  import { given } from '../../given.js';
9
10
  import { Button } from '../../core/button/Button.js';
@@ -42,7 +43,7 @@ import { ctaBannerSliderDefaults, ctaBannerSliderParts, } from './CtaBannerSlide
42
43
  * the same rise, a step apart. The slide leaving drops to waiting at once, as that sheet has every
43
44
  * part do, so the outgoing words never travel.
44
45
  */
45
- export function CtaBannerSlider({ slides, headingLevel = ctaBannerSliderDefaults.headingLevel, autoplay = ctaBannerSliderDefaults.autoplay, label = ctaBannerSliderDefaults.label, controlsLabel = ctaBannerSliderDefaults.controlsLabel, slideLabel = ctaBannerSliderDefaults.slideLabel, }) {
46
+ export function CtaBannerSlider({ slides, headingLevel = ctaBannerSliderDefaults.headingLevel, autoplay = ctaBannerSliderDefaults.autoplay, label = ctaBannerSliderDefaults.label, controlsLabel = ctaBannerSliderDefaults.controlsLabel, slideLabel = ctaBannerSliderDefaults.slideLabel, background, }) {
46
47
  const Title = headingLevel === 'h3' ? 'h3' : 'h2';
47
48
  const count = slides.length;
48
49
  // The band arriving on screen, as every block does; until then no slide has arrived.
@@ -54,7 +55,7 @@ export function CtaBannerSlider({ slides, headingLevel = ctaBannerSliderDefaults
54
55
  // leads a screen reader to an empty band.
55
56
  if (count === 0)
56
57
  return null;
57
- return (_jsx("section", { ref: root, className: ctaBannerSliderParts.root, "aria-label": label, ...region, children: _jsxs("div", { className: ctaBannerSliderParts.inner, ref: band, children: [_jsx("div", { className: ctaBannerSliderParts.track, ...track, children: slides.map((slide, at) => (_jsxs("div", { className: ctaBannerSliderParts.slide, ...item(at), "data-revealed": revealed && !away(at), children: [_jsxs("div", { className: ctaBannerSliderParts.text, children: [_jsx(Title, { className: ctaBannerSliderParts.title, ...arriving(0), children: slide.title }), given(slide.description) ? (_jsx("p", { className: ctaBannerSliderParts.description, ...arriving(1), children: slide.description })) : null] }), slide.primaryAction ? (_jsxs("div", { className: ctaBannerSliderParts.actions, ...arriving(2), children: [_jsx(Button, { variant: "inverse", tone: "brand", href: slide.primaryAction.href, children: slide.primaryAction.label }), slide.secondaryAction ? (_jsx(Button, { variant: "secondary", tone: "brand", href: slide.secondaryAction.href, children: slide.secondaryAction.label })) : null] })) : null] }, at))) }), rotating ? (_jsx("div", { className: ctaBannerSliderParts.controls, children: _jsx(PaginationDots, { ...dots,
58
- /* A dot pressed stops the dots' own rotation; the band's press handler stops this. */
59
- label: controlsLabel, itemLabel: slideLabel }) })) : null] }) }));
58
+ return (_jsxs("section", { ref: root, className: ctaBannerSliderParts.root, "aria-label": label, "data-background": background, ...region, children: [background === undefined ? null : (_jsx(Background, { tone: "brand", pattern: background, children: null })), _jsxs("div", { className: ctaBannerSliderParts.inner, ref: band, children: [_jsx("div", { className: ctaBannerSliderParts.track, ...track, children: slides.map((slide, at) => (_jsxs("div", { className: ctaBannerSliderParts.slide, ...item(at), "data-revealed": revealed && !away(at), children: [_jsxs("div", { className: ctaBannerSliderParts.text, children: [_jsx(Title, { className: ctaBannerSliderParts.title, ...arriving(0), children: slide.title }), given(slide.description) ? (_jsx("p", { className: ctaBannerSliderParts.description, ...arriving(1), children: slide.description })) : null] }), slide.primaryAction ? (_jsxs("div", { className: ctaBannerSliderParts.actions, ...arriving(2), children: [_jsx(Button, { variant: "inverse", tone: "brand", href: slide.primaryAction.href, children: slide.primaryAction.label }), slide.secondaryAction ? (_jsx(Button, { variant: "secondary", tone: "brand", href: slide.secondaryAction.href, children: slide.secondaryAction.label })) : null] })) : null] }, at))) }), rotating ? (_jsx("div", { className: ctaBannerSliderParts.controls, children: _jsx(PaginationDots, { ...dots,
59
+ /* A dot pressed stops the dots' own rotation; the band's press handler stops this. */
60
+ label: controlsLabel, itemLabel: slideLabel }) })) : null] })] }));
60
61
  }
@@ -17,6 +17,10 @@ export interface CtaBannerSliderOwnProps {
17
17
  controlsLabel?: string;
18
18
  /** The word each dot is announced by, before its number — 'Slide 2'. Translate it with the page, as controlsLabel is; a page that translates only the group's name still has every dot announced in English. */
19
19
  slideLabel?: string;
20
+ /**
21
+ * `grid` — A fine rule grid behind the slides: white hairlines on the coral, fading in towards the foot, brightening in a soft circle around the pointer. Drawn by Background as a layer under the band's content, by the stylesheet alone, so it holds still while the slides move and costs nothing at rest
22
+ */
23
+ background?: 'grid';
20
24
  }
21
25
  export type CtaBannerSliderProps = CtaBannerSliderOwnProps;
22
26
  /** The class each part carries. Apply these — do not retype them. */
@@ -71,3 +71,11 @@
71
71
  justify-content: center;
72
72
  }
73
73
  }
74
+
75
+ /*
76
+ * The band's column, lifted over the pattern layer when `background` asks for one. Not positioned
77
+ * otherwise — the band has no other layer for it to clear.
78
+ */
79
+ :where([data-dds-context="marketing"]) .dds-ctabannerslider[data-background] .dds-ctabannerslider__inner {
80
+ position: relative;
81
+ }
@@ -38,4 +38,4 @@ export type { LandingHeroProps };
38
38
  * title and the description; a photograph that carried meaning they do not would be a hero that
39
39
  * reads as empty to anyone who cannot see it, and the contract says so.
40
40
  */
41
- export declare function LandingHero({ eyebrow, title, headingLevel, description, actions, media, backgroundImage, }: LandingHeroProps): import("react").JSX.Element;
41
+ export declare function LandingHero({ eyebrow, title, headingLevel, description, actions, media, backgroundImage, background, }: LandingHeroProps): import("react").JSX.Element;
@@ -6,6 +6,7 @@ import './landing-hero-impl.css';
6
6
  import '../block-reveal.css';
7
7
  import { arriving, useReveal } from '../use-reveal.js';
8
8
  import { given } from '../../given.js';
9
+ import { Background } from '../../core/background/Background.js';
9
10
  import { landingHeroDefaults, landingHeroParts, } from './LandingHero.types.js';
10
11
  /**
11
12
  * The band that opens a marketing page.
@@ -42,13 +43,13 @@ import { landingHeroDefaults, landingHeroParts, } from './LandingHero.types.js';
42
43
  * title and the description; a photograph that carried meaning they do not would be a hero that
43
44
  * reads as empty to anyone who cannot see it, and the contract says so.
44
45
  */
45
- export function LandingHero({ eyebrow, title, headingLevel = landingHeroDefaults.headingLevel, description, actions, media, backgroundImage, }) {
46
+ export function LandingHero({ eyebrow, title, headingLevel = landingHeroDefaults.headingLevel, description, actions, media, backgroundImage, background, }) {
46
47
  const titleId = useId();
47
48
  const [band, revealed] = useReveal();
48
49
  const Title = headingLevel === 'h2' ? 'h2' : 'h1';
49
- return (_jsxs("section", { className: landingHeroParts.root, "aria-labelledby": titleId,
50
+ return (_jsxs("section", { className: landingHeroParts.root, "data-background": background, "aria-labelledby": titleId,
50
51
  /* The one inline style here, and it is a value rather than a decision: which photograph,
51
52
  supplied by the page. Everything about how it is drawn — the cover fit and the position — is in the stylesheet, where it can be reasoned about. A background shorthand
52
53
  would have reset those four properties every time the page passed an image. */
53
- style: given(backgroundImage) ? { backgroundImage: `url(${backgroundImage})` } : undefined, children: [media ? (_jsx("div", { className: landingHeroParts.media, "aria-hidden": "true", children: media })) : null, _jsx("div", { className: landingHeroParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: landingHeroParts.content, children: [_jsxs("div", { className: landingHeroParts.text, children: [given(eyebrow) ? (_jsx("p", { className: landingHeroParts.eyebrow, ...arriving(0), children: eyebrow })) : null, _jsx(Title, { className: landingHeroParts.title, ...arriving(1), id: titleId, children: title }), given(description) ? (_jsx("p", { className: landingHeroParts.description, ...arriving(2), children: description })) : null] }), actions ? (_jsx("div", { className: landingHeroParts.actions, ...arriving(3), children: actions })) : null] }) })] }));
54
+ style: given(backgroundImage) ? { backgroundImage: `url(${backgroundImage})` } : undefined, children: [background === 'halftone' ? _jsx(Background, { tone: "dark", children: null }) : null, media ? (_jsx("div", { className: landingHeroParts.media, "aria-hidden": "true", children: media })) : null, _jsx("div", { className: landingHeroParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: landingHeroParts.content, children: [_jsxs("div", { className: landingHeroParts.text, children: [given(eyebrow) ? (_jsx("p", { className: landingHeroParts.eyebrow, ...arriving(0), children: eyebrow })) : null, _jsx(Title, { className: landingHeroParts.title, ...arriving(1), id: titleId, children: title }), given(description) ? (_jsx("p", { className: landingHeroParts.description, ...arriving(2), children: description })) : null] }), actions ? (_jsx("div", { className: landingHeroParts.actions, ...arriving(3), children: actions })) : null] }) })] }));
54
55
  }
@@ -15,10 +15,14 @@ export interface LandingHeroOwnProps {
15
15
  actions?: ReactNode;
16
16
  /** The band's visual — the subject standing beside the claim, cropped by the band's own inline edge rather than floating clear of it */
17
17
  media?: ReactNode;
18
- /** A photograph filling the whole band. It is shown as it is — nothing is painted over it, so choose one already dark enough that the title reads on it */
19
- backgroundImage?: string;
20
18
  }
21
- export type LandingHeroProps = LandingHeroOwnProps;
19
+ export type LandingHeroProps = LandingHeroOwnProps & ({
20
+ background?: 'halftone';
21
+ backgroundImage?: undefined;
22
+ } | {
23
+ background?: undefined;
24
+ backgroundImage?: string;
25
+ });
22
26
  /** The class each part carries. Apply these — do not retype them. */
23
27
  export declare const landingHeroParts: {
24
28
  readonly root: "dds-landinghero";
@@ -31,4 +31,4 @@ export type { PageHeaderProps };
31
31
  * region landmark once it has a name, and a band that announced itself as an unnamed section is one
32
32
  * more thing a screen reader user walks through rather than a place they can skip to.
33
33
  */
34
- export declare function PageHeader({ eyebrow, title, headingLevel, description, actions, }: PageHeaderProps): import("react").JSX.Element;
34
+ export declare function PageHeader({ eyebrow, title, headingLevel, description, actions, background, }: PageHeaderProps): import("react").JSX.Element;
@@ -6,6 +6,7 @@ import './page-header-impl.css';
6
6
  import '../block-reveal.css';
7
7
  import { arriving, useReveal } from '../use-reveal.js';
8
8
  import { given } from '../../given.js';
9
+ import { Background } from '../../core/background/Background.js';
9
10
  import { pageHeaderDefaults, pageHeaderParts } from './PageHeader.types.js';
10
11
  /**
11
12
  * The band that opens a company or a legal page.
@@ -35,9 +36,9 @@ import { pageHeaderDefaults, pageHeaderParts } from './PageHeader.types.js';
35
36
  * region landmark once it has a name, and a band that announced itself as an unnamed section is one
36
37
  * more thing a screen reader user walks through rather than a place they can skip to.
37
38
  */
38
- export function PageHeader({ eyebrow, title, headingLevel = pageHeaderDefaults.headingLevel, description, actions, }) {
39
+ export function PageHeader({ eyebrow, title, headingLevel = pageHeaderDefaults.headingLevel, description, actions, background, }) {
39
40
  const titleId = useId();
40
41
  const [band, revealed] = useReveal();
41
42
  const Title = headingLevel === 'h2' ? 'h2' : 'h1';
42
- return (_jsx("section", { className: pageHeaderParts.root, "aria-labelledby": titleId, children: _jsx("div", { className: pageHeaderParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: pageHeaderParts.content, children: [_jsxs("div", { className: pageHeaderParts.text, children: [given(eyebrow) ? (_jsx("p", { className: pageHeaderParts.eyebrow, ...arriving(0), children: eyebrow })) : null, _jsx(Title, { className: pageHeaderParts.title, ...arriving(1), id: titleId, children: title }), given(description) ? (_jsx("p", { className: pageHeaderParts.description, ...arriving(2), children: description })) : null] }), actions ? (_jsx("div", { className: pageHeaderParts.actions, ...arriving(3), children: actions })) : null] }) }) }));
43
+ return (_jsxs("section", { className: pageHeaderParts.root, "data-background": background, "aria-labelledby": titleId, children: [background === 'halftone' ? _jsx(Background, { tone: "dark", children: null }) : null, _jsx("div", { className: pageHeaderParts.inner, ref: band, "data-revealed": revealed, children: _jsxs("div", { className: pageHeaderParts.content, children: [_jsxs("div", { className: pageHeaderParts.text, children: [given(eyebrow) ? (_jsx("p", { className: pageHeaderParts.eyebrow, ...arriving(0), children: eyebrow })) : null, _jsx(Title, { className: pageHeaderParts.title, ...arriving(1), id: titleId, children: title }), given(description) ? (_jsx("p", { className: pageHeaderParts.description, ...arriving(2), children: description })) : null] }), actions ? (_jsx("div", { className: pageHeaderParts.actions, ...arriving(3), children: actions })) : null] }) })] }));
43
44
  }
@@ -13,6 +13,10 @@ export interface PageHeaderOwnProps {
13
13
  description?: string;
14
14
  /** The one or two things the page wants a reader to do. Composed by the page rather than described to this component, so a call to action is the same Button here as anywhere else on the site */
15
15
  actions?: ReactNode;
16
+ /**
17
+ * `halftone` — A halftone drawn behind the band's content: coral dots blending to white at the glow's heart, on the band's own dark ground, following the pointer over the band. Drawn by Background, placed as a layer under the content, so the band's own layout is untouched. It replaces the rule grid rather than joining it: two patterns on one band are texture fighting texture.
18
+ */
19
+ background?: 'halftone';
16
20
  }
17
21
  export type PageHeaderProps = PageHeaderOwnProps;
18
22
  /** The class each part carries. Apply these — do not retype them. */
@@ -174,3 +174,11 @@
174
174
  inline-size: auto;
175
175
  }
176
176
  }
177
+
178
+ /*
179
+ * With the halftone the rule grid steps aside. Two patterns on one band are texture fighting
180
+ * texture, and the halftone is the one that was asked for.
181
+ */
182
+ :where([data-dds-context="marketing"]) .dds-pageheader[data-background='halftone'] {
183
+ background-image: none;
184
+ }
@@ -0,0 +1,69 @@
1
+ /* GENERATED from background.contract.json. Do not edit. */
2
+
3
+ :where([data-dds-context="marketing"]) .dds-background {
4
+ align-content: center;
5
+ display: grid;
6
+ inline-size: 100%;
7
+ isolation: isolate;
8
+ justify-items: center;
9
+ overflow: hidden;
10
+ position: relative;
11
+ }
12
+
13
+ :where([data-dds-context="marketing"]) .dds-background .dds-background__content {
14
+ box-sizing: border-box;
15
+ inline-size: 100%;
16
+ text-align: center;
17
+ max-inline-size: var(--size-content-max);
18
+ padding-inline: var(--space-inset-page);
19
+ }
20
+
21
+ :where([data-dds-context="marketing"]) .dds-background .dds-background__canvas {
22
+ transition-duration: var(--motion-duration-trail);
23
+ transition-timing-function: var(--motion-easing-enter);
24
+ }
25
+
26
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="brand"] {
27
+ background-color: var(--color-bg-brand);
28
+ }
29
+
30
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="brand"] .dds-background__canvas {
31
+ color: var(--color-fg-on-brand);
32
+ outline-color: var(--color-fg-on-brand);
33
+ }
34
+
35
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="brand"] .dds-background__content {
36
+ color: var(--color-fg-on-brand);
37
+ }
38
+
39
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="dark"] {
40
+ background-color: var(--color-bg-brand-strong);
41
+ }
42
+
43
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="dark"] .dds-background__canvas {
44
+ color: var(--color-bg-brand);
45
+ outline-color: var(--color-fg-on-brand-strong);
46
+ }
47
+
48
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="dark"] .dds-background__content {
49
+ color: var(--color-fg-on-brand-strong);
50
+ }
51
+
52
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="light"] {
53
+ background-color: var(--color-bg-page);
54
+ }
55
+
56
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="light"] .dds-background__canvas {
57
+ color: var(--color-bg-brand);
58
+ outline-color: var(--color-bg-brand);
59
+ }
60
+
61
+ :where([data-dds-context="marketing"]) .dds-background[data-tone="light"] .dds-background__content {
62
+ color: var(--color-fg-default);
63
+ }
64
+
65
+ @media (prefers-reduced-motion: reduce) {
66
+ :where([data-dds-context="marketing"]) .dds-background,
67
+ :where([data-dds-context="marketing"]) .dds-background * { transition: none !important; }
68
+ }
69
+
@@ -0,0 +1,21 @@
1
+ import './Background.css';
2
+ import './background-impl.css';
3
+ import { type BackgroundProps } from './Background.types.js';
4
+ export type { BackgroundProps };
5
+ /**
6
+ * A ground a section or a block sits on, painted with a pattern.
7
+ *
8
+ * The band paints the tone's flat colour, the pattern is laid over it, and the content over that.
9
+ * The flat colour is what shows before anything has drawn — on a server-rendered page, until
10
+ * hydration — so the band is finished from the first paint and the pattern only ever adds to it.
11
+ *
12
+ * Four patterns. The halftone is drawn by script on a canvas, through `useHalftone`, which owns
13
+ * every question of its cost: it draws only while something moves, pauses out of view and holds
14
+ * the bitmap to twice the pixel density. The aurora, the grid and the rings are painted by the
15
+ * stylesheet, with no canvas; `useSpotlight` only tells them where the pointer is.
16
+ *
17
+ * With nothing to hold it is a layer: it fills its positioned parent behind that parent's own
18
+ * content, and listens for the pointer on the parent, because the content covers it. That is how a
19
+ * block draws a pattern under itself without its own layout changing.
20
+ */
21
+ export declare function Background({ tone, pattern, animated, interactive, children, ...rest }: BackgroundProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import './Background.css';
4
+ import './background-impl.css';
5
+ import { backgroundDefaults, backgroundParts } from './Background.types.js';
6
+ import { useHalftone } from './use-halftone.js';
7
+ import { useOffscreen } from './use-offscreen.js';
8
+ import { useSpotlight } from './use-spotlight.js';
9
+ /**
10
+ * A ground a section or a block sits on, painted with a pattern.
11
+ *
12
+ * The band paints the tone's flat colour, the pattern is laid over it, and the content over that.
13
+ * The flat colour is what shows before anything has drawn — on a server-rendered page, until
14
+ * hydration — so the band is finished from the first paint and the pattern only ever adds to it.
15
+ *
16
+ * Four patterns. The halftone is drawn by script on a canvas, through `useHalftone`, which owns
17
+ * every question of its cost: it draws only while something moves, pauses out of view and holds
18
+ * the bitmap to twice the pixel density. The aurora, the grid and the rings are painted by the
19
+ * stylesheet, with no canvas; `useSpotlight` only tells them where the pointer is.
20
+ *
21
+ * With nothing to hold it is a layer: it fills its positioned parent behind that parent's own
22
+ * content, and listens for the pointer on the parent, because the content covers it. That is how a
23
+ * block draws a pattern under itself without its own layout changing.
24
+ */
25
+ export function Background({ tone = backgroundDefaults.tone, pattern = backgroundDefaults.pattern, animated = backgroundDefaults.animated, interactive = backgroundDefaults.interactive, children, ...rest }) {
26
+ // Nothing to hold: a layer under the parent's own content. See above.
27
+ const layer = children === undefined || children === null || children === false;
28
+ const [root, canvas] = useHalftone(pattern === 'halftone', interactive, animated, tone, layer);
29
+ // The patterns the stylesheet paints answer the pointer from where it is: the grid's lines and
30
+ // the rings brighten around it, and the aurora carries a pool of its light after it.
31
+ useSpotlight(root, pattern !== 'halftone' && interactive, layer);
32
+ // The patterns the stylesheet moves at rest wait while the band is off screen; the halftone's
33
+ // painter has its own observer.
34
+ useOffscreen(root, pattern !== 'halftone' && animated);
35
+ return (_jsxs("div", { ...rest, ref: root, className: backgroundParts.root, "data-tone": tone, "data-pattern": pattern, "data-animated": animated ? undefined : 'false', "data-layer": layer || undefined, "aria-hidden": layer || undefined, children: [pattern === 'halftone' ? (_jsx("canvas", { ref: canvas, className: backgroundParts.canvas, "aria-hidden": "true" })) : null, layer ? null : _jsx("div", { className: backgroundParts.content, children: children })] }));
36
+ }
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ export interface BackgroundOwnProps {
3
+ /**
4
+ * `dark` — The default, and the band the pattern was drawn for: coral dots glowing on color.bg.brand-strong, which is dark in both modes. Reach for it for the band a page opens or closes on, where the glow gives the band a centre
5
+ * `light` — Coral dots on the page's own ground, for a light page that wants one moment of brand colour without a solid coral band. The glow reads as warmth rather than light, so keep it for a section that is not competing with a dark one nearby
6
+ * `brand` — White dots on the coral brand fill, for a call to action standing on it. The heading is white too, and the glow rises from the foot of the band towards it, so check the words read on the coral alone — the dots can only add to what is behind them
7
+ */
8
+ tone?: 'dark' | 'light' | 'brand';
9
+ /**
10
+ * `halftone` — A grid of dots across the whole band that grow towards a soft glow and settle to a fine screen away from it. The default, and the one that moves most
11
+ * `aurora` — Broad soft pools of the brand's colour rising from the band's foot and drifting slowly into one another — light as colour rather than as points. Drawn by the stylesheet: no canvas. At rest the pools wander, there and back, over motion.duration.drift; on an interactive band a smaller pool trails the pointer. Reach for it where the halftone would be too busy — an opening band with a picture or a product on it, where dots behind the picture fight it and a wash of colour does not
12
+ * `grid` — A fine rule grid of hairlines, fading in towards the band's foot — the grid PageHeader paints, offered for any band. Drawn by the stylesheet: no canvas. On an interactive band the lines brighten in a soft circle around the pointer and fade back when it leaves; at rest the rules rise slowly, a pitch over motion.duration.drift. Reach for it where the band should read as deliberate and quiet rather than lit
13
+ * `rings` — Hairline circles spreading out from the middle of the band's foot, fading in towards it, at size.rings.pitch. Drawn by the stylesheet: no canvas. On an interactive band the rings brighten in a soft circle around the pointer and fade back when it leaves; at rest the rings spread slowly outward, a pitch over motion.duration.drift. As quiet as the grid, but round: reach for it where the band should feel open and centred rather than ruled — a call to action, a closing band with one thing to say
14
+ */
15
+ pattern?: 'halftone' | 'aurora' | 'grid' | 'rings';
16
+ /** On by default: every pattern moves slowly at rest, whether or not anybody is pointing at it — the halftone's glow and the aurora's pools drift along the band's foot, the grid's rules rise and the rings spread outward, each over motion.duration.drift. Turn it off for a band that should be still until somebody reaches for it — a band beside a video or an animation of its own, or one a page repeats further down — and the pattern rests where it is. What the pattern does under the pointer is `interactive`, which is set apart from this. */
17
+ animated?: boolean;
18
+ /** On by default: the pattern answers a pointer over the band. The halftone's glow follows it, gathers into a smaller pool and parts the dots it crosses, and a click sends a small ripple through them; the aurora sends a pool of its light after it; the grid's rules and the rings brighten around it. Turn it off for a band whose content is itself interactive enough that a moving ground would compete with it — a band of controls, a form — and the pattern ignores the pointer. Touch has no hover, so on a touch screen nothing follows a finger. Whether it moves at rest is `animated`, which is set apart from this. */
19
+ interactive?: boolean;
20
+ /** Allowed: any content */
21
+ children: ReactNode;
22
+ }
23
+ export type BackgroundProps = BackgroundOwnProps & Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'tone' | 'pattern' | 'animated' | 'interactive'>;
24
+ /** The class each part carries. Apply these — do not retype them. */
25
+ export declare const backgroundParts: {
26
+ readonly root: "dds-background";
27
+ readonly canvas: "dds-background__canvas";
28
+ readonly content: "dds-background__content";
29
+ };
30
+ /** Generated so defaults cannot drift from the contract. */
31
+ export declare const backgroundDefaults: {
32
+ readonly tone: "dark";
33
+ readonly pattern: "halftone";
34
+ readonly animated: true;
35
+ readonly interactive: true;
36
+ };
@@ -0,0 +1,9 @@
1
+ // GENERATED from background.contract.json. Do not edit.
2
+ /** The class each part carries. Apply these — do not retype them. */
3
+ export const backgroundParts = {
4
+ root: 'dds-background',
5
+ canvas: 'dds-background__canvas',
6
+ content: 'dds-background__content',
7
+ };
8
+ /** Generated so defaults cannot drift from the contract. */
9
+ export const backgroundDefaults = { tone: 'dark', pattern: 'halftone', animated: true, interactive: true };