@repobit/dex-system-design 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +43 -12
  3. package/package.json +6 -4
  4. package/schemas/component-manifest.json +207 -49
  5. package/schemas/manifest-omissions.json +2 -0
  6. package/src/components/accordion/accordion.css.js +12 -1
  7. package/src/components/agent-endpoints/agent-endpoints.js +1 -1
  8. package/src/components/alert/alert.css.js +22 -1
  9. package/src/components/anchor/anchor-nav.css.js +6 -1
  10. package/src/components/annotation/annotation.css.js +6 -1
  11. package/src/components/architecture.stories.js +6 -4
  12. package/src/components/avatar/avatar.js +37 -11
  13. package/src/components/avatar/avatar.stories.js +10 -6
  14. package/src/components/awards/awards-flex.css.js +31 -129
  15. package/src/components/awards/awards-flex.js +66 -116
  16. package/src/components/awards/awards-flex.stories.js +8 -6
  17. package/src/components/back/back.css.js +6 -1
  18. package/src/components/back/back.js +24 -12
  19. package/src/components/back/back.stories.js +3 -3
  20. package/src/components/badge/badge.js +6 -3
  21. package/src/components/badge/badge.stories.js +13 -9
  22. package/src/components/bento-grid/bento-grid.js +1 -1
  23. package/src/components/blockquote/blockquote.css.js +12 -3
  24. package/src/components/blockquote/blockquote.js +12 -5
  25. package/src/components/blockquote/blockquote.stories.js +6 -4
  26. package/src/components/button/button.css.js +46 -25
  27. package/src/components/button/button.js +44 -43
  28. package/src/components/button/button.stories.js +2 -2
  29. package/src/components/caption/caption.css.js +10 -0
  30. package/src/components/caption/caption.stories.js +4 -4
  31. package/src/components/carousel/carousel.css.js +19 -116
  32. package/src/components/carousel/carousel.js +70 -74
  33. package/src/components/carousel/carousel.stories.js +9 -8
  34. package/src/components/checkbox/checkbox.css.js +6 -1
  35. package/src/components/code/code.css.js +6 -1
  36. package/src/components/code/code.js +48 -7
  37. package/src/components/code/code.stories.js +7 -4
  38. package/src/components/divider/divider-horizontal.js +11 -3
  39. package/src/components/divider/divider-vertical.js +12 -5
  40. package/src/components/divider/divider.css.js +4 -1
  41. package/src/components/divider/divider.stories.js +6 -6
  42. package/src/components/dropdown/dropdown.js +67 -13
  43. package/src/components/dropdown/dropdown.stories.js +15 -14
  44. package/src/components/fieldset/fieldset.stories.js +41 -16
  45. package/src/components/footer/footer-nav-menu.css.js +6 -1
  46. package/src/components/footer/footer.css.js +6 -1
  47. package/src/components/header/header-code.js +1 -1
  48. package/src/components/header/header.css.js +9 -4
  49. package/src/components/heading/heading.stories.js +64 -31
  50. package/src/components/highlight/highlight.js +74 -34
  51. package/src/components/highlight/highlight.stories.js +5 -4
  52. package/src/components/icons/icon-element.js +49 -6
  53. package/src/components/icons/icons.stories.js +7 -0
  54. package/src/components/image/image.css.js +9 -98
  55. package/src/components/image/image.js +7 -2
  56. package/src/components/input/custom-form.stories.js +14 -19
  57. package/src/components/input/input-clipboard.css.js +6 -1
  58. package/src/components/input/input-clipboard.js +56 -8
  59. package/src/components/input/input.js +19 -2
  60. package/src/components/label/label.js +87 -1
  61. package/src/components/label/label.stories.js +21 -12
  62. package/src/components/link/link.css.js +52 -3
  63. package/src/components/link/link.js +52 -17
  64. package/src/components/link/link.stories.js +114 -3
  65. package/src/components/list/list-item/list-item.css.js +4 -6
  66. package/src/components/list/list-item/list-item.js +53 -5
  67. package/src/components/list/list-item/list-item.stories.js +22 -0
  68. package/src/components/list/list.js +27 -0
  69. package/src/components/modal/modal.css.js +6 -1
  70. package/src/components/pagination-dots/pagination-dots.css.js +161 -0
  71. package/src/components/pagination-dots/pagination-dots.js +202 -0
  72. package/src/components/pagination-dots/pagination-dots.stories.js +171 -0
  73. package/src/components/paragraph/paragraph.css.js +37 -1
  74. package/src/components/paragraph/paragraph.js +19 -2
  75. package/src/components/paragraph/paragraph.stories.js +40 -6
  76. package/src/components/picture/picture.css.js +8 -67
  77. package/src/components/picture/picture.js +15 -8
  78. package/src/components/pricing-cards/pricing-card.css.js +12 -2
  79. package/src/components/pricing-cards/pricing-card.js +4 -4
  80. package/src/components/progress/progress.js +24 -4
  81. package/src/components/progress/progress.stories.js +8 -6
  82. package/src/components/radio/radio.css.js +6 -1
  83. package/src/components/radio/radio.js +48 -3
  84. package/src/components/radio/radio.stories.js +1 -1
  85. package/src/components/shared/form-field.css.js +30 -4
  86. package/src/components/shared/token-scales.js +16 -0
  87. package/src/components/spinner/spinner.css.js +31 -4
  88. package/src/components/spinner/spinner.js +26 -4
  89. package/src/components/spinner/spinner.stories.js +10 -7
  90. package/src/components/switch/switch.css.js +21 -3
  91. package/src/components/switch/switch.js +8 -1
  92. package/src/components/switch/switch.stories.js +2 -1
  93. package/src/components/tab-list/tab-list.css.js +6 -1
  94. package/src/components/terminal/terminal-cursor.js +11 -0
  95. package/src/components/terminal/terminal-message.js +1 -1
  96. package/src/components/termsOfUse/terms.css.js +6 -1
  97. package/src/components/textarea/textarea.stories.js +23 -0
  98. package/src/components/tooltip/tooltip.css.js +24 -1
  99. package/src/components/tooltip/tooltip.js +135 -5
  100. package/src/components/tooltip/tooltip.stories.js +20 -14
  101. package/src/components/trust-panel/trust-panel.js +2 -2
  102. package/src/components/video/video.js +38 -3
  103. package/src/components/video/video.stories.js +10 -5
  104. package/src/components/video-player/video-player.css.js +6 -1
  105. package/src/tokens/elevation.stories.js +2 -3
  106. package/src/tokens/focus.stories.js +81 -15
  107. package/src/tokens/motion.stories.js +0 -6
  108. package/src/tokens/tokens.css +174 -97
  109. package/src/tokens/tokens.js +174 -97
  110. package/src/tokens/vertical-rhythm.stories.js +1 -1
  111. package/src/stories/Button.js +0 -20
  112. package/src/stories/Button.stories.js +0 -46
  113. package/src/stories/Configure.mdx +0 -364
  114. package/src/stories/Header.js +0 -45
  115. package/src/stories/Header.stories.js +0 -24
  116. package/src/stories/Page.js +0 -62
  117. package/src/stories/Page.stories.js +0 -20
  118. package/src/stories/assets/accessibility.png +0 -0
  119. package/src/stories/assets/accessibility.svg +0 -1
  120. package/src/stories/assets/addon-library.png +0 -0
  121. package/src/stories/assets/assets.png +0 -0
  122. package/src/stories/assets/avif-test-image.avif +0 -0
  123. package/src/stories/assets/context.png +0 -0
  124. package/src/stories/assets/discord.svg +0 -1
  125. package/src/stories/assets/docs.png +0 -0
  126. package/src/stories/assets/figma-plugin.png +0 -0
  127. package/src/stories/assets/github.svg +0 -1
  128. package/src/stories/assets/share.png +0 -0
  129. package/src/stories/assets/styling.png +0 -0
  130. package/src/stories/assets/testing.png +0 -0
  131. package/src/stories/assets/theming.png +0 -0
  132. package/src/stories/assets/tutorials.svg +0 -1
  133. package/src/stories/assets/youtube.svg +0 -1
  134. package/src/stories/button.css +0 -30
  135. package/src/stories/header.css +0 -32
  136. package/src/stories/page.css +0 -68
