/* view.css: the preview widget's half of the gallery's stylesheet, served joined to gallery.css as one file. The widget ([Desktop] [Mobile] [HTML] over one frame), the code block under it with its wrapper line and Copy button, and the five highlight colours codeview.Highlight writes; TestHighlightColoursPassAgainstTheCodeBackground reads those off this file by their exact lines. It is Rastrillo's widget block and code-block rules, the Template tab's rules dropped, with Fiesta's marks where Rastrillo reads its own: data-gallery-js (set by view.js, which is what shows a frame) for data-rst-js, and data-gallery-painted, the mark part 3 established, for data-ds-painted. */ /* ── The code block ────────────────────────────────────────────────── A source block can still scroll sideways where a single token is wider than its column, and a box that scrolls must be reachable from the keyboard (axe's scrollable-region-focusable; WCAG 2.1.1), so every pre.ds-src carries tabindex="0". */ .ds-src { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); margin: var(--rst-sp-3) 0; overflow-x: auto; padding: var(--rst-sp-4); } /* Source soft-wraps rather than scrolling sideways. An inline run (a meter's whole output, a choice card) is one line however long, because breaking inside it would change the space a reader sees; so the block wraps it on screen, and copying takes textContent, which has no soft breaks in it. */ .ds-src code { overflow-wrap: anywhere; white-space: pre-wrap; } /* Code is left to right in every language. Inheriting an Arabic page's rtl, a block sits against the right edge and the bidi algorithm mirrors its brackets and moves trailing punctuation to the front, so what a reader sees is not what Copy puts on the clipboard. isolate keeps a name inside a sentence (the wrapper line's) from reordering the words around it. */ .ds-src, .ds-wrap code { direction: ltr; text-align: start; unicode-bidi: isolate; } /* The five token colours, from theme tokens so they follow theme and scheme, each at 4.5:1 or better against --rst-surface, the block's background, in every theme and both schemes; the test reads these lines, one rule per element. */ ds-t { color: var(--rst-accent); } ds-a { color: var(--rst-tone-negative-fg); } ds-v { color: var(--rst-tone-positive-fg); } ds-x { color: var(--rst-text-muted); } ds-s { color: var(--rst-tone-warning-fg); } .ds-wrap { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); margin: 0 0 var(--rst-sp-2); } /* Sits directly before the block it copies. inline-size: fit-content with margin-inline-start: auto pushes a block-level button to the far edge without a flex wrapper around it, on the inline axis, so it lands on the right in a left-to-right page and the left in Arabic with no direction check anywhere in view.js. It reads the target floor itself, because tokens.css floors a button only through rst-btn; border-box so the floor is the whole button, not its text. */ .ds-copy { align-items: center; background: none; border: 1px solid var(--rst-line); border-radius: var(--rst-radius-sm); box-sizing: border-box; color: var(--rst-text-muted); cursor: pointer; display: flex; font: inherit; font-size: var(--rst-fs-sm); inline-size: fit-content; justify-content: center; margin-inline-start: auto; min-block-size: var(--rst-target); min-inline-size: var(--rst-target); padding-block: 0.2rem; padding-inline: 0.6rem; } .ds-copy:hover { background: var(--rst-accent-soft); color: var(--rst-text); } .ds-copy:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } /* ── A grouped widget's states ─────────────────────────────────────── Under a grouped widget every state has a block of its own: the state's name on a line of its own, and its note where the prose has one. The name and the note are two elements with nothing written between them, since a joiner would be copy no review saw. */ .ds-group-note { margin-block-start: var(--rst-sp-3); } .ds-note__state { display: block; font-weight: 600; } .ds-group-note > .ds-note { margin: var(--rst-sp-1) 0 0; } /* ── The preview widget ────────────────────────────────────────────── [Desktop] [Mobile] [HTML] over one frame. HTML is there only when the example has code to show, so a widget has two or three tabs. No script anywhere in it. The tabs are radio inputs sharing a name, hidden but focusable inside their labels, and :has() reads which one is checked from the wrapper, so the panels switch on the browser's own form behaviour and a reader with JavaScript off has the same views as everyone else. None of them starts checked, and that is the whole of how the opening view follows the reader. CSS cannot tell "the reader chose Desktop" from "Desktop is what the markup shipped checked", so a widget that opens on a checked radio can never let the width pick the opening view and still take an explicit choice. With nothing checked the width picks Desktop or Mobile, and any tab, once clicked, overrides it at any size. Which width picks is the stage's, asked with a container query on .ds-view, and not the viewport's. The viewport is the wrong ruler because the gallery's rail arrives at 800px and takes its width out of the column: a reader who widens their window makes the preview narrower, so a viewport rule switches to the rendering that needs the most room at exactly the width where it has the least. A container query measures the box a reader actually gets. One number does the deciding, written twice because CSS cannot put a custom property in a query condition: --ds-kmin, the least the frame may be scaled to, and the stage width at which the desktop rendering reaches it, --ds-wd × --ds-kmin. Keep them in step; the drives assert the identity off each widget's own --ds-wd. Two width classes. --ds-wd is the width the desktop rendering is laid out at: .ds-view--page 1200px a whole page: the layout documents, the two shell idioms, the modal, the demo application. A page frame wants a window to be a page frame in. .ds-view 900px everything else: a field, a pill, a row, a menu. A component is something inside a page, and never gets a window's width in an app either. The default is the component one, because nearly every example is a component. view.go's pageFrame() decides which is which. Laid out at 1200px, every component was drawn at four-fifths size in the stage the gallery's column allows; at 900px a field is a field. Not the full column, because tokens.css switches the shells to their narrow layout at 800px, and a "desktop" rendering under that width would be the mobile layout under a lit Desktop tab. Where 0.72 comes from: the type in these samples is 14px, 12.5px and 11.5px, and 12.5px carries most pages, so it is the one to protect; 9px is about where rendered text stops being read and starts being texture, and 12.5 × 0.72 = 9.0px exactly. That gives both the floor and the thresholds: 1200 × 0.72 = 864px = 54rem for a page frame, 900 × 0.72 = 648px = 40.5rem for a component. The arithmetic is exact at a 16px root and only there; a reader who scales their root type up moves the rem values and not the ratio, and the gap falls the safe way. The pair is written as (min-width: Nrem) and its exact negation rather than a min/max pair: a max-width one pixel under leaves a gap a fractional width can land in, where neither rule applies and no tab is lit. The highlight is driven by the same two queries, so the tab a reader sees lit is always the rendering in front of them. An engine with :has() but no container queries gets a third rule that lights Desktop to match the rendering it will be showing. The scale: the frame is laid out at a virtual --ds-w (its class's --ds-wd, or 390px on Mobile) and scaled to fit the reader's column, so the desktop rendering is the desktop rendering on a phone too. CSS divides two lengths into a plain number with tan(atan2(a, b)), and 100cqw is the container's own width, so --ds-k is that fraction; the 1 stops it scaling anything up and --ds-kmin stops it scaling anything into illegibility. Where the trig functions are missing the @supports block never applies, --ds-k stays 1, and the frame is a virtual-width page clipped to the column: smaller, not broken. Below its class's threshold the desktop rendering stops shrinking and starts panning: --ds-k holds at 0.72, the box stays as wide as the column, and the scaled page inside it scrolls horizontally within the box. The scroller is on the box itself, not on the state that made it necessary: the Mobile rendering clamps at 0.72 too, and under 281px of stage the opening view cropped 42px at a 320px window with no way to reach it. A box that can clamp can overflow, and one that can overflow has to scroll. The container is .ds-view and not .ds-view__stage, because the tabs are the stage's siblings and a query on the stage cannot reach them. .ds-view carries no padding and no border, so the two are the same number of pixels wide; the drives measure both and require them equal. --ds-h is the frame's virtual height, written per example by previewStyle from the measured table in heights.go. It sizes the box; the frame takes its height back off the box, 100% / --ds-k, which is the same number until a reader drags the resize grip and then is whatever they dragged it to. The box is a window on a document, not a fit to it: a taller sample scrolls inside its frame, and the grip is there for the ones a reader wants more of. min-block-size is insurance: block-size names --ds-k, and a --ds-k an engine parses but cannot resolve drops that declaration to auto, which for a box whose only child is absolutely positioned is zero. Its value is the smallest height the healthy cascade can produce, so it never binds when things work. The @supports guard tests the same shape the declaration uses, 100cqw and all. */ .ds-view { --ds-wd: 900px; --ds-w: var(--ds-wd); --ds-wm: 390px; --ds-h: 220px; --ds-hm: 330px; --ds-kmin: 0.72; container-name: ds-view; container-type: inline-size; margin: var(--rst-sp-3) 0; } .ds-view--page { --ds-wd: 1200px; } /* A grouped row's frame sits flush with its tabs, not centred: its states start at the inline start like the labels above them. */ .ds-view--rows .ds-view__box { margin-inline-start: 0; } /* Two pairs, the renderings and the source, that wrap as wholes: three tabs in one row can be wider than a sample's column on a 320px phone in a language with long words, and a label that may wrap breaks a tab into two lines of text. */ .ds-view__tabs { border: 0; display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); margin: 0 0 var(--rst-sp-2); padding: 0; } .ds-view__pair { display: inline-flex; } /* A tab is a target like any control: the floor in force, border-box so the border and padding are inside it. */ .ds-view__tab { align-items: center; border: 1px solid var(--rst-line); box-sizing: border-box; color: var(--rst-text-muted); cursor: pointer; display: inline-flex; font-size: var(--rst-fs-sm); min-block-size: var(--rst-target); padding: 0.2rem 0.7rem; white-space: nowrap; } .ds-view__tab + .ds-view__tab { border-inline-start: 0; } .ds-view__tab:first-of-type { border-end-start-radius: var(--rst-radius-sm); border-start-start-radius: var(--rst-radius-sm); } .ds-view__tab:last-of-type { border-end-end-radius: var(--rst-radius-sm); border-start-end-radius: var(--rst-radius-sm); } .ds-view__tab input { block-size: 1px; clip-path: inset(50%); inline-size: 1px; margin: 0; overflow: hidden; position: absolute; } .ds-view__tab:hover { background: var(--rst-accent-soft); color: var(--rst-text); } .ds-view__tab:has(input:checked) { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } /* One pair of queries per width class, and the two declarations each pair drives are written inside it: which rendering opens, and which tab is lit over it, are one decision, and two rules answering to two copies of the same condition is the shape a divergence hides in. */ @container ds-view (min-width: 40.5rem) { .ds-view:not(.ds-view--page):not(:has(input:checked)) .ds-view__tab--d { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } } @container ds-view not (min-width: 40.5rem) { .ds-view:not(.ds-view--page):not(:has(input:checked)) .ds-view__tab--m { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } .ds-view:not(.ds-view--page):not(:has(.ds-view__tab--d input:checked)) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); } } @container ds-view (min-width: 54rem) { .ds-view--page:not(:has(input:checked)) .ds-view__tab--d { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } } @container ds-view not (min-width: 54rem) { .ds-view--page:not(:has(input:checked)) .ds-view__tab--m { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } .ds-view--page:not(:has(.ds-view__tab--d input:checked)) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); } } @supports not (container-type: inline-size) { .ds-view:not(:has(input:checked)) .ds-view__tab--d { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } } .ds-view__tab:has(input:focus-visible) { outline: 2px solid var(--rst-accent); outline-offset: 2px; } .ds-view__box { --ds-k: 1; background: var(--rst-bg); block-size: calc(var(--ds-h) * var(--ds-k)); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); inline-size: calc(var(--ds-w) * var(--ds-k)); margin-inline: auto; max-inline-size: 100%; min-block-size: calc(var(--ds-h) * var(--ds-kmin)); overflow: hidden; overflow-x: auto; overscroll-behavior-x: contain; position: relative; resize: vertical; scrollbar-width: thin; } .ds-view__frame { block-size: calc(100% / var(--ds-k)); border: 0; inline-size: var(--ds-w); inset-block-start: 0; inset-inline-start: 0; min-block-size: 100%; position: absolute; transform: scale(var(--ds-k)); transform-origin: top left; } /* transform-origin has no logical keywords, so the side is a selector. Anchored at the physical left, a right-to-left box could not scroll to the frame's start. */ [dir="rtl"] .ds-view__frame { transform-origin: top right; } /* A frame shows nothing of its own until view.js marks it, once its document is parsed and styled and has the page's scheme written on it (load if that is missed), and again for each document a navigation inside the frame loads. Before that, in every scheme: - the frame holds the about:blank it was created with, which Safari painted white on a dark page (see the color-scheme rule below); - its document is parsing, or parsed without its stylesheets; - with Light or Dark chosen, it has painted in the OS's scheme while scrolling to a lazy frame, after a reload at a remembered scroll, or on a click inside one. Hidden, the box behind it shows this page's --rst-bg instead; a hidden frame still lazy-loads. data-gallery-js because view.js is what shows it: with scripts off nothing would, and the frames show as they come. */ :root[data-gallery-js] .ds-view__frame:not([data-gallery-painted]) { visibility: hidden; } /* A browser compares a frame's color-scheme with its document's, and Safari draws a document whose scheme differs on an opaque canvas. about:blank declares none, so it is light, and with the frame inheriting a dark page's scheme every frame was white until its document came. Normal matches it. A preview declares its own scheme and paints its whole canvas, so this changes nothing once it paints. It is all a reader without scripts gets, since nothing hides their frames. */ .ds-view__frame { color-scheme: normal; } @supports (block-size: calc(1px * clamp(0.5, tan(atan2(100cqw, 1200px)), 1))) { /* Mobile is the stage's width up to 390px: below 390px of stage, laying the page out at the stage's width is what that screen would show, and min() keeps a 760px window's Mobile the 390px phone rather than a 760px tablet. --ds-k is then at least 1, so Mobile is never scaled. The cqw resolves where --ds-w is used, inside .ds-view, its container. */ .ds-view { --ds-wm: min(390px, 100cqw); } .ds-view__box { --ds-k: clamp(var(--ds-kmin), tan(atan2(100cqw, var(--ds-w))), 1); } } /* An explicit Mobile beats the width, at either class and any size. */ .ds-view:has(.ds-view__tab--m input:checked) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); } /* A widget with a rest height (--ds-hr and --ds-hmr, written by previewStyle for a frame whose menus open far below its page) is marked data-ds-rest by view.js while no menu in its frame is open. The stage then hands the box the rest heights under the names its Desktop and Mobile rules read, so either rendering rests short. Unmarked, as with scripts off, the box keeps the open height and a menu opened in it is in view. */ .ds-view[data-ds-rest] > .ds-view__stage { --ds-h: var(--ds-hr); --ds-hm: var(--ds-hmr); } /* HTML replaces the frame with its panel, and only then: the radios are one group, so one is checked at most. */ .ds-view__code { display: none; margin-block-start: 0; } .ds-view:has(.ds-view__tab--h input:checked) .ds-view__stage { display: none; } .ds-view:has(.ds-view__tab--h input:checked) .ds-view__code--h { display: block; } /* On a phone the stage is the screen's width. The bleed is what sits between the widget and the screen's edge: the page column's inline padding. Written rather than measured, so a change to the column shows up as a sideways scroll in the reflow and phone drives instead of a quietly narrower stage. The tabs and the code stay in the text column. */ @media (max-width: 799.98px) { .ds-view { --ds-bleed: var(--rst-sp-4); margin-inline: calc(-1 * var(--ds-bleed)); } .ds-view__tabs, .ds-view__code { margin-inline: var(--ds-bleed); } /* Content-box sizing under max-inline-size: 100%: a border here would make a full-width box 2px wider than the screen. */ .ds-view__box { border-inline-width: 0; border-radius: 0; } }