@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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import { contrast, docsPage, refSection, resolve } from './story-chrome.js';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Foundations/Color/Responsibilities',
|
|
6
|
+
tags : ['autodocs']
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/* Measured as the page renders, so a palette change moves the numbers. */
|
|
10
|
+
const ratio = (a, b) => {
|
|
11
|
+
try {
|
|
12
|
+
return Math.round(contrast(a, b) * 100) / 100;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const chain = (primitive, role, component, ui) => html`
|
|
19
|
+
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:var(--spacing-8);font-family:var(--typography-fontFamily-mono);font-size:var(--typography-fontSize-xs)">
|
|
20
|
+
<code>${primitive}</code><span style="color:var(--color-text-muted)">→</span>
|
|
21
|
+
<code>${role}</code><span style="color:var(--color-text-muted)">→</span>
|
|
22
|
+
<code>${component}</code><span style="color:var(--color-text-muted)">→</span>
|
|
23
|
+
<span>${ui}</span>
|
|
24
|
+
</div>`;
|
|
25
|
+
|
|
26
|
+
const OWNERS = [
|
|
27
|
+
['Text hierarchy', '--color-text-*', 'primary, secondary, muted, subtle, disabled, inverse, muted-inverse'],
|
|
28
|
+
['Surfaces', '--color-surface-*', 'default, container, subtle, muted, brand, inverse, info'],
|
|
29
|
+
['Borders', '--color-border-*', 'container, subtle, default, strong, brand, disabled, container-inverse'],
|
|
30
|
+
['Actions', '--color-action-*', 'primary, -hover, -active; accent ramp for conversion; on-fill, the ink on any of them'],
|
|
31
|
+
['Status / feedback', '--status-{tone}-*', 'text, surface, surface-strong (not brand), border, accent, fill, on-fill'],
|
|
32
|
+
['Focus', '--focus-outline-*', 'one ring, never a border role'],
|
|
33
|
+
['Indicators', '--indicator-*', 'what marks the current item: the tab and nav underline, and the carousel dot']
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const Responsibilities = () =>
|
|
37
|
+
docsPage({
|
|
38
|
+
title: 'Colour responsibilities',
|
|
39
|
+
lede : `Which family owns which job, and how a colour gets from the palette to the screen.
|
|
40
|
+
Foundations/Surfaces answers "what does this surface mean"; this page answers the
|
|
41
|
+
question one level up — which vocabulary should you be asking at all.`,
|
|
42
|
+
note: html`<b>Do not choose a token because its numeric colour is closest to what you
|
|
43
|
+
want. Choose the role whose responsibility matches the UI.</b><br />
|
|
44
|
+
Fourteen primitives in this system serve more than one live role, and
|
|
45
|
+
<code>#006dff</code> alone serves six. A value tells you nothing about a job.`,
|
|
46
|
+
children: html`
|
|
47
|
+
${refSection({
|
|
48
|
+
name : 'The chain',
|
|
49
|
+
meta : 'primitive → responsibility → component → rendered UI',
|
|
50
|
+
children: html`
|
|
51
|
+
<p style="max-width:70ch">
|
|
52
|
+
Every colour that carries meaning should travel the whole chain. A component reaching
|
|
53
|
+
straight past the middle is not a style error — it is a missing sentence about what
|
|
54
|
+
the colour is <i>for</i>, and it is why the same job ends up spelled four ways.
|
|
55
|
+
</p>
|
|
56
|
+
<div style="display:grid;gap:var(--spacing-12);background:var(--color-surface-muted);padding:var(--spacing-24);border-radius:var(--radius-lg);overflow-x:auto">
|
|
57
|
+
${chain('--color-blue-500', '--color-action-primary', '--button-primary-background-default', 'a primary button')}
|
|
58
|
+
${chain('--color-neutral-0', '--color-text-inverse', '(used directly)', 'footer text on the masthead')}
|
|
59
|
+
${chain('--color-green-800', '--status-success-text', '(used directly)', 'a "supported" table cell')}
|
|
60
|
+
${chain('--color-neutral-300', '--color-text-muted-inverse', '--bd-terminalmsg-subdued', 'terminal dim text')}
|
|
61
|
+
</div>
|
|
62
|
+
<p style="max-width:70ch;margin-top:var(--spacing-24)">
|
|
63
|
+
The middle step is the only one that survives a redesign. Primitives get retuned and
|
|
64
|
+
components get rewritten; the responsibility is the thing that stays true.
|
|
65
|
+
</p>`
|
|
66
|
+
})}
|
|
67
|
+
|
|
68
|
+
${refSection({
|
|
69
|
+
name : 'Who owns what',
|
|
70
|
+
meta : `${OWNERS.length} families`,
|
|
71
|
+
children: html`
|
|
72
|
+
<div class="table-scroll">
|
|
73
|
+
<table>
|
|
74
|
+
<caption>Each family answers one question. Reaching into another family's
|
|
75
|
+
vocabulary is how a status colour ends up meaning "blue".</caption>
|
|
76
|
+
<thead><tr><th scope="col">Responsibility</th><th scope="col">Family</th><th scope="col">Roles</th></tr></thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
${OWNERS.map(([r, f, roles]) => html`
|
|
79
|
+
<tr><td><b>${r}</b></td><td><code>${f}</code></td><td>${roles}</td></tr>`)}
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>`
|
|
83
|
+
})}
|
|
84
|
+
|
|
85
|
+
${refSection({
|
|
86
|
+
name : 'General vs status',
|
|
87
|
+
meta : 'two systems, one seam',
|
|
88
|
+
children: html`
|
|
89
|
+
<p style="max-width:70ch">
|
|
90
|
+
<b>The general <code>--color-*</code> family owns neutral hierarchy, surfaces and
|
|
91
|
+
actions. The <code>--status-*</code> family owns feedback state.</b> That split was
|
|
92
|
+
not designed; it is what adoption settled on, and measuring it is how it became a
|
|
93
|
+
rule. The feedback members of the general family —
|
|
94
|
+
<code>--color-surface-error</code>, <code>-success</code>, <code>-warning</code> —
|
|
95
|
+
have no component consumers at all, while their status counterparts have two or
|
|
96
|
+
three each.
|
|
97
|
+
</p>
|
|
98
|
+
<p style="max-width:70ch">
|
|
99
|
+
<b>They are not deleted, and value equality is not the reason.</b>
|
|
100
|
+
<code>--color-surface-info</code> and <code>--status-info-surface</code> both resolve
|
|
101
|
+
to the same tint, and consolidating them would be wrong: <code>bd-highlight</code>
|
|
102
|
+
uses the first one for decorative emphasis, which is not feedback. Two roles that
|
|
103
|
+
happen to share a value are still two roles.
|
|
104
|
+
</p>
|
|
105
|
+
<p style="max-width:70ch">
|
|
106
|
+
<code>--color-text-success</code> and <code>--color-text-warning</code> look unused
|
|
107
|
+
in CSS and are not: <code>shared/token-scales.js</code> exposes them as
|
|
108
|
+
<code>color="success"</code> and <code>color="warning"</code> on every
|
|
109
|
+
<code>bd-h</code> and <code>bd-p</code>. Everything in <code>tokens.css</code> also
|
|
110
|
+
ships to consuming pages, so an unused-looking token may simply have consumers this
|
|
111
|
+
repository cannot see.
|
|
112
|
+
</p>`
|
|
113
|
+
})}
|
|
114
|
+
|
|
115
|
+
${refSection({
|
|
116
|
+
name : 'Action vs brand',
|
|
117
|
+
meta : 'a ramp, not a colour',
|
|
118
|
+
children: html`
|
|
119
|
+
<p style="max-width:70ch">
|
|
120
|
+
<code>--color-action-primary</code> → <code>-hover</code> → <code>-active</code> is a
|
|
121
|
+
state ramp. Brand blue is an identity. They overlap in value at the first step and
|
|
122
|
+
diverge immediately after, which is why "use the brand colour" is not an answer to
|
|
123
|
+
"what should hover look like".
|
|
124
|
+
</p>
|
|
125
|
+
<p style="max-width:70ch">
|
|
126
|
+
<b>Let the state pick the token.</b> There is no hover-blue role and there should not
|
|
127
|
+
be one — the question is what the element is doing, not what colour it is going.
|
|
128
|
+
<code>bd-link</code>'s resting colour is <code>--color-text-brand</code> rather than
|
|
129
|
+
<code>--color-action-primary</code> deliberately: blue-500 measures
|
|
130
|
+
${ratio(resolve('--color-action-primary'), resolve('--color-surface-default')) ?? '—'}:1
|
|
131
|
+
on white and fails on any tint, so links start one step down the ramp.
|
|
132
|
+
</p>`
|
|
133
|
+
})}
|
|
134
|
+
|
|
135
|
+
${refSection({
|
|
136
|
+
name : 'Subtle, muted, disabled',
|
|
137
|
+
meta : 'two are hierarchy, one is a state',
|
|
138
|
+
children: html`
|
|
139
|
+
<div class="table-scroll">
|
|
140
|
+
<table>
|
|
141
|
+
<caption>Measured on --color-surface-default as this page renders</caption>
|
|
142
|
+
<thead><tr><th scope="col">Role</th><th scope="col">Kind</th><th scope="col">On white</th><th scope="col">Use it for</th></tr></thead>
|
|
143
|
+
<tbody>
|
|
144
|
+
${[
|
|
145
|
+
['--color-text-secondary', 'hierarchy', 'supporting copy under a title'],
|
|
146
|
+
['--color-text-muted', 'hierarchy', 'metadata, captions, timestamps'],
|
|
147
|
+
['--color-text-subtle', 'hierarchy', 'the quietest readable step'],
|
|
148
|
+
['--color-text-disabled', 'STATE', 'a control the user cannot operate']
|
|
149
|
+
].map(([t, kind, use]) => html`
|
|
150
|
+
<tr>
|
|
151
|
+
<td><code>${t.replace('--color-text-', '')}</code></td>
|
|
152
|
+
<td>${kind === 'STATE' ? html`<b>${kind}</b>` : kind}</td>
|
|
153
|
+
<td class="num">${ratio(resolve(t), resolve('--color-surface-default')) ?? '—'}</td>
|
|
154
|
+
<td>${use}</td>
|
|
155
|
+
</tr>`)}
|
|
156
|
+
</tbody>
|
|
157
|
+
</table>
|
|
158
|
+
</div>
|
|
159
|
+
<p style="max-width:70ch">
|
|
160
|
+
<b>Disabled is a state, not a step on the hierarchy.</b> Reaching for a hierarchy role
|
|
161
|
+
to paint a disabled control makes disabled content indistinguishable from ordinary
|
|
162
|
+
quiet content, and it is what <code>bd-checkbox</code>, <code>bd-radio</code> and
|
|
163
|
+
<code>bd-switch</code> currently do. They are left alone on purpose: the value they
|
|
164
|
+
chose is the <i>more</i> legible one, and correcting the name at the cost of contrast
|
|
165
|
+
would be the wrong trade. That is a design decision about
|
|
166
|
+
<code>--color-text-disabled</code>'s value, recorded rather than made here.
|
|
167
|
+
</p>`
|
|
168
|
+
})}
|
|
169
|
+
|
|
170
|
+
${refSection({
|
|
171
|
+
name : 'Inverse',
|
|
172
|
+
meta : 'two steps, and why not three',
|
|
173
|
+
children: html`
|
|
174
|
+
<p style="max-width:70ch">
|
|
175
|
+
On an inverse surface only two text roles exist:
|
|
176
|
+
<code>--color-text-inverse</code> for high emphasis and
|
|
177
|
+
<code>--color-text-muted-inverse</code> for everything subordinate. The light-side
|
|
178
|
+
hierarchy cannot be reused — <code>-secondary</code>, <code>-muted</code> and
|
|
179
|
+
<code>-subtle</code> measure
|
|
180
|
+
${ratio(resolve('--color-text-secondary'), resolve('--color-surface-inverse')) ?? '—'},
|
|
181
|
+
${ratio(resolve('--color-text-muted'), resolve('--color-surface-inverse')) ?? '—'} and
|
|
182
|
+
${ratio(resolve('--color-text-subtle'), resolve('--color-surface-inverse')) ?? '—'}
|
|
183
|
+
against <code>--color-surface-inverse</code>.
|
|
184
|
+
</p>
|
|
185
|
+
<div style="background:var(--color-surface-inverse);padding:var(--spacing-24);border-radius:var(--radius-lg);max-width:52ch">
|
|
186
|
+
<p style="margin:0 0 var(--spacing-8);color:var(--color-text-inverse)">
|
|
187
|
+
High emphasis — <code>--color-text-inverse</code>,
|
|
188
|
+
${ratio(resolve('--color-text-inverse'), resolve('--color-surface-inverse')) ?? '—'}:1
|
|
189
|
+
</p>
|
|
190
|
+
<p style="margin:0;color:var(--color-text-muted-inverse);font-size:var(--typography-fontSize-sm)">
|
|
191
|
+
Subordinate — <code>--color-text-muted-inverse</code>,
|
|
192
|
+
${ratio(resolve('--color-text-muted-inverse'), resolve('--color-surface-inverse')) ?? '—'}:1
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
<p style="max-width:70ch;margin-top:var(--spacing-16)">
|
|
196
|
+
There is no third step, and the reason is worth keeping: one component wanted one, and
|
|
197
|
+
measurement showed its value was a contrast failure rather than a tier. A
|
|
198
|
+
responsibility needs independent consumers <i>and</i> a value that survives the
|
|
199
|
+
grounds it will be used on. Do not build inverse counterparts for symmetry — build
|
|
200
|
+
them when two components have already built them separately.
|
|
201
|
+
</p>`
|
|
202
|
+
})}
|
|
203
|
+
|
|
204
|
+
${refSection({
|
|
205
|
+
name : 'When a primitive is the right answer',
|
|
206
|
+
meta : 'not every colour has a responsibility',
|
|
207
|
+
children: html`
|
|
208
|
+
<p style="max-width:70ch">
|
|
209
|
+
Zero primitive references is not the goal. A primitive is right when the value
|
|
210
|
+
genuinely has no semantic job:
|
|
211
|
+
</p>
|
|
212
|
+
<ul style="max-width:70ch">
|
|
213
|
+
<li><b>Artwork.</b> The terminal's window dots are red, amber and green because that
|
|
214
|
+
is what a terminal looks like. They are not status.</li>
|
|
215
|
+
<li><b>Illustration and brand marks.</b> A logo's colours belong to the logo.</li>
|
|
216
|
+
<li><b>Product-specific comparison colours.</b> The compare bars name competitors, not
|
|
217
|
+
states.</li>
|
|
218
|
+
<li><b>A deliberate local visual API</b> a consuming page is meant to set.</li>
|
|
219
|
+
</ul>
|
|
220
|
+
<p style="max-width:70ch">
|
|
221
|
+
The test is not "is this a token" but <b>"does this colour communicate something the
|
|
222
|
+
system has a word for"</b>. If it does, use the word. If it does not, say so in a
|
|
223
|
+
comment so the next person does not have to work it out.
|
|
224
|
+
</p>`
|
|
225
|
+
})}
|
|
226
|
+
`
|
|
227
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dark value for every semantic colour role, derived from the light theme's
|
|
3
|
+
* own measurements. This is the method behind the 🔬 Dark mode study page in
|
|
4
|
+
* the Figma foundation file, and the data behind Foundations/Theming/Dark mode.
|
|
5
|
+
*
|
|
6
|
+
* Code has no dark theme. These are proposals, computed from tokens.css each
|
|
7
|
+
* time they are read, so they follow the palette instead of describing it.
|
|
8
|
+
*
|
|
9
|
+
* The method is the point: a dark value is chosen to KEEP what the light value
|
|
10
|
+
* measures, not to look like a dark theme.
|
|
11
|
+
*
|
|
12
|
+
* text keeps its contrast against the ground it is read on: the step
|
|
13
|
+
* whose ratio is nearest the light ratio, never below 4.5 where
|
|
14
|
+
* light clears 4.5, with the hierarchy order kept.
|
|
15
|
+
* grounds keep their tonal step from the page, matched by a step LIGHTER
|
|
16
|
+
* than the dark page (on dark, a container is one step lighter).
|
|
17
|
+
* edges keep their ratio class against the page: under 3:1 stays
|
|
18
|
+
* decorative, 3:1 and over stays a boundary.
|
|
19
|
+
* status tints sit at the dark end of the tone's ramp; ink is the
|
|
20
|
+
* nearest-to-light step that clears 4.5 on the tint; accent clears
|
|
21
|
+
* 3:1 on the page AND on its own tint; a fill keeps its light pair
|
|
22
|
+
* unless it would vanish into the dark page, then the chip inverts.
|
|
23
|
+
* action fills are self-contained, so they keep their light value, and
|
|
24
|
+
* their ink (--color-action-on-fill) stays white. The label
|
|
25
|
+
* clears 4.5:1 in every state; a RESTING fill also clears 3:1 on
|
|
26
|
+
* the page, kept by choice. Hover and press are exempt from the
|
|
27
|
+
* page bar: WCAG 1.4.11 treats a hover treatment as supplemental,
|
|
28
|
+
* and a button with a text label needs no boundary at all. On the
|
|
29
|
+
* dark page no ramp could keep it anyway -- label-on-fill times
|
|
30
|
+
* fill-on-page is 19.45 for every fill, so both bars leave a band
|
|
31
|
+
* only 1.44:1 wide (decision 4, decided 2026-10-02).
|
|
32
|
+
*
|
|
33
|
+
* A role is measured on every ground code draws it on, not only the page --
|
|
34
|
+
* the first version measured accent and brand text on the page alone, and the
|
|
35
|
+
* Alert atom is what showed it. Only existing primitives are used: where no
|
|
36
|
+
* step meets a rule, the nearest is returned and the row fails, because adding
|
|
37
|
+
* a primitive is its own decision.
|
|
38
|
+
*
|
|
39
|
+
* Not a story file: Storybook only globs *.stories.js. Pure, so it runs under
|
|
40
|
+
* Node too -- the parity check against the original script did.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const RAMP = /^--color-(blue|red|green|yellow|neutral)-(\d+)$/;
|
|
44
|
+
|
|
45
|
+
export const blank = text => text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
46
|
+
|
|
47
|
+
/** name -> declared value, first declaration wins. */
|
|
48
|
+
export function declarations(cssText) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
for (const m of blank(cssText).matchAll(/(?:^|[\s;{])(--[A-Za-z0-9-]+)\s*:\s*([^;]*);/g)) {
|
|
51
|
+
if (!out.has(m[1])) out.set(m[1], m[2]);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function luminance(hex) {
|
|
57
|
+
const c = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255)
|
|
58
|
+
.map(x => (x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4));
|
|
59
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function contrast(a, b) {
|
|
63
|
+
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
64
|
+
return (x + 0.05) / (y + 0.05);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const f2 = n => n.toFixed(2);
|
|
68
|
+
const f3 = n => n.toFixed(3);
|
|
69
|
+
|
|
70
|
+
export function derive(cssText) {
|
|
71
|
+
const decl = declarations(cssText);
|
|
72
|
+
const RAMPS = {};
|
|
73
|
+
for (const [k, v] of decl) {
|
|
74
|
+
const m = k.match(RAMP);
|
|
75
|
+
if (m) (RAMPS[m[1]] ??= {})[Number(m[2])] = v.trim().toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
const prim = ref => {
|
|
78
|
+
const m = ref.trim().match(/^var\(\s*--color-(blue|red|green|yellow|neutral)-(\d+)\s*\)$/);
|
|
79
|
+
return m ? [m[1], Number(m[2])] : null;
|
|
80
|
+
};
|
|
81
|
+
const hexof = p => RAMPS[p[0]][p[1]];
|
|
82
|
+
const steps = ramp => Object.keys(RAMPS[ramp]).map(Number)
|
|
83
|
+
.sort((a, b) => a - b);
|
|
84
|
+
const lightOf = t => (decl.has(t) ? prim(decl.get(t)) : null);
|
|
85
|
+
const same = (a, b) => a && b && a[0] === b[0] && a[1] === b[1];
|
|
86
|
+
const lum = luminance, ratio = contrast;
|
|
87
|
+
|
|
88
|
+
function nearest(ramp, ground, target, { floor = null, lighterThan = null, exclude = [], also = [] } = {}) {
|
|
89
|
+
let best = null;
|
|
90
|
+
for (const s of steps(ramp)) {
|
|
91
|
+
if (exclude.some(e => same(e, [ramp, s]))) continue;
|
|
92
|
+
const h = RAMPS[ramp][s];
|
|
93
|
+
if (lighterThan && lum(h) <= lum(lighterThan)) continue;
|
|
94
|
+
const r = ratio(h, ground);
|
|
95
|
+
if (floor && (r < floor || also.some(g => ratio(h, g) < floor))) continue;
|
|
96
|
+
const d = Math.abs(r - target);
|
|
97
|
+
if (best === null || d < best[0]) best = [d, [ramp, s]];
|
|
98
|
+
}
|
|
99
|
+
return best && best[1];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rows = [];
|
|
103
|
+
const row = (token, dark, against, againstLight, againstDark, rule, need = null) => {
|
|
104
|
+
const lp = lightOf(token);
|
|
105
|
+
const lightRatio = lp ? ratio(hexof(lp), againstLight) : null;
|
|
106
|
+
const darkRatio = dark ? ratio(hexof(dark), againstDark) : null;
|
|
107
|
+
const ok = need === null ? null : darkRatio !== null && darkRatio >= need;
|
|
108
|
+
rows.push({ token, light: lp, lightRatio, dark, darkRatio, against, rule, need, ok });
|
|
109
|
+
return dark;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const WHITE = RAMPS.neutral[0];
|
|
113
|
+
const L = {};
|
|
114
|
+
for (const t of decl.keys()) { const p = lightOf(t); if (p) L[t] = hexof(p); }
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------- grounds --
|
|
117
|
+
const PAGE = ['neutral', 950]; // near-black, so a lighter container step exists
|
|
118
|
+
const page = hexof(PAGE);
|
|
119
|
+
row('--color-surface-default', PAGE, 'itself', L['--color-surface-default'], page,
|
|
120
|
+
'the dark page: near-black rather than pure black, so a lighter container step exists above it');
|
|
121
|
+
// in light order, each further from the page than the last: a ground that
|
|
122
|
+
// lands on its neighbour's step has lost the step it exists for. Two grounds
|
|
123
|
+
// that share a value in light (muted and disabled) share it in dark.
|
|
124
|
+
const groundOf = {};
|
|
125
|
+
let prevGround = PAGE;
|
|
126
|
+
for (const t of ['--color-surface-container', '--color-surface-subtle', '--color-surface-muted', '--color-surface-disabled', '--color-surface-pressed']) {
|
|
127
|
+
if (!L[t]) continue;
|
|
128
|
+
const twin = Object.keys(groundOf).find(k => L[k] === L[t]);
|
|
129
|
+
const target = ratio(L[t], L['--color-surface-default']);
|
|
130
|
+
const d = twin ? groundOf[twin] : nearest('neutral', page, target, { lighterThan: hexof(prevGround) });
|
|
131
|
+
groundOf[t] = d;
|
|
132
|
+
prevGround = d;
|
|
133
|
+
row(t, d, 'surface-default', L['--color-surface-default'], page,
|
|
134
|
+
twin ? `shares ${twin.slice(16)}'s value in light, so shares it in dark` : `keeps its tonal step from the page (light ${f3(target)}), further out than the ground before it`);
|
|
135
|
+
}
|
|
136
|
+
row('--color-surface-inverse', ['neutral', 50], 'surface-default', L['--color-surface-default'], page,
|
|
137
|
+
'flips: on a dark theme the inverse ground is the light one (Foundations/Surfaces)');
|
|
138
|
+
row('--color-surface-brand', lightOf('--color-surface-brand'), 'surface-default', L['--color-surface-default'], page,
|
|
139
|
+
'follows the brand, not the neutral ramp: unchanged');
|
|
140
|
+
|
|
141
|
+
// ------------------------------------------------------------------- text --
|
|
142
|
+
let prev = null;
|
|
143
|
+
for (const t of ['--color-text-primary', '--color-text-secondary', '--color-text-muted', '--color-text-subtle']) {
|
|
144
|
+
const target = ratio(L[t], WHITE);
|
|
145
|
+
const floor = target >= 4.5 ? 4.5 : null;
|
|
146
|
+
let d = nearest('neutral', page, target, { floor, lighterThan: page });
|
|
147
|
+
if (prev && lum(hexof(d)) >= lum(hexof(prev))) { // keep the hierarchy strictly ordered
|
|
148
|
+
const cands = steps('neutral').filter(s => lum(RAMPS.neutral[s]) < lum(hexof(prev)) && (!floor || ratio(RAMPS.neutral[s], page) >= floor));
|
|
149
|
+
if (cands.length) d = ['neutral', Math.min(...cands)];
|
|
150
|
+
else { // nothing clears the floor: the nearest, and the row fails
|
|
151
|
+
const darker = steps('neutral').filter(s => lum(RAMPS.neutral[s]) < lum(hexof(prev)) && lum(RAMPS.neutral[s]) > lum(page));
|
|
152
|
+
d = ['neutral', darker.reduce((a, s) => (Math.abs(ratio(RAMPS.neutral[s], page) - target) < Math.abs(ratio(RAMPS.neutral[a], page) - target) ? s : a))];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
prev = d;
|
|
156
|
+
row(t, d, 'surface-default', WHITE, page, `keeps its contrast on the page (light ${f2(target)}:1)`, floor);
|
|
157
|
+
}
|
|
158
|
+
{
|
|
159
|
+
const t = '--color-text-disabled';
|
|
160
|
+
const target = ratio(L[t], WHITE);
|
|
161
|
+
row(t, nearest('neutral', page, target, { lighterThan: page }), 'surface-default', WHITE, page,
|
|
162
|
+
`STATE, exempt from the minimum: keeps its light ratio (${f2(target)}:1)`);
|
|
163
|
+
}
|
|
164
|
+
const invDark = hexof(['neutral', 50]);
|
|
165
|
+
for (const t of ['--color-text-inverse', '--color-text-muted-inverse']) {
|
|
166
|
+
const target = ratio(L[t], L['--color-surface-inverse']);
|
|
167
|
+
row(t, nearest('neutral', invDark, target, { floor: 4.5 }), 'surface-inverse', L['--color-surface-inverse'], invDark,
|
|
168
|
+
`keeps its contrast on the inverse ground (light ${f2(target)}:1)`, 4.5);
|
|
169
|
+
}
|
|
170
|
+
// feedback pairs: tint first, then the ink measured on it
|
|
171
|
+
for (const [tone, ramp] of [['success', 'green'], ['warning', 'yellow'], ['error', 'red'], ['info', 'blue']]) {
|
|
172
|
+
const st = `--color-surface-${tone}`;
|
|
173
|
+
const step = ratio(L[st], L['--color-surface-default']);
|
|
174
|
+
const tint = nearest(ramp, page, step, { lighterThan: page });
|
|
175
|
+
row(st, tint, 'surface-default', L['--color-surface-default'], page, `a tint one step off the page, like light (${f3(step)})`);
|
|
176
|
+
const tt = `--color-text-${tone}`;
|
|
177
|
+
if (L[tt]) {
|
|
178
|
+
const target = ratio(L[tt], L[st]);
|
|
179
|
+
row(tt, nearest(ramp, hexof(tint), target, { floor: 4.5 }), `surface-${tone}`, L[st], hexof(tint),
|
|
180
|
+
`keeps its contrast on its paired tint (light ${f2(target)}:1)`, 4.5);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ------------------------------------------------------------------ edges --
|
|
185
|
+
for (const t of ['--color-border-subtle', '--color-border-default', '--color-border-strong', '--color-border-disabled', '--color-border-container']) {
|
|
186
|
+
const target = ratio(L[t], WHITE);
|
|
187
|
+
const floor = target >= 3 ? 3 : null;
|
|
188
|
+
row(t, nearest('neutral', page, target, { floor, lighterThan: page }), 'surface-default', WHITE, page,
|
|
189
|
+
`keeps its ratio class on the page (light ${f2(target)}:1)`, floor);
|
|
190
|
+
}
|
|
191
|
+
{
|
|
192
|
+
const t = '--color-border-brand';
|
|
193
|
+
const target = ratio(L[t], WHITE);
|
|
194
|
+
row(t, nearest('blue', page, target, { floor: 3 }), 'surface-default', WHITE, page, `keeps its boundary contrast (light ${f2(target)}:1)`, 3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ----------------------------------------------------------------- action --
|
|
198
|
+
const FILLS = ['--color-action-primary', '--color-action-primary-hover', '--color-action-primary-active',
|
|
199
|
+
'--color-action-accent', '--color-action-accent-hover', '--color-action-accent-active'];
|
|
200
|
+
const isState = t => /-(hover|active)$/.test(t);
|
|
201
|
+
const fillRow = (t, lp, dk, label, ground, lead) => {
|
|
202
|
+
const state = isState(t);
|
|
203
|
+
rows.push({ token : t, light : lp, lightRatio : ratio(WHITE, hexof(lp)), dark : dk, darkRatio : label, against : 'white label',
|
|
204
|
+
rule : `${lead}: the white label keeps ${f2(label)}:1; against the dark page it measures ${f2(ground)}:1${state ? ', a state, so supplemental' : ', a resting fill, kept at 3:1'}`,
|
|
205
|
+
need : 4.5, ok : label >= 4.5 && (state || ground >= 3), groundRatio: ground, state });
|
|
206
|
+
};
|
|
207
|
+
for (const t of FILLS) {
|
|
208
|
+
const lp = lightOf(t);
|
|
209
|
+
fillRow(t, lp, lp, ratio(WHITE, hexof(lp)), ratio(hexof(lp), page), 'self-contained fill');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// the neutral fill cannot keep its light value: neutral-950 IS the dark page.
|
|
213
|
+
// Rest takes the step that clears 3:1 on the page and keeps the white label
|
|
214
|
+
// at 4.5; hover and press step away from the label's side of the ramp, the
|
|
215
|
+
// direction light's primary and accent ramps already go (decision 4).
|
|
216
|
+
if (decl.has('--color-action-secondary')) {
|
|
217
|
+
const usable = steps('neutral').filter(s => ratio(WHITE, RAMPS.neutral[s]) >= 4.5);
|
|
218
|
+
const rest = usable.filter(s => ratio(RAMPS.neutral[s], page) >= 3).reduce((a, s) => (ratio(RAMPS.neutral[s], page) > ratio(RAMPS.neutral[a], page) ? s : a), usable[0]);
|
|
219
|
+
const after = usable.filter(s => s > rest);
|
|
220
|
+
const pick = { '--color-action-secondary': rest, '--color-action-secondary-hover': after[0], '--color-action-secondary-active': after[1] };
|
|
221
|
+
for (const [t, step] of Object.entries(pick)) {
|
|
222
|
+
if (!decl.has(t) || step == null) continue;
|
|
223
|
+
const hex = RAMPS.neutral[step];
|
|
224
|
+
fillRow(t, lightOf(t), ['neutral', step], ratio(WHITE, hex), ratio(hex, page), 'the neutral fill moves off the dark page');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ----------------------------------------------------------------- status --
|
|
229
|
+
const TINTS = {};
|
|
230
|
+
const TONES = { neutral: 'neutral', brand: 'blue', info: 'blue', success: 'green', warning: 'yellow', error: 'red' };
|
|
231
|
+
for (const [tone, ramp] of Object.entries(TONES)) {
|
|
232
|
+
const p = `--status-${tone}-`;
|
|
233
|
+
const hasStrong = decl.has(`${p}surface-strong`);
|
|
234
|
+
const sTarget = ratio(L[`${p}surface`], WHITE);
|
|
235
|
+
const surf = nearest(ramp, page, sTarget, { lighterThan: page });
|
|
236
|
+
row(`${p}surface`, surf, 'surface-default', WHITE, page, `the light tint: one step off the page, like light (${f3(sTarget)})`);
|
|
237
|
+
let strong = null;
|
|
238
|
+
if (hasStrong) {
|
|
239
|
+
const ssTarget = ratio(L[`${p}surface-strong`], WHITE);
|
|
240
|
+
strong = nearest(ramp, page, ssTarget, { lighterThan: hexof(surf), exclude: [surf] });
|
|
241
|
+
row(`${p}surface-strong`, strong, 'surface-default', WHITE, page,
|
|
242
|
+
`the heavier tint: further from the page than -surface, like light (${f3(ssTarget)})`);
|
|
243
|
+
}
|
|
244
|
+
const inkGround = hexof(strong || surf);
|
|
245
|
+
const inkOn = hasStrong ? 'surface-strong' : 'surface';
|
|
246
|
+
const tTarget = ratio(L[`${p}text`], L[p + inkOn]);
|
|
247
|
+
row(`${p}text`, nearest(ramp, inkGround, tTarget, { floor: 4.5 }), inkOn, L[p + inkOn], inkGround,
|
|
248
|
+
`ink on the heavier tint a badge uses, keeping light (${f2(tTarget)}:1)`, 4.5);
|
|
249
|
+
const bTarget = ratio(L[`${p}border`], L[`${p}surface`]);
|
|
250
|
+
row(`${p}border`, nearest(ramp, hexof(surf), bTarget, { lighterThan: hexof(surf) }), 'surface', L[`${p}surface`], hexof(surf),
|
|
251
|
+
`the tag hairline on its own tint, keeping light (${f2(bTarget)}:1)`);
|
|
252
|
+
TINTS[tone] = hexof(surf);
|
|
253
|
+
const aTarget = ratio(L[`${p}accent`], WHITE);
|
|
254
|
+
const accent = nearest(ramp, page, aTarget, { floor: 3, also: [hexof(surf)] });
|
|
255
|
+
row(`${p}accent`, accent, 'surface-default', WHITE, page,
|
|
256
|
+
`the badge dot on the page AND the alert icon on its own tint: clears 3:1 on both (${f2(ratio(hexof(accent), hexof(surf)))}:1 on the tint; light ${f2(aTarget)}:1 on the page)`, 3);
|
|
257
|
+
let fill = lightOf(`${p}fill`), onfill = lightOf(`${p}on-fill`), why = 'self-contained chip: unchanged';
|
|
258
|
+
if (ratio(hexof(fill), page) < 3) { // would vanish into the page: invert the chip
|
|
259
|
+
fill = nearest(ramp, page, ratio(L[`${p}fill`], WHITE), { floor: 3 });
|
|
260
|
+
onfill = ['neutral', steps('neutral').reduce((a, s) => (ratio(RAMPS.neutral[s], hexof(fill)) > ratio(RAMPS.neutral[a], hexof(fill)) ? s : a))];
|
|
261
|
+
why = 'the light fill vanishes into the dark page, so the chip inverts';
|
|
262
|
+
}
|
|
263
|
+
row(`${p}fill`, fill, 'surface-default', WHITE, page, why, 3);
|
|
264
|
+
row(`${p}on-fill`, onfill, 'fill', L[`${p}fill`], hexof(fill), 'ink on the fill', 4.5);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ------------------------------------------------- brand text, every ground --
|
|
268
|
+
const tints = Object.values(TINTS);
|
|
269
|
+
const brandTarget = ratio(L['--color-text-brand'], WHITE);
|
|
270
|
+
const brand = nearest('blue', page, brandTarget, { floor: 4.5, also: tints });
|
|
271
|
+
const brandWorst = Math.min(...tints.map(g => ratio(hexof(brand), g)));
|
|
272
|
+
row('--color-text-brand', brand, 'surface-default', WHITE, page,
|
|
273
|
+
`keeps its contrast on the page (light ${f2(brandTarget)}:1) AND clears 4.5:1 on every status tint, where an alert sets its action link (${f2(brandWorst)}:1 on the weakest)`, 4.5);
|
|
274
|
+
|
|
275
|
+
// ------------------------------------------- roles added by 591c695a --
|
|
276
|
+
if (decl.has('--color-text-brand-hover')) {
|
|
277
|
+
const sep = ratio(L['--color-text-brand-hover'], L['--color-text-brand']);
|
|
278
|
+
const cands = steps('blue').filter(s => lum(RAMPS.blue[s]) > lum(hexof(brand)) && [page, ...tints].every(g => ratio(RAMPS.blue[s], g) >= 4.5));
|
|
279
|
+
const hover = ['blue', cands.reduce((a, s) => (Math.abs(ratio(RAMPS.blue[s], hexof(brand)) - sep) < Math.abs(ratio(RAMPS.blue[a], hexof(brand)) - sep) ? s : a))];
|
|
280
|
+
row('--color-text-brand-hover', hover, 'surface-default', WHITE, page,
|
|
281
|
+
`one step past the rest colour, away from the page as in light (${f2(ratio(hexof(hover), hexof(brand)))}:1 from it; light ${f2(sep)})`, 4.5);
|
|
282
|
+
}
|
|
283
|
+
if (decl.has('--color-action-on-fill')) {
|
|
284
|
+
const worst = Math.min(...FILLS.map(t => ratio(WHITE, hexof(lightOf(t)))));
|
|
285
|
+
rows.push({ token : '--color-action-on-fill', light : lightOf('--color-action-on-fill'), lightRatio: worst, dark : lightOf('--color-action-on-fill'), darkRatio : worst,
|
|
286
|
+
against : 'action fills', rule : `white in both themes: the fills do not change, so neither does their ink (weakest ${f2(worst)}:1)`, need : 4.5, ok : worst >= 4.5 });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ------------------------------------------ the media frame, decision 15 --
|
|
290
|
+
// fixed in every theme: a letterbox and a terminal are dark whatever the page
|
|
291
|
+
// is, and their ink is fixed with them. Measured on the frame, not the page.
|
|
292
|
+
if (decl.has('--color-surface-media')) {
|
|
293
|
+
const frame = lightOf('--color-surface-media');
|
|
294
|
+
row('--color-surface-media', frame, 'surface-default', L['--color-surface-default'], page,
|
|
295
|
+
'fixed: the frame around media is dark in every theme, so it keeps its light value');
|
|
296
|
+
for (const t of ['--color-text-on-media', '--color-text-on-media-muted']) {
|
|
297
|
+
if (!decl.has(t)) continue;
|
|
298
|
+
row(t, lightOf(t), 'surface-media', hexof(frame), hexof(frame),
|
|
299
|
+
`fixed with its ground: ${f2(ratio(L[t], hexof(frame)))}:1 on the frame in either theme`, 4.5);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const name = p => (p ? `${p[0]}/${p[1]}` : null);
|
|
304
|
+
return {
|
|
305
|
+
page : hexof(PAGE),
|
|
306
|
+
ramps: RAMPS,
|
|
307
|
+
rows : rows.map(r => ({ ...r, light: name(r.light), dark: name(r.dark), lightHex: r.light ? hexof(r.light) : null, darkHex: r.dark ? hexof(r.dark) : null }))
|
|
308
|
+
};
|
|
309
|
+
}
|