@@ -0,0 +1,171 @@
1
+ import { html } from 'lit';
2
+ import './pagination-dots.js';
3
+
4
+ export default {
5
+ title : 'Atoms/Pagination Dots',
6
+ tags : ['autodocs'],
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: `
11
+ \`<bd-pagination-dots>\` is the package's one set of slide dots: which slide
12
+ is showing, and a way to go to another. \`bd-awards-flex\` and
13
+ \`bd-carousel-section\` both use it.
14
+
15
+ \`\`\`html
16
+ <bd-pagination-dots count="5" current="1"></bd-pagination-dots>
17
+ <bd-pagination-dots count="5" current="1" autoplay delay="4000"></bd-pagination-dots>
18
+ \`\`\`
19
+
20
+ The carousel that owns the slides stays their owner. It sets \`current\`, and
21
+ listens for \`bd-change\`, whose \`detail.current\` is the slide to show --
22
+ picked by a dot or by the autoplay clock.
23
+
24
+ - **A group of buttons**, the current one marked \`aria-current\`. Not a
25
+ tablist: a tab needs a panel it controls, and this atom does not own the
26
+ panels.
27
+ - **Two names, both attributes.** \`label\` names the group (default
28
+ "Slides") and \`item-label\` starts each button's name (default "Go to
29
+ slide", so "Go to slide 2"). A page in another language sets both.
30
+ - **Every dot is a 24px target.** The dot is drawn inside it, so the dots sit
31
+ 16px apart rather than the design's 8. At 8 they would fail WCAG 2.5.8.
32
+ - **The current dot is a 40px pill** in the indicator colour, the same
33
+ colour as every other current-item mark in the package. The design draws it
34
+ neutral-900; that would make it the only current mark that is not the
35
+ indicator.
36
+ - **A resting dot is neutral-500**, the lightest step that clears 3:1 on
37
+ every light ground it can sit on. The design's faint grey measures 1.94:1.
38
+ - **\`autoplay\` puts the clock in the pill.**- **\`autoplay\` puts the clock in the pill.** The atom advances itself every
39
+ \`delay\` ms, wrapping after the last slide. The current pill is the
40
+ progress: a white track inside an indicator-colour edge, which the
41
+ indicator fills as the time runs, so the fill and the change of slide are
42
+ one event and cannot drift apart. Without autoplay the pill is filled whole.
43
+ - **The dots are how it stops** (WCAG 2.2.2). Picking one stops autoplay for
44
+ good and sets \`paused\`. The clock also holds while the pointer or focus is
45
+ on the dots, or while the owner sets \`held\` -- the carousel does that for
46
+ its own slides. Under \`prefers-reduced-motion\` it never starts.
47
+ - **\`bd-toggle\`** fires when the slides start or stop moving, with
48
+ \`detail.paused\` and \`detail.moving\`. An owner uses it for its live
49
+ region: off while slides change by themselves, polite once they are still.
50
+ - **\`count\` below 2 renders nothing.** One page has nowhere to go.
51
+ `
52
+ }
53
+ }
54
+ },
55
+ argTypes: {
56
+ count: {
57
+ control : { type: 'number', min: 0, max: 12, step: 1 },
58
+ description: 'How many dots. Below 2 nothing renders.',
59
+ table : { type: { summary: 'number' }, defaultValue: { summary: '0' } }
60
+ },
61
+ current: {
62
+ control : { type: 'number', min: 1, max: 12, step: 1 },
63
+ description: 'Which dot is current, 1-based. Held between 1 and `count`.',
64
+ table : { type: { summary: 'number' }, defaultValue: { summary: '1' } }
65
+ },
66
+ label: {
67
+ control : 'text',
68
+ description: 'The group\'s accessible name: what the dots move through.',
69
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'Slides' } }
70
+ },
71
+ itemLabel: {
72
+ control : 'text',
73
+ description: 'Starts each button\'s accessible name; the dot\'s number follows it.',
74
+ table : { type: { summary: 'string' }, defaultValue: { summary: 'Go to slide' } }
75
+ },
76
+ autoplay: {
77
+ control : 'boolean',
78
+ description: 'Advance every `delay` ms, with the progress in the current pill. Picking a dot stops it.',
79
+ table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
80
+ },
81
+ delay: {
82
+ control : { type: 'number', min: 1000, step: 500 },
83
+ description: 'Milliseconds per slide under autoplay. Held at 1000 or more.',
84
+ table : { type: { summary: 'number' }, defaultValue: { summary: '4000' } }
85
+ },
86
+ paused: {
87
+ control : 'boolean',
88
+ description: 'Autoplay stopped for good. Set when the visitor picks a dot.',
89
+ table : { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
90
+ }
91
+ }
92
+ };
93
+
94
+ const caption = text => html`
95
+ <p style="margin:var(--spacing-24) 0 var(--spacing-8);font:var(--typography-fontSize-xs)/1.4 var(--typography-fontFamily-sans);color:var(--color-text-muted)">${text}</p>
96
+ `;
97
+
98
+ export const Default = {
99
+ name : 'Default — as the homepage uses it',
100
+ render: () => html`
101
+ <div style="background:var(--color-surface-info);padding:var(--spacing-32);border-radius:var(--radius-3xl);display:flex;justify-content:center;">
102
+ <bd-pagination-dots count="5" current="1" label="Awards"></bd-pagination-dots>
103
+ </div>
104
+ `,
105
+ parameters: { docs: { description: { story: 'Five slides on the awards carousel\'s info tint. Each dot is a 24px target; tab through them to see the ring follow the target, not the dot.' } } }
106
+ };
107
+
108
+ export const Counts = {
109
+ name : 'Counts',
110
+ render: () => html`
111
+ ${[2, 3, 4, 5, 8].map(count => html`
112
+ ${caption(`count="${count}"`)}
113
+ <bd-pagination-dots count="${count}" current="2"></bd-pagination-dots>
114
+ `)}
115
+ ${caption('count="1" — renders nothing, because one page has nowhere to go')}
116
+ <bd-pagination-dots count="1"></bd-pagination-dots>
117
+ `,
118
+ parameters: { docs: { description: { story: 'The same atom at every count the homepage could need, current on the second dot.' } } }
119
+ };
120
+
121
+ export const OnGrounds = {
122
+ name : 'On every light ground',
123
+ render: () => html`
124
+ ${[['page — surface/default', 'var(--color-surface-default)'], ['card — surface/container', 'var(--color-surface-container)'], ['band — surface/subtle', 'var(--color-surface-subtle)'], ['awards — surface/info', 'var(--color-surface-info)']].map(([label, bg]) => html`
125
+ ${caption(label)}
126
+ <div style="background:${bg};padding:var(--spacing-16) var(--spacing-24);border-radius:var(--radius-md);display:inline-flex;">
127
+ <bd-pagination-dots count="5" current="3"></bd-pagination-dots>
128
+ </div>
129
+ `)}
130
+ `,
131
+ parameters: { docs: { description: { story: 'The resting dot clears 3:1 on all four (WCAG 1.4.11), at 4.29:1 or better; the current pill, the indicator role, at 4.05:1 or better.' } } }
132
+ };
133
+
134
+ export const Interactive = {
135
+ name : 'Picking a dot',
136
+ render: () => html`
137
+ <bd-pagination-dots
138
+ count="5"
139
+ current="1"
140
+ @bd-change=${e => { e.currentTarget.nextElementSibling.textContent = `bd-change — detail.current is ${e.detail.current}`; }}
141
+ ></bd-pagination-dots>
142
+ <p style="margin:var(--spacing-12) 0 0;font:var(--typography-fontSize-sm)/1.5 var(--typography-fontFamily-sans);color:var(--color-text-secondary)" aria-live="polite">Pick a dot.</p>
143
+ `,
144
+ parameters: { docs: { description: { story: 'The atom moves its own current dot and fires `bd-change`. Picking the current dot does nothing and fires nothing.' } } }
145
+ };
146
+
147
+ export const Autoplay = {
148
+ name : 'Autoplay — the pill is the clock',
149
+ render: () => html`
150
+ <div style="background:var(--color-surface-info);padding:var(--spacing-32);border-radius:var(--radius-3xl);display:flex;justify-content:center;">
151
+ <bd-pagination-dots count="3" current="1" label="Awards" autoplay delay="4000"></bd-pagination-dots>
152
+ </div>
153
+ `,
154
+ parameters: { docs: { description: { story: 'As bd-awards-flex uses it. The current pill fills over four seconds and the slide changes when it is full. Point at the dots or tab into them and the fill holds where it is; pick one and autoplay stops for good.' } } }
155
+ };
156
+
157
+ export const Playground = {
158
+ name : '🛝 Playground',
159
+ args : { count: 5, current: 1, label: 'Slides', itemLabel: 'Go to slide', autoplay: false, delay: 4000, paused: false },
160
+ render: args => html`
161
+ <bd-pagination-dots
162
+ count="${args.count}"
163
+ current="${args.current}"
164
+ label="${args.label}"
165
+ item-label="${args.itemLabel}"
166
+ ?autoplay=${args.autoplay}
167
+ delay="${args.delay}"
168
+ ?paused=${args.paused}
169
+ ></bd-pagination-dots>
170
+ `
171
+ };
@@ -1,4 +1,5 @@
1
- import { css } from "lit";
1
+ import { css, unsafeCSS } from "lit";
2
+ import { below } from "../shared/token-scales.js";
2
3
 
