@repobit/dex-system-design 0.25.12 → 0.26.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.
- package/CHANGELOG.md +187 -0
- package/README.md +131 -8
- package/package.json +95 -13
- package/schemas/component-manifest.json +3294 -405
- package/schemas/manifest-omissions.json +173 -0
- package/src/blog/src/breakpoints.js +6 -6
- package/src/blog/src/content/blog-atoms-showcase.js +1 -1
- package/src/blog/src/content/blog-page-layout.js +2 -2
- package/src/blog/src/lib/article-toc.js +7 -7
- package/src/blog/src/shell/blog-shell.js +1 -1
- package/src/blog/src/tokens/blog.js +2 -2
- package/src/blog/src/tokens/core.js +34 -35
- package/src/blog/src/tokens/legacy.js +0 -9
- package/src/blog/tokens/blog.js +2 -2
- package/src/blog/tokens/core.js +34 -35
- package/src/blog/tokens/legacy.js +0 -9
- package/src/blog/tokens.css +36 -46
- package/src/components/accordion/accordion-bg.css.js +83 -59
- package/src/components/accordion/accordion-bg.js +29 -10
- package/src/components/accordion/accordion-bg.stories.js +67 -67
- package/src/components/accordion/accordion-keyboard.js +80 -0
- package/src/components/accordion/accordion.css.js +142 -80
- package/src/components/accordion/accordion.js +37 -10
- package/src/components/accordion/accordion.stories.js +7 -7
- package/src/components/agent-endpoints/agent-endpoints.css.js +36 -39
- package/src/components/agent-endpoints/agent-endpoints.js +30 -20
- package/src/components/agent-endpoints/agent-endpoints.stories.js +1 -1
- package/src/components/ai-guardian-demo.stories.js +91 -91
- package/src/components/alert/alert.css.js +198 -0
- package/src/components/alert/alert.js +103 -0
- package/src/components/alert/alert.stories.js +206 -0
- package/src/components/anchor/anchor-nav.css.js +213 -101
- package/src/components/anchor/anchor-nav.js +26 -40
- package/src/components/anchor/anchor.stories.js +22 -12
- package/src/components/annotation/annotation.css.js +58 -0
- package/src/components/annotation/annotation.js +67 -0
- package/src/components/annotation/annotation.stories.js +63 -0
- package/src/components/architecture.stories.js +285 -0
- package/src/components/atoms.stories.js +342 -0
- package/src/components/avatar/avatar.css.js +67 -0
- package/src/components/avatar/avatar.js +42 -0
- package/src/components/avatar/avatar.stories.js +95 -0
- package/src/components/awards/awards-flex.css.js +209 -23
- package/src/components/awards/awards-flex.js +66 -7
- package/src/components/awards/awards-flex.stories.js +1 -1
- package/src/components/awards/{awards-light.stories.js → awards-summary.stories.js} +54 -64
- package/src/components/awards/awards.css.js +116 -58
- package/src/components/awards/awards.js +102 -28
- package/src/components/awards/awards.stories.js +1 -1
- package/src/components/back/back.css.js +51 -22
- package/src/components/back/back.js +28 -7
- package/src/components/back/back.stories.js +9 -3
- package/src/components/badge/badge.css.js +158 -46
- package/src/components/badge/badge.js +62 -74
- package/src/components/badge/badge.stories.js +176 -248
- package/src/components/bento-grid/bento-grid.css.js +21 -16
- package/src/components/bento-grid/bento-grid.js +16 -4
- package/src/components/bento-grid/bento-grid.stories.js +19 -18
- package/src/components/bento-grid/bento-item.css.js +52 -28
- package/src/components/bento-grid/bento-item.js +2 -2
- package/src/components/blockquote/blockquote.css.js +47 -0
- package/src/components/blockquote/blockquote.js +29 -0
- package/src/components/blockquote/blockquote.stories.js +62 -0
- package/src/components/breadcrumb/breadcrumb.css.js +92 -22
- package/src/components/breadcrumb/breadcrumb.js +56 -11
- package/src/components/button/button.css.js +531 -161
- package/src/components/button/button.js +206 -214
- package/src/components/button/button.stories.js +322 -174
- package/src/components/caption/caption.css.js +21 -0
- package/src/components/caption/caption.js +31 -0
- package/src/components/caption/caption.stories.js +62 -0
- package/src/components/card-features/card-features.css.js +14 -22
- package/src/components/card-features/card-features.js +68 -9
- package/src/components/card-features/card-features.stories.js +11 -11
- package/src/components/cards/card.css.js +60 -81
- package/src/components/cards/card.js +56 -25
- package/src/components/cards/card.stories.js +39 -39
- package/src/components/carousel/carousel.css.js +93 -99
- package/src/components/carousel/carousel.js +32 -4
- package/src/components/checkbox/checkbox.css.js +111 -93
- package/src/components/checkbox/checkbox.js +64 -90
- package/src/components/checkbox/checkbox.stories.js +96 -29
- package/src/components/code/code.css.js +70 -0
- package/src/components/code/code.js +40 -0
- package/src/components/code/code.stories.js +73 -0
- package/src/components/compare/compare.css.js +179 -43
- package/src/components/compare/compare.js +52 -25
- package/src/components/compare/compare.stories.js +61 -4
- package/src/components/comparison-panel/comparison-panel.css.js +38 -51
- package/src/components/comparison-panel/comparison-panel.js +5 -4
- package/src/components/comparison-panel/comparison-panel.stories.js +1 -1
- package/src/components/compatibility-table/compatibility-table.css.js +180 -80
- package/src/components/compatibility-table/compatibility-table.js +37 -9
- package/src/components/control-alignment.stories.js +393 -0
- package/src/components/cta-banner/cta-banner.css.js +41 -13
- package/src/components/cta-banner/cta-banner.js +6 -5
- package/src/components/cta-banner/cta-banner.stories.js +43 -98
- package/src/components/display/display.css.js +25 -1
- package/src/components/display/display.js +46 -13
- package/src/components/display/display.stories.js +270 -40
- package/src/components/divider/divider-horizontal.js +21 -5
- package/src/components/divider/divider-vertical.js +15 -7
- package/src/components/divider/divider.css.js +2 -2
- package/src/components/divider/divider.stories.js +38 -34
- package/src/components/dropdown/dropdown.css.js +93 -126
- package/src/components/dropdown/dropdown.js +114 -34
- package/src/components/dropdown/dropdown.stories.js +56 -24
- package/src/components/features/features.css.js +24 -11
- package/src/components/features/features.js +7 -3
- package/src/components/features/features.stories.js +37 -37
- package/src/components/features-tab/features-tab.css.js +19 -8
- package/src/components/features-tab/features-tab.js +6 -5
- package/src/components/features-tab/features-tab.stories.js +23 -23
- package/src/components/fieldset/fieldset.css.js +30 -0
- package/src/components/fieldset/fieldset.js +28 -0
- package/src/components/fieldset/fieldset.stories.js +84 -0
- package/src/components/footer/footer-links-group.css.js +10 -14
- package/src/components/footer/footer-links-group.js +4 -3
- package/src/components/footer/footer-lp.css.js +59 -30
- package/src/components/footer/footer-lp.js +17 -12
- package/src/components/footer/footer-lp.stories.js +63 -75
- package/src/components/footer/footer-nav-menu.css.js +26 -4
- package/src/components/footer/footer-nav-menu.js +3 -2
- package/src/components/footer/footer.css.js +146 -264
- package/src/components/footer/footer.js +176 -106
- package/src/components/footer/footer.stories.js +9 -9
- package/src/components/footer/localeMap.js +1 -1
- package/src/components/grid/grid.css.js +1 -1
- package/src/components/grid/grid.js +49 -7
- package/src/components/grid/grid.stories.js +111 -0
- package/src/components/header/header-code.css.js +61 -51
- package/src/components/header/header-code.js +28 -14
- package/src/components/header/header-code.stories.js +5 -5
- package/src/components/header/header.css.js +655 -176
- package/src/components/header/header.js +269 -36
- package/src/components/header/header.stories.js +841 -18
- package/src/components/heading/heading.css.js +14 -17
- package/src/components/heading/heading.js +48 -33
- package/src/components/heading/heading.stories.js +27 -25
- package/src/components/help-text/help-text.css.js +21 -0
- package/src/components/help-text/help-text.js +25 -0
- package/src/components/help-text/help-text.stories.js +66 -0
- package/src/components/highlight/highlight.css.js +86 -64
- package/src/components/highlight/highlight.js +18 -27
- package/src/components/highlight/highlight.stories.js +14 -3
- package/src/components/icons/analysis-icon.js +6 -16
- package/src/components/icons/arrow-down-icon.js +6 -19
- package/src/components/icons/arrow-icon.js +44 -0
- package/src/components/icons/arrow-right-icon.js +30 -23
- package/src/components/icons/arrow-up-icon.js +19 -18
- package/src/components/icons/bd-ai-icon.js +1 -1
- package/src/components/icons/brand-marks.stories.js +279 -0
- package/src/components/icons/brand-signature-icon.js +42 -0
- package/src/components/icons/check-icon.js +27 -0
- package/src/components/icons/circle-info-icon.js +33 -0
- package/src/components/icons/close-icon.js +6 -18
- package/src/components/icons/family-icon.js +8 -20
- package/src/components/icons/globe-icon.js +6 -18
- package/src/components/icons/guide-icon.js +6 -18
- package/src/components/icons/icon-element.js +99 -0
- package/src/components/icons/icons.js +36 -0
- package/src/components/icons/icons.stories.js +184 -52
- package/src/components/icons/individual-icon.js +6 -18
- package/src/components/icons/laurel-icon.js +9 -19
- package/src/components/icons/machine-learning-icon.js +6 -17
- package/src/components/icons/marks.js +41 -0
- package/src/components/icons/minus-icon.js +7 -21
- package/src/components/icons/missing-icon.js +52 -0
- package/src/components/icons/plus-icon.js +7 -21
- package/src/components/icons/quote-icon.js +19 -19
- package/src/components/image/image.css.js +72 -25
- package/src/components/image/image.js +11 -6
- package/src/components/image/image.stories.js +2 -2
- package/src/components/input/custom-form.stories.js +214 -62
- package/src/components/input/input-clipboard.css.js +51 -144
- package/src/components/input/input-clipboard.js +31 -15
- package/src/components/input/input.css.js +8 -146
- package/src/components/input/input.js +93 -38
- package/src/components/install-options/install-options.css.js +84 -77
- package/src/components/install-options/install-options.js +33 -30
- package/src/components/install-options/install-options.stories.js +3 -3
- package/src/components/label/label.css.js +35 -0
- package/src/components/label/label.js +38 -0
- package/src/components/label/label.stories.js +82 -0
- package/src/components/light-carousel/light-carousel.css.js +111 -63
- package/src/components/light-carousel/light-carousel.js +85 -11
- package/src/components/light-carousel/light-carousel.stories.js +126 -82
- package/src/components/link/link.css.js +150 -99
- package/src/components/link/link.js +86 -11
- package/src/components/link/link.stories.js +124 -30
- package/src/components/list/list-item/list-item.css.js +74 -22
- package/src/components/list/list-item/list-item.js +32 -10
- package/src/components/list/list-item/list-item.stories.js +23 -23
- package/src/components/list/list.css.js +8 -2
- package/src/components/list/list.js +3 -2
- package/src/components/list/list.stories.js +222 -0
- package/src/components/modal/modal.css.js +114 -24
- package/src/components/modal/modal.js +83 -7
- package/src/components/modal/modal.stories.js +9 -9
- package/src/components/overview/overview.css.js +19 -14
- package/src/components/overview/overview.js +6 -5
- package/src/components/overview/overview.stories.js +10 -10
- package/src/components/page-switcher/page-switcher.css.js +97 -40
- package/src/components/page-switcher/page-switcher.js +28 -5
- package/src/components/page-switcher/page-switcher.stories.js +95 -0
- package/src/components/paragraph/paragraph.css.js +15 -10
- package/src/components/paragraph/paragraph.js +41 -44
- package/src/components/paragraph/paragraph.stories.js +56 -21
- package/src/components/picture/picture.css.js +103 -44
- package/src/components/picture/picture.js +100 -30
- package/src/components/picture/picture.stories.js +1 -1
- package/src/components/pricing-cards/new-pricing-card.js +3 -3
- package/src/components/pricing-cards/new-pricing.css.js +24 -15
- package/src/components/pricing-cards/pricing-card-actions.css.js +1 -1
- package/src/components/pricing-cards/pricing-card-actions.js +3 -4
- package/src/components/pricing-cards/pricing-card-container.css.js +3 -3
- package/src/components/pricing-cards/pricing-card-container.js +3 -2
- package/src/components/pricing-cards/pricing-card-header.css.js +10 -8
- package/src/components/pricing-cards/pricing-card-header.js +6 -7
- package/src/components/pricing-cards/pricing-card-pricing.css.js +16 -9
- package/src/components/pricing-cards/pricing-card-pricing.js +8 -6
- package/src/components/pricing-cards/pricing-card-show-more.css.js +11 -3
- package/src/components/pricing-cards/pricing-card-show-more.js +8 -3
- package/src/components/pricing-cards/pricing-card.css.js +244 -190
- package/src/components/pricing-cards/pricing-card.js +54 -70
- package/src/components/pricing-cards/pricing-feature-item.css.js +25 -8
- package/src/components/pricing-cards/pricing-feature-item.js +8 -3
- package/src/components/process-steps/process-steps.css.js +26 -14
- package/src/components/process-steps/process-steps.js +23 -6
- package/src/components/process-steps/process-steps.stories.js +1 -1
- package/src/components/progress/progress.css.js +80 -0
- package/src/components/progress/progress.js +42 -0
- package/src/components/progress/progress.stories.js +79 -0
- package/src/components/quick-answer/quick-answer.css.js +75 -13
- package/src/components/quick-answer/quick-answer.js +8 -3
- package/src/components/radio/radio.css.js +92 -128
- package/src/components/radio/radio.js +106 -127
- package/src/components/radio/radio.stories.js +54 -19
- package/src/components/review/review-grid.css.js +5 -6
- package/src/components/review/review-grid.js +14 -11
- package/src/components/review/review.css.js +11 -6
- package/src/components/review/review.js +3 -8
- package/src/components/shared/ensemble.css.js +93 -0
- package/src/components/shared/form-field.css.js +206 -0
- package/src/components/shared/prop-vocabulary.js +48 -0
- package/src/components/shared/token-scales.js +306 -0
- package/src/components/skeleton/skeleton.css.js +34 -0
- package/src/components/skeleton/skeleton.js +32 -0
- package/src/components/skeleton/skeleton.stories.js +70 -0
- package/src/components/spinner/spinner.css.js +62 -0
- package/src/components/spinner/spinner.js +38 -0
- package/src/components/spinner/spinner.stories.js +92 -0
- package/src/components/spotlight-section/spotlight-section.css.js +14 -13
- package/src/components/spotlight-section/spotlight-section.js +42 -39
- package/src/components/switch/switch.css.js +103 -0
- package/src/components/switch/switch.js +44 -0
- package/src/components/switch/switch.stories.js +86 -0
- package/src/components/tab-list/content-switcher.stories.js +189 -0
- package/src/components/tab-list/tab-list.css.js +250 -0
- package/src/components/tab-list/tab-list.js +184 -0
- package/src/components/tab-list/tab-list.stories.js +221 -0
- package/src/components/tabs/tabs.css.js +14 -123
- package/src/components/tabs/tabs.js +16 -19
- package/src/components/tabs/tabs.stories.js +95 -95
- package/src/components/tag/tag.css.js +54 -0
- package/src/components/tag/tag.js +32 -0
- package/src/components/tag/tag.stories.js +126 -0
- package/src/components/terminal/terminal-message.css.js +164 -60
- package/src/components/terminal/terminal-message.js +5 -5
- package/src/components/terminal/terminal-message.stories.js +70 -1
- package/src/components/termsOfUse/terms.css.js +25 -20
- package/src/components/termsOfUse/terms.js +13 -9
- package/src/components/termsOfUse/terms.stories.js +1 -1
- package/src/components/textarea/textarea.css.js +51 -0
- package/src/components/textarea/textarea.js +136 -0
- package/src/components/textarea/textarea.stories.js +191 -0
- package/src/components/tooltip/tooltip.css.js +75 -0
- package/src/components/tooltip/tooltip.js +156 -0
- package/src/components/tooltip/tooltip.stories.js +161 -0
- package/src/components/trust-panel/trust-panel.css.js +85 -33
- package/src/components/trust-panel/trust-panel.js +18 -10
- package/src/components/trust-panel/trust-panel.stories.js +1 -1
- package/src/components/ultimate-security-demo.stories.js +431 -0
- package/src/components/video/video.css.js +17 -0
- package/src/components/video/video.js +40 -0
- package/src/components/video/video.stories.js +106 -0
- package/src/components/video-player/video-player.css.js +67 -11
- package/src/components/video-player/video-player.js +35 -4
- package/src/components/video-player/video-player.stories.js +1 -1
- package/src/components/visual-alignment.stories.js +411 -0
- package/src/components/visually-hidden/visually-hidden.css.js +24 -0
- package/src/components/visually-hidden/visually-hidden.js +16 -0
- package/src/components/visually-hidden/visually-hidden.stories.js +56 -0
- package/src/stories/Button.stories.js +17 -17
- package/src/stories/Header.stories.js +10 -10
- package/src/stories/Page.js +1 -1
- package/src/stories/Page.stories.js +6 -6
- package/src/tokens/borders.stories.js +339 -0
- package/src/tokens/color-responsibilities.stories.js +227 -0
- package/src/tokens/dark-derivation.js +309 -0
- package/src/tokens/dark-findings.js +197 -0
- package/src/tokens/dark-mode.stories.js +207 -0
- package/src/tokens/dimension.stories.js +251 -0
- package/src/tokens/elevation.stories.js +143 -0
- package/src/tokens/focus.stories.js +276 -0
- package/src/tokens/fonts.css +20 -20
- package/src/tokens/fonts.stories.js +354 -374
- package/src/tokens/layout.css +25 -11
- package/src/tokens/motion.stories.js +231 -0
- package/src/tokens/shape.stories.js +272 -0
- package/src/tokens/spacing-rhythm.stories.js +359 -0
- package/src/tokens/spacing.stories.js +208 -703
- package/src/tokens/story-chrome.js +503 -0
- package/src/tokens/surfaces.stories.js +224 -0
- package/src/tokens/text-on-surface.stories.js +164 -0
- package/src/tokens/token-map.js +220 -0
- package/src/tokens/token-map.stories.js +257 -0
- package/src/tokens/token-purposes.js +266 -0
- package/src/tokens/tokens-grid.stories.js +388 -900
- package/src/tokens/tokens.css +1633 -1045
- package/src/tokens/tokens.js +1130 -492
- package/src/tokens/tokens.stories.js +137 -47
- package/src/tokens/type-ceiling-words.js +299 -0
- package/src/tokens/typography.stories.js +1003 -965
- package/src/tokens/vertical-rhythm.stories.js +224 -0
- package/src/components/awards/awards-icon.js +0 -56
- package/src/components/awards/awards-light.js +0 -240
- package/src/components/banner/banner.js +0 -60
- package/src/components/banner/banner.stories.js +0 -51
- package/src/components/button/icons.js +0 -24
- package/src/components/input/index.js +0 -0
- package/src/components/light-carousel/light-carousel-simple.css.js +0 -183
- package/src/components/light-carousel/light-carousel-simple.js +0 -74
- package/src/tokens/colors.js +0 -121
- package/src/tokens/typography.css.js +0 -207
- package/src/tokens/typography.js +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
2
|
import './header.js';
|
|
3
|
+
/* The reference story below composes the same atoms the component does, and
|
|
4
|
+
borrows the component's OWN stylesheet rather than restating it -- see the
|
|
5
|
+
note on IntendedDesign for what that buys and what it does not cover. */
|
|
6
|
+
import headerCSS from './header.css.js';
|
|
7
|
+
import { ensembleCSS } from '../shared/ensemble.css.js';
|
|
8
|
+
import '../badge/badge.js';
|
|
9
|
+
import '../button/button.js';
|
|
10
|
+
import '../heading/heading.js';
|
|
11
|
+
import '../icons/brand-signature-icon.js';
|
|
12
|
+
import '../paragraph/paragraph.js';
|
|
3
13
|
|
|
4
14
|
export default {
|
|
5
15
|
title : 'Components/Header',
|
|
@@ -20,7 +30,7 @@ export default {
|
|
|
20
30
|
devices="5"
|
|
21
31
|
subscription="1"
|
|
22
32
|
>
|
|
23
|
-
<img slot="header-picture" src="/
|
|
33
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product" />
|
|
24
34
|
<span slot="disclaimer-top">Protection for 5 devices.</span>
|
|
25
35
|
</bd-header>
|
|
26
36
|
\`\`\`
|
|
@@ -34,9 +44,376 @@ export default {
|
|
|
34
44
|
| \`disclaimer-bottom-link\` | Link in the bottom disclaimer |
|
|
35
45
|
| \`disclaimer-bottom-text-end\` | End of the bottom disclaimer line |
|
|
36
46
|
|
|
47
|
+
### This is Header / v2's chosen configuration
|
|
48
|
+
|
|
49
|
+
Everything below was brought across from Figma \`Header / v2\` (node \`340:4\`),
|
|
50
|
+
which is the winning variant of the *Header height study* — variant **I**
|
|
51
|
+
(all type shrunk) at frame padding 16, with the brand signature and scrim. The
|
|
52
|
+
Figma component measures **1290 × 566**, under the 600px target the study was
|
|
53
|
+
run against; every value here was then measured in this Storybook at 1440.
|
|
54
|
+
|
|
55
|
+
**The type is all roles, and no new token was needed.** That is the headline
|
|
56
|
+
result: the as-built header needed a brand-new \`fontSize-64\` primitive and a
|
|
57
|
+
\`display-hero\` role for its 64px headline — **both since retired, because this
|
|
58
|
+
was their only reason to exist** — and the configuration that actually
|
|
59
|
+
won drops to 48 — where \`heading-h1\` already lives. Measured, in render order:
|
|
60
|
+
|
|
61
|
+
| | role | measured |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| product name | \`heading-h4\` role on a \`<p>\` | 24px / 600 / 31.2px, \`rgb(0 89 214)\` |
|
|
64
|
+
| headline | \`bd-h\` at \`visual-level="h1"\`, level from \`heading-level\` | 48px / 700 / 60px |
|
|
65
|
+
| description | \`bd-p variant="regular"\` | 16px / 400 / 24px |
|
|
66
|
+
| struck price | \`bd-p regular\` + \`font-weight="medium"\`, muted | 16px / 500 / 24px, \`rgb(86 94 102)\` |
|
|
67
|
+
| discount | \`bd-badge tone="success" variant="fill" size="lg"\` | 14px / 500, white on \`rgb(14 133 50)\` |
|
|
68
|
+
| final price | \`heading-h2\` | 36px / 700 / 45px |
|
|
69
|
+
| disclaimers | \`bd-p variant="xsmall"\`, muted | 12px / 400 / 17.4px |
|
|
70
|
+
| both CTAs | \`size="lg"\` | 16/32 padding, 18px label, 8px radius |
|
|
71
|
+
|
|
72
|
+
### The heading structure: one h1, and the eyebrow is not a heading
|
|
73
|
+
|
|
74
|
+
This changed, and it changes the outline of every page rendering the header, so
|
|
75
|
+
it is worth reading rather than skipping.
|
|
76
|
+
|
|
77
|
+
**Before:** the product name was \`as="h2"\` and the headline \`as="h4"\`, in
|
|
78
|
+
that DOM order. So the eyebrow was announced as a landmark and the actual
|
|
79
|
+
promise was announced two levels below it — a heading list read *"Bitdefender
|
|
80
|
+
Total Security"* (h2) then *"Complete protection for all your devices"* (h4),
|
|
81
|
+
in the wrong order of importance.
|
|
82
|
+
|
|
83
|
+
**Now:** the headline is the page's \`<h1>\` and the product name is a \`<p>\`.
|
|
84
|
+
|
|
85
|
+
- The headline is the main heading of the page. This component is the product
|
|
86
|
+
hero and there is one per page, which is the condition an \`h1\` needs.
|
|
87
|
+
\`bd-h\`'s h1 role is 5xl / bold / tight — 48/700/1.25 — which is Header /
|
|
88
|
+
v2's value-prop exactly, so \`as="h1"\` needs no \`visual-level\` beside it.
|
|
89
|
+
The outline level and the type role agree here instead of having to be
|
|
90
|
+
separated.
|
|
91
|
+
- The product name is a **kicker**: a label naming the product, and in Figma a
|
|
92
|
+
plain text layer called \`product-name\` rather than a heading. A kicker in
|
|
93
|
+
the outline is worse than no kicker in the outline — it adds a rung nobody
|
|
94
|
+
navigates by and pushes the real heading down. It resolves through the
|
|
95
|
+
\`heading-h4\` **role** on a non-heading element, which is the right way
|
|
96
|
+
round: the role names the type job ("the most prominent label on the
|
|
97
|
+
section"), and what the element *is* is settled by the tag.
|
|
98
|
+
|
|
99
|
+
**The alternative, and why not.** The other accessible eyebrow pattern nests
|
|
100
|
+
both inside the heading —
|
|
101
|
+
\`<h1><span>Bitdefender…</span><span>Complete protection…</span></h1>\` —
|
|
102
|
+
giving the heading an accessible name that carries the product as well as the
|
|
103
|
+
promise. Rejected on two counts: every heading list, skip link and screen-reader
|
|
104
|
+
rotor entry would read the whole run, and it would mean hand-rendering the
|
|
105
|
+
\`h1\` here instead of going through \`bd-h\`, which takes one slot. The product
|
|
106
|
+
is already named by the document title, the price block and the CTA; the
|
|
107
|
+
heading does not have to carry it too.
|
|
108
|
+
|
|
109
|
+
**If your page needs a different level, say so — that is your call, not this
|
|
110
|
+
component's.** \`heading-level\` takes h1–h6 and defaults to \`h1\`:
|
|
111
|
+
|
|
112
|
+
\`\`\`html
|
|
113
|
+
<bd-header heading-level="h2" subtitle="Complete protection…"></bd-header>
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
Two things about it worth knowing before you use it:
|
|
117
|
+
|
|
118
|
+
- **It moves the heading in the outline and changes nothing about how it
|
|
119
|
+
looks.** \`visual-level\` is pinned to \`h1\` in the component, so the hero
|
|
120
|
+
stays 48 / 700 / 60px at every level. The size is the design's; only the
|
|
121
|
+
level is the page's. Without the pin, \`bd-h\` follows \`as\` for both and
|
|
122
|
+
\`heading-level="h3"\` would have rendered the hero at the h3 role instead —
|
|
123
|
+
measured **32px at 1024 and 24px at 320**, against 48 and 30 — an SEO
|
|
124
|
+
decision repainting the design.
|
|
125
|
+
- **\`h1\` is the right answer far more often than not.** A hero that is not
|
|
126
|
+
the page's main heading is usually a sign the hero is in the wrong place, so
|
|
127
|
+
treat a non-\`h1\` here as something you decided rather than something you
|
|
128
|
+
reached for. The prop exists because the decision belongs to the page — the
|
|
129
|
+
dev team and the SEO team own the heading levels on this section — not
|
|
130
|
+
because the default is in doubt.
|
|
131
|
+
|
|
132
|
+
It was hardcoded until 2026-09-17, which is the part that was wrong: the rule
|
|
133
|
+
above says the composing page owns \`as=\`, and the component was keeping it.
|
|
134
|
+
|
|
135
|
+
**The spacing is variant I's 16/8/8.** \`:host\` pads the row 16 (was a bare
|
|
136
|
+
\`32px\`), the content column takes 24 back, the two columns are 64 apart (was
|
|
137
|
+
\`30px\`, on no step and 34 short of the design), and the gaps run
|
|
138
|
+
content 16 → body 16 → copy 8 → actions 8. Tightening copy and actions to 8 is
|
|
139
|
+
where most of the 94px the study saved came from.
|
|
140
|
+
|
|
141
|
+
**What this retired.** Not only additions — six things that were wrong went:
|
|
142
|
+
|
|
143
|
+
- a \`font-size: 44px\` on the final price, carrying a recorded \`no token:\`
|
|
144
|
+
exception because 44 is off the type scale. The design asks for 36, which
|
|
145
|
+
\`heading-h2\` is exactly, so the exception simply dissolved
|
|
146
|
+
- a hand-rolled discount badge — \`green-100\` behind \`text/success\` at 16px
|
|
147
|
+
semibold in a \`--radius-xl\` box, with two \`no role:\` markers saying it
|
|
148
|
+
should be a \`bd-badge\`. It is one now, and not the same one: that tint is
|
|
149
|
+
the atom's \`shade\`, and the design asks for \`fill\`
|
|
150
|
+
- three off-scale literals (\`30px\`, \`32px\` twice), all three previously
|
|
151
|
+
baselined in \`lint:tokens\`
|
|
152
|
+
- \`transform: scale(1.35) translateY(-7%)\` on the product image, a crop hack
|
|
153
|
+
that magnified the photo 135% so a wide image would fill a tall box. A real
|
|
154
|
+
\`object-fit: cover\` at \`height: 100%\` does that without throwing away a
|
|
155
|
+
third of the resolution — and it matters to the scrim, which is positioned
|
|
156
|
+
against the wrapper while a transformed image is somewhere else
|
|
157
|
+
- two dead declarations on each disclaimer. They set \`font-size\` and
|
|
158
|
+
\`font-weight\` on a \`bd-p\` host, which \`scripts/atom-inert-properties.json\`
|
|
159
|
+
records as INERT — the atom sets all five type axes on its inner element. So
|
|
160
|
+
the rendered size was 14 while the rule claimed otherwise; the size now comes
|
|
161
|
+
from the atom's own \`xsmall\` variant
|
|
162
|
+
- two \`.bd-header-image\` rules disagreeing about \`height\`
|
|
163
|
+
|
|
164
|
+
**One recorded divergence.** Figma binds \`fontSize\` to a variable on these
|
|
165
|
+
text nodes and leaves \`lineHeight\` and \`letterSpacing\` raw, so its \`-1px\`
|
|
166
|
+
product-name tracking, its \`-2.5%\` headline tracking and its \`150%\` disclaimer
|
|
167
|
+
leading were unbound numbers rather than tokens. CLAUDE.md's rule is to read
|
|
168
|
+
the intent and spell it with the role, so the code takes the roles' values —
|
|
169
|
+
and the Figma component was then set to match, so the two agree on one answer
|
|
170
|
+
rather than on two.
|
|
171
|
+
|
|
172
|
+
### Responsive: three steps on the breakpoint scale
|
|
173
|
+
|
|
174
|
+
**There is no mobile frame in Figma.** The height study was run at 1290 only,
|
|
175
|
+
so every width below that was decided here, from the grid arithmetic and then
|
|
176
|
+
by measuring. The page column caps at \`--grid-container-max-*\` with
|
|
177
|
+
\`--grid-padding-*\` either side, so once the design's 64px gap comes out, the
|
|
178
|
+
two-column arithmetic is fixed:
|
|
179
|
+
|
|
180
|
+
| step | content width | per column |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| xxl | 1320 − 80 = 1240 | 588 |
|
|
183
|
+
| xl | 1140 − 80 = 1060 | 498 |
|
|
184
|
+
| lg | 960 − 80 = 880 | 408 |
|
|
185
|
+
| md | 720 − 64 = 656 | 296 |
|
|
186
|
+
| sm | 540 − 48 = 492 | 214 |
|
|
187
|
+
|
|
188
|
+
The CTA row is **446px**. It fits at xl and xxl, wraps at lg, and at md there
|
|
189
|
+
is less room than one button needs. So:
|
|
190
|
+
|
|
191
|
+
- **\`below('lg')\` — stack.** One column, 32px row gap.
|
|
192
|
+
- **\`below('md')\` — stack the CTA.** Separate from the step above on purpose:
|
|
193
|
+
at md the column is 656px, where a side-by-side CTA row still fits.
|
|
194
|
+
- **\`below('md')\` — re-proportion the scrim band** (same query, different
|
|
195
|
+
concern: it is about the media panel, not the copy).
|
|
196
|
+
|
|
197
|
+
Measured across ten widths afterwards — no horizontal overflow at any of them,
|
|
198
|
+
and the buttons hold 183/246 at every width:
|
|
199
|
+
|
|
200
|
+
| width | cols | column | headline | description | band | total |
|
|
201
|
+
|---|---|---|---|---|---|---|
|
|
202
|
+
| 1440 | 2 | 588 | 48 × 2 | 16 × 3 | 30% | **566** |
|
|
203
|
+
| 1280 | 2 | 498 | 48 × 2 | 16 × 3 | 30% | 566 |
|
|
204
|
+
| 1100 | 2 | 408 | 48 × 2 | 16 × 4 | 25% | 661 |
|
|
205
|
+
| 1024 | 2 | 408 | 48 × 2 | 16 × 4 | 25% | 661 |
|
|
206
|
+
| 900 | 1 | 656 | 48 × 2 | 16 × 2 | 37% | 1012 |
|
|
207
|
+
| 768 | 1 | 656 | 48 × 2 | 16 × 2 | 37% | 1012 |
|
|
208
|
+
| 600 | 1 | 492 | 48 × 2 | 16 × 3 | 27% | 997 |
|
|
209
|
+
| 375 | 1 | 311 | 48 × 3 | 16 × 5 | 42% | 1002 |
|
|
210
|
+
| 320 | 1 | 256 | 48 × 4 | 16 × 6 | 51% | 1066 |
|
|
211
|
+
|
|
212
|
+
#### Why lg keeps two columns even though it costs 661px
|
|
213
|
+
|
|
214
|
+
It is the one arguable step, and it is arguable rather than good. Two columns
|
|
215
|
+
at lg cost the description a fourth line, the CTA row a wrap, and the section
|
|
216
|
+
**661px** against the design's 566. No gap change rescues it: the row needs 445
|
|
217
|
+
and dropping 64 to the lg grid gutter's 40, or to 24, or to 16, gives 420, 428
|
|
218
|
+
and 432. The column is about 40px short however the gutter is spent.
|
|
219
|
+
|
|
220
|
+
It keeps them because stacking is worse **on the same screen**. Stacked at lg
|
|
221
|
+
the copy gets one 880px column — the headline collapses to one line, which the
|
|
222
|
+
study measured as needing 678 — and then an 880px-wide photograph beneath it at
|
|
223
|
+
its own 1.497 ratio: about 1100px of hero in a window 992–1199 wide. Bounding
|
|
224
|
+
the stacked panel to 16/9 would still land near 978. 661 wins.
|
|
225
|
+
|
|
226
|
+
#### The scrim band now scales, because it is built from lengths
|
|
227
|
+
|
|
228
|
+
The band was four percentage stops of a fixed 160px — read straight off Figma,
|
|
229
|
+
which made the invariant arithmetic: the 70% stop landed on the mark's top edge
|
|
230
|
+
only because 0.3 × 160 happens to equal 24 + 24. It is now **length stops
|
|
231
|
+
measured from the bottom**, so the plateau *is* the mark's box by construction
|
|
232
|
+
at any size, and the narrow variant is two smaller numbers rather than a second
|
|
233
|
+
set of percentages. The stops still resolve to Figma's exactly at desktop:
|
|
234
|
+
plateau 0–48, 0.22 at 88, zero at 160.
|
|
235
|
+
|
|
236
|
+
That mattered because the band was a constant and the panel is not. The panel
|
|
237
|
+
follows the copy beside it when the columns are side by side, and its own
|
|
238
|
+
1.497 ratio once they stack — 311×208 at 375 and 256×171 at 320, which put a
|
|
239
|
+
160px band at **77%** and **94%** of the picture. Below md the inset drops a
|
|
240
|
+
step and the fade drops to 48, giving 40 + 48 = 88: **42%** and **51%**.
|
|
241
|
+
|
|
242
|
+
One thing that is inherent rather than fixed — a constant band on a variable
|
|
243
|
+
panel is a variable proportion. *Default* has less copy than the design, so its
|
|
244
|
+
panel is 399 tall at 1440 and the band reads 40% there against the design's 30%.
|
|
245
|
+
|
|
246
|
+
#### What was removed, and one claim corrected
|
|
247
|
+
|
|
248
|
+
Two of the three old media blocks were largely inert. \`flex-direction: column\`
|
|
249
|
+
on \`.bd-container\` is inert — the container is \`display: grid\`, and the
|
|
250
|
+
identical mistake was already recorded further down the file. \`gap: 2em\` was an
|
|
251
|
+
em literal on no scale and the only live declaration in that block, which is
|
|
252
|
+
why the gap read 32px at exactly 1024 and 64px at 1025. \`width: 100%\` on two
|
|
253
|
+
grid items is redundant, and \`::slotted([slot="right"])\` is one of the four
|
|
254
|
+
slot names this stylesheet targets that \`header.js\` never renders.
|
|
255
|
+
|
|
256
|
+
So the header's real stacking point was \`below('md')\`, and measured at 768 that
|
|
257
|
+
was far too late: two 312px columns, the headline at three lines, the
|
|
258
|
+
description at five, the buttons crushed from 183/246 to **150/146** with both
|
|
259
|
+
labels broken over three lines inside a box padded for one, and the section
|
|
260
|
+
**737px** tall — against a design that exists to hold it under 600. Flex items
|
|
261
|
+
shrink by default, so "nowrap" never meant "stays on one line"; it meant
|
|
262
|
+
"squashes". \`.bd-cta\` now wraps, which happens at whatever width runs out
|
|
263
|
+
rather than at a breakpoint somebody picks.
|
|
264
|
+
|
|
265
|
+
**A correction.** The \`(max-width: 480px)\` block set \`max-width: 90%\` and
|
|
266
|
+
\`max-height: 200px\` on \`.bd-header-image\` — a class \`header.js\` does not
|
|
267
|
+
render. For the component it was **dead**: measured at 375 and 320, the slotted
|
|
268
|
+
image computes \`max-width: none\`, \`max-height: none\` and sits flush with the
|
|
269
|
+
wrapper on every edge. It was live only in *Intended design*, which draws a
|
|
270
|
+
real \`<img class="bd-header-image">\`, and that is where a 90%-wide centred
|
|
271
|
+
image left the scrim and the mark — both positioned against the **wrapper** —
|
|
272
|
+
hanging past the photo on each side.
|
|
273
|
+
|
|
274
|
+
#### Three things only the render showed
|
|
275
|
+
|
|
276
|
+
The ten-width table above was all measured, and it missed all three of these.
|
|
277
|
+
Numbers answer "does it fit"; they do not answer "does it look deliberate".
|
|
278
|
+
|
|
279
|
+
1. **The price row was not centred** while everything around it was.
|
|
280
|
+
\`text-align\` centres a block's *inline content*, and
|
|
281
|
+
\`.bd-price-zone-top\` is \`display: flex\` — so the property reached its
|
|
282
|
+
children's text and moved the row itself not at all. The struck price and
|
|
283
|
+
the badge sat hard left while the final price, a block span, centred
|
|
284
|
+
underneath. It was patched with \`justify-content: center\`, and then the
|
|
285
|
+
patch became the argument for dropping the centring altogether — see below.
|
|
286
|
+
Both are gone.
|
|
287
|
+
2. **The stacked CTAs were ragged** — 183 above 246, both centred. The first
|
|
288
|
+
attempt made it *worse*: \`align-items: stretch\` took the hosts full width,
|
|
289
|
+
but each anchor is \`inline-flex\` and hugs its label, so both buttons ended
|
|
290
|
+
up hard against the left edge of a centred layout. The atom's own answer is
|
|
291
|
+
its \`fullwidth\` attribute, which a media query cannot set, so the host's
|
|
292
|
+
display does it instead: a lone child of a single-column grid fills its
|
|
293
|
+
track. Measured, hosts 492 and anchors 183/246 → 492/492.
|
|
294
|
+
3. **The mark dominated the panel at 320** — 182px on a 256px panel is **71%**,
|
|
295
|
+
against 30% in the design, so a corner signature became a caption laid
|
|
296
|
+
across the picture. The numbers said it fitted, and it did fit; fitting was
|
|
297
|
+
not the question. It steps to the \`sm\` icon step below md — 121px, 47% of
|
|
298
|
+
that panel — and \`--bd-header-mark-height\` steps with it so the scrim's
|
|
299
|
+
plateau still ends exactly on the mark's top edge.
|
|
300
|
+
|
|
301
|
+
Re-verified after, with the mark hidden and the composited backdrop sampled at
|
|
302
|
+
six widths: the plateau covers the mark at **every** width (48px desktop, 32px
|
|
303
|
+
narrow) and the worst contrast is **4.84:1** at 320, with 0% of the mark's area
|
|
304
|
+
under 4.5:1.
|
|
305
|
+
|
|
306
|
+
#### And one silent defect, which is why the lint gained a rule
|
|
307
|
+
|
|
308
|
+
An earlier edit in this pass dropped a single \`}\` from the \`below('md')\`
|
|
309
|
+
block. That is valid JavaScript: the module parsed, the import succeeded,
|
|
310
|
+
\`lint:tokens\` stayed clean, all fifteen \`check:ds\` gates passed and so did
|
|
311
|
+
all five browser gates. At runtime every rule after it folded into the media
|
|
312
|
+
query, so at desktop the product name silently collapsed from two lines to one
|
|
313
|
+
and the section lost 63px. **It was caught by looking at a screenshot.**
|
|
314
|
+
|
|
315
|
+
\`lint:tokens\` now carries \`unbalanced-css-braces\` beside the
|
|
316
|
+
\`declaration-inside-comment\` rule it is modelled on — counting braces per
|
|
317
|
+
\`css\`\`\` template after stripping comments, \`\${…}\` interpolations (every
|
|
318
|
+
one ends in a brace) and strings (\`content: "}"\` is legal CSS). Seven
|
|
319
|
+
selftest cases, and it was proved on the real defect: removing that brace again
|
|
320
|
+
makes it the only new finding, restoring it returns the lint to clean.
|
|
321
|
+
|
|
322
|
+
#### Two things deliberately left alone
|
|
323
|
+
|
|
324
|
+
- **The headline does not step down.** \`bd-h\`'s type lives on its inner element
|
|
325
|
+
inside its shadow root, so a \`font-size\` from here is inert, and no atom in
|
|
326
|
+
this package changes its type with the viewport. Measured, 48px runs two
|
|
327
|
+
lines to 500px, three at 375 and four at 320 — and at 320 that is one word
|
|
328
|
+
per line, which is the most visibly wrong thing left on this page.
|
|
329
|
+
|
|
330
|
+
Note the icon and the button above were both reachable because each atom's
|
|
331
|
+
rule is on \`:host\`, where an outer-tree rule wins. \`bd-h\` puts its type on
|
|
332
|
+
the **inner** element, so there is no CSS route at all — only two real ones:
|
|
333
|
+
give \`bd-h\` responsive roles, which is a foundation decision touching every
|
|
334
|
+
consumer and would be this package's first responsive type; or switch
|
|
335
|
+
\`visual-level\` from JS the way \`bd-footer\`'s \`_isMobile\` already reads the
|
|
336
|
+
viewport through \`matchesBelow()\`. Neither is free: the step to move to is
|
|
337
|
+
\`heading-h2\`, 36/bold, which is **also the final price's role**, so at 36 the
|
|
338
|
+
headline and the price become the same size and the hierarchy the semibold
|
|
339
|
+
kicker was added to sharpen flattens. 375 — the common phone width — is three
|
|
340
|
+
lines and reads fine; 320 is the edge. A design decision, not a CSS one.
|
|
341
|
+
- **Nothing else.** The stacked-CTA raggedness that was listed here is fixed
|
|
342
|
+
above.
|
|
343
|
+
|
|
344
|
+
#### The content is left-aligned at every width
|
|
345
|
+
|
|
346
|
+
It used to centre below \`md\`. It no longer does, and the reasoning is worth
|
|
347
|
+
keeping because the centring looked harmless:
|
|
348
|
+
|
|
349
|
+
- **The design never asked for it.** \`Header / v2\` is left-aligned and there is
|
|
350
|
+
no mobile frame in the file that centres. Changing the axis at a breakpoint was
|
|
351
|
+
a decision nobody recorded making.
|
|
352
|
+
- **The content is the wrong shape for it.** Six stacked blocks of very different
|
|
353
|
+
widths — a two-line kicker, a two-line headline, a five-line description, a
|
|
354
|
+
price pair, two CTAs, a two-line disclaimer — share no edge when centred, so the
|
|
355
|
+
eye gets no vertical line to return to down a five-line paragraph at 288px.
|
|
356
|
+
Centring suits a poster: a headline and one button. This is a specification.
|
|
357
|
+
- **It needed a patch to stop looking broken, and that is the argument.** The price
|
|
358
|
+
row had to be given \`justify-content: center\` because \`text-align\` cannot
|
|
359
|
+
move a flex row. A treatment that needs a correction to look deliberate usually
|
|
360
|
+
is not the right treatment — and dropping the centring removed the patch with it.
|
|
361
|
+
|
|
362
|
+
The full-width stacked CTAs stay: equal tap targets that do not vary with the copy
|
|
363
|
+
hold either way.
|
|
364
|
+
|
|
365
|
+
### The brand signature over the image
|
|
366
|
+
|
|
367
|
+
When \`header-picture\` is filled, the header lays two things over it — a
|
|
368
|
+
\`--color-blue-950\` scrim band across the bottom, and
|
|
369
|
+
\`<bd-brand-signature-icon>\` 24px in from the bottom-right corner (measured:
|
|
370
|
+
24 and 24). Both come from \`Header / v2\`, whose media-panel stacks
|
|
371
|
+
\`media → scrim → brand-signature\` in that order.
|
|
372
|
+
|
|
373
|
+
Neither is a slot and neither is a prop. The signature is the brand's, not the
|
|
374
|
+
page's, so there is nothing for a consumer to decide; the scrim exists so the
|
|
375
|
+
white mark is legible over an image the design system has never seen, so
|
|
376
|
+
turning it off would be turning off the only thing making that true. What the
|
|
377
|
+
consumer controls is the image.
|
|
378
|
+
|
|
379
|
+
They appear **only** when something is actually slotted. A blue band and a
|
|
380
|
+
white mark over an empty column would be a wash of colour and an invisible
|
|
381
|
+
mark, so \`_hasPicture\` is tracked from the slot's own \`slotchange\` and the
|
|
382
|
+
pair is not rendered at all otherwise — see *No Product Image* below, which is
|
|
383
|
+
a documented state rather than an accident.
|
|
384
|
+
|
|
385
|
+
**What replaced what.** This used to be the words "Trusted. Always." at 32px
|
|
386
|
+
bold, 16px from the corner. That was type doing a mark's job, it was English
|
|
387
|
+
hardcoded next to a stylesheet, and it clipped: measured at 320px the 133px run
|
|
388
|
+
sat inside a 96px column with \`overflow: hidden\` and read "rusted. lways.".
|
|
389
|
+
The mark is the same words as artwork, so nothing needs translating and the
|
|
390
|
+
width is fixed at 182.
|
|
391
|
+
|
|
392
|
+
### The prices are empty in Storybook
|
|
393
|
+
|
|
394
|
+
The struck price, the discount and the final price are store-driven —
|
|
395
|
+
\`data-store-render\` with \`data-store-price\` / \`data-store-discount\` — and
|
|
396
|
+
there is no store in Storybook, so all three render empty. The discount badge
|
|
397
|
+
therefore shows as a small green pill with no text. That is the harness, not
|
|
398
|
+
the design: on a real page \`data-store-hide\` removes the whole row when the
|
|
399
|
+
option is not discounted.
|
|
400
|
+
|
|
37
401
|
### Notes
|
|
38
402
|
- Pricing zones use \`data-store-render\` and \`data-store-price\` attributes for store integration
|
|
39
|
-
- \`shadow\`
|
|
403
|
+
- \`shadow\` reflects as an attribute and defaults to \`true\`, and it is **not a
|
|
404
|
+
styling flag** despite the name. It is how
|
|
405
|
+
\`@repobit/dex-store-elements\` discovers that this component's shadow root
|
|
406
|
+
should be walked for \`[data-store-render]\` and \`[data-store-hide]\` nodes
|
|
407
|
+
(its \`SHADOW_HOST_SELECTOR\` is \`'[shadow], [data-bd-watch-shadow]'\`). Every
|
|
408
|
+
price zone in this component is inside that shadow root, so dropping the
|
|
409
|
+
attribute silently stops the prices rendering on a real page
|
|
410
|
+
- **\`fullPrice\`, \`discount\` and \`finalPrice\` have been removed.** Nothing read
|
|
411
|
+
them -- not the template, not the stylesheet, not the store, which computes
|
|
412
|
+
prices from \`product-id\` + \`devices\` + \`subscription\` and never looks at a
|
|
413
|
+
host property. They were listed in the component manifest as if they worked,
|
|
414
|
+
which is the worse half of the defect: that file is the contract a page
|
|
415
|
+
generator is handed. Wiring them instead would have given one value two
|
|
416
|
+
sources that can disagree, with no answer to which wins
|
|
40
417
|
`
|
|
41
418
|
}
|
|
42
419
|
}
|
|
@@ -44,19 +421,58 @@ export default {
|
|
|
44
421
|
argTypes: {
|
|
45
422
|
title: {
|
|
46
423
|
control : 'text',
|
|
47
|
-
description: '
|
|
48
|
-
|
|
424
|
+
description: 'The product name — the kicker above the headline. Rendered as a '
|
|
425
|
+
+ '`<p class="bd-product-name">` resolving through the `heading-h4` role, '
|
|
426
|
+
+ 'NOT as a heading: a kicker in the document outline adds a rung nobody '
|
|
427
|
+
+ 'navigates by. (This used to say `<bd-h as="h2">`, which is what it was '
|
|
428
|
+
+ 'before the heading structure was fixed.)',
|
|
429
|
+
table: { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
|
|
49
430
|
},
|
|
50
431
|
subtitle: {
|
|
51
432
|
control : 'text',
|
|
52
|
-
description: '
|
|
53
|
-
|
|
433
|
+
description: 'The value proposition — the actual heading of the section, rendered '
|
|
434
|
+
+ 'as `<bd-h as="h1" visual-level="h1">`. Its level is the `headingLevel` prop; '
|
|
435
|
+
+ 'its size is fixed. (This used to say `<bd-h as="h4">`, from before the fix.)',
|
|
436
|
+
table: { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
|
|
54
437
|
},
|
|
55
438
|
description: {
|
|
56
439
|
control : 'text',
|
|
57
|
-
description: 'Description paragraph rendered as `<bd-p
|
|
440
|
+
description: 'Description paragraph rendered as `<bd-p variant="regular">`',
|
|
58
441
|
table : { type: { summary: 'string' }, defaultValue: { summary: '' }, category: 'Content' }
|
|
59
442
|
},
|
|
443
|
+
ctaLabel: {
|
|
444
|
+
control : 'text',
|
|
445
|
+
description: 'Visible label on the primary CTA. Defaults to `Buy now`, the string this '
|
|
446
|
+
+ 'component has always rendered; the design reads **Get Protection**. Its '
|
|
447
|
+
+ 'destination is not a prop \u2014 the primary carries `buyLink`, so the store '
|
|
448
|
+
+ 'injects the buy URL at runtime.',
|
|
449
|
+
table: { type: { summary: 'string' }, defaultValue: { summary: 'Buy now' }, category: 'CTA' }
|
|
450
|
+
},
|
|
451
|
+
ctaSecondaryLabel: {
|
|
452
|
+
control : 'text',
|
|
453
|
+
description: 'Visible label on the secondary CTA. Defaults to `View more plans`; the '
|
|
454
|
+
+ 'design reads **Start your 30-day trial**.',
|
|
455
|
+
table: { type: { summary: 'string' }, defaultValue: { summary: 'View more plans' }, category: 'CTA' }
|
|
456
|
+
},
|
|
457
|
+
ctaSecondaryHref: {
|
|
458
|
+
control : 'text',
|
|
459
|
+
description: '**Set this.** Destination for the secondary CTA, with no default \u2014 and '
|
|
460
|
+
+ 'until a page gives it one the control is dead: `bd-button-link` drops the `href` '
|
|
461
|
+
+ 'attribute when it is empty, so the anchor is not focusable, is not a link to a '
|
|
462
|
+
+ 'screen reader, and does nothing on click. It has rendered that way since the '
|
|
463
|
+
+ 'component was written.',
|
|
464
|
+
table: { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'CTA' }
|
|
465
|
+
},
|
|
466
|
+
headingLevel: {
|
|
467
|
+
control : { type: 'select' },
|
|
468
|
+
options : ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
|
469
|
+
description: 'Where the headline sits in the **document outline**. Defaults to '
|
|
470
|
+
+ '`h1`, which is what a product hero is on the page it leads. Changing it '
|
|
471
|
+
+ 'does not change how the headline looks — `visual-level` is pinned to `h1`, '
|
|
472
|
+
+ 'so the hero stays 48/700/1.25 at every level. Off-list values fall back to '
|
|
473
|
+
+ '`h2` in `bd-h`: still a heading, never the page\'s.',
|
|
474
|
+
table: { type: { summary: "'h1'|'h2'|'h3'|'h4'|'h5'|'h6'" }, defaultValue: { summary: 'h1' }, category: 'Content' }
|
|
475
|
+
},
|
|
60
476
|
productId: {
|
|
61
477
|
control : 'text',
|
|
62
478
|
description: 'Product ID passed to `<bd-product>` for store price lookup',
|
|
@@ -74,8 +490,11 @@ export default {
|
|
|
74
490
|
},
|
|
75
491
|
shadow: {
|
|
76
492
|
control : 'boolean',
|
|
77
|
-
description: '
|
|
78
|
-
|
|
493
|
+
description: 'NOT styling. It is how @repobit/dex-store-elements finds this '
|
|
494
|
+
+ "component's shadow root -- its SHADOW_HOST_SELECTOR is "
|
|
495
|
+
+ '\'[shadow], [data-bd-watch-shadow]\' -- so every [data-store-render] price '
|
|
496
|
+
+ 'zone inside the shadow tree depends on it. Load-bearing; leave it set.',
|
|
497
|
+
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' }, category: 'Appearance' }
|
|
79
498
|
},
|
|
80
499
|
dataLayerEvent: {
|
|
81
500
|
control : 'text',
|
|
@@ -90,13 +509,17 @@ const render = (args) => html`
|
|
|
90
509
|
title="${args.title}"
|
|
91
510
|
subtitle="${args.subtitle}"
|
|
92
511
|
description="${args.description}"
|
|
512
|
+
heading-level="${args.headingLevel}"
|
|
513
|
+
cta-label="${args.ctaLabel}"
|
|
514
|
+
cta-secondary-label="${args.ctaSecondaryLabel}"
|
|
515
|
+
cta-secondary-href="${args.ctaSecondaryHref}"
|
|
93
516
|
product-id="${args.productId}"
|
|
94
517
|
devices="${args.devices}"
|
|
95
518
|
subscription="${args.subscription}"
|
|
96
519
|
?shadow="${args.shadow}"
|
|
97
520
|
data-layer-event="${args.dataLayerEvent}"
|
|
98
521
|
>
|
|
99
|
-
<img slot="header-picture" src="/
|
|
522
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
|
|
100
523
|
<span slot="disclaimer-top">
|
|
101
524
|
Protection for 1 account & ${args.devices} PCs, Macs, tablets, or smartphones.
|
|
102
525
|
Windows® | macOS® | Android™ | iOS®
|
|
@@ -105,11 +528,15 @@ const render = (args) => html`
|
|
|
105
528
|
`;
|
|
106
529
|
|
|
107
530
|
const defaultArgs = {
|
|
108
|
-
title
|
|
109
|
-
subtitle
|
|
110
|
-
description
|
|
111
|
-
|
|
112
|
-
|
|
531
|
+
title : 'Bitdefender Total Security',
|
|
532
|
+
subtitle : 'Complete real-time data protection',
|
|
533
|
+
description : 'Award-winning cybersecurity for all your devices. Protection against viruses, malware, ransomware, and more.',
|
|
534
|
+
headingLevel : 'h1',
|
|
535
|
+
ctaLabel : 'Buy now',
|
|
536
|
+
ctaSecondaryLabel: 'View more plans',
|
|
537
|
+
ctaSecondaryHref : '',
|
|
538
|
+
productId : 'ts', devices : '5', subscription : '1',
|
|
539
|
+
shadow : true, dataLayerEvent : 'info'
|
|
113
540
|
};
|
|
114
541
|
|
|
115
542
|
// ─── Stories ───────────────────────────────────────────────────────────────
|
|
@@ -143,7 +570,7 @@ export const WithDisclaimers = {
|
|
|
143
570
|
description="Unlimited devices, unlimited VPN, identity theft protection."
|
|
144
571
|
product-id="ps" devices="unlimited" subscription="1"
|
|
145
572
|
>
|
|
146
|
-
<img slot="header-picture" src="/
|
|
573
|
+
<img slot="header-picture" src="/images/bd-header-us.jpg" alt="Premium Security" />
|
|
147
574
|
<span slot="disclaimer-top">Protects unlimited devices.</span>
|
|
148
575
|
<span slot="disclaimer-bottom-text-start">By purchasing you agree to our </span>
|
|
149
576
|
<a slot="disclaimer-bottom-link" href="/terms">Terms of Service</a>
|
|
@@ -179,5 +606,401 @@ export const Playground = {
|
|
|
179
606
|
name : '🛝 Playground',
|
|
180
607
|
args : { ...defaultArgs },
|
|
181
608
|
render,
|
|
182
|
-
parameters: { docs: { description: { story: 'Interactive playground. Adjust title, subtitle, description, product-id, devices, and subscription via Controls.' } } }
|
|
183
|
-
};
|
|
609
|
+
parameters: { docs: { description: { story: 'Interactive playground. Adjust title, subtitle, description, heading-level, product-id, devices, and subscription via Controls.' } } }
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
export const HeadingLevel = {
|
|
613
|
+
name : 'Heading Level (the outline is the page\'s)',
|
|
614
|
+
parameters: {
|
|
615
|
+
docs: {
|
|
616
|
+
description: {
|
|
617
|
+
story: `Same header twice. The top one takes the default — the headline
|
|
618
|
+
is the page's \`<h1>\`. The bottom one is \`heading-level="h3"\`.
|
|
619
|
+
|
|
620
|
+
**Look at the type: nothing moved.** Both headlines render 48px / 700 /
|
|
621
|
+
60px, because \`visual-level\` is pinned to \`h1\` inside the component. The
|
|
622
|
+
level is the document outline and the page owns it; the size is the design and
|
|
623
|
+
the component owns it. \`bd-h\` keeps those on separate props for exactly this
|
|
624
|
+
case, and pinning one of them is what stops an outline decision from
|
|
625
|
+
repainting the design — unpinned, this second header would have rendered its
|
|
626
|
+
hero at the h3 role: measured 32px here and 24px at 320, against 48 and 30.
|
|
627
|
+
|
|
628
|
+
Inspect the shadow roots to see the difference, because that is the only place
|
|
629
|
+
it exists: \`h1.bd-heading.h1\` above, \`h3.bd-heading.h1\` below. The class is
|
|
630
|
+
the type role and stays \`h1\`; the tag is the level and follows the prop.
|
|
631
|
+
|
|
632
|
+
Measured, both headers, in this story: 48px / 700 / 60px and a section
|
|
633
|
+
**484px** tall at 1024, 30px and **728px** tall at 320 — identical to the
|
|
634
|
+
pixel, differing only in tag name. A level off the h1–h6 list behaves the same
|
|
635
|
+
way on the size and falls back on the level: \`heading-level="zzz"\` renders
|
|
636
|
+
\`h2.bd-heading.h1\` at 48px, which is \`bd-h\`'s own fallback — still a
|
|
637
|
+
heading, never the page's.
|
|
638
|
+
|
|
639
|
+
**Reach for this deliberately.** A hero that is not the page's main heading is
|
|
640
|
+
usually a sign the hero is in the wrong place. The prop exists because the
|
|
641
|
+
decision belongs to the dev and SEO teams composing the page, not because
|
|
642
|
+
\`h1\` is in doubt.`
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
render: () => html`
|
|
647
|
+
<div style="display:flex; flex-direction:column; gap:var(--spacing-64);">
|
|
648
|
+
<div>
|
|
649
|
+
<p style="margin:0 0 var(--spacing-8); font: 500 12px/1.4 var(--typography-fontFamily-sans); color: var(--color-text-muted);">
|
|
650
|
+
default — renders <h1 class="bd-heading h1">
|
|
651
|
+
</p>
|
|
652
|
+
<bd-header
|
|
653
|
+
title="Bitdefender Total Security"
|
|
654
|
+
subtitle="Complete real-time data protection"
|
|
655
|
+
description="Award-winning cybersecurity for all your devices."
|
|
656
|
+
product-id="ts" devices="5" subscription="1">
|
|
657
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
|
|
658
|
+
</bd-header>
|
|
659
|
+
</div>
|
|
660
|
+
<div>
|
|
661
|
+
<p style="margin:0 0 var(--spacing-8); font: 500 12px/1.4 var(--typography-fontFamily-sans); color: var(--color-text-muted);">
|
|
662
|
+
heading-level="h3" — renders <h3 class="bd-heading h1">, same 48px
|
|
663
|
+
</p>
|
|
664
|
+
<bd-header
|
|
665
|
+
heading-level="h3"
|
|
666
|
+
title="Bitdefender Total Security"
|
|
667
|
+
subtitle="Complete real-time data protection"
|
|
668
|
+
description="Award-winning cybersecurity for all your devices."
|
|
669
|
+
product-id="ts" devices="5" subscription="1">
|
|
670
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
|
|
671
|
+
</bd-header>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
`
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
export const CtaCopy = {
|
|
678
|
+
name : 'Design copy & a live secondary',
|
|
679
|
+
parameters: {
|
|
680
|
+
docs: {
|
|
681
|
+
description: {
|
|
682
|
+
story: `Two headers, same component. **Top** is the default: \`Buy now\` /
|
|
683
|
+
\`View more plans\`, and the secondary has no \`href\`. **Bottom** sets the strings
|
|
684
|
+
Header / v2 actually reads and gives the secondary somewhere to go.
|
|
685
|
+
|
|
686
|
+
Inspect the two secondary buttons and the difference is not the words — the top one
|
|
687
|
+
renders \`<a>\` with **no href attribute at all**, because \`bd-button-link\` drops it
|
|
688
|
+
when the prop is empty. That anchor is not focusable, does not appear in a screen
|
|
689
|
+
reader's list of links, and does nothing when clicked. It has been that way since the
|
|
690
|
+
component was written, and it is what \`cta-secondary-href\` is for.
|
|
691
|
+
|
|
692
|
+
The primary needs no href either way: it carries \`buyLink\`, so
|
|
693
|
+
\`@repobit/dex-store\` injects the buy URL at runtime — which is also why there is no
|
|
694
|
+
\`cta-href\` prop to match the label one.
|
|
695
|
+
|
|
696
|
+
**Careful reading this page, though: in Storybook BOTH primaries are inert too**, for
|
|
697
|
+
the same reason the price zones are empty here — there is no store to inject anything.
|
|
698
|
+
Measured on this story: all three hrefless anchors refuse focus and match neither
|
|
699
|
+
\`a[href]\` nor \`:enabled\`, while the one with \`cta-secondary-href\` takes focus
|
|
700
|
+
normally. On a real page the primary gets its href from the store and the secondary
|
|
701
|
+
still does not, unless you set it.
|
|
702
|
+
|
|
703
|
+
Accessible names come from the visible text now rather than from a \`label\` attribute —
|
|
704
|
+
measured here as "Buy now", "View more plans", "Get Protection" and "Start your 30-day
|
|
705
|
+
trial". Passing both, as this component used to, gave the anchor an \`aria-label\`
|
|
706
|
+
duplicating its own content.`
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
render: () => html`
|
|
711
|
+
<div style="display:flex; flex-direction:column; gap:var(--spacing-64)">
|
|
712
|
+
<div>
|
|
713
|
+
<p style="margin:0 0 var(--spacing-8); font:500 12px/1.4 var(--typography-fontFamily-sans);
|
|
714
|
+
color:var(--color-text-muted)">
|
|
715
|
+
defaults — and the secondary CTA has no destination
|
|
716
|
+
</p>
|
|
717
|
+
<bd-header
|
|
718
|
+
title="Bitdefender Total Security"
|
|
719
|
+
subtitle="Complete real-time data protection"
|
|
720
|
+
description="Award-winning cybersecurity for all your devices."
|
|
721
|
+
product-id="ts" devices="5" subscription="1">
|
|
722
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
|
|
723
|
+
</bd-header>
|
|
724
|
+
</div>
|
|
725
|
+
<div>
|
|
726
|
+
<p style="margin:0 0 var(--spacing-8); font:500 12px/1.4 var(--typography-fontFamily-sans);
|
|
727
|
+
color:var(--color-text-muted)">
|
|
728
|
+
the design's own copy, and a secondary that navigates
|
|
729
|
+
</p>
|
|
730
|
+
<bd-header
|
|
731
|
+
title="Bitdefender Ultimate Security"
|
|
732
|
+
subtitle="Complete real-time data protection"
|
|
733
|
+
description="Award-winning cybersecurity for all your devices."
|
|
734
|
+
cta-label="Get Protection"
|
|
735
|
+
cta-secondary-label="Start your 30-day trial"
|
|
736
|
+
cta-secondary-href="#trial"
|
|
737
|
+
product-id="ts" devices="5" subscription="1">
|
|
738
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Product screenshot" />
|
|
739
|
+
</bd-header>
|
|
740
|
+
</div>
|
|
741
|
+
</div>
|
|
742
|
+
`
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
export const BrandSignature = {
|
|
746
|
+
name : 'Brand Signature & Scrim',
|
|
747
|
+
parameters: {
|
|
748
|
+
docs: {
|
|
749
|
+
description: {
|
|
750
|
+
story: `The two hardest grounds the mark has to survive, side by side.
|
|
751
|
+
|
|
752
|
+
**Left** is the real product photo. **Right** is a deliberately pale panel
|
|
753
|
+
standing in for a light image — no asset in the package is bright enough to be
|
|
754
|
+
the worst case, and the worst case is the one worth showing, because it is the
|
|
755
|
+
one that decided the colour. A bright brand blue cannot serve here at all:
|
|
756
|
+
white on \`--color-blue-500\` is **4.53:1 solid**, so no partial coverage of it
|
|
757
|
+
can ever reach the minimum. \`--color-blue-950\` at 60% holds white above 4.5:1
|
|
758
|
+
over any image including a white one.
|
|
759
|
+
|
|
760
|
+
Measured against the photo on the left: worst neighbourhood **8.64:1**, nothing
|
|
761
|
+
under 4.5. With the scrim removed the same crop measured **1.00:1**, with 33.5%
|
|
762
|
+
of the mark's area under the 3:1 WCAG 1.4.11 asks of a non-text graphic — so
|
|
763
|
+
before this the mark was legible by luck of the crop.
|
|
764
|
+
|
|
765
|
+
The band's geometry is derived rather than tuned: it is 48 + 112 tall
|
|
766
|
+
(measured: 160px), where 48 is the mark's own box — a \`--spacing-24\` inset
|
|
767
|
+
plus the 24px the artwork is drawn at — so the 60% plateau stop at 70% lands
|
|
768
|
+
exactly on the mark's top edge. The arithmetic and the one case it does not
|
|
769
|
+
hold — a root font size *below* 16px — are written out in \`header.css.js\`.
|
|
770
|
+
|
|
771
|
+
Both headers here are the full chosen configuration, so this story doubles as
|
|
772
|
+
the reference for the type scale and the 16/8/8 rhythm described above.`
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
render: () => html`
|
|
777
|
+
<div style="display:flex; flex-direction:column; gap:var(--spacing-64);">
|
|
778
|
+
<bd-header
|
|
779
|
+
title="Bitdefender Total Security"
|
|
780
|
+
subtitle="Complete protection for all your devices"
|
|
781
|
+
description="Award-winning security for Windows, macOS, Android and iOS."
|
|
782
|
+
product-id="ts" devices="5" subscription="1"
|
|
783
|
+
>
|
|
784
|
+
<img slot="header-picture" src="/images/bd-header-img.jpg" alt="Total Security" />
|
|
785
|
+
</bd-header>
|
|
786
|
+
|
|
787
|
+
<bd-header
|
|
788
|
+
title="Bitdefender Total Security"
|
|
789
|
+
subtitle="The same mark on a pale image"
|
|
790
|
+
description="The stand-in panel on the right is as light as an image can get."
|
|
791
|
+
product-id="ts" devices="5" subscription="1"
|
|
792
|
+
>
|
|
793
|
+
<div
|
|
794
|
+
slot="header-picture"
|
|
795
|
+
role="presentation"
|
|
796
|
+
style="
|
|
797
|
+
width: 100%;
|
|
798
|
+
aspect-ratio: 613 / 456;
|
|
799
|
+
background: linear-gradient(
|
|
800
|
+
140deg,
|
|
801
|
+
var(--color-neutral-0),
|
|
802
|
+
var(--color-blue-100) 55%,
|
|
803
|
+
var(--color-neutral-0)
|
|
804
|
+
);
|
|
805
|
+
"
|
|
806
|
+
></div>
|
|
807
|
+
</bd-header>
|
|
808
|
+
</div>
|
|
809
|
+
`
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
/* ---------------------------------------------------------------------------
|
|
813
|
+
THE TARGET REFERENCE.
|
|
814
|
+
|
|
815
|
+
This is the one story in the file that is NOT bd-header. It is Figma
|
|
816
|
+
`Header / v2` (node 340:4) drawn with this package's atoms and filled with
|
|
817
|
+
the file's own dummy content, so a developer can see what the component is
|
|
818
|
+
meant to become rather than read a description of it.
|
|
819
|
+
|
|
820
|
+
IT BORROWS THE COMPONENT'S STYLESHEET. headerCSS is injected verbatim, so
|
|
821
|
+
every gap, every type role and the scrim geometry come from the same source
|
|
822
|
+
the component uses and cannot drift from it. Exactly three declarations do
|
|
823
|
+
not survive the move out of shadow DOM, and they are restated below with
|
|
824
|
+
that reason attached: :host and two ::slotted() rules. That is also the
|
|
825
|
+
point the story is making -- the delta between this and bd-header is MARKUP,
|
|
826
|
+
not styling.
|
|
827
|
+
|
|
828
|
+
THE CONTENT IS COPIED, NOT INVENTED. Every string is Figma's, read off the
|
|
829
|
+
node rather than retyped: "Bitdefender" is the fixed brand line and
|
|
830
|
+
"Ultimate Security" is the productName property's default, which is why the
|
|
831
|
+
name is two lines and where the split falls. */
|
|
832
|
+
const FIGMA = {
|
|
833
|
+
brand : 'Bitdefender',
|
|
834
|
+
product : 'Ultimate Security',
|
|
835
|
+
valueProp : 'Your Ultimate Digital Protection',
|
|
836
|
+
description: 'Award-winning protection against digital threats, including '
|
|
837
|
+
+ 'advanced scams, with unlimited VPN for private browsing, password '
|
|
838
|
+
+ 'manager and digital identity protection.',
|
|
839
|
+
fullPrice : '$159.99',
|
|
840
|
+
discount : '44% OFF',
|
|
841
|
+
finalPrice : '$89.99',
|
|
842
|
+
ctaPrimary : 'Get Protection',
|
|
843
|
+
ctaSecondary : 'Start your 30-day trial',
|
|
844
|
+
disclaimerTop : 'Works on: Windows® | macOS® | Android™ | iOS®.',
|
|
845
|
+
disclaimerNext: 'First year price. Sales tax included. See Terms of Use below.'
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
export const IntendedDesign = {
|
|
849
|
+
name : 'Intended design (Figma reference)',
|
|
850
|
+
parameters: {
|
|
851
|
+
controls: { disable: true },
|
|
852
|
+
docs : {
|
|
853
|
+
description: {
|
|
854
|
+
story: `**This is the target, not the component.** Every other story on
|
|
855
|
+
this page renders \`<bd-header>\`; this one is Figma \`Header / v2\`
|
|
856
|
+
(node \`340:4\`, 1290 × 566) rebuilt from this package's atoms and filled with
|
|
857
|
+
the Figma file's own dummy content. Build against it.
|
|
858
|
+
|
|
859
|
+
It is **not** a second implementation to copy markup from. It injects
|
|
860
|
+
\`headerCSS\` — the component's real stylesheet — so the spacing, the type
|
|
861
|
+
roles and the scrim all come from the same source \`bd-header\` uses and cannot
|
|
862
|
+
drift from it. Only three declarations do not survive the move out of shadow
|
|
863
|
+
DOM, and the story restates them: \`:host\` (the section's ground and its 16px
|
|
864
|
+
padding) and two \`::slotted()\` rules (the picture's cover fill, the disclaimer
|
|
865
|
+
link's ink). **So the delta below is markup, not styling** — the type scale and
|
|
866
|
+
the 16/8/8 rhythm already match, and were measured doing so.
|
|
867
|
+
|
|
868
|
+
### What \`bd-header\` still cannot render
|
|
869
|
+
|
|
870
|
+
| intended | \`bd-header\` today | what it needs |
|
|
871
|
+
|---|---|---|
|
|
872
|
+
| The product name on **two lines** — "Bitdefender" fixed, "Ultimate Security" variable | one \`title\` string, one line | **decided 2026-09-17: nothing, here.** The page builds it. \`title\` stays one string and one line; a page that needs the two-line kicker composes it from the atoms against this story, which is what this story is for. See below |
|
|
873
|
+
| CTA labels "Get Protection" / "Start your 30-day trial" | \`cta-label\` and \`cta-secondary-label\`, defaulting to "Buy now" / "View more plans" | **the page sets them, as of 2026-09-17.** Which words sell a subscription is still funnel copy and still not ours — but it is now a value in markup rather than a literal in this file |
|
|
874
|
+
| \`$159.99\` / \`44% OFF\` / \`$89.99\` visible | store-driven, so empty in Storybook | **nothing.** This is correct behaviour — the reference hardcodes them only so the price block's layout is visible here |
|
|
875
|
+
| the disclaimer as one two-line paragraph | two \`bd-p\` blocks behind three slots | **nothing.** Measured identical: 8px from the CTA row, 0 between the lines |
|
|
876
|
+
|
|
877
|
+
The Figma component exposes seven properties (\`productName\`, \`subtitle\`,
|
|
878
|
+
\`description\`, \`fullPrice\`, \`finalPrice\`, \`disclaimer\`, and \`media\` as an
|
|
879
|
+
instance swap), and the "Bitdefender" line is deliberately not one of them — it
|
|
880
|
+
is the same word on every product, so the design holds it fixed and lets only
|
|
881
|
+
the rest of the name vary.
|
|
882
|
+
|
|
883
|
+
### Where each of the two gaps landed
|
|
884
|
+
|
|
885
|
+
**The two-line product name is the page's, not the component's.** Decided
|
|
886
|
+
2026-09-17. \`bd-header\` keeps one \`title\` string, because the split is
|
|
887
|
+
structural rather than textual — Figma has a fixed brand layer and a variable
|
|
888
|
+
\`productName\`, and no single escaped string reproduces that — and the honest
|
|
889
|
+
place to reproduce structure is markup the page owns. So this story is the
|
|
890
|
+
deliverable, not an interim sketch: it shows the two text nodes in a zero-gap
|
|
891
|
+
vertical stack, with the real \`headerCSS\` behind them, which is what a page
|
|
892
|
+
implementing the two-line kicker builds against. If it later turns out every
|
|
893
|
+
consumer wants it, that is the moment to give \`bd-header\` the second prop —
|
|
894
|
+
not before, because an API change with live consumers should be paid for by
|
|
895
|
+
more than one page.
|
|
896
|
+
|
|
897
|
+
**The CTA labels are the page's now**, which is as far as this package can
|
|
898
|
+
take them. \`cta-label\` and \`cta-secondary-label\` default to the strings
|
|
899
|
+
this component always rendered, so nothing changes for a consumer who says
|
|
900
|
+
nothing, and the copy owner's answer no longer needs a code change to land.
|
|
901
|
+
Props rather than slots: a label is a plain string with no markup, which is
|
|
902
|
+
what \`title\` and \`subtitle\` already are, and props appear in the manifest a
|
|
903
|
+
page generator reads while slots do not.
|
|
904
|
+
|
|
905
|
+
**And doing that turned up a dead control.** The secondary CTA has never had a
|
|
906
|
+
destination — \`bd-button-link\` drops the \`href\` attribute when it is empty,
|
|
907
|
+
so it renders an \`<a>\` with no href: not focusable, not a link to a screen
|
|
908
|
+
reader, nothing on click. The primary is fine, because \`buyLink\` has the store
|
|
909
|
+
inject the buy URL. So \`cta-secondary-href\` has no default and wants setting;
|
|
910
|
+
*Design copy & a live secondary* below shows both.
|
|
911
|
+
|
|
912
|
+
**And the heading level is no longer a gap.** It was a third one, unlisted
|
|
913
|
+
above because it is not visible in a screenshot: the headline's \`as="h1"\` was
|
|
914
|
+
hardcoded, so a page could not place the hero in its own outline. It is the
|
|
915
|
+
\`heading-level\` prop as of 2026-09-17 — see the heading-structure section in
|
|
916
|
+
this component's docs. Same decision as the product name, reached the other
|
|
917
|
+
way: the page owns the outline, so the page gets the say.`
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
render: () => html`
|
|
922
|
+
<style>
|
|
923
|
+
${ensembleCSS.cssText}
|
|
924
|
+
${headerCSS.cssText}
|
|
925
|
+
|
|
926
|
+
/* (1) stands in for :host, which cannot match in light DOM. The two
|
|
927
|
+
declarations are headerCSS's own -- the section's ground and the 16px
|
|
928
|
+
row padding the height study bought. */
|
|
929
|
+
.bd-header-reference {
|
|
930
|
+
display: block;
|
|
931
|
+
background: var(--color-surface-default);
|
|
932
|
+
padding-block: var(--spacing-16);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/* (2) stands in for ::slotted([slot="header-picture"]). Same four
|
|
936
|
+
declarations; the picture is a real child here rather than slotted,
|
|
937
|
+
so the shadow pseudo-element never matches it. */
|
|
938
|
+
.bd-header-reference .bd-header-image {
|
|
939
|
+
width: 100%;
|
|
940
|
+
height: 100%;
|
|
941
|
+
object-fit: cover;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/* (3) stands in for ::slotted([slot="disclaimer-bottom-link"]). */
|
|
945
|
+
.bd-header-reference .bd-disclaimer-bottom a {
|
|
946
|
+
color: var(--color-text-primary);
|
|
947
|
+
text-decoration: underline;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/* NOT a stand-in -- the one thing the reference draws that the component
|
|
951
|
+
has no markup for yet, and the whole reason this story exists. Figma
|
|
952
|
+
stacks the product name as two text nodes in a zero-gap vertical
|
|
953
|
+
frame, which is two block lines on the kicker's own 1.3 leading. The
|
|
954
|
+
type comes from .bd-product-name in headerCSS above; these spans only
|
|
955
|
+
make the break. */
|
|
956
|
+
.bd-header-reference .bd-product-name span {
|
|
957
|
+
display: block;
|
|
958
|
+
}
|
|
959
|
+
</style>
|
|
960
|
+
|
|
961
|
+
<div class="bd-header-reference">
|
|
962
|
+
<div class="bd-ensemble bd-container">
|
|
963
|
+
<div class="bd-header-left">
|
|
964
|
+
<p class="bd-product-name">
|
|
965
|
+
<span>${FIGMA.brand}</span><span>${FIGMA.product}</span>
|
|
966
|
+
</p>
|
|
967
|
+
<bd-h as="h1" class="bd-value-prop">${FIGMA.valueProp}</bd-h>
|
|
968
|
+
<bd-p variant="regular" class="bd-description">${FIGMA.description}</bd-p>
|
|
969
|
+
|
|
970
|
+
<div class="bd-price-zone-top">
|
|
971
|
+
<bd-p variant="regular" font-weight="medium" class="original-price">
|
|
972
|
+
<s>${FIGMA.fullPrice}</s>
|
|
973
|
+
</bd-p>
|
|
974
|
+
<bd-badge tone="success" variant="fill" size="lg" class="bd-discount"
|
|
975
|
+
>${FIGMA.discount}</bd-badge
|
|
976
|
+
>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
<span class="bd-price-zone-final">${FIGMA.finalPrice}</span>
|
|
980
|
+
|
|
981
|
+
<div class="bd-cta">
|
|
982
|
+
<bd-button-link label=${FIGMA.ctaPrimary} variant="accent" size="lg">
|
|
983
|
+
${FIGMA.ctaPrimary}
|
|
984
|
+
</bd-button-link>
|
|
985
|
+
<bd-button-link label=${FIGMA.ctaSecondary} variant="tertiary" size="lg">
|
|
986
|
+
${FIGMA.ctaSecondary}
|
|
987
|
+
</bd-button-link>
|
|
988
|
+
</div>
|
|
989
|
+
|
|
990
|
+
<bd-p variant="xsmall" class="bd-disclaimer-top">${FIGMA.disclaimerTop}</bd-p>
|
|
991
|
+
<bd-p variant="xsmall" class="bd-disclaimer-bottom"
|
|
992
|
+
>${FIGMA.disclaimerNext}</bd-p
|
|
993
|
+
>
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div class="bd-header-right">
|
|
997
|
+
<div class="bd-header-image-wrapper">
|
|
998
|
+
<img class="bd-header-image" src="/images/bd-header-img.jpg" alt="" />
|
|
999
|
+
<div class="bd-header-scrim" aria-hidden="true"></div>
|
|
1000
|
+
<bd-brand-signature-icon class="bd-header-signature"></bd-brand-signature-icon>
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
`
|
|
1006
|
+
};
|