diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 4fd525f5712..87fb70230eb 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -1,120 +1,143 @@ @import url('./benchmark-charts.css'); -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,300..800;1,300..800&family=Martian+Mono:wdth,wght@75..112.5,300..700&display=swap'); -/* Shared with the homepage: forest ink, mint surfaces, and plum accents. */ +/* + * TUnit's identity is the red/green bar every unit-testing tool has shown since JUnit. + * Greens are muted emerald and sage so long reading stays calm; red appears only for failures. + */ :root { - --tunit-ink: #214b3c; - --tunit-body: #365747; - --tunit-muted: #50695d; - --tunit-paper: #f8fbf9; - --tunit-surface: #d5f4e5; - --tunit-border: #b5d0c1; - --tunit-accent: #654b87; - --tunit-accent-surface: #e4def3; - --tunit-code: #edf6f0; - --tunit-on-primary: #f8fbf9; - --tunit-warning: #624915; - --tunit-warning-surface: #f6e9c6; - --tunit-danger: #7f3431; - --tunit-danger-surface: #f5dfdc; - --tunit-chart-primary: #365e4b; - --tunit-chart-secondary: #654b87; - --tunit-chart-comparison: #50695d; - --tunit-chart-label: #f8fbf9; + --tunit-ink: #142019; + --tunit-body: #36423b; + --tunit-muted: #5f6b64; + --tunit-paper: #f6f7f4; + --tunit-surface: #edf0eb; + --tunit-border: #dce1da; + --tunit-accent: #1d7348; + --tunit-accent-surface: #e1efe5; + --tunit-code: #eef1ec; + --tunit-on-primary: #f6f7f4; + --tunit-pass: #1d7348; + --tunit-pass-fill: #2e9a63; + --tunit-pass-light: #a6d5b8; + --tunit-pass-mid: #62b98a; + --tunit-fail: #c0442f; + --tunit-cell: #e2e6df; + --tunit-warning: #6b4a00; + --tunit-warning-surface: #f7efd6; + --tunit-danger: #9a3322; + --tunit-danger-surface: #f8e6e1; + /* Bar labels sit on these colours, so each keeps at least 4.5:1 contrast with --tunit-chart-label. */ + --tunit-chart-primary: #1d7348; + --tunit-chart-secondary: #142019; + --tunit-chart-comparison: #5f6b64; + --tunit-chart-label: #f6f7f4; - --ifm-color-primary: #365e4b; - --ifm-color-primary-dark: #305443; - --ifm-color-primary-darker: #2b4b3c; - --ifm-color-primary-darkest: #214b3c; - --ifm-color-primary-light: #3b6652; - --ifm-color-primary-lighter: #406e58; - --ifm-color-primary-lightest: #45775f; - --ifm-color-emphasis-100: #edf6f0; - --ifm-color-emphasis-200: #dce8df; - --ifm-color-emphasis-300: #b5d0c1; - --ifm-color-emphasis-400: #91b09f; - --ifm-color-emphasis-500: #708e7d; - --ifm-color-emphasis-600: #50695d; - --ifm-color-emphasis-700: #365747; - --ifm-color-emphasis-800: #294f3f; - --ifm-color-emphasis-900: #214b3c; - --syntax-keyword: #654b87; - --syntax-string: #286044; - --syntax-function: #704472; - --syntax-class: #296462; - --syntax-number: #795320; - --syntax-comment: #607568; + --ifm-color-primary: #1d7348; + --ifm-color-primary-dark: #1a6841; + --ifm-color-primary-darker: #19623d; + --ifm-color-primary-darkest: #145132; + --ifm-color-primary-light: #207e4f; + --ifm-color-primary-lighter: #218453; + --ifm-color-primary-lightest: #26965e; + --ifm-color-emphasis-100: #eef1ec; + --ifm-color-emphasis-200: #e4e8e2; + --ifm-color-emphasis-300: #dce1da; + --ifm-color-emphasis-400: #b7bfb9; + --ifm-color-emphasis-500: #8e9891; + --ifm-color-emphasis-600: #5f6b64; + --ifm-color-emphasis-700: #36423b; + --ifm-color-emphasis-800: #24302a; + --ifm-color-emphasis-900: #142019; + --syntax-keyword: #3a5ea8; + --syntax-string: #9a5528; + --syntax-function: #7b4a8f; + --syntax-class: #2c6e72; + --syntax-number: #1d7348; + --syntax-comment: #5c6760; } html[data-theme='dark'] { - --tunit-ink: #d5f4e5; - --tunit-body: #c1d9cc; - --tunit-muted: #acc5b8; - --tunit-paper: #172f27; - --tunit-surface: #203e32; - --tunit-border: #426351; - --tunit-accent: #d2bafa; - --tunit-accent-surface: #3e3450; - --tunit-code: #203e32; - --tunit-on-primary: #172f27; - --tunit-warning: #eed39a; - --tunit-warning-surface: #423a27; - --tunit-danger: #f4bab5; - --tunit-danger-surface: #452c2d; + --tunit-ink: #e5ece7; + --tunit-body: #c2ccc5; + --tunit-muted: #919d95; + --tunit-paper: #121815; + --tunit-surface: #18201b; + --tunit-border: #263029; + --tunit-accent: #7fcf9f; + --tunit-accent-surface: #1b2c22; + --tunit-code: #161e19; + --tunit-on-primary: #121815; + --tunit-pass: #7fcf9f; + --tunit-pass-fill: #4fb47c; + --tunit-pass-light: #2b5a40; + --tunit-pass-mid: #3b8a5f; + --tunit-fail: #f08a76; + --tunit-cell: #1f2923; + --tunit-warning: #ead39a; + --tunit-warning-surface: #2a2719; + --tunit-danger: #f4b2a5; + --tunit-danger-surface: #34201b; + --tunit-chart-primary: #4fb47c; + --tunit-chart-secondary: #e5ece7; + --tunit-chart-comparison: #8f9c94; + --tunit-chart-label: #121815; - --ifm-color-primary: #b6dbc6; - --ifm-color-primary-dark: #a2d1b7; - --ifm-color-primary-darker: #8ec7a8; - --ifm-color-primary-darkest: #7abd99; - --ifm-color-primary-light: #c6e7d5; - --ifm-color-primary-lighter: #d5f4e5; - --ifm-color-primary-lightest: #e5f9ee; - --ifm-color-emphasis-100: #203e32; - --ifm-color-emphasis-200: #2e4e3f; - --ifm-color-emphasis-300: #426351; - --ifm-color-emphasis-400: #628570; - --ifm-color-emphasis-500: #86a694; - --ifm-color-emphasis-600: #acc5b8; - --ifm-color-emphasis-700: #c1d9cc; - --ifm-color-emphasis-800: #cbe7d8; - --ifm-color-emphasis-900: #d5f4e5; - --syntax-keyword: #d2bafa; - --syntax-string: #b6dbc6; - --syntax-function: #e0bddb; - --syntax-class: #a6d9d2; - --syntax-number: #eed39a; - --syntax-comment: #acc5b8; + --ifm-color-primary: #7fcf9f; + --ifm-color-primary-dark: #69c68e; + --ifm-color-primary-darker: #5ec285; + --ifm-color-primary-darkest: #42ab6c; + --ifm-color-primary-light: #95d8b0; + --ifm-color-primary-lighter: #a0dcb8; + --ifm-color-primary-lightest: #c1e8d0; + --ifm-color-emphasis-100: #18201b; + --ifm-color-emphasis-200: #1e2721; + --ifm-color-emphasis-300: #263029; + --ifm-color-emphasis-400: #3a463e; + --ifm-color-emphasis-500: #5c6a61; + --ifm-color-emphasis-600: #919d95; + --ifm-color-emphasis-700: #c2ccc5; + --ifm-color-emphasis-800: #d6ded8; + --ifm-color-emphasis-900: #e5ece7; + --syntax-keyword: #9db8ec; + --syntax-string: #e2b48c; + --syntax-function: #d3b0e3; + --syntax-class: #8fcfd2; + --syntax-number: #8fd6aa; + --syntax-comment: #7e8b83; } /* Define aliases in both themes so inherited values resolve against that theme. */ :root, html[data-theme='dark'] { - --ifm-font-family-base: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --tunit-mono: 'Martian Mono', 'Cascadia Code', Consolas, monospace; + --ifm-font-family-base: 'Hanken Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --ifm-font-family-monospace: 'Martian Mono', 'Cascadia Code', Consolas, monospace; + --ifm-line-height-base: 1.7; --ifm-heading-font-family: var(--ifm-font-family-base); - --ifm-heading-font-weight: 600; + --ifm-heading-font-weight: 650; --ifm-font-color-base: var(--tunit-body); --ifm-heading-color: var(--tunit-ink); --ifm-background-color: var(--tunit-paper); --ifm-background-surface-color: var(--tunit-surface); - --ifm-navbar-background-color: var(--tunit-surface); + --ifm-navbar-background-color: var(--tunit-paper); --ifm-navbar-link-color: var(--tunit-ink); --ifm-navbar-link-hover-color: var(--tunit-accent); --ifm-link-color: var(--tunit-accent); --ifm-link-hover-color: var(--tunit-ink); --ifm-menu-color: var(--tunit-muted); --ifm-menu-color-active: var(--tunit-ink); - --ifm-menu-color-background-active: var(--tunit-surface); - --ifm-menu-color-background-hover: var(--tunit-code); - --ifm-breadcrumb-color-active: var(--tunit-accent); - --ifm-breadcrumb-item-background-active: var(--tunit-accent-surface); + --ifm-menu-color-background-active: transparent; + --ifm-menu-color-background-hover: var(--tunit-surface); + --ifm-breadcrumb-color-active: var(--tunit-ink); + --ifm-breadcrumb-item-background-active: var(--tunit-surface); --ifm-toc-border-color: var(--tunit-border); --ifm-toc-link-color: var(--tunit-muted); --ifm-color-content-secondary: var(--tunit-muted); --ifm-card-background-color: var(--tunit-code); --ifm-code-background: var(--tunit-code); - --ifm-code-font-size: 95%; + --ifm-code-font-size: 80%; --ifm-table-border-color: var(--tunit-border); --ifm-table-head-background: var(--tunit-surface); - --ifm-table-stripe-background: var(--tunit-code); + --ifm-table-stripe-background: transparent; --ifm-hr-background-color: var(--tunit-border); --docusaurus-highlighted-code-line-bg: var(--tunit-accent-surface); --text-primary: var(--tunit-ink); @@ -129,6 +152,8 @@ html[data-theme='dark'] { } body { margin: 0; } +/* Martian Mono would otherwise join characters like `->`, which misleads in code and CLI flags. */ +code, pre, kbd { font-variant-ligatures: none; font-stretch: 87.5%; } a { text-underline-offset: 3px; } :where(a, button, input, select, summary, [tabindex]):focus-visible { outline: 3px solid var(--tunit-accent); @@ -136,34 +161,53 @@ a { text-underline-offset: 3px; } } .navbar { + position: sticky; border-bottom: 1px solid var(--tunit-border); box-shadow: none; } -.navbar__brand { gap: 0.65rem; } -.navbar__title { color: var(--tunit-ink); font-size: 1.15rem; letter-spacing: -0.04em; } -.navbar__link { font-size: 0.85rem; font-weight: 600; } -.navbar__logo { height: 1.85rem; } +.navbar__brand { gap: 0.6rem; } +.navbar__title { color: var(--tunit-ink); font-size: 1.2rem; font-weight: 750; letter-spacing: -0.03em; } +.navbar__link { font-size: 0.95rem; font-weight: 550; } +.navbar__link--active { color: var(--tunit-accent); } +.navbar__logo { height: 1.7rem; } .navbar__toggle, .navbar-sidebar__close { color: var(--tunit-ink); } .navbar-sidebar { background: var(--tunit-paper); } -.navbar-sidebar__brand { background: var(--tunit-surface); border-bottom: 1px solid var(--tunit-border); } -.navbar-sidebar__back { color: var(--tunit-ink); background: var(--tunit-code); } -.navbar-sidebar .menu__link--active { color: var(--tunit-ink); background: var(--tunit-surface); } +.navbar-sidebar__brand { background: var(--tunit-paper); border-bottom: 1px solid var(--tunit-border); } +.navbar-sidebar__back { color: var(--tunit-ink); background: var(--tunit-surface); } + +/* On doc pages, the navbar's edge is a test bar that turns green as you read. */ +@supports (animation-timeline: scroll()) { + html.docs-doc-page .navbar::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 2px; + background: var(--tunit-pass-fill); + transform-origin: left; + animation: tunit-reading-bar linear both; + animation-timeline: scroll(root block); + } +} +@keyframes tunit-reading-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } } .footer, .footer--dark { - --ifm-footer-background-color: var(--tunit-surface); + --ifm-footer-background-color: var(--tunit-paper); --ifm-footer-color: var(--tunit-muted); --ifm-footer-title-color: var(--tunit-ink); --ifm-footer-link-color: var(--tunit-body); --ifm-footer-link-hover-color: var(--tunit-accent); border-top: 1px solid var(--tunit-border); } +.footer__title { font-size: 0.9rem; } /* The same palette also applies to the Algolia dialog, not just its trigger. */ html[data-theme] { --docsearch-primary-color: var(--tunit-accent); --docsearch-text-color: var(--tunit-ink); --docsearch-muted-color: var(--tunit-muted); - --docsearch-container-background: rgb(23 47 39 / 65%); + --docsearch-container-background: rgb(20 32 25 / 55%); --docsearch-modal-background: var(--tunit-paper); --docsearch-searchbox-background: var(--tunit-code); --docsearch-searchbox-focus-background: var(--tunit-paper); @@ -179,29 +223,79 @@ html[data-theme] { --docsearch-footer-shadow: 0 -1px 0 var(--tunit-border); --docsearch-logo-color: var(--tunit-muted); } -.DocSearch-Button { background: var(--tunit-paper); border: 1px solid var(--tunit-border); color: var(--tunit-muted); border-radius: 6px; } +.DocSearch-Button { background: var(--tunit-surface); border: 1px solid var(--tunit-border); color: var(--tunit-muted); border-radius: 6px; } .DocSearch-Button:hover { background: var(--tunit-code); color: var(--tunit-ink); box-shadow: inset 0 0 0 1px var(--tunit-accent); } -.DocSearch-Button-Key { background: var(--tunit-code); border: 1px solid var(--tunit-border); color: var(--tunit-muted); box-shadow: none; } +.DocSearch-Button-Key { background: var(--tunit-paper); border: 1px solid var(--tunit-border); color: var(--tunit-muted); box-shadow: none; } .DocSearch-Hit[aria-selected='true'] a { background: var(--tunit-accent-surface); } .DocSearch-Hit[aria-selected='true'] :is(.DocSearch-Hit-title, .DocSearch-Hit-path, .DocSearch-Hit-icon, mark) { color: var(--tunit-ink) !important; } .theme-doc-sidebar-container { background: var(--tunit-paper); border-color: var(--tunit-border) !important; } -.menu__link { border-radius: 5px; } -.menu__link--active { font-weight: 600; } -.table-of-contents__link--active, .table-of-contents__link:hover { color: var(--tunit-accent); } -.theme-doc-markdown h1 { letter-spacing: -0.04em; } -.theme-doc-markdown h2 { letter-spacing: -0.03em; } +.theme-doc-sidebar-menu { font-size: 0.95rem; } +.menu { padding-top: 1.25rem; } +.menu__link { border-radius: 3px; } +.menu__link:hover { color: var(--tunit-ink); } +.menu__list-item-collapsible { border-radius: 3px; } +.menu__list-item-collapsible:hover { background: var(--tunit-surface); } +:is(.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible, .theme-doc-sidebar-item-link-level-1) > .menu__link { color: var(--tunit-ink); font-weight: 600; } + +/* Nested sidebar levels read like a test explorer tree. The open page is marked with a passed test cell. */ +.theme-doc-sidebar-menu .menu__list .menu__list { + margin-left: 0.85rem; + padding-left: 0.35rem; + border-left: 1px solid var(--tunit-border); +} +.theme-doc-sidebar-menu .menu__link--active:not(.menu__link--sublist) { + position: relative; + color: var(--tunit-ink); + font-weight: 600; + background: var(--tunit-surface); +} +.theme-doc-sidebar-menu .menu__list .menu__list .menu__link--active:not(.menu__link--sublist)::before { + content: ''; + position: absolute; + left: calc(-0.35rem - 4px); + top: calc(50% - 3.5px); + width: 7px; + height: 7px; + border-radius: 1px; + background: var(--tunit-pass-fill); +} + +.table-of-contents { font-size: 0.85rem; } +.table-of-contents__link--active { color: var(--tunit-accent); font-weight: 600; } +.table-of-contents__link:hover { color: var(--tunit-accent); } +.theme-doc-markdown { --ifm-h1-font-size: 2.4rem; --ifm-h2-font-size: 1.55rem; --ifm-h3-font-size: 1.2rem; } +.theme-doc-markdown h1 { font-weight: 700; letter-spacing: -0.035em; line-height: 1.1; } +.theme-doc-markdown h2 { font-weight: 650; letter-spacing: -0.025em; margin-top: 3rem; } +.theme-doc-markdown h3 { font-weight: 650; letter-spacing: -0.015em; margin-top: 2rem; } .theme-doc-markdown :is(h2, h3) { scroll-margin-top: calc(var(--ifm-navbar-height) + 1rem); } -.pagination-nav__link { background: var(--tunit-code); border-color: var(--tunit-border); border-radius: 6px; } -.pagination-nav__link:hover { border-color: var(--tunit-accent); } -.pagination-nav__label { color: var(--tunit-ink); } +.theme-doc-markdown > p, .theme-doc-markdown > ul, .theme-doc-markdown > ol { max-width: 72ch; } +.theme-doc-markdown { font-size: 1.04rem; } +.breadcrumbs__link { font-size: 0.82rem; } +.pagination-nav__link { background: transparent; border-color: var(--tunit-border); border-radius: 4px; } +.pagination-nav__link:hover { border-color: var(--tunit-pass-fill); background: var(--tunit-surface); } +.pagination-nav__label { color: var(--tunit-ink); font-weight: 600; } .pagination-nav__sublabel { color: var(--tunit-muted); } -code { color: var(--tunit-ink); border-color: var(--tunit-border); } -.theme-code-block { background: var(--tunit-code); border: 1px solid var(--tunit-border); box-shadow: none; } -.theme-code-block pre { background: var(--tunit-code) !important; color: var(--tunit-ink) !important; } -.theme-code-block pre code { background: transparent; color: inherit; border: 0; } -.theme-code-block [class*='codeBlockTitle'] { background: var(--tunit-surface); border-color: var(--tunit-border); color: var(--tunit-ink); } +:not(pre) > code { color: var(--tunit-ink); background: var(--tunit-code); border: 0; padding: 0.12em 0.4em; border-radius: 3px; } +.theme-code-block { --prism-background-color: var(--tunit-code); background: var(--tunit-code); border: 1px solid var(--tunit-border); border-radius: 6px; box-shadow: none; } +.theme-code-block pre { background: var(--tunit-code) !important; color: var(--tunit-ink) !important; font-size: 0.8rem; line-height: 1.75; } +.theme-code-block pre code { background: transparent; color: inherit; border: 0; padding-block: 0; } +/* Titled code blocks look like an editor tab, since the title is almost always a file name. */ +.theme-code-block [class*='codeBlockTitle'] { + display: inline-block; + margin: 0.5rem 0 0 0.75rem; + padding: 0.35rem 0.8rem; + background: var(--tunit-paper); + border: 1px solid var(--tunit-border); + border-bottom: 0; + border-radius: 3px 3px 0 0; + box-shadow: inset 0 2px 0 var(--tunit-pass-fill); + color: var(--tunit-ink); + font-family: var(--ifm-font-family-monospace); + font-size: 0.72rem; +} +.theme-code-block [class*='codeBlockTitle'] + div { border-top: 1px solid var(--tunit-border); } .theme-code-block button { color: var(--tunit-ink); background: var(--tunit-paper); border-color: var(--tunit-border); } .theme-code-block .token:is(.keyword, .tag, .namespace) { color: var(--syntax-keyword) !important; } .theme-code-block .token:is(.string, .attr-value) { color: var(--syntax-string) !important; } @@ -210,22 +304,24 @@ code { color: var(--tunit-ink); border-color: var(--tunit-border); } .theme-code-block .token:is(.number, .boolean, .constant) { color: var(--syntax-number) !important; } .theme-code-block .token:is(.comment, .prolog, .doctype, .cdata) { color: var(--syntax-comment) !important; } .theme-code-block .token:is(.plain, .punctuation, .operator, .variable, .parameter) { color: var(--tunit-ink) !important; } +.theme-code-block .token { font-style: normal !important; } -blockquote { background: var(--tunit-code); border-left-color: var(--tunit-accent); color: var(--tunit-body); padding: 1rem; } +blockquote { background: transparent; border-left: 3px solid var(--tunit-border); color: var(--tunit-body); padding: 0.25rem 1rem; } blockquote > :last-child { margin-bottom: 0; } -.tabs__item--active { color: var(--tunit-accent); border-bottom-color: var(--tunit-accent); } -.tabs__item:hover { background: var(--tunit-code); } +.tabs__item { border-radius: 3px 3px 0 0; } +.tabs__item--active { color: var(--tunit-ink); border-bottom-color: var(--tunit-pass-fill); } +.tabs__item:hover { background: var(--tunit-surface); } .table-of-contents, details { border-color: var(--tunit-border); } -/* Keep warnings and errors distinct while bringing informational notes into the brand palette. */ -.theme-admonition:is(.alert--info, .alert--secondary, .alert--performance, .alert--from-xunit, .alert--from-nunit, .alert--from-mstest) { - --ifm-alert-background-color: var(--tunit-accent-surface); - --ifm-alert-border-color: var(--tunit-accent); +/* Admonitions follow the test-result colours: green for success and tips, red for danger. */ +.theme-admonition:is(.alert--info, .alert--secondary, .alert--from-xunit, .alert--from-nunit, .alert--from-mstest) { + --ifm-alert-background-color: var(--tunit-surface); + --ifm-alert-border-color: var(--tunit-muted); --ifm-alert-foreground-color: var(--tunit-ink); } -.theme-admonition.alert--success { - --ifm-alert-background-color: var(--tunit-surface); - --ifm-alert-border-color: var(--ifm-color-primary); +.theme-admonition:is(.alert--success, .alert--performance) { + --ifm-alert-background-color: var(--tunit-accent-surface); + --ifm-alert-border-color: var(--tunit-pass-fill); --ifm-alert-foreground-color: var(--tunit-ink); } .theme-admonition.alert--warning { @@ -235,18 +331,18 @@ blockquote > :last-child { margin-bottom: 0; } } .theme-admonition.alert--danger { --ifm-alert-background-color: var(--tunit-danger-surface); - --ifm-alert-border-color: var(--tunit-danger); + --ifm-alert-border-color: var(--tunit-fail); --ifm-alert-foreground-color: var(--tunit-danger); } -.theme-admonition { color: var(--ifm-alert-foreground-color); box-shadow: none; } +.theme-admonition { color: var(--ifm-alert-foreground-color); box-shadow: none; border-width: 0 0 0 3px; border-radius: 0 6px 6px 0; } .theme-admonition :is(p, h5) { color: inherit; } .theme-admonition a { color: inherit; text-decoration: underline; } -.theme-admonition code { background: var(--tunit-paper); color: var(--tunit-ink); } +.theme-admonition :not(pre) > code { background: var(--tunit-paper); color: var(--tunit-ink); } .button--primary { --ifm-button-color: var(--tunit-on-primary); } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: var(--tunit-paper); } -::-webkit-scrollbar-thumb { background: var(--tunit-border); border-radius: 5px; } +::-webkit-scrollbar-thumb { background: var(--tunit-border); border-radius: 2px; } ::-webkit-scrollbar-thumb:hover { background: var(--tunit-muted); } @media (max-width: 996px) { @@ -256,13 +352,12 @@ blockquote > :last-child { margin-bottom: 0; } } @media (max-width: 640px) { - .theme-doc-markdown h1 { font-size: 2rem; } - .theme-doc-markdown h2 { font-size: 1.6rem; } - .theme-doc-markdown h3 { font-size: 1.3rem; } + .theme-doc-markdown { --ifm-h1-font-size: 1.75rem; --ifm-h2-font-size: 1.25rem; font-size: 1rem; } .DocSearch-Button { padding-inline: 0.5rem; } } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { transition: none !important; animation: none !important; } + html.docs-doc-page .navbar::after { display: none; } } diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css index 62ad6f84c8c..16453ffb555 100644 --- a/docs/src/pages/index.module.css +++ b/docs/src/pages/index.module.css @@ -1,210 +1,119 @@ .page { - --forest: var(--tunit-ink); - --muted: var(--tunit-muted); - --mint: var(--tunit-surface); - --paper: var(--tunit-paper); - --line: var(--tunit-border); - --lilac: #e4def3; - --plum: var(--tunit-accent); - background: var(--paper); - color: var(--forest); - font-family: 'Space Grotesk', 'Segoe UI', sans-serif; + background: var(--tunit-paper); + color: var(--tunit-ink); } -.container { width: min(1280px, calc(100% - 6rem)); margin-inline: auto; } -.page h1, .page h2, .page h3 { color: var(--forest); } -.page p { color: var(--muted); } -.page a { color: var(--forest); text-underline-offset: 5px; } -.page a:hover { color: var(--plum); } -.page :is(a, button, pre):focus-visible { outline: 3px solid var(--plum); outline-offset: 5px; } +.container { width: min(1180px, calc(100% - 4rem)); margin-inline: auto; } +.page h1, .page h2, .page h3 { color: var(--tunit-ink); } +.page p { color: var(--tunit-body); } +.page a { color: var(--tunit-accent); font-weight: 600; text-underline-offset: 4px; } +.page a:hover { color: var(--tunit-ink); } +.page :is(a, button, pre, summary):focus-visible { outline: 2px solid var(--tunit-accent); outline-offset: 3px; } .page button { font: inherit; cursor: pointer; } -.page h2 { font-size: clamp(2.4rem, 4.2vw, 3.8rem); line-height: 1.08; letter-spacing: -0.06em; font-weight: 600; } -.page h3 { letter-spacing: -0.035em; font-weight: 600; } -.hero { padding: 2rem 0 0; background: var(--mint); } -.heroIntro { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.85rem; } -.heroIntro a { text-decoration: underline; } -.heroGrid { display: grid; grid-template-columns: 0.9fr 1.1fr; gap: 4rem; align-items: center; padding: 3rem 0 3.5rem; } -.wordmark { display: flex; align-items: center; gap: 0.18em; margin: 0 0 1.4rem -0.055em; font-size: clamp(6rem, 11.5vw, 10rem); letter-spacing: -0.085em; line-height: 1; font-weight: 700; } -.wordmarkCheck { display: grid; place-items: center; width: 0.48em; height: 0.48em; border: 3px solid currentColor; border-radius: 0.1em; transform: rotate(-8deg); } -.wordmarkCheck svg { width: 85%; height: 85%; } -.page .heroTitle { color: var(--forest); font-size: clamp(2.6rem, 4vw, 3.8rem); line-height: 1.06; font-weight: 500; letter-spacing: -0.065em; margin-bottom: 1.25rem; } -.heroLead { max-width: 24rem; font-size: 1.08rem; line-height: 1.65; } -.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.7rem; margin-top: 1.8rem; } -.page .primaryButton, .page .secondaryButton { display: inline-flex; align-items: center; justify-content: center; min-height: 3.2rem; padding: 0.8rem 1.4rem; border-radius: 6px; font-weight: 600; text-decoration: none; } -.page .primaryButton { background: var(--forest); color: var(--paper); border: 1px solid var(--forest); } -.page .primaryButton:hover { background: #654b87; border-color: #654b87; color: #fff; } -.page .secondaryButton { border: 1px solid var(--forest); white-space: nowrap; } -.page .secondaryButton:hover { background: var(--forest); color: var(--paper); } -.quietLink { font-weight: 600; text-decoration: underline; } -.heroFootnote { font-size: 0.78rem; margin: 1.7rem 0 0; } +.sectionTitle { margin: 0 0 0.75rem; font-size: clamp(1.6rem, 2.6vw, 2.1rem); font-weight: 650; line-height: 1.15; letter-spacing: -0.03em; } +.sectionLead { max-width: 52ch; margin: 0 0 2.5rem; font-size: 1.08rem; line-height: 1.6; } -.workbench { min-width: 0; background: #fff; color: #214b3c; border: 1px solid #8eb69f; border-radius: 12px; box-shadow: 8px 8px 0 #b6dbc6; overflow: hidden; } -.workbenchHeading { display: flex; align-items: center; justify-content: space-between; padding: 1.1rem 1.5rem; gap: 1rem; background: #edf6f0; font-weight: 600; font-size: 0.95rem; } -.language { font-size: 0.75rem; color: #50695d; } -.examplePicker { display: flex; padding: 0 1rem; gap: 0.5rem; border-bottom: 1px solid #dce8df; } -.examplePicker button { position: relative; min-height: 3.1rem; padding: 0.7rem 0.5rem; border: 0; border-bottom: 3px solid transparent; background: transparent; color: #50695d; font-size: 0.76rem; } -.examplePicker button[aria-pressed='true'] { border-bottom-color: #654b87; color: #654b87; font-weight: 700; } -.examplePicker button:hover { background: #f4f0fa; color: #654b87; } -.page .examplePicker button:focus-visible { outline-offset: -4px; } -.page .workbench :is(button, pre, a):focus-visible { outline-color: #654b87; } -.fileName { display: flex; align-items: center; gap: 0.55rem; margin: 1.2rem 1.5rem 0.6rem; font-size: 0.7rem; color: #50695d; } -.fileName > span:first-child { color: #654b87; font-weight: 700; } -.fileName > span:last-child { margin-left: auto; color: #50695d; font-size: 0.65rem; } -.page .code { min-height: 16rem; margin: 0; padding: 0.5rem 1.5rem 1rem; background: #fff !important; border-radius: 0; color: #214b3c !important; font-size: 0.77rem; line-height: 1.85; tab-size: 4; overflow-x: auto; } -.page .code code { display: block; border: 0; padding: 0; background: transparent !important; color: #214b3c !important; font-size: inherit !important; } -.page .code span, -:global([data-theme='dark']) .page .code span { color: var(--preview-token, #214b3c) !important; } -.page .code .lineNumber { --preview-token: #607568; display: inline-block; width: 2.1rem; user-select: none; } -.testResults { min-height: 9.8rem; border-top: 1px solid #dce8df; padding: 1rem 1.5rem; background: #f4f9f5; } -.resultsHeading { display: flex; justify-content: space-between; gap: 1rem; margin-bottom: 0.65rem; font-size: 0.72rem; } -.resultsHeading strong { color: #286044; font-weight: 600; } -.testResults ul { padding: 0; margin: 0; list-style: none; } -.testResults li { display: flex; align-items: center; gap: 0.55rem; padding: 0.2rem 0; font-family: var(--ifm-font-family-monospace); font-size: 0.7rem; } -.testResults li svg { flex-shrink: 0; width: 1rem; height: 1rem; color: #286044; } -.testResults li > span:nth-child(2) { overflow-wrap: anywhere; } -.testResults li > span:last-child { margin-left: auto; font-family: 'Space Grotesk', sans-serif; color: #50695d; font-size: 0.65rem; } -.exampleNote { display: flex; justify-content: space-between; align-items: center; gap: 1rem; min-height: 4rem; padding: 0.8rem 1.5rem; background: #e4def3; font-size: 0.7rem; } -.page .exampleNote p { color: #513c6e; margin: 0; max-width: 30ch; } -.page .exampleNote a { color: #513c6e; white-space: nowrap; text-decoration: underline; font-weight: 600; } -.compilePath { display: flex; align-items: center; gap: 1.3rem; border-top: 1px solid var(--line); padding: 1.4rem 0; font-size: 0.82rem; } -.pathLine { flex: 1; height: 1px; background: #8eb69f; position: relative; } -.pathLine::after { content: ''; position: absolute; right: 0; top: -3px; width: 7px; height: 7px; border-top: 1px solid #8eb69f; border-right: 1px solid #8eb69f; transform: rotate(45deg); } -.parallelMark { display: flex; flex-direction: column; gap: 4px; } -.parallelMark i { width: 1.8rem; height: 3px; background: var(--forest); border-radius: 2px; } +.primaryButton, .textLink { display: inline-flex; align-items: center; min-height: 2.9rem; } +.page .primaryButton { padding: 0.7rem 1.35rem; border-radius: 6px; background: var(--tunit-ink); color: var(--tunit-paper); font-weight: 600; text-decoration: none; } +.page .primaryButton:hover { background: var(--tunit-accent); color: var(--tunit-paper); } +.page .textLink { text-decoration: underline; text-decoration-thickness: 1px; } -.quickStart { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 4rem; padding-block: 5.5rem; } -.page .quickStartIntro h2 { font-size: 2.25rem; } -.quickStartIntro p { font-size: 0.9rem; } -.quickStartIntro a { font-size: 0.85rem; text-decoration: underline; } -.installCommands { min-width: 0; } -.installCommands ol { list-style: none; padding: 0; margin: 0; } -.installCommands li { display: flex; align-items: center; gap: 1.2rem; padding: 1.1rem 0; border-bottom: 1px solid var(--line); } -.installCommands li:first-child { padding-top: 0; } -.stepNumber { display: grid; flex-shrink: 0; place-items: center; width: 1.6rem; height: 1.6rem; border: 1px solid var(--line); border-radius: 50%; font-size: 0.75rem; } -.installCommands li > div { min-width: 0; flex: 1; } -.stepLabel { display: block; margin-bottom: 0.3rem; font-size: 0.72rem; color: var(--muted); } -.page .installCommands code { border: 0; padding: 0; background: transparent !important; color: var(--forest) !important; font-size: 0.83rem; overflow-wrap: anywhere; } -.installCommands button { border: 1px solid var(--line); background: transparent; color: var(--forest); border-radius: 4px; font-size: 0.7rem; padding: 0.5rem 0.6rem; min-height: 2.5rem; } -.installCommands button:hover { background: var(--mint); } -.copyStatus { margin: 0.5rem 0 0; font-size: 0.72rem; } +/* Hero */ +.hero { padding: clamp(3rem, 7vw, 5.5rem) 0 clamp(3rem, 6vw, 4.5rem); } +.heroTop { display: grid; gap: 1.25rem; margin-bottom: 3rem; } +.heroTitle { margin: 0; font-size: clamp(2.6rem, 5.6vw, 4.4rem); font-weight: 700; line-height: 1; letter-spacing: -0.045em; } +.heroAside p { max-width: 50ch; margin: 0 0 1.6rem; font-size: 1.15rem; line-height: 1.6; } +.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; } -.architecture { background: var(--mint); padding-block: 5.5rem; } -.architectureGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 7rem; } -.architectureIntro > p { max-width: 36ch; line-height: 1.7; margin-block: 1.5rem; } -.architectureIntro > a { font-size: 0.85rem; } -.discoveryDiagram { display: grid; grid-template-columns: repeat(3, 1fr); width: min(100%, 340px); margin-top: 3rem; text-align: center; } -.discoveryDiagram > span { justify-self: center; padding: 0.4rem 0.8rem; background: var(--paper); border: 1px solid var(--line); border-radius: 4px; font-family: var(--ifm-font-family-monospace); font-size: 0.75rem; } -.discoveryBus { grid-column: 1 / -1; height: 1.6rem; margin-inline: 16.66%; border: solid #8eb69f; border-width: 0 1px 1px; background: linear-gradient(#8eb69f, #8eb69f) center / 1px 100% no-repeat; } -.discoveryDiagram > strong { grid-column: 1 / -1; position: relative; margin-top: 1.5rem; padding: 0.7rem; border: 1px solid var(--forest); border-radius: 5px; font-size: 0.85rem; font-weight: 500; } -.discoveryDiagram > strong::before { content: ''; position: absolute; width: 1px; height: 1.5rem; bottom: 100%; left: 50%; background: #8eb69f; } -.discoveryOutput { grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; gap: 0.4rem; padding-top: 0.8rem; font-size: 0.7rem; } -.discoveryOutput svg { width: 1rem; height: 1rem; } -.featureList article { padding: 1.8rem 0; border-bottom: 1px solid var(--line); } -.featureList article:first-child { padding-top: 0; } -.featureList article:last-child { border: 0; padding-bottom: 0; } -.featureList h3 { font-size: 1.45rem; } -.featureList p { max-width: 48ch; line-height: 1.7; font-size: 0.95rem; } -.featureList a { font-size: 0.8rem; text-decoration: underline; } +/* The wall of tests */ +.wall { margin: 0; padding: 1.5rem 1.5rem 1.25rem; border: 1px solid var(--tunit-border); border-radius: 10px; background: var(--tunit-surface); } +.wallCanvas { position: relative; } +.wallCanvas canvas { display: block; width: 100%; cursor: crosshair; touch-action: pan-y; } +.tooltip { position: absolute; z-index: 2; display: grid; gap: 0.1rem; padding: 0.45rem 0.65rem; margin: 0.8rem 0 0 0.8rem; border-radius: 6px; background: var(--tunit-ink); color: var(--tunit-paper); font-family: var(--tunit-mono); font-size: 0.68rem; font-stretch: 87.5%; white-space: nowrap; pointer-events: none; } +.tooltip span:last-child { opacity: 0.7; } +/* Past the middle of the wall, the tooltip opens to the left so it stays on screen. */ +.tooltipFlip { margin: 0.8rem 0 0 -0.8rem; transform: translateX(-100%); } +.visuallyHidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } +.stats { display: flex; align-items: baseline; gap: 1.5rem; margin-top: 1.25rem; color: var(--tunit-muted); font-family: var(--tunit-mono); font-size: 0.75rem; font-stretch: 87.5%; } +.stats strong { color: var(--tunit-ink); font-weight: 600; } +.barTime { margin-left: auto; } +.bar { height: 4px; margin-top: 0.6rem; overflow: hidden; border-radius: 2px; background: var(--tunit-cell); } +.barFill { height: 100%; background: var(--tunit-pass-fill); transform-origin: left; } +.wallCaption { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem 2rem; margin-top: 1rem; color: var(--tunit-muted); font-size: 0.9rem; } +.page .wallCaption button { min-height: 2.25rem; padding: 0.3rem 0.85rem; border: 1px solid var(--tunit-border); border-radius: 6px; background: var(--tunit-paper); color: var(--tunit-ink); font-size: 0.85rem; font-weight: 550; } +.page .wallCaption button:hover:not(:disabled) { border-color: var(--tunit-muted); } +.page .wallCaption button:disabled { cursor: default; opacity: 0.5; } -.ecosystem { padding-block: 5.5rem; } -.sectionHeading { display: flex; justify-content: space-between; align-items: end; gap: 2rem; margin-bottom: 2.5rem; } -.sectionHeading h2 { margin: 0; } -.sectionHeading p { max-width: 27ch; margin: 0; line-height: 1.6; } -.integrationList { display: grid; } -.integration { display: grid; grid-template-columns: 1fr 1.2fr 2fr auto; gap: 2rem; align-items: center; border-top: 1px solid var(--line); padding: 1.7rem 0; text-decoration: none !important; } -.integration:last-child { border-bottom: 1px solid var(--line); } -.integrationKind { color: var(--muted); font-size: 0.75rem; } -.integration h3 { font-size: 1.3rem; margin: 0; } -.integration p { font-size: 0.83rem; line-height: 1.65; margin: 0; max-width: 44ch; } -.integrationAction { font-size: 0.75rem; } -.integrationAction > span { padding-left: 0.5rem; } -.integration:hover h3, .integration:hover .integrationAction { color: var(--plum); text-decoration: underline; text-underline-offset: 4px; } -.benchmarkNote { display: flex; align-items: center; justify-content: space-between; gap: 2rem; margin-top: 3rem; padding: 2rem; background: var(--lilac); border-radius: 8px; } -.page .benchmarkNote h3 { color: #513c6e; font-size: 1.5rem; } -.page .benchmarkNote p { color: #513c6e; font-size: 0.88rem; margin: 0; max-width: 60ch; } -.page .benchmarkNote a { border-color: #654b87; color: #513c6e; font-size: 0.8rem; } -.page .benchmarkNote a:hover { background: #654b87; color: #fff; } +/* Claims, written as tests */ +.claims { padding-block: clamp(3.5rem, 7vw, 5.5rem); border-top: 1px solid var(--tunit-border); } +.claimList { border-top: 1px solid var(--tunit-border); } +.claim { border-bottom: 1px solid var(--tunit-border); } +.claim summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 1rem; padding: 1.15rem 0.25rem; cursor: pointer; list-style: none; } +.claim summary::-webkit-details-marker { display: none; } +.claim summary:hover .claimName { color: var(--tunit-accent); } +.passCell { position: relative; width: 1rem; height: 1rem; border-radius: 3px; background: var(--tunit-pass-fill); } +.passCell::after { content: ''; position: absolute; left: 0.32rem; top: 0.15rem; width: 0.28rem; height: 0.52rem; border: solid var(--tunit-on-primary); border-width: 0 2px 2px 0; transform: rotate(45deg); } +.claimName { font-family: var(--tunit-mono); font-size: clamp(0.92rem, 1.5vw, 1.1rem); font-stretch: 87.5%; font-weight: 500; line-height: 1.4; letter-spacing: -0.02em; overflow-wrap: anywhere; } +.claimStatus { display: flex; align-items: center; gap: 0.75rem; color: var(--tunit-pass); font-size: 0.88rem; font-weight: 600; } +.claimStatus::after { content: ''; width: 0.45rem; height: 0.45rem; border: solid var(--tunit-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg) translate(-2px, -2px); transition: transform 0.2s ease; } +.claim[open] .claimStatus::after { transform: rotate(-135deg) translate(-2px, -2px); } +.claimBody { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: 2.5rem; padding: 0.25rem 0.25rem 2rem 2.25rem; } +.claimText p { max-width: 46ch; font-size: 1.02rem; line-height: 1.65; } -.nextSteps { background: var(--mint); padding-block: 4.5rem; } -.nextStepsGrid { display: grid; grid-template-columns: 1fr 1fr; align-items: center; gap: 7rem; } -.nextSteps h2 { margin-bottom: 2rem; } -.migration { padding-left: 2rem; border-left: 1px solid var(--line); } -.migration h3 { font-size: 1.45rem; } -.migration p { max-width: 40ch; line-height: 1.7; } -.migration a { font-size: 0.85rem; } +.page .code { margin: 0; padding: 1rem 1.2rem; border: 1px solid var(--tunit-border); border-radius: 8px; background: var(--tunit-code); color: var(--tunit-ink); font-size: 0.76rem; line-height: 1.8; tab-size: 4; overflow-x: auto; } +.page .code code { display: block; padding: 0; border: 0; background: transparent; color: inherit; font-size: inherit; } +.code :global(.token.keyword) { color: var(--syntax-keyword); } +.code :global(.token.string) { color: var(--syntax-string); } +.code :global(.token:is(.function, .method)) { color: var(--syntax-function); } +.code :global(.token:is(.class-name, .builtin)) { color: var(--syntax-class); } +.code :global(.token:is(.number, .boolean)) { color: var(--syntax-number); } +.code :global(.token.comment) { color: var(--syntax-comment); } -@media (min-width: 1600px) { .heroGrid { padding-block: 4.5rem; } } -@media (max-width: 1100px) { - .container { width: calc(100% - 4rem); } - .heroGrid { gap: 2rem; grid-template-columns: 0.85fr 1.15fr; } - .wordmark { font-size: 10vw; } - .architectureGrid, .nextStepsGrid { gap: 4rem; } - .integration { grid-template-columns: 0.9fr 1.2fr 1.8fr; gap: 1.5rem; } - .integrationAction { display: none; } -} +.diagnostic { margin: 0; } +.diagnostic .code { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } +.squiggle { text-decoration: underline wavy var(--tunit-fail); text-decoration-thickness: 1px; text-underline-offset: 5px; text-decoration-skip-ink: none; } +.diagnostic figcaption { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.25rem 0.75rem; padding: 0.75rem 1.2rem; border: 1px solid var(--tunit-border); border-top: 0; border-radius: 0 0 8px 8px; background: var(--tunit-danger-surface); color: var(--tunit-danger); font-size: 0.9rem; } +.diagnosticId { font-family: var(--tunit-mono); font-size: 0.72rem; font-stretch: 87.5%; font-weight: 600; } -@media (max-width: 800px) { - .heroGrid { grid-template-columns: 1fr; gap: 2.5rem; padding-top: 2rem; } - .heroCopy { display: grid; grid-template-columns: 1fr 1fr; column-gap: 2rem; align-items: start; } - .wordmark { grid-column: 1 / -1; font-size: 8rem; margin-bottom: 1.5rem; } - .page .heroTitle { grid-row: span 2; font-size: 2.8rem; } - .heroLead { margin-bottom: 0; font-size: 0.95rem; } - .actions { gap: 1rem; margin-top: 1rem; } - .heroFootnote { grid-column: 1 / -1; margin-top: 1.3rem; } - .workbench { max-width: 620px; width: 100%; justify-self: center; } - .quickStart { grid-template-columns: 1fr; gap: 2rem; } - .quickStartIntro h2 br, .quickStartIntro p br { display: none; } - .architectureGrid { grid-template-columns: 1fr 1fr; gap: 2.5rem; } - .integration { grid-template-columns: 1fr 1.3fr; gap: 0.5rem 1.5rem; } - .integrationKind { grid-column: 1; } - .integration h3 { grid-column: 1; } - .integration p { grid-column: 2; grid-row: 1 / 3; } - .benchmarkNote { align-items: start; flex-direction: column; gap: 1.2rem; } - .nextStepsGrid { gap: 2rem; } -} +.integrations { display: grid; grid-template-columns: 1fr 1fr; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--tunit-border); } +.integrations li { display: grid; gap: 0.15rem; padding: 0.9rem 0; border-bottom: 1px solid var(--tunit-border); } +.integrations li:nth-child(odd) { padding-right: 1.5rem; } +.page .integrations a { font-size: 1.02rem; text-decoration: none; } +.page .integrations a:hover { text-decoration: underline; } +.integrations span { color: var(--tunit-muted); font-size: 0.92rem; } -@media (max-width: 580px) { - .container { width: calc(100% - 2.5rem); } - .hero { padding-top: 1.2rem; } - .heroIntro { font-size: 0.72rem; } - .heroIntro > a { display: none; } - .heroCopy { display: block; } - .wordmark { font-size: clamp(6.2rem, 24vw, 8rem); } - .page .heroTitle { font-size: 2.75rem; } - .heroLead { max-width: 32ch; } - .actions { margin-top: 1.5rem; gap: 1.5rem; } - .heroFootnote { font-size: 0.7rem; } - .heroGrid { padding-bottom: 2rem; } - .workbenchHeading { padding: 1rem; font-size: 0.83rem; } - .examplePicker { padding: 0 0.5rem; gap: 0; justify-content: space-between; } - .examplePicker button { font-size: 0.7rem; padding-inline: 0.35rem; } - .fileName { margin-inline: 1rem; font-size: 0.65rem; } - .fileName > span:last-child { font-size: 0.6rem; } - .page .code { padding-inline: 0.7rem; font-size: 0.72rem; min-height: 14rem; } - .page .code .lineNumber { width: 1.6rem; } - .testResults { padding-inline: 1rem; } - .testResults li { font-size: 0.65rem; } - .exampleNote { padding-inline: 1rem; font-size: 0.65rem; } - .compilePath { gap: 0.6rem; font-size: 0.62rem; } - .compilePath > span:not(.pathLine) { max-width: 25%; } - .parallelMark { display: none; } - .quickStart, .architecture, .ecosystem { padding-block: 3.5rem; } - .installCommands li { gap: 0.65rem; } - .page .installCommands code { font-size: 0.72rem; } - .installCommands button { padding-inline: 0.4rem; font-size: 0.65rem; } - .stepNumber { width: 1.4rem; height: 1.4rem; } - .architectureGrid, .nextStepsGrid { grid-template-columns: 1fr; gap: 2.5rem; } - .discoveryDiagram { margin-top: 2rem; } - .sectionHeading { display: block; } - .sectionHeading p { margin-top: 1rem; max-width: 32ch; } - .integration { grid-template-columns: 1fr; padding-block: 1.4rem; } - .integration p { grid-column: 1; grid-row: auto; } - .integrationAction { display: block; margin-top: 0.4rem; } - .benchmarkNote { padding: 1.5rem; } - .page .benchmarkNote h3 { font-size: 1.35rem; } - .nextSteps { padding-block: 3.5rem; } - .migration { border-left: 0; border-top: 1px solid var(--line); padding: 2rem 0 0; } +/* Getting started */ +.start { padding-block: clamp(3.5rem, 7vw, 5rem); background: var(--tunit-surface); border-top: 1px solid var(--tunit-border); } +.startGrid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 5rem; align-items: end; } +.start .sectionTitle { margin-bottom: 1.75rem; } +.terminal { display: grid; gap: 0.15rem; margin: 0; padding: 0.85rem 1.1rem; list-style: none; border-radius: 8px; background: #17231c; } +:global([data-theme='dark']) .terminal { background: var(--tunit-paper); border: 1px solid var(--tunit-border); } +.terminal li { display: flex; align-items: center; gap: 0.25rem; min-height: 2.6rem; } +.prompt { margin-right: 0.5rem; color: #7fcf9f; font-family: var(--tunit-mono); } +.page .terminal code { flex: 1; min-width: 0; padding: 0; border: 0; background: transparent; color: #e5ece7; font-size: 0.78rem; overflow-wrap: anywhere; } +.page .terminal button { min-height: 2.1rem; padding: 0.25rem 0.7rem; border: 1px solid #34453b; border-radius: 6px; background: transparent; color: #e5ece7; font-size: 0.8rem; } +.page .terminal button:hover { border-color: #91a197; } +.page .terminal button:focus-visible { outline-color: #7fcf9f; } +.page .copyStatus { min-height: 1.5rem; margin: 0.5rem 0 1rem; font-size: 0.85rem; } +.migration { padding-left: 1.5rem; border-left: 2px solid var(--tunit-pass-fill); } +.migration h3 { font-size: 1.2rem; font-weight: 650; letter-spacing: -0.015em; } +.migration p { max-width: 40ch; line-height: 1.65; } + +@media (max-width: 996px) { + .heroTop { grid-template-columns: 1fr; } + .claimBody { grid-template-columns: 1fr; gap: 1.5rem; } + .startGrid { grid-template-columns: 1fr; gap: 3rem; } } -@media (prefers-reduced-motion: reduce) { .page *, .page *::before, .page *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } } +@media (max-width: 640px) { + .container { width: calc(100% - 2rem); } + .wall { padding: 1rem; } + .stats { gap: 1rem; } + .claim summary { gap: 0.75rem; } + .claimStatus { font-size: 0; gap: 0; } + .claimBody { padding-left: 0.25rem; } + .integrations { grid-template-columns: 1fr; } + .integrations li:nth-child(odd) { padding-right: 0; } + .page .code { font-size: 0.7rem; padding: 0.85rem; } +} diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index b7395831d3e..d7224b36292 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -1,240 +1,565 @@ import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; import {Highlight, themes} from 'prism-react-renderer'; -import {useState, type CSSProperties, type JSX} from 'react'; +import {useCallback, useEffect, useRef, useState, type CSSProperties, type JSX, type PointerEvent} from 'react'; import styles from './index.module.css'; -const examples = [ - { - label: 'Data-driven tests', - file: 'CalculatorTests.cs', - code: `[Test] -[Arguments(1, 2, 3)] -[Arguments(5, 8, 13)] -[Arguments(-1, 1, 0)] -public async Task Add(int a, int b, int expected) -{ - await Assert.That(a + b) - .IsEqualTo(expected); -}`, - results: ['Add(1, 2, 3)', 'Add(5, 8, 13)', 'Add(-1, 1, 0)'], - note: 'One test. Three cases. Discovered at compile time.', - href: '/docs/writing-tests/data-driven-overview', - }, - { - label: 'Fluent assertions', - file: 'GreetingTests.cs', - code: `[Test] -public async Task Greeting_contains_name() -{ - var greeting = "Hello, Ada!"; - - await Assert.That(greeting) - .StartsWith("Hello") - .And.Contains("Ada"); -}`, - results: ['Greeting_contains_name'], - note: 'Awaitable assertions that read like your intent.', - href: '/docs/assertions/getting-started', - }, - { - label: 'Lifecycle hooks', - file: 'LifecycleTests.cs', - code: `[Before(Test)] -public void Setup() => _value = 42; +/* ---------- The test wall: every square is one test in a simulated parallel run. ---------- */ -[Test] -public async Task Setup_runs_before_test() -{ - await Assert.That(_value).IsEqualTo(42); -} +const cellSize = 8; +const cellPitch = 10; +const workerCount = 12; +const animationMs = 3200; +/* Durations below these thresholds get the lightest and middle shades of green. */ +const quickTestMs = 4; +const steadyTestMs = 12; -private int _value;`, - results: ['Setup_runs_before_test'], - note: 'Setup and cleanup, right where they belong.', - href: '/docs/writing-tests/hooks', - }, +const classNames = [ + 'OrderTests', 'CartTests', 'InvoiceTests', 'LoginTests', 'SearchTests', 'QueueTests', 'PaymentTests', + 'CalculatorTests', 'UserApiTests', 'SettingsTests', 'EmailTests', 'ReportTests', 'InventoryTests', + 'CheckoutTests', 'TokenTests', 'CacheTests', 'ShippingTests', 'AuditLogTests', 'PricingTests', 'WebhookTests', ]; - -const installSteps = [ - {label: 'Install the template', command: 'dotnet new install TUnit.Templates'}, - {label: 'Create your project', command: 'dotnet new TUnit -n MyTests'}, - {label: 'Run your tests', command: 'dotnet run --project MyTests'}, +const methodNames = [ + 'Applies_discount', 'Rejects_empty_input', 'Returns_201_when_created', 'Retries_twice', 'Parses_iso_dates', + 'Rounds_to_two_places', 'Sends_confirmation', 'Expires_after_one_hour', 'Ranks_exact_matches_first', + 'Total_includes_tax', 'Handles_concurrent_writes', 'Survives_restart', 'Maps_to_dto', 'Logs_failed_attempts', + 'Renders_pdf', 'Caches_second_lookup', 'Requires_admin_role', 'Uses_utc', 'Streams_large_files', 'Is_idempotent', ]; -const integrations = [ - {title: 'ASP.NET Core', kind: 'API tests', copy: 'Test your endpoints with isolated clients and shared infrastructure.', href: '/docs/examples/aspnet'}, - {title: 'Aspire', kind: 'Distributed apps', copy: 'Bring your whole application into the test lifecycle.', href: '/docs/examples/aspire'}, - {title: 'Playwright', kind: 'Browser tests', copy: 'Give every test a clean browser context and page.', href: '/docs/examples/playwright'}, - {title: 'TUnit.Mocks', kind: 'Test doubles', copy: 'Set up and verify source-generated, AOT-compatible mocks.', href: '/docs/writing-tests/mocking'}, -]; +type RunPlan = { + width: number; + cols: number; + rows: number; + offsetX: number; + start: Float32Array; + end: Float32Array; + durationMs: Float32Array; + testClass: Uint16Array; + method: Uint16Array; + makespanMs: number; + sortedEnd: Float32Array; +}; + +function random(seed: number): () => number { + let a = seed; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/* Tests are grouped into classes that sit together on the wall; workers pick classes in shuffled order. */ +function planRun(width: number, rows: number): RunPlan { + const cols = Math.max(1, Math.floor((width + cellPitch - cellSize) / cellPitch)); + const total = cols * rows; + const offsetX = Math.floor((width - (cols * cellPitch - (cellPitch - cellSize))) / 2); + const next = random(1729); + + const groups: number[][] = []; + const testClass = new Uint16Array(total); + const method = new Uint16Array(total); + for (let i = 0; i < total;) { + const size = Math.min(total - i, 3 + Math.floor(next() * 12)); + const group: number[] = []; + for (let j = 0; j < size; j++) { + testClass[i + j] = groups.length; + method[i + j] = Math.floor(next() * methodNames.length); + group.push(i + j); + } + groups.push(group); + i += size; + } + for (let i = groups.length - 1; i > 0; i--) { + const j = Math.floor(next() * (i + 1)); + [groups[i], groups[j]] = [groups[j], groups[i]]; + } + + const durationMs = new Float32Array(total); + const start = new Float32Array(total); + const end = new Float32Array(total); + const workers = new Array(workerCount).fill(0); + for (const group of groups) { + for (const index of group) { + let worker = 0; + for (let w = 1; w < workerCount; w++) if (workers[w] < workers[worker]) worker = w; + const duration = 1 + Math.pow(next(), 3) * 40; + durationMs[index] = duration; + start[index] = workers[worker]; + workers[worker] += duration; + end[index] = workers[worker]; + } + } + const makespanMs = Math.max(...workers); + for (let i = 0; i < total; i++) { + start[i] /= makespanMs; + end[i] /= makespanMs; + } + const sortedEnd = end.slice().sort(); + return {width, cols, rows, offsetX, start, end, durationMs, testClass, method, makespanMs, sortedEnd}; +} + +function testName(run: RunPlan, index: number): string { + return `${classNames[run.testClass[index] % classNames.length]}.${methodNames[run.method[index]]}`; +} + +type Hover = {x: number; y: number; flip: boolean; name: string; status: string}; +type Palette = {pending: string; running: string; quick: string; steady: string; slow: string}; -function Checkmark(): JSX.Element { - return ; +/* Number of tests that finished by time t; sortedEnd is ascending. */ +function countPassed(run: RunPlan, t: number): number { + let low = 0; + let high = run.sortedEnd.length; + while (low < high) { + const mid = (low + high) >>> 1; + if (run.sortedEnd[mid] <= t) low = mid + 1; + else high = mid; + } + return low; } -function TestWorkbench(): JSX.Element { - const [selected, setSelected] = useState(0); - const example = examples[selected]; +function TestWall(): JSX.Element { + const canvasRef = useRef(null); + const passedRef = useRef(null); + const timeRef = useRef(null); + const fillRef = useRef(null); + const planRef = useRef(null); + const paletteRef = useRef(null); + const progressRef = useRef(1); + const frame = useRef(0); + const replayed = useRef(false); + const [done, setDone] = useState(true); + const [announcement, setAnnouncement] = useState(''); + const [hover, setHover] = useState(null); + + const readPalette = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const css = getComputedStyle(canvas); + const colour = (name: string) => css.getPropertyValue(name).trim(); + paletteRef.current = { + pending: colour('--tunit-cell'), + running: colour('--tunit-muted'), + quick: colour('--tunit-pass-light'), + steady: colour('--tunit-pass-mid'), + slow: colour('--tunit-pass-fill'), + }; + }, []); + + // The canvas and the readout are updated directly each frame so React stays out of the animation loop. + const draw = useCallback(() => { + const canvas = canvasRef.current; + const run = planRef.current; + const palette = paletteRef.current; + if (!canvas || !run || !palette) return; + const context = canvas.getContext('2d'); + if (!context) return; + const t = progressRef.current; + const ratio = window.devicePixelRatio || 1; + context.setTransform(ratio, 0, 0, ratio, 0, 0); + context.clearRect(0, 0, run.width, canvas.height); + + // Passed tests are shaded by duration, so slow tests stand out as darker squares. + const buckets: number[][] = [[], [], [], [], []]; + for (let i = 0; i < run.start.length; i++) { + const bucket = t < run.start[i] ? 0 + : t < run.end[i] ? 1 + : run.durationMs[i] < quickTestMs ? 2 + : run.durationMs[i] < steadyTestMs ? 3 : 4; + buckets[bucket].push(i); + } + const colours = [palette.pending, palette.running, palette.quick, palette.steady, palette.slow]; + buckets.forEach((cells, bucket) => { + context.fillStyle = colours[bucket]; + for (const i of cells) { + context.fillRect(run.offsetX + (i % run.cols) * cellPitch, Math.floor(i / run.cols) * cellPitch, cellSize, cellSize); + } + }); + + const passed = countPassed(run, t); + if (passedRef.current) passedRef.current.textContent = passed.toLocaleString('en-US'); + if (timeRef.current) timeRef.current.textContent = `${(run.makespanMs * t / 1000).toFixed(2)}s`; + if (fillRef.current) fillRef.current.style.transform = `scaleX(${passed / run.start.length})`; + }, []); + + const layout = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const width = canvas.getBoundingClientRect().width; + const rows = width < 640 ? 16 : 14; + const run = planRun(width, rows); + planRef.current = run; + const height = rows * cellPitch - (cellPitch - cellSize); + const ratio = window.devicePixelRatio || 1; + canvas.width = Math.round(width * ratio); + canvas.height = Math.round(height * ratio); + canvas.style.height = `${height}px`; + draw(); + }, [draw]); + + const play = useCallback(() => { + cancelAnimationFrame(frame.current); + setHover(null); + const finish = () => { + progressRef.current = 1; + draw(); + setDone(true); + if (replayed.current && planRef.current) { + setAnnouncement(`Run finished: ${planRef.current.start.length.toLocaleString('en-US')} tests passed, 0 failed.`); + } + }; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + finish(); + return; + } + setDone(false); + setAnnouncement(''); + const begin = performance.now(); + const step = (now: number) => { + progressRef.current = Math.min((now - begin) / animationMs, 1); + if (progressRef.current < 1) { + draw(); + frame.current = requestAnimationFrame(step); + } else { + finish(); + } + }; + progressRef.current = 0; + draw(); + frame.current = requestAnimationFrame(step); + }, [draw]); + + const replay = () => { + replayed.current = true; + play(); + }; + + useEffect(() => { + readPalette(); + layout(); + play(); + let width = canvasRef.current?.getBoundingClientRect().width; + const resize = new ResizeObserver(() => { + const next = canvasRef.current?.getBoundingClientRect().width; + if (next === width) return; + width = next; + // A new width means a new plan, so a run in progress starts again on the new wall. + const running = progressRef.current < 1; + layout(); + if (running) play(); + }); + if (canvasRef.current) resize.observe(canvasRef.current); + const theme = new MutationObserver(() => { + readPalette(); + draw(); + }); + theme.observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']}); + return () => { + cancelAnimationFrame(frame.current); + resize.disconnect(); + theme.disconnect(); + }; + }, [readPalette, layout, play, draw]); + + const inspect = (event: PointerEvent) => { + const run = planRef.current; + if (!run) return; + const bounds = event.currentTarget.getBoundingClientRect(); + const x = event.clientX - bounds.left; + const y = event.clientY - bounds.top; + // Map from the displayed size back to the plan's coordinates in case the two differ. + const scale = bounds.width > 0 ? run.width / bounds.width : 1; + const col = Math.floor((x * scale - run.offsetX) / cellPitch); + const row = Math.floor(y / cellPitch); + if (col < 0 || col >= run.cols || row < 0 || row >= run.rows) { + setHover(null); + return; + } + const index = row * run.cols + col; + const t = progressRef.current; + const status = t >= run.end[index] + ? `Passed in ${Math.max(1, Math.round(run.durationMs[index]))} ms` + : t >= run.start[index] ? 'Running' : 'Waiting for a worker'; + setHover({x, y, flip: x > bounds.width / 2, name: testName(run, index), status}); + }; return ( -
-
- A little C#. A lot covered. - .NET +
+
+ setHover(null)} aria-hidden="true" /> + {hover && ( + + )}
-
- {examples.map((item, index) => ( - - ))} + -
-
{example.file}Class members
- - {({tokens, getLineProps, getTokenProps}) => ( -
-              
-                {tokens.map((line, lineIndex) => (
-                  
- - {line.map((token, tokenIndex) => { - const props = getTokenProps({token}); - return ; - })} -
- ))} -
-
- )} -
-
-
Example results{example.results.length} passed
-
    - {example.results.map(result =>
  • {result}Passed
  • )} -