3
4
  export default css`
4
5
  /* --color-text-primary, not --color-neutral-1000. Body copy was set in pure
@@ -52,4 +53,39 @@ export default css`
52
53
  line-height: var(--typography-body-lead-lineHeight);
53
54
  letter-spacing: var(--typography-body-lead-letterSpacing);
54
55
  }
56
+
57
+ /* The eyebrow: a short label above a section title. It takes the
58
+ label-large role -- the role whose job is a short label rather than
59
+ running text -- which is 16px at 1.5 like body-regular and Medium 500
60
+ where body-regular is 400. Brand ink is part of the pattern: all five
61
+ eyebrows in the package and every one on the homepage are text-brand.
62
+ A color attribute still wins, for an eyebrow on a dark band. */
63
+ :host([variant="eyebrow"]) p {
64
+ font-size: var(--typography-label-large-fontSize);
65
+ font-weight: var(--typography-label-large-fontWeight);
66
+ line-height: var(--typography-label-large-lineHeight);
67
+ letter-spacing: var(--typography-label-large-letterSpacing);
68
+ font-family: var(--typography-label-large-fontFamily);
69
+ color: var(--color-text-brand);
70
+ }
71
+
72
+ /* Below lg the eyebrow steps down to label-default: 14px, the same Medium
73
+ 500 at 1.5. The homepage draws it 14 on the 390 and 768 frames and 16 on
74
+ desktop, so the step sits at lg, which is also where every fluid role in
75
+ the system stops growing. A step between two declared roles rather than a
76
+ fluid size, because the label family has no fluid role, and one for a
77
+ single variant would be a new token where two existing roles already
78
+ answer both ends exactly. */
79
+ @media ${unsafeCSS(below("lg"))} {
80
+ :host([variant="eyebrow"]) p {
81
+ font-size: var(--typography-label-default-fontSize);
82
+ font-weight: var(--typography-label-default-fontWeight);
83
+ line-height: var(--typography-label-default-lineHeight);
84
+ letter-spacing: var(--typography-label-default-letterSpacing);
85
+ }
86
+ }
87
+
88
+
89
+
90
+
55
91
  `;
@@ -6,8 +6,21 @@ import {
6
6
  } from "../shared/token-scales.js";
7
7
  import paragraphCSS from "./paragraph.css.js";
8
8
 
9
- /** The five body roles the stylesheet carries. */
10
- const VARIANTS = ["xsmall", "small", "regular", "large", "lead"];
9
+ /**
10
+ * The five body roles the stylesheet carries, and the eyebrow.
11
+ *
12
+ * eyebrow is the short brand-ink label above a section title. It reads the
13
+ * label-large role -- 16px, Medium 500, 1.5 -- from lg up, and label-default
14
+ * (14px, the same weight and leading) below lg, rather than a role of its
15
+ * own, so it adds no token. It exists because five components drew the pattern
16
+ * and disagreed on weight: bento-grid and header-code at the body role's 400,
17
+ * trust-panel twice and agent-endpoints at a semibold 600 passed through
18
+ * font-weight, and the homepage design at 500. The phase-1 eyebrow
19
+ * investigation found no role was needed while every eyebrow resolved
20
+ * through body-regular; once three of them reached past it for a weight, one
21
+ * spelling became the thing that keeps them together.
22
+ */
23
+ const VARIANTS = ["xsmall", "small", "regular", "large", "lead", "eyebrow"];
11
24
 
12
25
  export class BdP extends LitElement {
13
26
  static properties = {
@@ -21,6 +34,10 @@ export class BdP extends LitElement {
21
34
  fontWeight : { type: String, attribute: "font-weight" },
22
35
  fontSize : { type: String, attribute: "font-size" },
23
36
  letterSpacing: { type: String, attribute: "letter-spacing" },
37
+ /* A semantic role, the same closed set bd-h and bd-display take. Without
38
+ it a paragraph's ink could only come from the composing component's
39
+ CSS, so a page could not say an eyebrow is brand-blue or a lead on a
40
+ brand band is white. */
24
41
  color : { type: String }
25
42
  };
26
43
 
@@ -1,5 +1,6 @@
1
1
  import { html } from 'lit';
2
2
  import './paragraph.js';
3
+ import '../heading/heading.js';
3
4
 
4
5
  export default {
5
6
  title : 'Atoms/Paragraph',
@@ -51,9 +52,15 @@ layout around them, not to the paragraph itself.
51
52
  argTypes: {
52
53
  variant: {
53
54
  control : { type: 'select' },
54
- options : ['large', 'regular', 'small'],
55
- description: 'Typography size variant, reflected as an attribute for CSS targeting',
56
- table : { type: { summary: "'large'|'regular'|'small'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
55
+ options : ['lead', 'large', 'regular', 'small', 'xsmall', 'eyebrow'],
56
+ description: 'Typography role, reflected as an attribute for CSS targeting. `eyebrow` is the short brand-ink label above a section title.',
57
+ table : { type: { summary: "'lead'|'large'|'regular'|'small'|'xsmall'|'eyebrow'" }, defaultValue: { summary: 'regular' }, category: 'Appearance' }
58
+ },
59
+ color: {
60
+ control : { type: 'select' },
61
+ options : ['', 'primary', 'strong', 'muted', 'subtle', 'inverse', 'success', 'warning', 'error'],
62
+ description: 'A semantic colour role, the same set `bd-h` and `bd-display` take. `primary` is brand blue; `inverse` is white on a dark or brand band.',
63
+ table : { type: { summary: 'string' }, defaultValue: { summary: "''" }, category: 'Appearance' }
57
64
  },
58
65
  padding: {
59
66
  control : 'text',
@@ -145,9 +152,36 @@ export const MultiParagraph = {
145
152
  parameters: { docs: { description: { story: 'Three paragraphs of different kinds used together as a typical content block.' } } }
146
153
  };
147
154
 
155
+ export const Eyebrow = {
156
+ name : 'Eyebrow',
157
+ render: () => html`
158
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-16); max-width:588px;">
159
+ <bd-p variant="eyebrow">15+ years of AI-driven security innovation</bd-p>
160
+ <bd-h as="h2" visual-level="h2">AI Across Bitdefender</bd-h>
161
+ </div>
162
+ <div style="margin-top:var(--spacing-32); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
163
+ <bd-p variant="eyebrow" color="inverse">On a brand band, color="inverse"</bd-p>
164
+ </div>
165
+ `,
166
+ parameters: { docs: { description: { story: 'The label above a section title, in text-brand: the label-large role (16px Medium 500 at 1.5) from lg up, and label-default (14px, same weight and leading) below lg, as the homepage draws it on tablet and mobile. It is how all five eyebrows in the package are spelled now (bento grid, header code, trust panel twice, agent endpoints), and what the homepage draws. `color` overrides the ink.' } } }
167
+ };
168
+
169
+ export const Color = {
170
+ name : 'Color',
171
+ render: () => html`
172
+ <div style="display:flex; flex-direction:column; gap:var(--spacing-12); max-width:588px;">
173
+ ${['strong', 'primary', 'muted', 'subtle', 'success', 'warning', 'error'].map(c => html`<bd-p color="${c}">color="${c}" — ${sampleText}</bd-p>`)}
174
+ </div>
175
+ <div style="margin-top:var(--spacing-24); background:var(--color-surface-brand); padding:var(--spacing-24); border-radius:var(--radius-md); max-width:588px;">
176
+ <bd-p variant="large" color="inverse">color="inverse" — Bitdefender protects consumers, small businesses, and enterprises with cybersecurity powered by world-class threat intelligence.</bd-p>
177
+ </div>
178
+ `,
179
+ parameters: { docs: { description: { story: 'A semantic role, resolved to a token. Without it a paragraph\'s ink could only come from the composing component\'s CSS.' } } }
180
+ };
181
+
148
182
  export const Playground = {
149
183
  name : '🛝 Playground',
150
- args : { kind: 'regular' },
151
- render : (args) => html`<bd-p variant="${args.kind}">${sampleText}</bd-p>`,
152
- parameters: { docs: { description: { story: 'Interactive playground. Switch between large, regular, and small via Controls.' } } }
184
+ args : { kind: 'regular', color: '' },
185
+ render : (args) => html`<bd-p variant="${args.kind}" color="${args.color}">${sampleText}</bd-p>`,
186
+ parameters: { docs: { description: { story: 'Interactive playground. Switch the variant and the colour role via Controls.' } } }
153
187
  };
@@ -68,53 +68,11 @@ export default css`
68
68
  border-radius: var(--radius-full);
