@repobit/dex-system-design 0.25.12 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +204 -0
- package/README.md +131 -8
- package/package.json +96 -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,1011 +1,1049 @@
|
|
|
1
1
|
import { html } from 'lit';
|
|
2
|
+
import { until } from 'lit/directives/until.js';
|
|
3
|
+
import { ceilingWords, sampledLanguages, unsampledLanguages } from './type-ceiling-words.js';
|
|
4
|
+
import {
|
|
5
|
+
copyToken,
|
|
6
|
+
discoverConsumers,
|
|
7
|
+
discoverTokens,
|
|
8
|
+
docsPage,
|
|
9
|
+
readAllCssText,
|
|
10
|
+
refSection,
|
|
11
|
+
referenceNote,
|
|
12
|
+
resolve,
|
|
13
|
+
toPx,
|
|
14
|
+
usageCell
|
|
15
|
+
} from './story-chrome.js';
|
|
2
16
|
|
|
3
17
|
export default {
|
|
4
|
-
title: '
|
|
5
|
-
tags : ['autodocs']
|
|
6
|
-
|
|
7
|
-
argTypes: {
|
|
8
|
-
sampleText: {
|
|
9
|
-
control : 'text',
|
|
10
|
-
description: 'Sample text to display typography examples',
|
|
11
|
-
table : {
|
|
12
|
-
type : { summary: 'string' },
|
|
13
|
-
defaultValue: { summary: 'The quick brown fox jumps over the lazy dog.' }
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
showLineHeight: {
|
|
17
|
-
control : 'boolean',
|
|
18
|
-
description: 'Show line height visualization',
|
|
19
|
-
table : {
|
|
20
|
-
type : { summary: 'boolean' },
|
|
21
|
-
defaultValue: { summary: 'true' }
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
showLetterSpacing: {
|
|
25
|
-
control : 'boolean',
|
|
26
|
-
description: 'Show letter spacing visualization',
|
|
27
|
-
table : {
|
|
28
|
-
type : { summary: 'boolean' },
|
|
29
|
-
defaultValue: { summary: 'false' }
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
parameters: {
|
|
35
|
-
docs: {
|
|
36
|
-
description: {
|
|
37
|
-
component: `
|
|
38
|
-
# Typography System
|
|
39
|
-
|
|
40
|
-
Complete typography system with font families, sizes, weights, and spacing tokens for consistent text styling across the application.
|
|
41
|
-
|
|
42
|
-
## Overview
|
|
43
|
-
|
|
44
|
-
The typography system uses CSS custom properties to ensure consistency in font styling. It includes comprehensive scales for font sizes, weights, line heights, and letter spacing.
|
|
45
|
-
|
|
46
|
-
## Design Tokens
|
|
47
|
-
|
|
48
|
-
### Font Families
|
|
49
|
-
| Token | Value | Use Case |
|
|
50
|
-
|-------|-------|----------|
|
|
51
|
-
| \`--font-family-sans\` | 'IBM Plex Sans', Arial, sans-serif | Primary UI font |
|
|
52
|
-
| \`--font-family-mono\` | 'IBM Plex Mono', monospace | Code, technical content |
|
|
53
|
-
|
|
54
|
-
### Font Sizes (Modular Scale)
|
|
55
|
-
| Token | Value (rem) | Value (px) | Use Case |
|
|
56
|
-
|-------|-------------|------------|----------|
|
|
57
|
-
| \`--font-size-xs\` | 0.75rem | 12px | Labels, captions |
|
|
58
|
-
| \`--font-size-sm\` | 0.875rem | 14px | Small body text |
|
|
59
|
-
| \`--font-size-md\` | 1rem | 16px | Base body text |
|
|
60
|
-
| \`--font-size-lg\` | 1.25rem | 20px | Subheadings |
|
|
61
|
-
| \`--font-size-xl\` | 1.5rem | 24px | Headings |
|
|
62
|
-
| \`--font-size-2xl\` | 2rem | 32px | Large headings |
|
|
63
|
-
| \`--font-size-3xl\` | 2.5rem | 40px | Page titles |
|
|
64
|
-
| \`--font-size-4xl\` | 3rem | 48px | Hero text |
|
|
65
|
-
|
|
66
|
-
### Font Weights
|
|
67
|
-
| Token | Value | Use Case |
|
|
68
|
-
|-------|-------|----------|
|
|
69
|
-
| \`--font-weight-light\` | 300 | Light text, subtle emphasis |
|
|
70
|
-
| \`--font-weight-regular\` | 400 | Normal body text |
|
|
71
|
-
| \`--font-weight-medium\` | 500 | Medium emphasis |
|
|
72
|
-
| \`--font-weight-semibold\` | 600 | Strong emphasis, subheadings |
|
|
73
|
-
| \`--font-weight-bold\` | 700 | Bold text, headings |
|
|
74
|
-
| \`--font-weight-extrabold\` | 800 | Heavy emphasis, display text |
|
|
75
|
-
|
|
76
|
-
### Line Heights
|
|
77
|
-
| Token | Value | Use Case |
|
|
78
|
-
|-------|-------|----------|
|
|
79
|
-
| \`--line-height-tight\` | 1.2 | Headings, single lines |
|
|
80
|
-
| \`--line-height-normal\` | 1.5 | Body text, paragraphs |
|
|
81
|
-
| \`--line-height-relaxed\` | 1.75 | Long-form content |
|
|
82
|
-
| \`--line-height-loose\` | 2 | Poetry, special layouts |
|
|
83
|
-
|
|
84
|
-
### Letter Spacing
|
|
85
|
-
| Token | Value | Use Case |
|
|
86
|
-
|-------|-------|----------|
|
|
87
|
-
| \`--letter-spacing-tighter\` | -0.05em | Condensed text |
|
|
88
|
-
| \`--letter-spacing-tight\` | -0.025em | Slightly condensed |
|
|
89
|
-
| \`--letter-spacing-normal\` | 0 | Normal text |
|
|
90
|
-
| \`--letter-spacing-wide\` | 0.025em | Slightly expanded |
|
|
91
|
-
| \`--letter-spacing-wider\` | 0.05em | Expanded text |
|
|
92
|
-
| \`--letter-spacing-widest\` | 0.1em | Display text, headers |
|
|
93
|
-
|
|
94
|
-
## Usage
|
|
95
|
-
|
|
96
|
-
### Basic Typography
|
|
97
|
-
\`\`\`css
|
|
98
|
-
/* Using CSS custom properties */
|
|
99
|
-
body {
|
|
100
|
-
font-family: var(--font-family-sans);
|
|
101
|
-
font-size: var(--font-size-md);
|
|
102
|
-
line-height: var(--line-height-normal);
|
|
103
|
-
font-weight: var(--font-weight-regular);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
h1 {
|
|
107
|
-
font-size: var(--font-size-3xl);
|
|
108
|
-
font-weight: var(--font-weight-bold);
|
|
109
|
-
line-height: var(--line-height-tight);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
code {
|
|
113
|
-
font-family: var(--font-family-mono);
|
|
114
|
-
font-size: var(--font-size-sm);
|
|
115
|
-
}
|
|
116
|
-
\`\`\`
|
|
117
|
-
|
|
118
|
-
### Semantic CSS Classes
|
|
119
|
-
\`\`\`html
|
|
120
|
-
<p class="text-sm">Small text</p>
|
|
121
|
-
<p class="text-lg font-semibold">Large semi-bold text</p>
|
|
122
|
-
<p class="text-xl font-bold leading-tight">Extra large bold text with tight leading</p>
|
|
123
|
-
<div class="font-mono text-sm">Monospace code text</div>
|
|
124
|
-
\`\`\`
|
|
125
|
-
|
|
126
|
-
### Responsive Typography
|
|
127
|
-
\`\`\`css
|
|
128
|
-
.heading {
|
|
129
|
-
font-size: var(--font-size-lg);
|
|
130
|
-
|
|
131
|
-
@media (min-width: 768px) {
|
|
132
|
-
font-size: var(--font-size-xl);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@media (min-width: 1024px) {
|
|
136
|
-
font-size: var(--font-size-2xl);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
\`\`\`
|
|
140
|
-
|
|
141
|
-
## Complete CSS Custom Properties
|
|
142
|
-
|
|
143
|
-
\`\`\`css
|
|
144
|
-
:root {
|
|
145
|
-
/* Font Families */
|
|
146
|
-
--font-family-sans: 'IBM Plex Sans', Arial, sans-serif;
|
|
147
|
-
--font-family-mono: 'IBM Plex Mono', monospace;
|
|
148
|
-
|
|
149
|
-
/* Font Sizes */
|
|
150
|
-
--font-size-xs: 0.75rem;
|
|
151
|
-
--font-size-sm: 0.875rem;
|
|
152
|
-
--font-size-md: 1rem;
|
|
153
|
-
--font-size-lg: 1.25rem;
|
|
154
|
-
--font-size-xl: 1.5rem;
|
|
155
|
-
--font-size-2xl: 2rem;
|
|
156
|
-
--font-size-3xl: 2.5rem;
|
|
157
|
-
--font-size-4xl: 3rem;
|
|
158
|
-
|
|
159
|
-
/* Font Weights */
|
|
160
|
-
--font-weight-light: 300;
|
|
161
|
-
--font-weight-regular: 400;
|
|
162
|
-
--font-weight-medium: 500;
|
|
163
|
-
--font-weight-semibold: 600;
|
|
164
|
-
--font-weight-bold: 700;
|
|
165
|
-
--font-weight-extrabold: 800;
|
|
166
|
-
|
|
167
|
-
/* Line Heights */
|
|
168
|
-
--line-height-tight: 1.2;
|
|
169
|
-
--line-height-normal: 1.5;
|
|
170
|
-
--line-height-relaxed: 1.75;
|
|
171
|
-
--line-height-loose: 2;
|
|
172
|
-
|
|
173
|
-
/* Letter Spacing */
|
|
174
|
-
--letter-spacing-tighter: -0.05em;
|
|
175
|
-
--letter-spacing-tight: -0.025em;
|
|
176
|
-
--letter-spacing-normal: 0;
|
|
177
|
-
--letter-spacing-wide: 0.025em;
|
|
178
|
-
--letter-spacing-wider: 0.05em;
|
|
179
|
-
--letter-spacing-widest: 0.1em;
|
|
180
|
-
|
|
181
|
-
/* Text Colors */
|
|
182
|
-
--text-color-primary: #212529;
|
|
183
|
-
--text-color-secondary: #6c757d;
|
|
184
|
-
--text-color-muted: #adb5bd;
|
|
185
|
-
--text-color-on-dark: #ffffff;
|
|
186
|
-
}
|
|
187
|
-
\`\`\`
|
|
188
|
-
|
|
189
|
-
## Best Practices
|
|
190
|
-
|
|
191
|
-
### 1. Use Relative Units
|
|
192
|
-
\`\`\`css
|
|
193
|
-
/* Good ✅ - Uses rem for accessibility */
|
|
194
|
-
font-size: var(--font-size-md);
|
|
195
|
-
|
|
196
|
-
/* Bad ❌ - Fixed pixels ignore user preferences */
|
|
197
|
-
font-size: 16px;
|
|
198
|
-
\`\`\`
|
|
199
|
-
|
|
200
|
-
### 2. Maintain Hierarchy
|
|
201
|
-
\`\`\`css
|
|
202
|
-
h1 { font-size: var(--font-size-3xl); }
|
|
203
|
-
h2 { font-size: var(--font-size-2xl); }
|
|
204
|
-
h3 { font-size: var(--font-size-xl); }
|
|
205
|
-
h4 { font-size: var(--font-size-lg); }
|
|
206
|
-
h5 { font-size: var(--font-size-md); }
|
|
207
|
-
h6 { font-size: var(--font-size-sm); }
|
|
208
|
-
\`\`\`
|
|
209
|
-
|
|
210
|
-
### 3. Ensure Readability
|
|
211
|
-
- Minimum 16px (1rem) for body text
|
|
212
|
-
- Line length between 45-75 characters
|
|
213
|
-
- Contrast ratio of at least 4.5:1 for normal text
|
|
214
|
-
- Adequate line spacing (1.5× for body text)
|
|
215
|
-
|
|
216
|
-
### 4. Responsive Considerations
|
|
217
|
-
- Increase font sizes on larger screens
|
|
218
|
-
- Adjust line heights for different viewports
|
|
219
|
-
- Consider touch targets for interactive text
|
|
220
|
-
|
|
221
|
-
## Accessibility
|
|
222
|
-
|
|
223
|
-
### Text Scaling
|
|
224
|
-
All typography tokens use \`rem\` units, which respect the user's browser font size settings.
|
|
225
|
-
|
|
226
|
-
### Color Contrast
|
|
227
|
-
Ensure text meets WCAG 2.1 contrast requirements:
|
|
228
|
-
- Normal text: 4.5:1 minimum
|
|
229
|
-
- Large text (18px+): 3:1 minimum
|
|
230
|
-
|
|
231
|
-
### Screen Readers
|
|
232
|
-
- Use semantic HTML headings (h1-h6)
|
|
233
|
-
- Provide alternative text for images
|
|
234
|
-
- Ensure proper reading order
|
|
235
|
-
- Test with screen readers
|
|
236
|
-
|
|
237
|
-
## Browser Support
|
|
238
|
-
|
|
239
|
-
All typography features are supported in modern browsers:
|
|
240
|
-
- Chrome 60+
|
|
241
|
-
- Firefox 55+
|
|
242
|
-
- Safari 11+
|
|
243
|
-
- Edge 79+
|
|
244
|
-
|
|
245
|
-
For variable fonts support (if using IBM Plex Variable):
|
|
246
|
-
- Chrome 60+
|
|
247
|
-
- Firefox 62+
|
|
248
|
-
- Safari 11+
|
|
249
|
-
- Edge 17+
|
|
250
|
-
|
|
251
|
-
## Performance
|
|
252
|
-
|
|
253
|
-
- Use \`font-display: swap\` for web fonts
|
|
254
|
-
- Subset fonts when possible
|
|
255
|
-
- Consider system fonts as fallbacks
|
|
256
|
-
- Implement font loading strategies
|
|
257
|
-
|
|
258
|
-
## Testing
|
|
259
|
-
|
|
260
|
-
Test typography with:
|
|
261
|
-
- Different screen sizes and resolutions
|
|
262
|
-
- High contrast modes
|
|
263
|
-
- Screen magnifiers
|
|
264
|
-
- Various browser zoom levels
|
|
265
|
-
- Different operating systems
|
|
266
|
-
`
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const typographyTokens = {
|
|
273
|
-
families: {
|
|
274
|
-
'Sans': 'var(--font-family-sans, "IBM Plex Sans", Arial, sans-serif)',
|
|
275
|
-
'Mono': 'var(--font-family-mono, "IBM Plex Mono", monospace)'
|
|
276
|
-
},
|
|
277
|
-
sizes: {
|
|
278
|
-
'xs' : 'var(--font-size-xs, 0.75rem)',
|
|
279
|
-
'sm' : 'var(--font-size-sm, 0.875rem)',
|
|
280
|
-
'md' : 'var(--font-size-md, 1rem)',
|
|
281
|
-
'lg' : 'var(--font-size-lg, 1.25rem)',
|
|
282
|
-
'xl' : 'var(--font-size-xl, 1.5rem)',
|
|
283
|
-
'2xl': 'var(--font-size-2xl, 2rem)',
|
|
284
|
-
'3xl': 'var(--font-size-3xl, 2.5rem)',
|
|
285
|
-
'4xl': 'var(--font-size-4xl, 3rem)'
|
|
286
|
-
},
|
|
287
|
-
weights: {
|
|
288
|
-
'light' : 'var(--font-weight-light, 300)',
|
|
289
|
-
'regular' : 'var(--font-weight-regular, 400)',
|
|
290
|
-
'medium' : 'var(--font-weight-medium, 500)',
|
|
291
|
-
'semibold' : 'var(--font-weight-semibold, 600)',
|
|
292
|
-
'bold' : 'var(--font-weight-bold, 700)',
|
|
293
|
-
'extrabold': 'var(--font-weight-extrabold, 800)'
|
|
294
|
-
},
|
|
295
|
-
lineHeights: {
|
|
296
|
-
'tight' : 'var(--line-height-tight, 1.2)',
|
|
297
|
-
'normal' : 'var(--line-height-normal, 1.5)',
|
|
298
|
-
'relaxed': 'var(--line-height-relaxed, 1.75)',
|
|
299
|
-
'loose' : 'var(--line-height-loose, 2)'
|
|
300
|
-
},
|
|
301
|
-
letterSpacing: {
|
|
302
|
-
'tighter': 'var(--letter-spacing-tighter, -0.05em)',
|
|
303
|
-
'tight' : 'var(--letter-spacing-tight, -0.025em)',
|
|
304
|
-
'normal' : 'var(--letter-spacing-normal, 0)',
|
|
305
|
-
'wide' : 'var(--letter-spacing-wide, 0.025em)',
|
|
306
|
-
'wider' : 'var(--letter-spacing-wider, 0.05em)',
|
|
307
|
-
'widest' : 'var(--letter-spacing-widest, 0.1em)'
|
|
308
|
-
}
|
|
18
|
+
title: 'Foundations/Typography',
|
|
19
|
+
tags : ['autodocs']
|
|
309
20
|
};
|
|
310
21
|
|
|
311
|
-
const
|
|
22
|
+
const typeStyles = html`
|
|
312
23
|
<style>
|
|
313
|
-
.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
padding: var(--space-lg);
|
|
317
|
-
background: white;
|
|
318
|
-
border-radius: 8px;
|
|
319
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
24
|
+
.specimen-row {
|
|
25
|
+
padding: var(--spacing-16) 0;
|
|
26
|
+
border-bottom: var(--border-width-1) solid var(--color-neutral-100);
|
|
320
27
|
}
|
|
321
|
-
|
|
322
|
-
.
|
|
323
|
-
|
|
324
|
-
padding-bottom: var(--space-lg);
|
|
325
|
-
border-bottom: 2px solid #f0f0f0;
|
|
28
|
+
|
|
29
|
+
.specimen-row:last-child {
|
|
30
|
+
border-bottom: 0;
|
|
326
31
|
}
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
font-family: var(--font-family-sans);
|
|
330
|
-
font-size: var(--font-size-xl);
|
|
331
|
-
font-weight: var(--font-weight-bold);
|
|
332
|
-
margin: 0 0 var(--space-lg) 0;
|
|
333
|
-
color: #212529;
|
|
32
|
+
|
|
33
|
+
.specimen-meta {
|
|
334
34
|
display: flex;
|
|
335
|
-
|
|
336
|
-
gap: var(--
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
width: 4px;
|
|
342
|
-
height: 24px;
|
|
343
|
-
background: #007bff;
|
|
344
|
-
border-radius: 2px;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
.token-grid {
|
|
348
|
-
display: grid;
|
|
349
|
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
350
|
-
gap: var(--space-md);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.token-card {
|
|
354
|
-
padding: var(--space-md);
|
|
355
|
-
background: #f8f9fa;
|
|
356
|
-
border-radius: 6px;
|
|
357
|
-
border: 1px solid #e9ecef;
|
|
358
|
-
transition: all 0.2s ease;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: var(--spacing-4) var(--spacing-12);
|
|
37
|
+
margin-bottom: var(--spacing-8);
|
|
38
|
+
font-family: var(--typography-fontFamily-mono);
|
|
39
|
+
font-size: var(--typography-fontSize-xxs);
|
|
40
|
+
color: var(--color-neutral-500);
|
|
359
41
|
}
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
border-color: #007bff;
|
|
42
|
+
|
|
43
|
+
.specimen-meta .name {
|
|
44
|
+
color: var(--color-neutral-900);
|
|
45
|
+
font-weight: var(--typography-fontWeight-semibold);
|
|
365
46
|
}
|
|
366
|
-
|
|
367
|
-
.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
47
|
+
|
|
48
|
+
/* Sample text is deliberately unstyled apart from the tokens under test. */
|
|
49
|
+
.specimen-text {
|
|
50
|
+
margin: 0;
|
|
51
|
+
color: var(--color-neutral-900);
|
|
52
|
+
/* long specimens should not force the page wide */
|
|
53
|
+
overflow-wrap: anywhere;
|
|
372
54
|
}
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
color: #007bff;
|
|
378
|
-
font-weight: var(--font-weight-semibold);
|
|
379
|
-
background: white;
|
|
380
|
-
padding: 2px 8px;
|
|
381
|
-
border-radius: 4px;
|
|
382
|
-
border: 1px solid #dee2e6;
|
|
55
|
+
|
|
56
|
+
.sample-cell {
|
|
57
|
+
color: var(--color-neutral-900);
|
|
58
|
+
white-space: nowrap;
|
|
383
59
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
60
|
+
|
|
61
|
+
/* Leading cannot be judged on one line, so this sample is deliberately
|
|
62
|
+
narrow enough to wrap. */
|
|
63
|
+
.leading-sample {
|
|
64
|
+
margin: 0;
|
|
65
|
+
width: 30ch;
|
|
66
|
+
min-width: 22ch;
|
|
67
|
+
font-size: var(--typography-fontSize-sm);
|
|
68
|
+
color: var(--color-neutral-900);
|
|
69
|
+
background: var(--color-blue-50);
|
|
393
70
|
}
|
|
394
|
-
|
|
395
|
-
.
|
|
396
|
-
font-family: inherit;
|
|
397
|
-
font-size: inherit;
|
|
398
|
-
font-weight: inherit;
|
|
399
|
-
line-height: inherit;
|
|
400
|
-
letter-spacing: inherit;
|
|
401
|
-
margin: var(--space-sm) 0;
|
|
402
|
-
padding: var(--space-sm);
|
|
403
|
-
background: white;
|
|
404
|
-
border-radius: 4px;
|
|
405
|
-
border: 1px solid #dee2e6;
|
|
406
|
-
min-height: 60px;
|
|
71
|
+
|
|
72
|
+
.ratio-demo {
|
|
407
73
|
display: flex;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
.line-height-visual {
|
|
412
|
-
height: 40px;
|
|
413
|
-
background: linear-gradient(to bottom,
|
|
414
|
-
transparent 0%,
|
|
415
|
-
transparent calc(50% - 0.6px),
|
|
416
|
-
#007bff calc(50% - 0.6px),
|
|
417
|
-
#007bff calc(50% + 0.6px),
|
|
418
|
-
transparent calc(50% + 0.6px),
|
|
419
|
-
transparent 100%
|
|
420
|
-
);
|
|
421
|
-
margin-top: var(--space-xs);
|
|
422
|
-
position: relative;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
gap: var(--spacing-24);
|
|
423
76
|
}
|
|
424
|
-
|
|
425
|
-
.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
transform: translateY(-50%);
|
|
430
|
-
font-family: var(--font-family-mono);
|
|
431
|
-
font-size: var(--font-size-xs);
|
|
432
|
-
color: #6c757d;
|
|
433
|
-
background: white;
|
|
434
|
-
padding: 2px 6px;
|
|
435
|
-
border-radius: 3px;
|
|
436
|
-
border: 1px solid #dee2e6;
|
|
77
|
+
|
|
78
|
+
.ratio-demo figure {
|
|
79
|
+
margin: 0;
|
|
80
|
+
flex: 1 1 200px;
|
|
81
|
+
min-width: 180px;
|
|
437
82
|
}
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
margin-
|
|
444
|
-
padding: var(--space-xs);
|
|
445
|
-
background: white;
|
|
446
|
-
border-radius: 4px;
|
|
447
|
-
border: 1px solid #dee2e6;
|
|
83
|
+
|
|
84
|
+
.ratio-demo figcaption {
|
|
85
|
+
font-family: var(--typography-fontFamily-mono);
|
|
86
|
+
font-size: var(--typography-fontSize-xxs);
|
|
87
|
+
color: var(--color-neutral-600);
|
|
88
|
+
margin-bottom: var(--spacing-6);
|
|
448
89
|
}
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
border-radius: 2px;
|
|
455
|
-
opacity: 0.7;
|
|
90
|
+
|
|
91
|
+
.ratio-demo p {
|
|
92
|
+
margin: 0;
|
|
93
|
+
background: var(--color-blue-50);
|
|
94
|
+
color: var(--color-neutral-900);
|
|
456
95
|
}
|
|
457
|
-
|
|
458
|
-
.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
96
|
+
|
|
97
|
+
.rule-list {
|
|
98
|
+
margin: 0;
|
|
99
|
+
padding-left: var(--spacing-20);
|
|
100
|
+
font-size: var(--typography-fontSize-xs);
|
|
101
|
+
line-height: var(--typography-lineHeight-normal);
|
|
102
|
+
color: var(--color-neutral-700);
|
|
464
103
|
}
|
|
465
|
-
|
|
466
|
-
.
|
|
467
|
-
|
|
468
|
-
font-size: var(--font-size-lg);
|
|
469
|
-
font-weight: var(--font-weight-bold);
|
|
470
|
-
margin: 0 0 var(--space-sm) 0;
|
|
104
|
+
|
|
105
|
+
.rule-list li + li {
|
|
106
|
+
margin-top: var(--spacing-6);
|
|
471
107
|
}
|
|
472
|
-
|
|
473
|
-
.
|
|
474
|
-
font-family: var(--
|
|
475
|
-
font-size: var(--font-size-sm);
|
|
476
|
-
opacity: 0.9;
|
|
477
|
-
margin: 0;
|
|
108
|
+
|
|
109
|
+
.rule-list code {
|
|
110
|
+
font-family: var(--typography-fontFamily-mono);
|
|
478
111
|
}
|
|
479
112
|
</style>
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
/* ---------- primitive scales ---------- */
|
|
480
116
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
<
|
|
117
|
+
function primitiveTable({ caption, tokens, consumers, sample, leading }) {
|
|
118
|
+
return html`
|
|
119
|
+
<div class="table-scroll">
|
|
120
|
+
<table>
|
|
121
|
+
<caption>${caption}</caption>
|
|
122
|
+
<thead>
|
|
123
|
+
<tr>
|
|
124
|
+
<th scope="col">Token</th>
|
|
125
|
+
<th scope="col">Value</th>
|
|
126
|
+
${leading ? html`<th scope="col">At 16px</th>` : ''}
|
|
127
|
+
${sample ? html`<th scope="col">Sample</th>` : ''}
|
|
128
|
+
${leading ? html`<th scope="col">Leading</th>` : ''}
|
|
129
|
+
<th scope="col">Referenced by</th>
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
${tokens.map(token => {
|
|
134
|
+
const value = resolve(token);
|
|
135
|
+
// a rem step is hard to place without the pixel value beside it
|
|
136
|
+
const px = toPx(value);
|
|
137
|
+
return html`
|
|
138
|
+
<tr>
|
|
139
|
+
<td>${copyToken(token)}</td>
|
|
140
|
+
<td class="num">
|
|
141
|
+
${value}${px ? html`<span class="px-hint">${px}</span>` : ''}
|
|
142
|
+
</td>
|
|
143
|
+
${leading
|
|
144
|
+
? html`<td class="num muted">${(parseFloat(value) * 16).toFixed(parseFloat(value) * 16 % 1 ? 2 : 0)}px</td>`
|
|
145
|
+
: ''}
|
|
146
|
+
${sample
|
|
147
|
+
? html`<td class="sample-cell" style=${sample(token)}>Aa Bb Cc 123</td>`
|
|
148
|
+
: ''}
|
|
149
|
+
${leading
|
|
150
|
+
? html`<td>
|
|
151
|
+
<p class="leading-sample" style="line-height: var(${token})">
|
|
152
|
+
Leading only becomes visible across more than one line, so this
|
|
153
|
+
sample is deliberately narrow enough to wrap onto three.
|
|
154
|
+
</p>
|
|
155
|
+
</td>`
|
|
156
|
+
: ''}
|
|
157
|
+
<td>${usageCell(consumers.get(token))}</td>
|
|
158
|
+
</tr>
|
|
159
|
+
`;
|
|
160
|
+
})}
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
485
163
|
</div>
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tokensMatching(cssText, pattern) {
|
|
168
|
+
return discoverTokens(cssText, pattern).map(match => match[1]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ---------------------------------------------------------------------------
|
|
172
|
+
Nine roles are fluid. This page renders them live at the current viewport
|
|
173
|
+
AND states what they resolve to at each of the seven column widths, so the
|
|
174
|
+
numbers can be checked against what the browser is actually doing rather
|
|
175
|
+
than against a table somebody typed.
|
|
176
|
+
|
|
177
|
+
The column widths are derived from the grid tokens, not measured: a
|
|
178
|
+
Storybook story sits inside 16px of harness body padding either side, and
|
|
179
|
+
measuring one is how a 288px column once got recorded as 256 and moved a
|
|
180
|
+
conclusion by a whole step of the type scale. */
|
|
181
|
+
const FLUID = [
|
|
182
|
+
['heading-h1', 48, 30, 'Bold'],
|
|
183
|
+
['heading-h2', 36, 27, 'Bold'],
|
|
184
|
+
['heading-h3', 32, 24, 'SemiBold'],
|
|
185
|
+
['heading-h4', 24, 21, 'SemiBold'],
|
|
186
|
+
['heading-h5', 20, 18, 'Medium'],
|
|
187
|
+
['heading-h6', 18, 16, 'Medium'],
|
|
188
|
+
['display-md', 48, 30, 'Bold'],
|
|
189
|
+
['display-sm', 36, 30, 'Bold'],
|
|
190
|
+
['body-lead', 20, 18, 'Regular']
|
|
191
|
+
];
|
|
192
|
+
/* viewport -> column, from --grid-container-max-* minus 2x --grid-padding-* */
|
|
193
|
+
const COLUMNS = [[320, 288], [375, 343], [576, 492], [768, 656], [992, 880], [1200, 1060], [1400, 1240]];
|
|
194
|
+
const WINDOW = [320, 992];
|
|
195
|
+
|
|
196
|
+
/** What the clamp resolves to at a given viewport width, in px. */
|
|
197
|
+
const fluidAt = (vw, min, max) => {
|
|
198
|
+
const [v1, v2] = WINDOW;
|
|
199
|
+
if (vw <= v1) return min;
|
|
200
|
+
if (vw >= v2) return max;
|
|
201
|
+
return min + ((max - min) * (vw - v1)) / (v2 - v1);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/* ---------------------------------------------------------------------------
|
|
205
|
+
The evidence the mobile ladder was chosen from, rendered LIVE rather than
|
|
206
|
+
transcribed from the Figma study.
|
|
207
|
+
|
|
208
|
+
Live matters here: it measures the real IBM Plex Sans in the reader's own
|
|
209
|
+
browser at the reader's own viewport, so it cannot drift from the file the
|
|
210
|
+
way a pasted table can, and it re-measures when you resize. The Figma page
|
|
211
|
+
"Type on mobile" holds the same study as a fixed artifact for designers;
|
|
212
|
+
this is the developer's copy, and if the two ever disagree the browser wins.
|
|
213
|
+
|
|
214
|
+
The words are representative compounds written for the study, NOT strings
|
|
215
|
+
from the real translation memory -- English is roughly twice as permissive
|
|
216
|
+
as German and Finnish, so English alone would flatter every number here. */
|
|
217
|
+
/* From src/tokens/type-ceiling-words.js, which carries the provenance of every
|
|
218
|
+
entry -- `tm` for a real translation-memory string, `representative` for a
|
|
219
|
+
plausible compound written for the study, `outlier` for a deliberately
|
|
220
|
+
extreme one that is shown but never sets a ceiling. Entries marked `retired`
|
|
221
|
+
are excluded from the worst case: Finnish does not ship, and the three fi
|
|
222
|
+
words are kept only because the shipped ladder was calibrated against the
|
|
223
|
+
widest of them. */
|
|
224
|
+
const CEILING_WORDS = ceilingWords.words
|
|
225
|
+
.filter(w => !w.retired)
|
|
226
|
+
.map(w => [w.lang, w.word, w.source, w.observedAt || 0]);
|
|
227
|
+
const RETIRED_WORDS = ceilingWords.words
|
|
228
|
+
.filter(w => w.retired)
|
|
229
|
+
.map(w => [w.lang, w.word, w.source, 0]);
|
|
486
230
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
231
|
+
/* The face every measurement on this page depends on. Spelled once, because
|
|
232
|
+
it is passed to fonts.load(), asserted with fonts.check(), and named in the
|
|
233
|
+
banner the story shows when the assertion fails. */
|
|
234
|
+
const PLEX_BOLD = '700 100px "IBM Plex Sans"';
|
|
235
|
+
|
|
236
|
+
/* Set by widthPer100 whenever it is asked to measure before the face is
|
|
237
|
+
available. It must stay false; the story renders a red banner if it does not,
|
|
238
|
+
because the alternative is a page of numbers that are quietly ~3% wrong. */
|
|
239
|
+
let measuredWithoutPlex = false;
|
|
240
|
+
|
|
241
|
+
/** Width of `text` at 100px in `weight`, measured in this browser. */
|
|
242
|
+
function widthPer100(text, weight) {
|
|
243
|
+
/* THE BUG THIS GUARDS, measured 2026-09-17. Before the await was added below,
|
|
244
|
+
this page rendered its tables at whatever font was available when the story
|
|
245
|
+
function ran -- which on a cold load is system-ui, not Plex. The same word
|
|
246
|
+
measured 9.045 px per px of size in Plex Bold and 8.778 in system-ui Bold,
|
|
247
|
+
so every number on the page came out about 3% low, INTERMITTENTLY, and the
|
|
248
|
+
ranking itself could change: `worst` is chosen by width, so a fallback
|
|
249
|
+
render can hand the title to a different word.
|
|
250
|
+
That 3% is the whole of the "Figma renders 3.5% wider than the browser"
|
|
251
|
+
discrepancy this study recorded and then reasoned from. There is no
|
|
252
|
+
renderer difference. There was a font race.
|
|
253
|
+
scripts/verify-runtime.mjs has carried the same lesson since P1.5 -- and
|
|
254
|
+
its own header says why it recurred: the await had been written
|
|
255
|
+
per-investigation. This was another investigation. */
|
|
256
|
+
if (!document.fonts.check(PLEX_BOLD)) measuredWithoutPlex = true;
|
|
257
|
+
const el = document.createElement('span');
|
|
258
|
+
el.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;'
|
|
259
|
+
+ 'font-family:var(--typography-fontFamily-sans);letter-spacing:0;font-size:100px;'
|
|
260
|
+
+ `font-weight:${weight}`;
|
|
261
|
+
el.textContent = text;
|
|
262
|
+
document.body.appendChild(el);
|
|
263
|
+
const w = el.getBoundingClientRect().width / 100;
|
|
264
|
+
el.remove();
|
|
265
|
+
return w;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const whyTheseSizes = async () => {
|
|
269
|
+
/* Nothing below may run before the face lands -- see widthPer100. load()
|
|
270
|
+
rather than ready(), because ready() resolves when nothing more is
|
|
271
|
+
PENDING, and a face nobody has asked for yet is not pending. */
|
|
272
|
+
await document.fonts.load(PLEX_BOLD);
|
|
273
|
+
await document.fonts.ready;
|
|
274
|
+
/* measured on demand, so a font swap or a metric change shows up here */
|
|
275
|
+
const measure = ([lang, word, source, observedAt]) => {
|
|
276
|
+
const per = widthPer100(word, 700);
|
|
277
|
+
return {
|
|
278
|
+
lang, word, source, observedAt, per,
|
|
279
|
+
tier : observedAt >= 40 ? 'headline' : 'feature',
|
|
280
|
+
ceilings: COLUMNS.map(([, col]) => Math.floor(col / per))
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const rows = CEILING_WORDS.map(measure);
|
|
284
|
+
const retired = RETIRED_WORDS.map(measure);
|
|
285
|
+
/* An `outlier` is shown and never sets a ceiling -- that is what the source
|
|
286
|
+
vocabulary is for, rather than excluding a word by name. */
|
|
287
|
+
const worst = rows.filter(r => r.source !== 'outlier')
|
|
288
|
+
.reduce((a, r) => (r.per > a.per ? r : a));
|
|
289
|
+
const worstRetired = retired.length
|
|
290
|
+
? retired.reduce((a, r) => (r.per > a.per ? r : a))
|
|
291
|
+
: null;
|
|
292
|
+
/* The tier split is the whole point of the 2026-09-17 harvest: the widest
|
|
293
|
+
tokens are real, and they are FEATURE copy. `worst` is the widest token in
|
|
294
|
+
any tier -- the conservative ceiling. `headlineWorst` is the widest token
|
|
295
|
+
actually observed playing the part heading-h1 plays, which is the one that
|
|
296
|
+
decides whether h1's mobile size is wrong or merely tight. */
|
|
297
|
+
const headline = rows.filter(r => r.tier === 'headline' && r.source !== 'outlier');
|
|
298
|
+
const headlineWorst = headline.length
|
|
299
|
+
? headline.reduce((a, r) => (r.per > a.per ? r : a))
|
|
300
|
+
: null;
|
|
301
|
+
/* Per role, against both worsts, on the two phone columns. */
|
|
302
|
+
const exposure = FLUID.map(([name, , min]) => ({
|
|
303
|
+
name,
|
|
304
|
+
min,
|
|
305
|
+
all : [288, 343].map(col => Math.round(worst.per * min) <= col),
|
|
306
|
+
headline: headlineWorst
|
|
307
|
+
? [288, 343].map(col => Math.round(headlineWorst.per * min) <= col)
|
|
308
|
+
: [true, true],
|
|
309
|
+
needAll : Math.round(worst.per * min),
|
|
310
|
+
needHeadline: headlineWorst ? Math.round(headlineWorst.per * min) : 0
|
|
311
|
+
}));
|
|
312
|
+
const overAnywhere = exposure.filter(e => !e.all[0] || !e.all[1]);
|
|
313
|
+
|
|
314
|
+
return html`
|
|
315
|
+
${typeStyles}
|
|
316
|
+
${docsPage({
|
|
317
|
+
title: 'Why these sizes',
|
|
318
|
+
lede : html`The measurement the mobile ladder was derived from, run live in your browser.
|
|
319
|
+
For large type the binding constraint is not how many lines a sentence takes — it is
|
|
320
|
+
whether the longest <b>unbreakable word</b> fits the column. A word wider than its column
|
|
321
|
+
overflows or hyphenates, and neither is a size anyone chose.`,
|
|
322
|
+
note: html`<b>These are real words now.</b> Every entry was harvested on 2026-09-17 from
|
|
323
|
+
live localized <code>bitdefender.com</code> copy rendering at 24px or more — nine
|
|
324
|
+
languages, two product pages each — and carries the page it came from and the size that
|
|
325
|
+
page sets it at. The invented compounds this study started with are gone, and so is the
|
|
326
|
+
Finnish word the shipped ladder was actually calibrated against: Finnish does not sell.
|
|
327
|
+
The widest English token in the sample is 31% narrower than the widest Swedish one —
|
|
328
|
+
a 41px ceiling against 28 on the same column — so an English-only check does not merely
|
|
329
|
+
pass one size that overflows elsewhere, it passes every size on the ladder.
|
|
330
|
+
${referenceNote}`,
|
|
331
|
+
children: html`
|
|
332
|
+
${measuredWithoutPlex
|
|
333
|
+
? html`<p style="border: 2px solid var(--color-text-error); padding: var(--spacing-16);
|
|
334
|
+
color: var(--color-text-error)">
|
|
335
|
+
<b>These numbers are wrong — do not read them.</b> At least one measurement ran before
|
|
336
|
+
<code>${PLEX_BOLD}</code> had a loaded face, so it measured a fallback font, which is
|
|
337
|
+
about 3% narrower. Reload. This banner exists because the page used to do this silently.
|
|
338
|
+
</p>`
|
|
339
|
+
: ''}
|
|
340
|
+
<p>
|
|
341
|
+
Word width scales linearly with font size, so one measurement per word gives the
|
|
342
|
+
ceiling at every size: <b>ceiling = column ÷ (width per px of size)</b>. The widest
|
|
343
|
+
real token in the sample is <b>${worst.word}</b> (${worst.lang},
|
|
344
|
+
${(worst.per * 100).toFixed(0)}px per 100px), which puts the ceiling on a 288px column
|
|
345
|
+
at <b>${Math.floor(288 / worst.per)}px</b> — below <code>heading-h1</code>'s mobile
|
|
346
|
+
30. ${headlineWorst
|
|
347
|
+
? html`But it is <b>feature copy</b>, set at ${worst.observedAt}px on its own page. The
|
|
348
|
+
widest token observed doing <code>heading-h1</code>'s job is
|
|
349
|
+
<b>${headlineWorst.word}</b> (${headlineWorst.lang},
|
|
350
|
+
${(headlineWorst.per * 100).toFixed(0)}px per 100px), whose ceiling is
|
|
351
|
+
<b>${Math.floor(288 / headlineWorst.per)}px</b> — above 30, by one.`
|
|
352
|
+
: ''}
|
|
353
|
+
</p>
|
|
354
|
+
<p>
|
|
355
|
+
<b>That gap is the finding, and it is why the ladder did not move.</b> Both numbers are
|
|
356
|
+
real. Which one binds depends on whether a twenty-character compound from a feature
|
|
357
|
+
list can ever be promoted into a headline — a copy question, not a token question.
|
|
358
|
+
</p>
|
|
359
|
+
${refSection({
|
|
360
|
+
name : 'Language coverage — complete, for every language this model applies to',
|
|
361
|
+
meta : `${sampledLanguages.length} of ${ceilingWords.shippingLanguages.languages.length} shipping languages sampled from production copy · the other ${Object.keys(ceilingWords.wrappingCaveats).length} wrap per character`,
|
|
362
|
+
children: html`
|
|
363
|
+
<p>
|
|
364
|
+
<b>Sampled, from shipped copy:</b>
|
|
365
|
+
${sampledLanguages.map(l => html`<code>${l}</code> `)} — all nine of the
|
|
366
|
+
eleven shipping languages that wrap on spaces, the other two being
|
|
367
|
+
${Object.keys(ceilingWords.wrappingCaveats).map(l => html`<code>${l}</code> `)}
|
|
368
|
+
below —
|
|
369
|
+
${rows.length} tokens, harvested ${ceilingWords.harvest.date} from
|
|
370
|
+
<code>/consumer/total-security</code> and
|
|
371
|
+
<code>/consumer/ultimate-security</code> in each market.
|
|
372
|
+
</p>
|
|
373
|
+
<p>
|
|
374
|
+
<b>The ladder that shipped was calibrated against a language this product does not
|
|
375
|
+
sell in.</b> Its worst case was Finnish <i>Henkilöllisyyssuoja</i>${worstRetired
|
|
376
|
+
? html` (${(worstRetired.per * 100).toFixed(0)}px per 100px)`
|
|
377
|
+
: ''}, and there is no <code>fi-FI</code> locale in <code>localeMap.js</code>.
|
|
378
|
+
The three Finnish words stay in the sample file marked <code>retired</code> —
|
|
379
|
+
deleting them would erase the record of what the shipped ladder was sized against —
|
|
380
|
+
and they set no number on this page. <b>The production harvest has since found
|
|
381
|
+
three real tokens wider than that Finnish one</b>, in Swedish, Dutch and German.
|
|
382
|
+
</p>
|
|
383
|
+
<p>
|
|
384
|
+
<b>Not sampled: ${unsampledLanguages.length
|
|
385
|
+
? html`${unsampledLanguages.map(l => html`<code>${l}</code> `)}`
|
|
386
|
+
: html`nothing`}.</b> Coverage is complete for every shipping language this
|
|
387
|
+
model applies to, and the last gap closed by turning out not to be one.
|
|
388
|
+
Hungarian was listed here as an untested compounding language because no
|
|
389
|
+
Hungarian copy could be found. There is none: <b>Hungary is an English
|
|
390
|
+
market</b>, served at <code>/en-hu/</code>.
|
|
391
|
+
</p>
|
|
392
|
+
<p>
|
|
393
|
+
Finding that took a defect with it. The footer had the locale as
|
|
394
|
+
<code>hu-HU</code>, and <code>footer.js</code> navigates to
|
|
395
|
+
<code>/{locale}/</code> on country click — so picking Hungary sent the user
|
|
396
|
+
to <code>/hu-hu/</code>, which is rewritten to <code>/en-us/hu-hu/</code>
|
|
397
|
+
and returns 404. It was the only one of the 46 roots that did not resolve.
|
|
398
|
+
<code>localeMap.js</code> says <code>en-HU</code> — <i>Hungary -
|
|
399
|
+
English</i> — now, and all 46 resolve. Hungarian is no longer claimed as a
|
|
400
|
+
language this product sells in, which is why it is no longer counted as
|
|
401
|
+
unsampled.
|
|
402
|
+
</p>
|
|
403
|
+
<p>
|
|
404
|
+
<b>Not applicable:</b>
|
|
405
|
+
${Object.keys(ceilingWords.wrappingCaveats).map(l => html`<code>${l}</code> `)} —
|
|
406
|
+
they wrap between characters, so the longest-word constraint barely applies. The
|
|
407
|
+
relevant question there is characters per line at roughly one em per glyph, which
|
|
408
|
+
this study still does not model.
|
|
409
|
+
</p>
|
|
410
|
+
<p>
|
|
411
|
+
<b>What changed, and what did not.</b> The margin is measured now instead of
|
|
412
|
+
unknown. The numbers did not move the ladder: they moved the worst case
|
|
413
|
+
<i>up</i> in the feature tier and confirmed it in the headline tier, and those two
|
|
414
|
+
results argue in opposite directions — see the next section.
|
|
415
|
+
</p>
|
|
416
|
+
`
|
|
417
|
+
})}
|
|
418
|
+
${refSection({
|
|
419
|
+
name: 'Which role is actually exposed',
|
|
420
|
+
meta: overAnywhere.length
|
|
421
|
+
? `${overAnywhere.length} of ${exposure.length} roles overflow against the widest real token — all three at the same 30px floor`
|
|
422
|
+
: 'no role overflows against the widest real token',
|
|
423
|
+
children: html`
|
|
424
|
+
<p>
|
|
425
|
+
The ladder is nine fluid roles, and only the mobile end of each can overflow. Each
|
|
426
|
+
row asks the same question twice: does the role's <b>320px size</b> fit the column
|
|
427
|
+
when set in the widest real token of any tier, and when set in the widest token
|
|
428
|
+
observed doing headline work?
|
|
429
|
+
</p>
|
|
430
|
+
<div style="overflow-x: auto">
|
|
431
|
+
<table class="tokens-table">
|
|
432
|
+
<caption>
|
|
433
|
+
Fits at the role's 320px size · any-tier worst is
|
|
434
|
+
<b>${worst.word}</b> (${worst.lang})${headlineWorst
|
|
435
|
+
? html` · headline-tier worst is <b>${headlineWorst.word}</b> (${headlineWorst.lang})`
|
|
436
|
+
: ''}
|
|
437
|
+
</caption>
|
|
438
|
+
<thead>
|
|
439
|
+
<tr>
|
|
440
|
+
<th>role</th><th>size at 320</th>
|
|
441
|
+
<th>any tier<br /><small>288px col</small></th>
|
|
442
|
+
<th>any tier<br /><small>343px col</small></th>
|
|
443
|
+
<th>headline tier<br /><small>288px col</small></th>
|
|
444
|
+
<th>headline tier<br /><small>343px col</small></th>
|
|
445
|
+
</tr>
|
|
446
|
+
</thead>
|
|
447
|
+
<tbody>
|
|
448
|
+
${exposure.map(e => html`
|
|
449
|
+
<tr>
|
|
450
|
+
<td><code>${e.name}</code></td>
|
|
451
|
+
<td>${e.min}px</td>
|
|
452
|
+
<td style="color: ${e.all[0] ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
|
|
453
|
+
${e.needAll}px — ${e.all[0] ? 'fits' : 'OVER'}
|
|
454
|
+
</td>
|
|
455
|
+
<td style="color: ${e.all[1] ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
|
|
456
|
+
${e.needAll}px — ${e.all[1] ? 'fits' : 'OVER'}
|
|
457
|
+
</td>
|
|
458
|
+
<td style="color: ${e.headline[0] ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
|
|
459
|
+
${e.needHeadline}px — ${e.headline[0] ? 'fits' : 'OVER'}
|
|
460
|
+
</td>
|
|
461
|
+
<td style="color: ${e.headline[1] ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
|
|
462
|
+
${e.needHeadline}px — ${e.headline[1] ? 'fits' : 'OVER'}
|
|
463
|
+
</td>
|
|
464
|
+
</tr>
|
|
465
|
+
`)}
|
|
466
|
+
</tbody>
|
|
467
|
+
</table>
|
|
496
468
|
</div>
|
|
497
|
-
<
|
|
498
|
-
|
|
469
|
+
<p style="margin-top: var(--spacing-16)">
|
|
470
|
+
<b>The exposure is one size, on one tier, at one viewport.</b> Everything at
|
|
471
|
+
27px and below fits every real token in the sample at both phone columns. The
|
|
472
|
+
30px floor is shared by three roles — <code>heading-h1</code>,
|
|
473
|
+
<code>display-md</code> and <code>display-sm</code> — and all three clear the
|
|
474
|
+
headline-tier worst and fail the feature-tier worst, on the 288px column only,
|
|
475
|
+
because 343px absorbs even the widest token in the set.
|
|
476
|
+
</p>
|
|
477
|
+
<p>
|
|
478
|
+
<b>So the ladder stays at 30, and now for a reason rather than an artefact.</b>
|
|
479
|
+
Dropping the floor to ${Math.floor(288 / worst.per)} would buy protection against a
|
|
480
|
+
headline nobody has written — no twenty-character compound appears above 24px in
|
|
481
|
+
any of the nine markets — and would spend a real step of hierarchy to do it, on the
|
|
482
|
+
one role whose whole job is to outrank the rest. What the measurement buys instead
|
|
483
|
+
is a copy constraint that can be stated: <b>a headline token wider than
|
|
484
|
+
${(288 / 30 * 100).toFixed(0)}px per 100px of size overflows a 320px phone</b>, and
|
|
485
|
+
five real tokens in <code>sv</code>, <code>nl</code> and <code>de</code> exceed it.
|
|
486
|
+
None of them is a headline today.
|
|
487
|
+
</p>
|
|
488
|
+
`
|
|
489
|
+
})}
|
|
490
|
+
${refSection({
|
|
491
|
+
name : 'The ceiling, per word and per column',
|
|
492
|
+
meta : `${rows.length} words · Bold · measured in this browser`,
|
|
493
|
+
children: html`
|
|
494
|
+
<div style="overflow-x: auto">
|
|
495
|
+
<table class="tokens-table">
|
|
496
|
+
<caption>Largest font size at which the word still fits the column</caption>
|
|
497
|
+
<thead>
|
|
498
|
+
<tr>
|
|
499
|
+
<th>lang</th><th>word</th><th>source</th><th>seen at</th><th>w / 100px</th>
|
|
500
|
+
${COLUMNS.map(([vw, col]) => html`<th>${vw}<br /><small>${col}px</small></th>`)}
|
|
501
|
+
</tr>
|
|
502
|
+
</thead>
|
|
503
|
+
<tbody>
|
|
504
|
+
${rows.map(r => html`
|
|
505
|
+
<tr>
|
|
506
|
+
<td><code>${r.lang}</code></td>
|
|
507
|
+
<td>${r.word}</td>
|
|
508
|
+
<td><code>${r.source}</code></td>
|
|
509
|
+
<td>${r.observedAt ? html`${r.observedAt}px · ${r.tier}` : '—'}</td>
|
|
510
|
+
<td>${(r.per * 100).toFixed(0)}</td>
|
|
511
|
+
${r.ceilings.map(c => html`<td>${c}</td>`)}
|
|
512
|
+
</tr>
|
|
513
|
+
`)}
|
|
514
|
+
</tbody>
|
|
515
|
+
</table>
|
|
499
516
|
</div>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
517
|
+
`
|
|
518
|
+
})}
|
|
519
|
+
${refSection({
|
|
520
|
+
name : 'The same thing, visible',
|
|
521
|
+
meta : 'the dashed edge is the column — red type crosses it',
|
|
522
|
+
children: html`
|
|
523
|
+
<p style="margin-bottom: var(--spacing-16)">
|
|
524
|
+
<b>The mobile ladder's own steps</b> — 16 / 18 / 21 / 24 / 27 / 30 — plus the two
|
|
525
|
+
desktop sizes above them, set in <b>${worst.word}</b> inside a 288px column, which
|
|
526
|
+
is what type gets on a 320px phone. Read down: the first size that crosses the
|
|
527
|
+
dashed edge is where this word stops fitting, and it lands exactly on the ladder's
|
|
528
|
+
top step.
|
|
529
|
+
</p>
|
|
530
|
+
<p style="margin-bottom: var(--spacing-16)">
|
|
531
|
+
<b>Correction, 2026-09-17: Figma and this browser agree to within 0.05%, and the
|
|
532
|
+
3.5% margin this study thought it had was a font race in this very page.</b> The
|
|
533
|
+
Figma study on page <i>Type on mobile</i> records the Finnish compound at
|
|
534
|
+
<b>907px per 100px</b>. Measured here in Plex Bold it is
|
|
535
|
+
${worstRetired ? html`<b>${(worstRetired.per * 100).toFixed(1)}</b>` : ''} — the
|
|
536
|
+
same number, and it is not one lucky word: <b>seven were then measured in both
|
|
537
|
+
tools</b> — 1017/1016.2, 1015/1014.8, 1011/1010.5, 905/904.5, 792/791.4,
|
|
538
|
+
907/906.6, 844/843.5 — agreeing to within 0.1% every time. What reproduces the 875
|
|
539
|
+
that was written down as "the browser" is <b>the same word in system-ui Bold:
|
|
540
|
+
874.1</b>, because this page measured before the Plex face had loaded and silently
|
|
541
|
+
read the fallback. Same cause, same 3%, and
|
|
542
|
+
it could also change <i>which</i> word won the worst-case title, since the ranking
|
|
543
|
+
is by width. The measurement now waits for the face and says so if it ever cannot.
|
|
544
|
+
The rule that came out of the discrepancy survives, because it never depended on
|
|
545
|
+
the number: where the two disagree about whether something <i>renders</i>, the
|
|
546
|
+
browser is right; where they disagree about what to <i>ship</i>, the conservative
|
|
547
|
+
number is. But there was no renderer margin, and the ladder must not be credited
|
|
548
|
+
with one. The word on screen here is <b>${worst.word}</b> at
|
|
549
|
+
${(worst.per * 100).toFixed(0)}px per 100px, production copy the Figma study never
|
|
550
|
+
saw.
|
|
551
|
+
</p>
|
|
552
|
+
<div style="width: 288px; max-width: 100%; border: 2px dashed var(--color-text-error);
|
|
553
|
+
padding-block: var(--spacing-16); display: grid; gap: var(--spacing-16)">
|
|
554
|
+
${[16, 18, 21, 24, 27, 30, 36, 48].map(size => {
|
|
555
|
+
const need = Math.round(worst.per * size);
|
|
556
|
+
const fits = need <= 288;
|
|
557
|
+
return html`
|
|
558
|
+
<div>
|
|
559
|
+
<div style="font-family: var(--typography-fontFamily-mono);
|
|
560
|
+
font-size: var(--typography-fontSize-xxs);
|
|
561
|
+
color: ${fits ? 'var(--color-text-muted)' : 'var(--color-text-error)'}">
|
|
562
|
+
${size}px · needs ${need}px · ${fits ? `fits by ${288 - need}` : `OVER by ${need - 288}`}
|
|
563
|
+
</div>
|
|
564
|
+
<div style="font-size: ${size}px; font-weight: var(--typography-fontWeight-bold);
|
|
565
|
+
line-height: var(--typography-lineHeight-tight);
|
|
566
|
+
white-space: nowrap;
|
|
567
|
+
color: ${fits ? 'var(--color-text-primary)' : 'var(--color-text-error)'}">
|
|
568
|
+
${worst.word}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
`;
|
|
572
|
+
})}
|
|
514
573
|
</div>
|
|
515
|
-
|
|
516
|
-
|
|
574
|
+
`
|
|
575
|
+
})}
|
|
576
|
+
${refSection({
|
|
577
|
+
name : 'And what the roles do about it',
|
|
578
|
+
meta : 'the same word, at each fluid role, live at your viewport',
|
|
579
|
+
children: html`
|
|
580
|
+
<p style="margin-bottom: var(--spacing-16)">
|
|
581
|
+
Resize the window: every line below changes size, and none of them crosses its
|
|
582
|
+
column at any width. Compare with the block above, where the sizes are fixed.
|
|
583
|
+
</p>
|
|
584
|
+
<div style="display: grid; gap: var(--spacing-16)">
|
|
585
|
+
${FLUID.map(([name, max, min]) => html`
|
|
586
|
+
<div>
|
|
587
|
+
<div style="font-family: var(--typography-fontFamily-mono);
|
|
588
|
+
font-size: var(--typography-fontSize-xxs);
|
|
589
|
+
color: var(--color-text-muted)">
|
|
590
|
+
${name} · ${min}px at 320 → ${max}px at 992
|
|
591
|
+
</div>
|
|
592
|
+
<div style="font-size: var(--typography-${name}-fontSize);
|
|
593
|
+
font-weight: var(--typography-${name}-fontWeight);
|
|
594
|
+
line-height: var(--typography-${name}-lineHeight);
|
|
595
|
+
color: var(--color-text-primary)">${worst.word}</div>
|
|
596
|
+
</div>
|
|
597
|
+
`)}
|
|
517
598
|
</div>
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
: ''}
|
|
525
|
-
</div>
|
|
526
|
-
`)}
|
|
527
|
-
</div>
|
|
528
|
-
</div>
|
|
599
|
+
`
|
|
600
|
+
})}
|
|
601
|
+
`
|
|
602
|
+
})}
|
|
603
|
+
`;
|
|
604
|
+
};
|
|
529
605
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
606
|
+
/* The measurement is async now, so the story resolves a promise rather than
|
|
607
|
+
returning a template. `until` renders the placeholder for the one frame the
|
|
608
|
+
font takes on a warm cache, and for however long it takes on a cold one --
|
|
609
|
+
which is the point: a visible "measuring" beats an invisible wrong number. */
|
|
610
|
+
export const WhyTheseSizes = () => html`${until(
|
|
611
|
+
whyTheseSizes(),
|
|
612
|
+
html`<p style="font-family: var(--typography-fontFamily-mono);
|
|
613
|
+
font-size: var(--typography-fontSize-xs);
|
|
614
|
+
color: var(--color-text-muted)">
|
|
615
|
+
Loading IBM Plex Sans Bold before measuring anything…
|
|
616
|
+
</p>`
|
|
617
|
+
)}`;
|
|
618
|
+
|
|
619
|
+
export const FluidType = () => {
|
|
620
|
+
const cssText = readAllCssText();
|
|
621
|
+
const consumers = discoverConsumers(cssText);
|
|
622
|
+
return html`
|
|
623
|
+
${typeStyles}
|
|
624
|
+
${docsPage({
|
|
625
|
+
title: 'Fluid type',
|
|
626
|
+
lede : html`Nine roles change size with the viewport. The minimum is reached at
|
|
627
|
+
<b>320px</b> and the maximum — the size this system already shipped — at
|
|
628
|
+
<b>992px</b>, the <code>lg</code> breakpoint. <b>Above lg nothing changed</b>, so every
|
|
629
|
+
desktop layout that has been reviewed renders exactly as reviewed.`,
|
|
630
|
+
note: html`<b>Why the role and not <code>bd-h</code>.</b> More than one thing reads these
|
|
631
|
+
roles — the hero's final price reads <code>--typography-heading-h2-*</code> directly, and
|
|
632
|
+
so does <code>blog-heading</code>. A media query inside <code>heading.css.js</code> would
|
|
633
|
+
have made one role render 36px in some places and 32px in others, and would have failed at
|
|
634
|
+
the job it was for: the headline would have dropped while the price stayed, flattening the
|
|
635
|
+
hierarchy it was meant to protect. ${referenceNote}`,
|
|
636
|
+
children: html`
|
|
637
|
+
<p>
|
|
638
|
+
<b>The constraint is a long word, not a long sentence.</b> What decides whether large
|
|
639
|
+
type works on a phone is whether the longest <b>unbreakable word</b> fits the column —
|
|
640
|
+
and the column is the viewport minus twice <code>--grid-padding-*</code>, so a 320px
|
|
641
|
+
phone gives type 288px. Against that column the widest token in shipped copy
|
|
642
|
+
(Swedish <i>kalenderinbjudningar</i>, harvested 2026-09-17) tops out at <b>28px</b>,
|
|
643
|
+
and the widest token observed doing a headline's job (German
|
|
644
|
+
<i>Gründungsmitglied</i>) at <b>31px</b>. <code>heading-h1</code> is 48, so it
|
|
645
|
+
overflowed at 375 as well as 320 — in German, Dutch and Swedish. See
|
|
646
|
+
<i>Why these sizes</i> for the per-role exposure and the tier split.
|
|
647
|
+
</p>
|
|
648
|
+
<p>
|
|
649
|
+
<b>A breakpoint step was tried first and abandoned.</b> Stepping h1 to 36 below
|
|
650
|
+
<code>sm</code> still overflows at 320 and changes nothing at 576–767, where the ceiling
|
|
651
|
+
is already 54. The constraint moves continuously with the column, because the column is
|
|
652
|
+
a function of viewport width — a step function approximates a line with a staircase.
|
|
653
|
+
</p>
|
|
654
|
+
<p>
|
|
655
|
+
<b>The middle term carries a <code>rem</code>, and that is not optional.</b>
|
|
656
|
+
<code>clamp(2rem, 4vw, 3rem)</code> is the spelling everyone reaches for and it breaks
|
|
657
|
+
text resize: a size depending only on viewport width ignores the reader's own font-size
|
|
658
|
+
setting, which is WCAG 1.4.4. Same principle as unitless <code>line-height</code> and
|
|
659
|
+
rem <code>--spacing-*</code>.
|
|
660
|
+
</p>
|
|
661
|
+
<p>
|
|
662
|
+
<b>The mobile ladder is the design decision</b> — 30 / 27 / 24 / 21 / 18 / 16 for h1–h6,
|
|
663
|
+
held in six <code>--typography-*-fontSizeMin</code> tokens. Its ratios are even
|
|
664
|
+
(1.11–1.17) against the desktop ladder's irregular 1.33 / 1.13 / 1.33 / 1.20 / 1.11,
|
|
665
|
+
because a phone has less range to spend: 16 to the 31px ceiling is a factor of 1.94
|
|
666
|
+
where desktop has 18 to 48, a factor of 2.67.
|
|
667
|
+
</p>
|
|
668
|
+
<p>
|
|
669
|
+
<b><code>display-lg</code> and above are desktop-only.</b> Five roles from 60 to 128px
|
|
670
|
+
cannot squeeze between 30px and a 31px ceiling, so the family has no mobile identity
|
|
671
|
+
above <code>display-md</code>. A component that needs one on a phone should take a
|
|
672
|
+
heading role.
|
|
673
|
+
</p>
|
|
674
|
+
<p>
|
|
675
|
+
<b>Do not judge a phone's measure against 45–75 characters per line.</b> That band is a
|
|
676
|
+
print and desktop yardstick. Reaching 45 CPL on a 288px column needs 14.2px body text
|
|
677
|
+
and 50 CPL needs 12.8px, so applying it to a phone trades the 16px floor for a soft
|
|
678
|
+
target. <code>body-regular</code> runs 40 characters there and stays 16px.
|
|
679
|
+
<code>body-lead</code> is the one prose role that moves, 20 → 18.
|
|
680
|
+
</p>
|
|
681
|
+
${refSection({
|
|
682
|
+
name : 'What each role resolves to',
|
|
683
|
+
meta : `${FLUID.length} fluid roles · clamp(min, rem + vw, max) · window 320–992`,
|
|
684
|
+
children: html`
|
|
685
|
+
<div style="overflow-x: auto">
|
|
686
|
+
<table class="tokens-table">
|
|
687
|
+
<caption>Resolved size in px at each viewport. Column = viewport − 2 × gutter.</caption>
|
|
688
|
+
<thead>
|
|
689
|
+
<tr>
|
|
690
|
+
<th>role</th><th>weight</th>
|
|
691
|
+
${COLUMNS.map(([vw, col]) => html`<th>${vw}<br /><small>${col}px col</small></th>`)}
|
|
692
|
+
</tr>
|
|
693
|
+
</thead>
|
|
694
|
+
<tbody>
|
|
695
|
+
${FLUID.map(([name, max, min, weight]) => html`
|
|
696
|
+
<tr>
|
|
697
|
+
<td><code>--typography-${name}-fontSize</code></td>
|
|
698
|
+
<td>${weight}</td>
|
|
699
|
+
${COLUMNS.map(([vw]) => html`<td>${fluidAt(vw, min, max).toFixed(1)}</td>`)}
|
|
700
|
+
</tr>
|
|
701
|
+
`)}
|
|
702
|
+
</tbody>
|
|
703
|
+
</table>
|
|
539
704
|
</div>
|
|
540
|
-
|
|
541
|
-
|
|
705
|
+
`
|
|
706
|
+
})}
|
|
707
|
+
${refSection({
|
|
708
|
+
name : 'Live, at this viewport',
|
|
709
|
+
meta : 'resize the window — every line below moves',
|
|
710
|
+
children: html`
|
|
711
|
+
<div style="display: grid; gap: var(--spacing-16)">
|
|
712
|
+
${FLUID.map(([name]) => html`
|
|
713
|
+
<div>
|
|
714
|
+
<div style="font-family: var(--typography-fontFamily-mono);
|
|
715
|
+
font-size: var(--typography-fontSize-xs);
|
|
716
|
+
color: var(--color-text-muted)">${name}</div>
|
|
717
|
+
<div style="font-size: var(--typography-${name}-fontSize);
|
|
718
|
+
font-weight: var(--typography-${name}-fontWeight);
|
|
719
|
+
line-height: var(--typography-${name}-lineHeight);
|
|
720
|
+
letter-spacing: var(--typography-${name}-letterSpacing);
|
|
721
|
+
font-family: var(--typography-${name}-fontFamily);
|
|
722
|
+
color: var(--color-text-primary)">Henkilöllisyyssuoja</div>
|
|
723
|
+
</div>
|
|
724
|
+
`)}
|
|
542
725
|
</div>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
726
|
+
`
|
|
727
|
+
})}
|
|
728
|
+
${refSection({
|
|
729
|
+
name : 'The minimums',
|
|
730
|
+
meta : '9 tokens · --typography-*-fontSizeMin · the mobile ladder',
|
|
731
|
+
children: primitiveTable({
|
|
732
|
+
caption: 'Mobile endpoint tokens',
|
|
733
|
+
tokens : FLUID.map(([name]) => `--typography-${name}-fontSizeMin`),
|
|
734
|
+
consumers,
|
|
735
|
+
sample : token => `font-size: var(${token})`
|
|
736
|
+
})
|
|
737
|
+
})}
|
|
738
|
+
`
|
|
739
|
+
})}
|
|
740
|
+
`;
|
|
741
|
+
};
|
|
547
742
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
743
|
+
export const TypographyScale = () => {
|
|
744
|
+
const cssText = readAllCssText();
|
|
745
|
+
const consumers = discoverConsumers(cssText);
|
|
746
|
+
|
|
747
|
+
const families = tokensMatching(cssText, /(--typography-fontFamily-[a-zA-Z0-9-]+)\s*:/g);
|
|
748
|
+
const sizes = tokensMatching(cssText, /(--typography-fontSize-[a-zA-Z0-9]+)\s*:/g);
|
|
749
|
+
const weights = tokensMatching(cssText, /(--typography-fontWeight-[a-zA-Z0-9]+)\s*:/g);
|
|
750
|
+
const lineHeights = tokensMatching(cssText, /(--typography-lineHeight-[a-zA-Z0-9]+)\s*:/g);
|
|
751
|
+
const tracking = tokensMatching(cssText, /(--typography-letterSpacing-[a-zA-Z0-9]+)\s*:/g);
|
|
752
|
+
|
|
753
|
+
return html`
|
|
754
|
+
${typeStyles}
|
|
755
|
+
${docsPage({
|
|
756
|
+
title: 'Type scale',
|
|
757
|
+
lede : html`The primitive typography tokens, read from the loaded token CSS at render time
|
|
758
|
+
so this page always matches <code>src/tokens/tokens.css</code>. Click a token name to copy
|
|
759
|
+
its <code>var()</code> reference.`,
|
|
760
|
+
note: html`These are the raw scales. For a style you can apply directly to a heading, a
|
|
761
|
+
paragraph or a label, use the composed tokens on the <b>Typography</b> page — those are
|
|
762
|
+
what components consume. ${referenceNote}`,
|
|
763
|
+
children: html`
|
|
764
|
+
${refSection({
|
|
765
|
+
name : 'Font family',
|
|
766
|
+
meta : `${families.length} tokens · --typography-fontFamily-*`,
|
|
767
|
+
children: primitiveTable({
|
|
768
|
+
caption: 'Font family tokens',
|
|
769
|
+
tokens : families,
|
|
770
|
+
consumers,
|
|
771
|
+
sample : token => `font-family: var(${token})`
|
|
772
|
+
})
|
|
773
|
+
})}
|
|
774
|
+
${refSection({
|
|
775
|
+
name : 'Font size',
|
|
776
|
+
meta : `${sizes.length} steps · --typography-fontSize-*`,
|
|
777
|
+
children: primitiveTable({
|
|
778
|
+
caption: 'Font size scale',
|
|
779
|
+
tokens : sizes,
|
|
780
|
+
consumers,
|
|
781
|
+
sample : token => `font-size: var(${token}); line-height: 1.2`
|
|
782
|
+
})
|
|
783
|
+
})}
|
|
784
|
+
${refSection({
|
|
785
|
+
name : 'Font weight',
|
|
786
|
+
meta : `${weights.length} steps · --typography-fontWeight-*`,
|
|
787
|
+
children: primitiveTable({
|
|
788
|
+
caption: 'Font weight scale',
|
|
789
|
+
tokens : weights,
|
|
790
|
+
consumers,
|
|
791
|
+
sample : token => `font-weight: var(${token}); font-size: var(--typography-fontSize-lg)`
|
|
792
|
+
})
|
|
793
|
+
})}
|
|
794
|
+
${refSection({
|
|
795
|
+
name : 'Line height',
|
|
796
|
+
meta : `${lineHeights.length} steps · --typography-lineHeight-*`,
|
|
797
|
+
children: primitiveTable({
|
|
798
|
+
caption: 'Line height scale',
|
|
799
|
+
tokens : lineHeights,
|
|
800
|
+
consumers,
|
|
801
|
+
leading: true
|
|
802
|
+
})
|
|
803
|
+
})}
|
|
804
|
+
${refSection({
|
|
805
|
+
name : 'A ratio is not a fixed leading',
|
|
806
|
+
meta : '--typography-lineHeight-normal at three sizes',
|
|
807
|
+
children: html`
|
|
808
|
+
<div class="specimen">
|
|
809
|
+
<p class="docs-lede" style="margin-top: 0">
|
|
810
|
+
One unitless value produces different leading at every size, because each element
|
|
811
|
+
resolves it against its own font-size. That is the whole reason these tokens are
|
|
812
|
+
ratios and not lengths.
|
|
813
|
+
</p>
|
|
814
|
+
<div class="ratio-demo">
|
|
815
|
+
${['--typography-fontSize-xs', '--typography-fontSize-base', '--typography-fontSize-2xl'].map(
|
|
816
|
+
sizeToken => {
|
|
817
|
+
const px = parseFloat(resolve(sizeToken)) * 16;
|
|
818
|
+
const ratio = parseFloat(resolve('--typography-lineHeight-normal'));
|
|
819
|
+
return html`
|
|
820
|
+
<figure>
|
|
821
|
+
<figcaption>
|
|
822
|
+
${sizeToken.replace('--typography-', '')} · ${px}px × ${ratio} =
|
|
823
|
+
${(px * ratio).toFixed(0)}px
|
|
824
|
+
</figcaption>
|
|
825
|
+
<p
|
|
826
|
+
style="font-size: var(${sizeToken}); line-height: var(--typography-lineHeight-normal)"
|
|
827
|
+
>
|
|
828
|
+
The line box grows with the text, so the rhythm holds at any size.
|
|
829
|
+
</p>
|
|
830
|
+
</figure>
|
|
831
|
+
`;
|
|
832
|
+
}
|
|
833
|
+
)}
|
|
566
834
|
</div>
|
|
567
835
|
</div>
|
|
568
|
-
`
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
<
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
<
|
|
592
|
-
|
|
593
|
-
|
|
836
|
+
`
|
|
837
|
+
})}
|
|
838
|
+
|
|
839
|
+
${refSection({
|
|
840
|
+
name : 'Rules',
|
|
841
|
+
children: html`
|
|
842
|
+
<div class="docs-note">
|
|
843
|
+
<ol class="rule-list">
|
|
844
|
+
<li>
|
|
845
|
+
<b>Always unitless.</b> Never <code>px</code>, <code>rem</code> or
|
|
846
|
+
<code>em</code>. A unitless value inherits as a number and each child
|
|
847
|
+
recomputes it against its own font-size; a length inherits as a fixed line box,
|
|
848
|
+
so nested text of a different size gets the wrong leading and stops growing when
|
|
849
|
+
a reader raises their default font size.
|
|
850
|
+
</li>
|
|
851
|
+
<li>
|
|
852
|
+
<b>Always from this scale.</b> Use
|
|
853
|
+
<code>--typography-lineHeight-*</code>, or the composed style token for the role
|
|
854
|
+
you are styling — <code>--typography-heading-h2-lineHeight</code>,
|
|
855
|
+
<code>--typography-body-regular-lineHeight</code>. Do not invent a ratio.
|
|
856
|
+
</li>
|
|
857
|
+
<li>
|
|
858
|
+
<b>Never a spacing or dimension token.</b>
|
|
859
|
+
<code>line-height: var(--spacing-18)</code> is both an absolute value and a
|
|
860
|
+
category error.
|
|
861
|
+
</li>
|
|
862
|
+
<li>
|
|
863
|
+
<b>Anything that can wrap needs at least
|
|
864
|
+
<code>tight</code></b> (${resolve('--typography-lineHeight-tight')}). A ratio at
|
|
865
|
+
or below 1 makes the line box shorter than the text, which clips descenders and
|
|
866
|
+
collides wrapped lines.
|
|
867
|
+
</li>
|
|
868
|
+
<li>
|
|
869
|
+
<b>Tighter as text gets bigger.</b> Display and the two largest headings sit on
|
|
870
|
+
<code>tight</code>, h3 and h4 on <code>130</code>, the smaller headings and most
|
|
871
|
+
body copy on <code>140</code> and <code>145</code>, and running prose at 16px on
|
|
872
|
+
<code>normal</code>. The ramp tightens as the size grows rather than holding one
|
|
873
|
+
value per family. The composed tokens already encode this, which is the simplest
|
|
874
|
+
reason to use them rather than picking a primitive by hand.
|
|
875
|
+
</li>
|
|
876
|
+
<li>
|
|
877
|
+
<b>No fallback.</b> A fallback of the form
|
|
878
|
+
<code>var(--typography-lineHeight-normal, 1.5)</code> restates the token's
|
|
879
|
+
current value and silently lies the moment it changes.
|
|
880
|
+
</li>
|
|
881
|
+
</ol>
|
|
594
882
|
</div>
|
|
595
|
-
`
|
|
596
|
-
|
|
597
|
-
</div>
|
|
598
|
-
`
|
|
599
|
-
: ''}
|
|
600
|
-
</div>
|
|
601
|
-
`;
|
|
883
|
+
`
|
|
884
|
+
})}
|
|
602
885
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
886
|
+
${refSection({
|
|
887
|
+
name : 'Letter spacing',
|
|
888
|
+
meta : `${tracking.length} steps · --typography-letterSpacing-*`,
|
|
889
|
+
children: primitiveTable({
|
|
890
|
+
caption: 'Letter spacing scale',
|
|
891
|
+
tokens : tracking,
|
|
892
|
+
consumers,
|
|
893
|
+
sample : token =>
|
|
894
|
+
`letter-spacing: var(${token}); font-size: var(--typography-fontSize-base)`
|
|
895
|
+
})
|
|
896
|
+
})}
|
|
897
|
+
`
|
|
898
|
+
})}
|
|
899
|
+
`;
|
|
615
900
|
};
|
|
616
901
|
|
|
617
|
-
|
|
618
|
-
<style>
|
|
619
|
-
.scale-container {
|
|
620
|
-
max-width: 800px;
|
|
621
|
-
margin: 0 auto;
|
|
622
|
-
padding: var(--space-xl);
|
|
623
|
-
background: white;
|
|
624
|
-
border-radius: 8px;
|
|
625
|
-
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.scale-title {
|
|
629
|
-
font-family: var(--font-family-sans);
|
|
630
|
-
font-size: var(--font-size-2xl);
|
|
631
|
-
font-weight: var(--font-weight-bold);
|
|
632
|
-
margin: 0 0 var(--space-xl) 0;
|
|
633
|
-
color: #212529;
|
|
634
|
-
text-align: center;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
.scale-item {
|
|
638
|
-
display: flex;
|
|
639
|
-
align-items: center;
|
|
640
|
-
margin-bottom: var(--space-md);
|
|
641
|
-
padding: var(--space-sm);
|
|
642
|
-
border-radius: 6px;
|
|
643
|
-
background: #f8f9fa;
|
|
644
|
-
border-left: 4px solid #007bff;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.scale-size {
|
|
648
|
-
font-family: var(--font-family-mono);
|
|
649
|
-
font-size: var(--font-size-sm);
|
|
650
|
-
color: #007bff;
|
|
651
|
-
font-weight: var(--font-weight-semibold);
|
|
652
|
-
min-width: 120px;
|
|
653
|
-
padding-right: var(--space-md);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
.scale-preview {
|
|
657
|
-
flex: 1;
|
|
658
|
-
font-family: var(--font-family-sans);
|
|
659
|
-
line-height: var(--line-height-normal);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
.scale-label {
|
|
663
|
-
font-family: var(--font-family-mono);
|
|
664
|
-
font-size: var(--font-size-xs);
|
|
665
|
-
color: #6c757d;
|
|
666
|
-
background: white;
|
|
667
|
-
padding: 2px 6px;
|
|
668
|
-
border-radius: 3px;
|
|
669
|
-
border: 1px solid #dee2e6;
|
|
670
|
-
margin-left: var(--space-sm);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
.heading-hierarchy {
|
|
674
|
-
margin-top: var(--space-2xl);
|
|
675
|
-
padding-top: var(--space-xl);
|
|
676
|
-
border-top: 2px solid #f0f0f0;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
.hierarchy-title {
|
|
680
|
-
font-family: var(--font-family-sans);
|
|
681
|
-
font-size: var(--font-size-xl);
|
|
682
|
-
font-weight: var(--font-weight-bold);
|
|
683
|
-
margin: 0 0 var(--space-lg) 0;
|
|
684
|
-
color: #212529;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
.heading-sample {
|
|
688
|
-
margin-bottom: var(--space-lg);
|
|
689
|
-
padding: var(--space-md);
|
|
690
|
-
background: white;
|
|
691
|
-
border-radius: 6px;
|
|
692
|
-
border: 1px solid #e9ecef;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.heading-info {
|
|
696
|
-
display: flex;
|
|
697
|
-
align-items: center;
|
|
698
|
-
gap: var(--space-md);
|
|
699
|
-
margin-top: var(--space-sm);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
.heading-token {
|
|
703
|
-
font-family: var(--font-family-mono);
|
|
704
|
-
font-size: var(--font-size-xs);
|
|
705
|
-
color: #6c757d;
|
|
706
|
-
}
|
|
707
|
-
</style>
|
|
902
|
+
/* ---------- composed styles ---------- */
|
|
708
903
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
</div>
|
|
746
|
-
|
|
747
|
-
<div class="heading-sample">
|
|
748
|
-
<h3 style="font-size: var(--font-size-2xl); font-weight: var(--font-weight-semibold); margin: 0 0 var(--space-sm) 0;">
|
|
749
|
-
Heading 3 (h3)
|
|
750
|
-
</h3>
|
|
751
|
-
<div class="heading-info">
|
|
752
|
-
<span class="heading-token">--font-size-2xl</span>
|
|
753
|
-
<span class="heading-token">--font-weight-semibold</span>
|
|
754
|
-
<span class="heading-token">--line-height-tight</span>
|
|
755
|
-
</div>
|
|
756
|
-
</div>
|
|
757
|
-
|
|
758
|
-
<div class="heading-sample">
|
|
759
|
-
<h4 style="font-size: var(--font-size-xl); font-weight: var(--font-weight-semibold); margin: 0 0 var(--space-sm) 0;">
|
|
760
|
-
Heading 4 (h4)
|
|
761
|
-
</h4>
|
|
762
|
-
<div class="heading-info">
|
|
763
|
-
<span class="heading-token">--font-size-xl</span>
|
|
764
|
-
<span class="heading-token">--font-weight-semibold</span>
|
|
765
|
-
<span class="heading-token">--line-height-normal</span>
|
|
766
|
-
</div>
|
|
767
|
-
</div>
|
|
768
|
-
|
|
769
|
-
<div class="heading-sample">
|
|
770
|
-
<h5 style="font-size: var(--font-size-lg); font-weight: var(--font-weight-medium); margin: 0 0 var(--space-sm) 0;">
|
|
771
|
-
Heading 5 (h5)
|
|
772
|
-
</h5>
|
|
773
|
-
<div class="heading-info">
|
|
774
|
-
<span class="heading-token">--font-size-lg</span>
|
|
775
|
-
<span class="heading-token">--font-weight-medium</span>
|
|
776
|
-
<span class="heading-token">--line-height-normal</span>
|
|
777
|
-
</div>
|
|
778
|
-
</div>
|
|
779
|
-
|
|
780
|
-
<div class="heading-sample">
|
|
781
|
-
<h6 style="font-size: var(--font-size-md); font-weight: var(--font-weight-medium); margin: 0 0 var(--space-sm) 0;">
|
|
782
|
-
Heading 6 (h6)
|
|
783
|
-
</h6>
|
|
784
|
-
<div class="heading-info">
|
|
785
|
-
<span class="heading-token">--font-size-md</span>
|
|
786
|
-
<span class="heading-token">--font-weight-medium</span>
|
|
787
|
-
<span class="heading-token">--line-height-normal</span>
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
790
|
-
</div>
|
|
791
|
-
</div>
|
|
792
|
-
`;
|
|
793
|
-
TypographyScale.storyName = 'Typography Scale';
|
|
794
|
-
TypographyScale.parameters = {
|
|
795
|
-
docs: {
|
|
796
|
-
description: {
|
|
797
|
-
story: 'Complete typography scale showing all font sizes with visual hierarchy and proper heading structure examples.'
|
|
798
|
-
}
|
|
904
|
+
const PROPERTIES = [
|
|
905
|
+
['fontFamily', 'font-family'],
|
|
906
|
+
['fontSize', 'font-size'],
|
|
907
|
+
['fontWeight', 'font-weight'],
|
|
908
|
+
['lineHeight', 'line-height'],
|
|
909
|
+
['letterSpacing', 'letter-spacing']
|
|
910
|
+
];
|
|
911
|
+
|
|
912
|
+
const SAMPLE = 'Security that keeps up with you';
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Finds every composed style — any --typography-<group>-<variant>-fontSize —
|
|
916
|
+
* and groups them, so a style added to the tokens appears here automatically.
|
|
917
|
+
*/
|
|
918
|
+
function discoverComposedStyles(cssText) {
|
|
919
|
+
const groups = new Map();
|
|
920
|
+
const pattern = /--typography-([a-z]+)-([a-z0-9-]+?)-fontSize\s*:/g;
|
|
921
|
+
for (const [, group, variant] of cssText.matchAll(pattern)) {
|
|
922
|
+
if (!groups.has(group)) groups.set(group, new Set());
|
|
923
|
+
groups.get(group).add(variant);
|
|
924
|
+
}
|
|
925
|
+
return [...groups].map(([group, variants]) => ({ group, variants: [...variants] }));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function styleFor(group, variant) {
|
|
929
|
+
const declarations = [];
|
|
930
|
+
const used = [];
|
|
931
|
+
for (const [suffix, property] of PROPERTIES) {
|
|
932
|
+
const token = `--typography-${group}-${variant}-${suffix}`;
|
|
933
|
+
if (!resolve(token)) continue;
|
|
934
|
+
declarations.push(`${property}: var(${token})`);
|
|
935
|
+
// a full fallback stack is too long for the meta line; the stack itself is
|
|
936
|
+
// documented on the Fonts page
|
|
937
|
+
const raw = resolve(token);
|
|
938
|
+
const value = property === 'font-family' ? raw.split(',')[0].replace(/['"]/g, '').trim() : raw;
|
|
939
|
+
used.push({ token, property, value });
|
|
799
940
|
}
|
|
941
|
+
return { css: declarations.join('; '), used };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export const Typography = () => {
|
|
945
|
+
const cssText = readAllCssText();
|
|
946
|
+
const groups = discoverComposedStyles(cssText);
|
|
947
|
+
|
|
948
|
+
return html`
|
|
949
|
+
${typeStyles}
|
|
950
|
+
${docsPage({
|
|
951
|
+
title: 'Typography',
|
|
952
|
+
lede : html`The composed type styles components actually use. Each specimen is rendered by
|
|
953
|
+
applying that style's own tokens, so what you see is what the token produces — nothing is
|
|
954
|
+
hardcoded.`,
|
|
955
|
+
note: html`Every style is discovered from the loaded CSS by looking for a
|
|
956
|
+
<code>-fontSize</code> token, so adding a style to
|
|
957
|
+
<code>src/tokens/tokens.css</code> makes it appear here without touching this page.`,
|
|
958
|
+
children: groups.map(({ group, variants }) =>
|
|
959
|
+
refSection({
|
|
960
|
+
name : group,
|
|
961
|
+
meta : `${variants.length} styles · --typography-${group}-*`,
|
|
962
|
+
children: html`
|
|
963
|
+
<div class="specimen">
|
|
964
|
+
${variants.map(variant => {
|
|
965
|
+
const { css, used } = styleFor(group, variant);
|
|
966
|
+
if (!used.length) return html``;
|
|
967
|
+
return html`
|
|
968
|
+
<div class="specimen-row">
|
|
969
|
+
<div class="specimen-meta">
|
|
970
|
+
<span class="name">${group}-${variant}</span>
|
|
971
|
+
${used.map(
|
|
972
|
+
({ property, value }) => html`<span>${property}: ${value}</span>`
|
|
973
|
+
)}
|
|
974
|
+
</div>
|
|
975
|
+
<p class="specimen-text" style=${css}>${SAMPLE}</p>
|
|
976
|
+
</div>
|
|
977
|
+
`;
|
|
978
|
+
})}
|
|
979
|
+
</div>
|
|
980
|
+
`
|
|
981
|
+
})
|
|
982
|
+
)
|
|
983
|
+
})}
|
|
984
|
+
`;
|
|
800
985
|
};
|
|
801
986
|
|
|
802
|
-
export const TypographyCombinations = () =>
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
.combination-preview {
|
|
835
|
-
font-family: inherit;
|
|
836
|
-
font-size: inherit;
|
|
837
|
-
font-weight: inherit;
|
|
838
|
-
line-height: inherit;
|
|
839
|
-
letter-spacing: inherit;
|
|
840
|
-
margin: var(--space-md) 0;
|
|
841
|
-
padding: var(--space-md);
|
|
842
|
-
background: white;
|
|
843
|
-
border-radius: 6px;
|
|
844
|
-
border: 1px solid #dee2e6;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
.combination-tokens {
|
|
848
|
-
display: flex;
|
|
849
|
-
flex-wrap: wrap;
|
|
850
|
-
gap: var(--space-xs);
|
|
851
|
-
margin-top: var(--space-sm);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
.token-tag {
|
|
855
|
-
font-family: var(--font-family-mono);
|
|
856
|
-
font-size: var(--font-size-xs);
|
|
857
|
-
color: #6c757d;
|
|
858
|
-
background: white;
|
|
859
|
-
padding: 2px 6px;
|
|
860
|
-
border-radius: 3px;
|
|
861
|
-
border: 1px solid #dee2e6;
|
|
987
|
+
export const TypographyCombinations = () => {
|
|
988
|
+
const pairings = [
|
|
989
|
+
{
|
|
990
|
+
name : 'Page header',
|
|
991
|
+
layers: [
|
|
992
|
+
['display', 'md', 'Complete digital security'],
|
|
993
|
+
['body', 'lead', 'One plan for every device you own, with privacy and identity cover built in.']
|
|
994
|
+
]
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name : 'Section',
|
|
998
|
+
layers: [
|
|
999
|
+
['heading', 'h2', 'Why it matters'],
|
|
1000
|
+
['body', 'regular', 'Threats move faster than annual reviews. Protection has to keep pace, quietly, without asking you to manage it.']
|
|
1001
|
+
]
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name : 'Card',
|
|
1005
|
+
layers: [
|
|
1006
|
+
['heading', 'h5', 'Password Manager'],
|
|
1007
|
+
['body', 'small', 'Generate, store and autofill strong credentials across your devices.'],
|
|
1008
|
+
['caption', 'default', 'Included in every plan']
|
|
1009
|
+
]
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
name : 'Form field',
|
|
1013
|
+
layers: [
|
|
1014
|
+
['label', 'default', 'Email address'],
|
|
1015
|
+
['caption', 'small', 'We only use this for your licence key.']
|
|
1016
|
+
]
|
|
862
1017
|
}
|
|
863
|
-
|
|
1018
|
+
];
|
|
864
1019
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
font-family: var(--font-family-sans);
|
|
896
|
-
font-size: var(--font-size-3xl);
|
|
897
|
-
font-weight: var(--font-weight-bold);
|
|
898
|
-
line-height: var(--line-height-tight);
|
|
899
|
-
letter-spacing: var(--letter-spacing-tight);
|
|
900
|
-
">
|
|
901
|
-
Main Page Title
|
|
902
|
-
</div>
|
|
903
|
-
<div class="combination-tokens">
|
|
904
|
-
<span class="token-tag">--font-family-sans</span>
|
|
905
|
-
<span class="token-tag">--font-size-3xl</span>
|
|
906
|
-
<span class="token-tag">--font-weight-bold</span>
|
|
907
|
-
<span class="token-tag">--line-height-tight</span>
|
|
908
|
-
</div>
|
|
909
|
-
</div>
|
|
910
|
-
|
|
911
|
-
<!-- Code Block -->
|
|
912
|
-
<div class="combination-card">
|
|
913
|
-
<h3 class="combination-title">Code Snippet</h3>
|
|
914
|
-
<div class="combination-preview" style="
|
|
915
|
-
font-family: var(--font-family-mono);
|
|
916
|
-
font-size: var(--font-size-sm);
|
|
917
|
-
font-weight: var(--font-weight-regular);
|
|
918
|
-
line-height: var(--line-height-normal);
|
|
919
|
-
letter-spacing: var(--letter-spacing-normal);
|
|
920
|
-
background: #1a1a1a;
|
|
921
|
-
color: #f8f8f2;
|
|
922
|
-
">
|
|
923
|
-
const greeting = "Hello, World!";<br>
|
|
924
|
-
console.log(greeting);
|
|
925
|
-
</div>
|
|
926
|
-
<div class="combination-tokens">
|
|
927
|
-
<span class="token-tag">--font-family-mono</span>
|
|
928
|
-
<span class="token-tag">--font-size-sm</span>
|
|
929
|
-
<span class="token-tag">--font-weight-regular</span>
|
|
930
|
-
</div>
|
|
931
|
-
</div>
|
|
932
|
-
|
|
933
|
-
<!-- Label -->
|
|
934
|
-
<div class="combination-card">
|
|
935
|
-
<h3 class="combination-title">Form Label</h3>
|
|
936
|
-
<div class="combination-preview" style="
|
|
937
|
-
font-family: var(--font-family-sans);
|
|
938
|
-
font-size: var(--font-size-sm);
|
|
939
|
-
font-weight: var(--font-weight-semibold);
|
|
940
|
-
line-height: var(--line-height-tight);
|
|
941
|
-
letter-spacing: var(--letter-spacing-wide);
|
|
942
|
-
text-transform: uppercase;
|
|
943
|
-
">
|
|
944
|
-
Email Address
|
|
945
|
-
</div>
|
|
946
|
-
<div class="combination-tokens">
|
|
947
|
-
<span class="token-tag">--font-family-sans</span>
|
|
948
|
-
<span class="token-tag">--font-size-sm</span>
|
|
949
|
-
<span class="token-tag">--font-weight-semibold</span>
|
|
950
|
-
<span class="token-tag">--letter-spacing-wide</span>
|
|
951
|
-
</div>
|
|
952
|
-
</div>
|
|
953
|
-
|
|
954
|
-
<!-- Quote -->
|
|
955
|
-
<div class="combination-card">
|
|
956
|
-
<h3 class="combination-title">Block Quote</h3>
|
|
957
|
-
<div class="combination-preview" style="
|
|
958
|
-
font-family: var(--font-family-sans);
|
|
959
|
-
font-size: var(--font-size-lg);
|
|
960
|
-
font-weight: var(--font-weight-light);
|
|
961
|
-
line-height: var(--line-height-relaxed);
|
|
962
|
-
letter-spacing: var(--letter-spacing-normal);
|
|
963
|
-
font-style: italic;
|
|
964
|
-
border-left: 4px solid #007bff;
|
|
965
|
-
padding-left: var(--space-md);
|
|
966
|
-
">
|
|
967
|
-
The only way to do great work is to love what you do.
|
|
968
|
-
</div>
|
|
969
|
-
<div class="combination-tokens">
|
|
970
|
-
<span class="token-tag">--font-family-sans</span>
|
|
971
|
-
<span class="token-tag">--font-size-lg</span>
|
|
972
|
-
<span class="token-tag">--font-weight-light</span>
|
|
973
|
-
<span class="token-tag">--line-height-relaxed</span>
|
|
974
|
-
</div>
|
|
975
|
-
</div>
|
|
976
|
-
|
|
977
|
-
<!-- Button Text -->
|
|
978
|
-
<div class="combination-card">
|
|
979
|
-
<h3 class="combination-title">Button Text</h3>
|
|
980
|
-
<div class="combination-preview" style="
|
|
981
|
-
font-family: var(--font-family-sans);
|
|
982
|
-
font-size: var(--font-size-md);
|
|
983
|
-
font-weight: var(--font-weight-semibold);
|
|
984
|
-
line-height: var(--line-height-tight);
|
|
985
|
-
letter-spacing: var(--letter-spacing-wide);
|
|
986
|
-
text-align: center;
|
|
987
|
-
background: #007bff;
|
|
988
|
-
color: white;
|
|
989
|
-
padding: var(--space-sm) var(--space-lg);
|
|
990
|
-
border-radius: 4px;
|
|
991
|
-
">
|
|
992
|
-
Submit Form
|
|
993
|
-
</div>
|
|
994
|
-
<div class="combination-tokens">
|
|
995
|
-
<span class="token-tag">--font-family-sans</span>
|
|
996
|
-
<span class="token-tag">--font-size-md</span>
|
|
997
|
-
<span class="token-tag">--font-weight-semibold</span>
|
|
998
|
-
<span class="token-tag">--letter-spacing-wide</span>
|
|
999
|
-
</div>
|
|
1000
|
-
</div>
|
|
1001
|
-
</div>
|
|
1002
|
-
</div>
|
|
1003
|
-
`;
|
|
1004
|
-
TypographyCombinations.storyName = 'Common Combinations';
|
|
1005
|
-
TypographyCombinations.parameters = {
|
|
1006
|
-
docs: {
|
|
1007
|
-
description: {
|
|
1008
|
-
story: 'Practical typography combinations for common UI elements like headings, body text, code blocks, labels, quotes, and buttons.'
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1020
|
+
return html`
|
|
1021
|
+
${typeStyles}
|
|
1022
|
+
${docsPage({
|
|
1023
|
+
title : 'Type pairings',
|
|
1024
|
+
lede : 'Composed styles combined the way pages actually use them, so the relative scale and weight can be judged in context rather than one specimen at a time.',
|
|
1025
|
+
children: pairings.map(({ name, layers }) =>
|
|
1026
|
+
refSection({
|
|
1027
|
+
name,
|
|
1028
|
+
children: html`
|
|
1029
|
+
<div class="specimen">
|
|
1030
|
+
${layers.map(([group, variant, text]) => {
|
|
1031
|
+
const { css, used } = styleFor(group, variant);
|
|
1032
|
+
if (!used.length) return html``;
|
|
1033
|
+
return html`
|
|
1034
|
+
<p
|
|
1035
|
+
class="specimen-text"
|
|
1036
|
+
style="${css}; margin-bottom: var(--spacing-8)"
|
|
1037
|
+
title="--typography-${group}-${variant}-*"
|
|
1038
|
+
>
|
|
1039
|
+
${text}
|
|
1040
|
+
</p>
|
|
1041
|
+
`;
|
|
1042
|
+
})}
|
|
1043
|
+
</div>
|
|
1044
|
+
`
|
|
1045
|
+
})
|
|
1046
|
+
)
|
|
1047
|
+
})}
|
|
1048
|
+
`;
|
|
1049
|
+
};
|