diff --git a/fern/assets/styles.css b/fern/assets/styles.css index 7f7c9bbbc4..40e20a92a2 100644 --- a/fern/assets/styles.css +++ b/fern/assets/styles.css @@ -1016,23 +1016,6 @@ h1, h2, h3 { -.sdk-rive { - aspect-ratio: 369/93; - width: 100%; - display: block; - overflow: visible; /* Changed from hidden - was clipping interactive areas */ - transform: translateY(0px); - transition: transform 250ms ease-out !important; - will-change: transform; - /* Ensure click events can reach the canvas */ - pointer-events: auto; - min-height: 104px; -} - -.sdk-rive:hover { - transform: translateY(-3px) !important; -} - .docs-rive { min-height: 213px; } @@ -1042,10 +1025,6 @@ h1, h2, h3 { } @media (max-width: 640px) { - .sdk-rive { - min-height: inherit; - } - .docs-rive { min-height: inherit; } @@ -1084,6 +1063,302 @@ h1, h2, h3 { display: inline-block; } +/* SDK language tabs. + Sizes and positions are expressed in --u, one unit of the 369x93 grid the card is laid out on, + so everything scales with the card width. */ +.sdk-card { + --u: 0.271003cqw; /* 100cqw / 369 */ + --tab-pitch: 39.03; + --tab-first: 9.8; + --tab-index: 0; + --underline-width: calc(37.1 * var(--u)); + --sdk-card-bg: #fcfcfd; + --sdk-card-subdued-bg: #f5f5f6; + --sdk-card-border: #e0e1e6; + --sdk-card-skeleton: #cacaca; + --sdk-card-text: #191919; + --sdk-card-subdued-text: #5a5a5b; + --sdk-card-copy: #b3b3b4; + --sdk-card-active: #008700; + container-type: inline-size; + box-sizing: border-box; + position: relative; + aspect-ratio: 369/93; + width: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 4px; + background-color: var(--sdk-card-bg); + transition: transform 250ms ease-out; +} + +/* Drawn over the rows rather than as a border on the card, so the 369x93 grid keeps its + full size and the opaque command row cannot paint over the bottom of the outline. */ +.sdk-card::after { + content: ""; + position: absolute; + inset: 0; + border: 1px solid var(--sdk-card-border); + border-radius: 4px; + pointer-events: none; +} + +.dark .sdk-card { + --sdk-card-bg: #111113; + --sdk-card-subdued-bg: #1b1b1d; + --sdk-card-border: #303136; + --sdk-card-skeleton: #47484f; + --sdk-card-text: #e7e7e7; + --sdk-card-subdued-text: #a3a4a8; + --sdk-card-copy: #2d2e33; + --sdk-card-active: #9cdd8d; +} + +.sdk-card:hover { + transform: translateY(-3px); +} + +.sdk-card * { + box-sizing: border-box; +} + +/* Visually hidden, but still focusable and operable with the arrow keys */ +.sdk-card-radio { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.sdk-card-header { + flex: none; + height: calc(19.25 * var(--u)); + padding: calc(8.6 * var(--u)) 0 0 calc(6.15 * var(--u)); + border-bottom: 1px solid var(--sdk-card-border); +} + +.sdk-card-skeleton { + display: block; + width: calc(51.6 * var(--u)); + height: calc(2 * var(--u)); + border-radius: 999px; + background-color: var(--sdk-card-skeleton); +} + +.sdk-card-tabs { + position: relative; + flex: 1; + display: flex; + padding: 0 calc(7.9 * var(--u)) 0 calc(var(--tab-first) * var(--u)); + border-bottom: 1px solid var(--sdk-card-border); +} + +.sdk-card-tab { + flex: 1; + display: grid; + grid-template-rows: calc(13.7 * var(--u)) auto; + justify-items: center; + align-items: center; + gap: calc(0.5 * var(--u)); + padding-top: calc(10.9 * var(--u)); + margin: 0; + cursor: pointer; + transform-origin: 50% 60%; + transition: transform 150ms ease-out; +} + +.sdk-card-tab:hover { + transform: scale(1.048); +} + +.sdk-card-tab-icon { + width: var(--icon-size); + height: var(--icon-size); + margin: 0; + object-fit: contain; +} + +.sdk-card-tab-typescript { --icon-size: calc(10.63 * var(--u)); } +.sdk-card-tab-python { --icon-size: calc(13.62 * var(--u)); } +.sdk-card-tab-go { --icon-size: calc(13.06 * var(--u)); } +.sdk-card-tab-java { --icon-size: calc(13.66 * var(--u)); } +.sdk-card-tab-dotnet { --icon-size: calc(13.27 * var(--u)); } +.sdk-card-tab-php { --icon-size: calc(13.68 * var(--u)); } +.sdk-card-tab-ruby { --icon-size: calc(10.57 * var(--u)); } +.sdk-card-tab-swift { --icon-size: calc(10.07 * var(--u)); } +.sdk-card-tab-rust { --icon-size: calc(10.02 * var(--u)); } + +.sdk-card-tab-label { + font-family: var(--font-code); + font-size: calc(6.16 * var(--u)); + line-height: calc(7.3 * var(--u)); + color: var(--sdk-card-text); + white-space: nowrap; +} + +.sdk-card-underline { + position: absolute; + bottom: -1px; /* sits on the separator, like the animation did */ + left: calc((var(--tab-first) + (var(--tab-index) + 0.5) * var(--tab-pitch)) * var(--u) - var(--underline-width) / 2); + width: var(--underline-width); + height: calc(0.9 * var(--u)); + background-color: var(--sdk-card-active); + transition: left 150ms ease-out, width 150ms ease-out; +} + +#sdk-card-python:checked ~ .sdk-card-tabs { --tab-index: 1; --underline-width: calc(22.4 * var(--u)); } +#sdk-card-go:checked ~ .sdk-card-tabs { --tab-index: 2; --underline-width: calc(19 * var(--u)); } +#sdk-card-java:checked ~ .sdk-card-tabs { --tab-index: 3; --underline-width: calc(19 * var(--u)); } +#sdk-card-dotnet:checked ~ .sdk-card-tabs { --tab-index: 4; --underline-width: calc(18.7 * var(--u)); } +#sdk-card-php:checked ~ .sdk-card-tabs { --tab-index: 5; --underline-width: calc(18.5 * var(--u)); } +#sdk-card-ruby:checked ~ .sdk-card-tabs { --tab-index: 6; --underline-width: calc(19.9 * var(--u)); } +#sdk-card-swift:checked ~ .sdk-card-tabs { --tab-index: 7; --underline-width: calc(18.6 * var(--u)); } +#sdk-card-rust:checked ~ .sdk-card-tabs { --tab-index: 8; --underline-width: calc(19 * var(--u)); } + +#sdk-card-typescript:focus-visible ~ .sdk-card-tabs .sdk-card-tab-typescript, +#sdk-card-python:focus-visible ~ .sdk-card-tabs .sdk-card-tab-python, +#sdk-card-go:focus-visible ~ .sdk-card-tabs .sdk-card-tab-go, +#sdk-card-java:focus-visible ~ .sdk-card-tabs .sdk-card-tab-java, +#sdk-card-dotnet:focus-visible ~ .sdk-card-tabs .sdk-card-tab-dotnet, +#sdk-card-php:focus-visible ~ .sdk-card-tabs .sdk-card-tab-php, +#sdk-card-ruby:focus-visible ~ .sdk-card-tabs .sdk-card-tab-ruby, +#sdk-card-swift:focus-visible ~ .sdk-card-tabs .sdk-card-tab-swift, +#sdk-card-rust:focus-visible ~ .sdk-card-tabs .sdk-card-tab-rust { + outline: 2px solid var(--sdk-card-active); + outline-offset: calc(-1 * var(--u)); + border-radius: 2px; +} + +.sdk-card-command { + flex: none; + height: calc(30.5 * var(--u)); + display: flex; + align-items: center; + gap: calc(4 * var(--u)); + padding: 0 calc(8.12 * var(--u)) 0 calc(7 * var(--u)); + background-color: var(--sdk-card-subdued-bg); +} + +/* Wraps every command so the typing reveal runs once on load, rather than restarting each time + a command is shown. Shrinks to the visible command, which makes the clip-path steps below + land on character boundaries. */ +.sdk-card-command-typing { + display: block; + width: fit-content; + max-width: 100%; + min-width: 0; + overflow: hidden; +} + +.sdk-card[data-drawing-in] .sdk-card-command-typing { + animation: sdk-card-type-in 400ms steps(24) 120ms backwards; +} + +/* 24 steps, one per character of the Typescript command shown on load */ +@keyframes sdk-card-type-in { + from { + clip-path: inset(0 100% 0 0); + } + + to { + clip-path: inset(0); + } +} + +.sdk-card-command-text { + display: none; + overflow: hidden; + padding: 0; + border: none; + border-radius: 0; + box-shadow: none; + background: none; + font-family: var(--font-code); + font-size: calc(6.97 * var(--u)); + line-height: 1.2; + white-space: nowrap; + color: var(--sdk-card-subdued-text); +} + +#sdk-card-typescript:checked ~ .sdk-card-command .sdk-card-command-typescript, +#sdk-card-python:checked ~ .sdk-card-command .sdk-card-command-python, +#sdk-card-go:checked ~ .sdk-card-command .sdk-card-command-go, +#sdk-card-java:checked ~ .sdk-card-command .sdk-card-command-java, +#sdk-card-dotnet:checked ~ .sdk-card-command .sdk-card-command-dotnet, +#sdk-card-php:checked ~ .sdk-card-command .sdk-card-command-php, +#sdk-card-ruby:checked ~ .sdk-card-command .sdk-card-command-ruby, +#sdk-card-swift:checked ~ .sdk-card-command .sdk-card-command-swift, +#sdk-card-rust:checked ~ .sdk-card-command .sdk-card-command-rust { + display: block; +} + +.sdk-card-copy { + flex: none; + margin-left: auto; + width: calc(6.88 * var(--u)); + height: calc(8 * var(--u)); + color: var(--sdk-card-copy); +} + +/* Draws the card in on load: a curtain in the row background colours that retreats to the right, + revealing the outline, separators and tabs in turn. Its soft trailing edge is what makes each + tab fade up rather than snap in, and it starts one softness-width off the left edge so the + card is fully covered on the first frame. sdk-card-draw-in.js adds the attribute once the page + is loaded and the card is on screen, the way the Rive canvases start once their runtime is + fetched; without it the animation would run against the browser's first frame, which on a + refresh can be composited before the page is presented and so go unseen. */ +.sdk-card-intro { + display: none; + --draw-softness: calc(127 * var(--u)); + position: absolute; + top: 0; + bottom: 0; + left: calc(-1 * var(--draw-softness)); + width: calc(369 * var(--u) + var(--draw-softness)); + z-index: 2; + pointer-events: none; + background: linear-gradient( + to bottom, + var(--sdk-card-bg) calc(62.5 * var(--u)), + var(--sdk-card-subdued-bg) calc(62.5 * var(--u)) + ); + -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness)); + mask-image: linear-gradient(to right, transparent, #000 var(--draw-softness)); +} + +.sdk-card[data-drawing-in] .sdk-card-intro { + display: block; + animation: sdk-card-draw-in 700ms cubic-bezier(0.15, 0.2, 0.4, 1) forwards; +} + +@keyframes sdk-card-draw-in { + from { + transform: translateX(0); + } + + to { + transform: translateX(100%); + } +} + +@media (prefers-reduced-motion: reduce) { + .sdk-card, + .sdk-card-tab, + .sdk-card-underline { + transition: none; + } + + .sdk-card[data-drawing-in] .sdk-card-intro { + display: none; + } + + .sdk-card[data-drawing-in] .sdk-card-command-typing { + animation: none; + } +} + /*** END -- LANDING PAGE STYLING ***/ /* HACK: add a slash to markdown snippet documentation (otherwise it gets parsed by CLI) */ diff --git a/fern/docs.yml b/fern/docs.yml index 0dbe7a493e..e028885611 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -189,6 +189,8 @@ settings: js: - path: ./rive-animation.js strategy: afterInteractive + - path: ./sdk-card-draw-in.js + strategy: afterInteractive agents: page-directive: "For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt." diff --git a/fern/products/home/pages/welcome.mdx b/fern/products/home/pages/welcome.mdx index 0de8f47702..868c6af3cc 100644 --- a/fern/products/home/pages/welcome.mdx +++ b/fern/products/home/pages/welcome.mdx @@ -41,11 +41,82 @@ layout: custom

- {/* Rive Animation */} -
-
- + {/* SDK language tabs */} +
+ + + + + + + + + + +
+ +
+ +
+ + + + + + + + + +
+ +
+ + npm install your-company + pip install your-company + go get github.com/your-company/go-sdk + {'compile "com.your-company:java-sdk:1.0.0"'} + {'dotnet add package YourCompany.Net --version 1.0.0'} + composer require your-company/your-company-php + gem install your_company + {'.package(url: "https://github.com/your-company/swift-sdk")'} + cargo add your-company + + +
+ +
{/* Language Icons */} diff --git a/fern/rive-animation.js b/fern/rive-animation.js index 24b8489c21..960766ed98 100644 --- a/fern/rive-animation.js +++ b/fern/rive-animation.js @@ -236,27 +236,6 @@ return; } - // SDK Animation with native Rive interactions - createRiveAnimation({ - canvasSelector: '#sdk-rive-canvas', - riveUrl: 'https://cdn.prod.website-files.com/67880ff570cdb1a85eee946f/68802bc752aef23fab76e6fc_12235f640f9ad3339b42e8d026c6345a_sdk-rive.riv', - aspectRatio: 369/93, - stateMachine: "State Machine 1", - fallbackImages: [ - { - src: './images/sdks-preview-light.svg', - className: 'sdks-preview-img dark:hidden', - alt: 'SDK Generation Preview' - }, - { - src: './images/sdks-preview-dark.svg', - className: 'sdks-preview-img hidden dark:block', - alt: 'SDK Generation Preview' - } - ] - // No eventHandlers - this animation only uses native interactions - }); - // Docs Animation createRiveAnimation({ canvasSelector: '#docs-rive-canvas', diff --git a/fern/sdk-card-draw-in.js b/fern/sdk-card-draw-in.js new file mode 100644 index 0000000000..1ce525ec8a --- /dev/null +++ b/fern/sdk-card-draw-in.js @@ -0,0 +1,80 @@ +// Plays the landing page SDK card's draw-in, the counterpart to the Rive canvases' intro. +// The animation itself lives in styles.css and is gated on [data-drawing-in]: left to CSS alone it +// starts against the browser's first frame of the document, which on a reload can be composited +// before the page is presented and while the card is still scrolled out of view, so the reveal is +// over by the time anyone sees the card. This waits for the page to load and the card to be on +// screen, which is also what makes the Rive intros visible on every refresh. +(function () { + const DRAWING_ATTRIBUTE = 'data-drawing-in'; + const observed = new WeakSet(); + + function play(card) { + card.setAttribute(DRAWING_ATTRIBUTE, ''); + const intro = card.querySelector('.sdk-card-intro'); + if (!intro) { + return; + } + // Drop the attribute once the curtain has cleared, so the overlay leaves the layer tree and + // the animation can be played again if the card is re-created by a client-side navigation. + intro.addEventListener( + 'animationend', + () => card.removeAttribute(DRAWING_ATTRIBUTE), + { once: true } + ); + } + + function playWhenOnScreen(card) { + if (observed.has(card)) { + return; + } + observed.add(card); + + if (typeof IntersectionObserver === 'undefined') { + play(card); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + observer.disconnect(); + play(card); + } + } + }, + { threshold: 0.5 } + ); + observer.observe(card); + } + + function initializeCards() { + document.querySelectorAll('.sdk-card').forEach(playWhenOnScreen); + } + + function whenLoaded(callback) { + if (document.readyState === 'complete') { + callback(); + } else { + window.addEventListener('load', callback, { once: true }); + } + } + + whenLoaded(() => { + initializeCards(); + + // Catch cards rendered later, either by hydration or by a client-side navigation. Mutations + // arrive in bursts, so collapse each burst into one pass. + let scheduled = false; + new MutationObserver(() => { + if (scheduled) { + return; + } + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; + initializeCards(); + }); + }).observe(document.body, { subtree: true, childList: true }); + }); +})();