69
69
  }
70
70
 
71
- /* Responsive Behavior */
72
- .bd-picture.responsive {
73
- width: 100%;
74
- }
75
-
76
- .bd-picture.responsive img {
77
- width: 100%;
78
- height: auto;
79
- }
80
-
81
- /* Aspect Ratio Containers */
82
- .bd-picture.aspect-16-9 {
83
- aspect-ratio: 16 / 9;
84
- }
85
-
86
- .bd-picture.aspect-16-9 img {
87
- width: 100%;
88
- height: 100%;
89
- object-fit: cover;
90
- }
91
-
92
- .bd-picture.aspect-4-3 {
93
- aspect-ratio: 4 / 3;
94
- }
95
-
96
- .bd-picture.aspect-4-3 img {
97
- width: 100%;
98
- height: 100%;
99
- object-fit: cover;
100
- }
101
-
102
- .bd-picture.aspect-1-1 {
103
- aspect-ratio: 1 / 1;
104
- }
105
-
106
- .bd-picture.aspect-1-1 img {
107
- width: 100%;
108
- height: 100%;
109
- object-fit: cover;
110
- }
111
-
112
- .bd-picture:focus-within {
113
- /* the one focus ring — see Foundations/Focus */
114
- outline: none;
115
- box-shadow: var(--focus-outline-primary);
116
- border-radius: var(--focus-outline-radius);
117
- }
71
+ /* Gone from here, because nothing could reach them: responsive,
72
+ aspect-16-9 / 4-3 / 1-1 and the hover-zoom and hover-lift effects had
73
+ no prop that sets them -- fit is written as fit-*, so
74
+ not even a misspelt fit could produce one. :focus-within ringed a
75
+ <picture> holding nothing focusable. */
118
76
 