-
-
-

{example.note}

- Read the guide -
+ +

{announcement}

+
+ + Each square is one test, run by one of {workerCount} workers. Darker squares took longer. + Point at one to see its name. + + +
+
); } function Hero(): JSX.Element { return (
-
-
The testing framework for modern .NETOpen source, from the start
-
-
-

TUnit

-

Give your tests
a head start.

-

Discover tests while you compile. Run them in parallel. - Get back to the code you came to write.

-
- Start testing - Why TUnit? -
-

Source generated. Async native. AOT ready.

+
+

Keep it green.

+
+

+ TUnit is a testing framework for modern .NET. Tests are discovered while you build + and run in parallel by default, so you get to green sooner. +

+
+ Get started + Why TUnit?
- -
-
- Your C# tests
+
); } -function QuickStart(): JSX.Element { - const [copyStatus, setCopyStatus] = useState<{index: number; ok: boolean} | null>(null); +/* ---------- Claims, written as tests. ---------- */ - async function copyCommand(command: string, index: number) { - try { - await navigator.clipboard.writeText(command); - setCopyStatus({index, ok: true}); - } catch { - setCopyStatus({index, ok: false}); - } - } +type Token = {types: string[]; content: string}; +type TokenPropsGetter = (input: {token: Token}) => {style?: CSSProperties; className: string; children: string}; +/* Underlines a line the way an editor marks a diagnostic, leaving its indentation alone. */ +function FlaggedTokens({line, getTokenProps}: {line: Token[]; getTokenProps: TokenPropsGetter}): JSX.Element { + const indent = line[0]?.content.match(/^\s*/)?.[0] ?? ''; + const tokens = line.map((token, index) => index === 0 ? {...token, content: token.content.slice(indent.length)} : token); return ( -
-
-

Meet your next
test project.

-

Have the .NET SDK?
You’re three commands away.

- Installation guide -
-
-
    - {installSteps.map((step, index) => ( -
  1. - -
    {step.label}{step.command}
    - -
  2. - ))} -
-

{copyStatus ? (copyStatus.ok ? `${installSteps[copyStatus.index].label}: command copied.` : 'Couldn’t copy automatically. Select and copy the command above.') : '\u00a0'}

-
-
+ <> + {indent} + + {tokens.map((token, index) => { + const {style: _style, ...tokenProps} = getTokenProps({token}); + return ; + })} + + ); } -function Architecture(): JSX.Element { +function Code({code, flaggedLine}: {code: string; flaggedLine?: number}): JSX.Element { return ( -
-
-
-

Good tests.
Less machinery.

-

The compiler already knows your code. TUnit puts that knowledge to work before your tests even start.

- Explore the design -
- ); + + + ), + }, + { + name: 'Flags_assertions_you_forgot_to_await', + body: ( + <> +
+

Assertions are awaited and read left to right. Analyzers check them while you type, along with hook signatures and data sources, so mistakes show up in the editor instead of in CI.

+ Awaiting assertions +
+
+ +
+ TUnitAssertions0002 + Assert statements must be awaited - all TUnit assertions return Task +
+
+ + ), + }, + { + name: 'Tests_whole_systems_not_just_units', + body: ( + <> +
+

The same framework drives integration tests, with shared infrastructure that starts once and is cleaned up after the last test using it finishes.

+ See the benchmarks +
+
    +
  • ASP.NET CoreEndpoints with isolated clients
  • +
  • AspireYour whole distributed app
  • +
  • PlaywrightA clean browser page per test
  • +
  • TUnit.MocksSource-generated, AOT-safe mocks
  • +
+ + ), + }, +]; + +function TestName({name}: {name: string}): JSX.Element { + const parts = name.split('_'); + return <>{parts.map((part, index) => {part}{index < parts.length - 1 && <>_})}; } -function Ecosystem(): JSX.Element { +function Claims(): JSX.Element { return ( -
-

Small units.
Whole systems.

The same testing framework, wherever your code takes you.

-
- {integrations.map(item => {item.kind}

{item.title}

{item.copy}

View example )} +
+
+

Our claims, written as tests.

+

Open any result to see the code behind it.

+
+ {claims.map((claim, index) => ( +
+ + +
{claim.body}
+
+ ))} +
-
); } -function NextSteps(): JSX.Element { +/* ---------- Getting started. ---------- */ + +const installSteps = [ + {label: 'Install the template', command: 'dotnet new install TUnit.Templates'}, + {label: 'Create a project', command: 'dotnet new TUnit -n MyTests'}, + {label: 'Run the tests', command: 'dotnet run --project MyTests'}, +]; + +function Start(): JSX.Element { + const [copyStatus, setCopyStatus] = useState<{index: number; ok: boolean} | null>(null); + + async function copyCommand(command: string, index: number) { + try { + await navigator.clipboard.writeText(command); + setCopyStatus({index, ok: true}); + } catch { + setCopyStatus({index, ok: false}); + } + } + return ( -
-
-

Your next test
starts here.

Write your first test
-

Already have a test suite?

See how TUnit compares with xUnit, NUnit, and MSTest, and find a familiar place to begin.

Compare frameworks
+
+
+
+

Turn your first bar green.

+
    + {installSteps.map((step, index) => ( +
  1. + + {step.command} + +
  2. + ))} +
+

+ {copyStatus ? (copyStatus.ok ? `${installSteps[copyStatus.index].label}: command copied.` : 'Couldn’t copy automatically. Select the command and copy it.') : ' '} +

+ Write your first test +
+
+

Moving from xUnit, NUnit, or MSTest?

+

The migration guides map each attribute, assertion, and hook you use today to its TUnit equivalent.

+ Compare frameworks +
); @@ -242,8 +567,8 @@ function NextSteps(): JSX.Element { export default function Home(): JSX.Element { return ( - -
+ +
); } diff --git a/docs/static/img/tunit-favicon.svg b/docs/static/img/tunit-favicon.svg index 1b0946f9e6b..7deec053d1d 100644 --- a/docs/static/img/tunit-favicon.svg +++ b/docs/static/img/tunit-favicon.svg @@ -1,4 +1,4 @@ - - + + diff --git a/docs/static/img/tunit-social-card.png b/docs/static/img/tunit-social-card.png index 64ecb4edf4b..6cd70d0a627 100644 Binary files a/docs/static/img/tunit-social-card.png and b/docs/static/img/tunit-social-card.png differ