119
77
  /* Loading state.
120
78
 
@@ -169,8 +127,9 @@ export default css`
169
127
  color: var(--color-text-muted);
170
128
  }
171
129
 
172
- /* One sentence for this state, shared with bd-img, and a local property
173
- rather than a string in the stylesheet so it can be translated. */
130
+ /* A local property rather than a string in the stylesheet, so it can be
131
+ translated. It keeps the --bd-img- name it always had, which is the one
132
+ pages already set. */
174
133
  .bd-picture:not([loaded])::before {
175
134
  content: var(--bd-img-loading-label, "Loading image");
176
135
  /* no role: the pre-load placeholder -- a glyph/marker/placeholder box, not running
@@ -199,16 +158,6 @@ export default css`
199
158
  transform: scale(1);
200
159
  }
201
160
 
202
- /* Hover effects */
203
- .bd-picture.hover-zoom:hover img {
204
- transform: scale(1.05);
205
- }
206
-
207
- .bd-picture.hover-lift:hover {
208
- transform: translateY(-2px);
209
- transition: transform var(--transition-duration-slow) var(--transition-easing-smooth);
210
- }
211
-
212
161
  /* Performance optimizations */
213
162
  .bd-picture img {
214
163
  will-change: transform, opacity;
@@ -248,14 +197,6 @@ export default css`
248
197
  .bd-picture img {
249
198
  transition: none;
250
199
  }
251
-
252
- .bd-picture.hover-zoom:hover img {
253
- transform: none;
254
- }
255
-
256
- .bd-picture.hover-lift:hover {
257
- transform: none;
258
- }
259
200
  }
260
201
 
261
202
  /* No @media (prefers-color-scheme: dark) block. This was the one atom that
@@ -5,6 +5,7 @@ import pictureCSS from "./picture.css.js";
5
5
 
6
6
  // the radius steps picture.css.js implements — the full --radius-* scale
7
7
  const RADIUS_STEPS = ['none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
8
+ const FITS = ['cover', 'contain', 'fill', 'none', 'scale-down'];
8
9
 
9
10
  export class BdPicture extends LitElement {
10
11
  static properties = {
@@ -50,20 +51,26 @@ export class BdPicture extends LitElement {
50
51
  }
51
52
 
52
53
  render() {
54
+ // fit becomes a class, so only the values picture.css.js implements get
55
+ // through; an unknown one used to leave the image with no object-fit
53
56
  const classes = [
54
57
  'bd-picture',
55
- `fit-${this.fit}`,
58
+ `fit-${FITS.includes(this.fit) ? this.fit : 'cover'}`,
56
59
  `radius-${RADIUS_STEPS.includes(this.radius) ? this.radius : 'none'}`
57
60
  ].filter(Boolean).join(' ');
58
61
 
62
+ // Each source's optional attributes are left off when not given. type
63
+ // used to be written as-is, so a source without one rendered
64
+ // type="undefined" -- a MIME type no browser supports, so it skipped the
65
+ // source and fell through to the next one or the <img>.
59
66
  return html`
60
67
  <picture class="${classes}">
61
68
  ${this.sources.map(source => html`
62
- <source
63
- type="${source.type}"
69
+ <source
70
+ type="${source.type || nothing}"
64
71
  srcset="${source.srcset}"
65
- media="${source.media || ''}"
66
- sizes="${source.sizes || ''}"
72
+ media="${source.media || nothing}"
73
+ sizes="${source.sizes || nothing}"
67
74
  >
68
75
  `)}
69
76
  <img
@@ -102,9 +109,9 @@ export class BdPicture extends LitElement {
102
109
  and a default-configured bd-picture then renders a fully loaded image at
103
110
  opacity 0. The two defects had to be fixed together or neither is visible.
104
111
 
105
- bd-img is the shape this follows. It keys its placeholder on
106
- `:not([src])` -- a fact the element already knows -- and never hides the
107
- image at all, which is why bd-img has no equivalent bug. The minimal
112
+ bd-img never hides its image at all, which is why it has no equivalent
113
+ bug -- and no placeholder either, since an <img> cannot draw one. The
114
+ minimal
108
115
  change here is to make this component tell the truth it already asks CSS
109
116
  about, rather than to restyle it: the fade on load is a deliberate
110
117
  nicety, and it is kept.
@@ -20,12 +20,22 @@ export default css`
20
20
 
21
21
  button:focus-visible {
22
22
  /* the one focus ring — see Design Tokens/Focus */
23
- outline: none;
23
+ /* forced-colors mode does not paint box-shadow, so the ring
24
+ vanishes there; it does paint an outline, in a system colour. This one
25
+ is transparent everywhere else, and the offset lands it on the ring's
26
+ outer band. */
27
+ outline: var(--border-width-2) solid transparent;
28
+ outline-offset: var(--focus-outline-offset);
24
29
  box-shadow: var(--focus-outline-primary);
25
30
  }
26
31
  a:focus-visible {
27
32
  /* the one focus ring — see Design Tokens/Focus */
28
- outline: none;
33
+ /* forced-colors mode does not paint box-shadow, so the ring
34
+ vanishes there; it does paint an outline, in a system colour. This one
35
+ is transparent everywhere else, and the offset lands it on the ring's
36
+ outer band. */
37
+ outline: var(--border-width-2) solid transparent;
38
+ outline-offset: var(--focus-outline-offset);
29
39
  box-shadow: var(--focus-outline-primary);
30
40
  border-radius: var(--focus-outline-radius);
31
41
  }
@@ -204,7 +204,7 @@ class PricingCard extends LitElement {
204
204
  </bd-badge>
205
205
 
206
206
  <bd-p variant="small" class="badge-subtitle">1 account & 5 devices</bd-p>
207
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
207
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
208
208
  </div>
209
209
 
210
210
  <!-- Pricing section with store integration -->
@@ -249,7 +249,7 @@ class PricingCard extends LitElement {
249
249
  </bd-button>
250
250
  </div>
251
251
 
252
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
252
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
253
253
 
254
254
  <!-- Features sections -->
255
255
  ${this.features ? html`<pricing-card-features .shadow=${this.shadow}></pricing-card-features>` : ""}
@@ -316,7 +316,7 @@ class PricingCardFamily extends LitElement {
316
316
  </bd-badge>
317
317
 
318
318
  <bd-p variant="small" class="badge-subtitle">5 accounts & 25 devices</bd-p>
319
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
319
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
320
320
  </div>
321
321
 
322
322
  <!-- Pricing section with store integration -->
@@ -361,7 +361,7 @@ class PricingCardFamily extends LitElement {
361
361
  </bd-button>
362
362
  </div>
363
363
 
364
- <bd-divider-horizontal color="subtle"></bd-divider-horizontal>
364
+ <bd-divider-horizontal color="subtle" opacity="0.25"></bd-divider-horizontal>
365
365
 
366
366
  <!-- Features sections -->
367
367
  ${this.features
@@ -3,6 +3,9 @@ import { LitElement, html, nothing } from 'lit';
3
3
  import '../../tokens/tokens.js';
4
4
  import { progressCSS } from './progress.css.js';
5
5
 
6
+ /* The tones progress.css.js styles. No tone is the default fill. */
7
+ const TONES = ['info', 'success', 'error'];
8
+
6
9
  export class BdProgress extends LitElement {
7
10
  static styles = [progressCSS];
8
11
 
@@ -20,20 +23,37 @@ export class BdProgress extends LitElement {
20
23
  this.label = '';
21
24
  }
22
25
 
26
+ /* tone reflects, so an unknown one used to sit on the element as if it
27
+ meant something -- tone="warning" or the retired "default" -- while the
28
+ bar drew the default fill. It is cleared instead, so the attribute says
29
+ what is drawn. */
30
+ willUpdate(changed) {
31
+ if (changed.has('tone') && this.tone != null && !TONES.includes(this.tone)) this.tone = undefined;
32
+ }
33
+
23
34
  render() {
35
+ /* One clamped value for all three readers. Only the fill used to be
36
+ clamped: value 150 read "150%" beside a full bar and reported an
37
+ aria-valuenow above aria-valuemax, a negative value drew nothing and
38
+ read "-20%", and max 0 read "NaN%". A max that is not a positive number
39
+ falls back to the default, as an unknown variant does elsewhere. */
40
+ const max = Number.isFinite(this.max) && this.max > 0 ? this.max : 100;
41
+ const value = Number.isFinite(this.value) ? Math.min(Math.max(this.value, 0), max) : 0;
42
+ const percent = (value / max) * 100;
43
+
24
44
  return html`
25
45
  ${this.label
26
- ? html`<span class="label"><span>${this.label}</span><span>${Math.round((this.value / this.max) * 100)}%</span></span>`
46
+ ? html`<span class="label"><span>${this.label}</span><span>${Math.round(percent)}%</span></span>`
27
47
  : nothing}
28
48
  <div
29
49
  class="track"
30
50
  role="progressbar"
31
- aria-valuenow=${this.value}
51
+ aria-valuenow=${value}
32
52
  aria-valuemin="0"
33
- aria-valuemax=${this.max}
53
+ aria-valuemax=${max}
34
54
  aria-label=${this.label || 'Progress'}
35
55
  >
36
- <div class="fill" style="width: ${Math.min(100, (this.value / this.max) * 100)}%"></div>
56
+ <div class="fill" style="width: ${percent}%"></div>
37
57
  </div>
38
58
  `;
39
59
  }
@@ -24,15 +24,17 @@ quantity something is. For work of unknown length use \`<bd-spinner>\`.
24
24
  | \`value\` | number, current progress | \`0\` |
25
25
  | \`max\` | number, the total | \`100\` |
26
26
  | \`label\` | caption shown above the bar, with a percentage | \`''\` |
27
- | \`tone\` | \`default\`, \`success\`, \`error\` | \`default\` |
27
+ | \`tone\` | \`info\`, \`success\`, \`error\` — anything else is cleared | unset: the default fill |
28
28
 
29
29
  ### Notes
30
30
  When \`label\` is set, a row above the bar shows the label on the left and the
31
31
  rounded percentage on the right, so the number is visible and not only
32
32
  announced.
33
33
 
34
- The fill is clamped at 100%, so a \`value\` above \`max\` cannot overflow the
35
- track.
34
+ \`value\` is clamped to 0…\`max\`, and the fill, the percentage and
35
+ \`aria-valuenow\` all read the clamped number, so a value above \`max\` shows a
36
+ full bar, "100%" and a valuenow equal to the max. A \`max\` that is not a
37
+ positive number falls back to \`100\`.
36
38
 
37
39
  ### Accessibility
38
40
  The track carries \`role="progressbar"\` with \`aria-valuenow\`, \`aria-valuemin\`
@@ -61,9 +63,9 @@ and \`aria-valuemax\`, so assistive tech reports real progress rather than just
61
63
  },
62
64
  tone: {
63
65
  control : { type: 'select' },
64
- options : ['default', 'success', 'error'],
65
- description: 'Colour of the fill.',
66
- table : { type: { summary: 'string' }, defaultValue: { summary: "default" }, category: 'Appearance' }
66
+ options : ['', 'info', 'success', 'error'],
67
+ description: 'Colour of the fill. Unset is the default fill; a value that is not a tone is cleared.',
68
+ table : { type: { summary: "'info' | 'success' | 'error'" }, defaultValue: { summary: 'unset' }, category: 'Appearance' }
67
69
  }
68
70
  }
69
71
  };
@@ -39,7 +39,12 @@ export default css`
39
39
  /* the one focus ring — see Foundations/Focus. A two-colour ring whose
40
40
  white inner band keeps it visible on a coloured or dark surface, which a
41
41
  single-colour outline cannot do. */
42
- outline: none;
42
+ /* forced-colors mode does not paint box-shadow, so the ring
43
+ vanishes there; it does paint an outline, in a system colour. This one
44
+ is transparent everywhere else, and the offset lands it on the ring's
45
+ outer band. */
46
+ outline: var(--border-width-2) solid transparent;
47
+ outline-offset: var(--focus-outline-offset);
43
48
  box-shadow: var(--focus-outline-primary);
44
49
  border-radius: var(--focus-outline-radius);
45
50
  }