| 1 | /* view.css: the preview widget's half of the gallery's stylesheet, |
| 2 | served joined to gallery.css as one file. The widget ([Desktop] |
| 3 | [Mobile] [HTML] over one frame), the code block under it with its |
| 4 | wrapper line and Copy button, and the five highlight colours |
| 5 | codeview.Highlight writes; TestHighlightColoursPassAgainstTheCodeBackground |
| 6 | reads those off this file by their exact lines. It is Rastrillo's |
| 7 | widget block and code-block rules, the Template tab's rules dropped, |
| 8 | with Fiesta's marks where Rastrillo reads its own: data-gallery-js |
| 9 | (set by view.js, which is what shows a frame) for data-rst-js, and |
| 10 | data-gallery-painted, the mark part 3 established, for |
| 11 | data-ds-painted. */ |
| 12 | |
| 13 | /* ── The code block ────────────────────────────────────────────────── |
| 14 | A source block can still scroll sideways where a single token is |
| 15 | wider than its column, and a box that scrolls must be reachable from |
| 16 | the keyboard (axe's scrollable-region-focusable; WCAG 2.1.1), so |
| 17 | every pre.ds-src carries tabindex="0". */ |
| 18 | .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); } |
| 19 | /* Source soft-wraps rather than scrolling sideways. An inline run (a |
| 20 | meter's whole output, a choice card) is one line however long, |
| 21 | because breaking inside it would change the space a reader sees; so |
| 22 | the block wraps it on screen, and copying takes textContent, which |
| 23 | has no soft breaks in it. */ |
| 24 | .ds-src code { overflow-wrap: anywhere; white-space: pre-wrap; } |
| 25 | /* Code is left to right in every language. Inheriting an Arabic page's |
| 26 | rtl, a block sits against the right edge and the bidi algorithm |
| 27 | mirrors its brackets and moves trailing punctuation to the front, so |
| 28 | what a reader sees is not what Copy puts on the clipboard. isolate |
| 29 | keeps a name inside a sentence (the wrapper line's) from reordering |
| 30 | the words around it. */ |
| 31 | .ds-src, .ds-wrap code { direction: ltr; text-align: start; unicode-bidi: isolate; } |
| 32 | /* The five token colours, from theme tokens so they follow theme and |
| 33 | scheme, each at 4.5:1 or better against --rst-surface, the block's |
| 34 | background, in every theme and both schemes; the test reads these |
| 35 | lines, one rule per element. */ |
| 36 | ds-t { color: var(--rst-accent); } |
| 37 | ds-a { color: var(--rst-tone-negative-fg); } |
| 38 | ds-v { color: var(--rst-tone-positive-fg); } |
| 39 | ds-x { color: var(--rst-text-muted); } |
| 40 | ds-s { color: var(--rst-tone-warning-fg); } |
| 41 | .ds-wrap { color: var(--rst-text-muted); font-size: var(--rst-fs-sm); margin: 0 0 var(--rst-sp-2); } |
| 42 | /* Sits directly before the block it copies. inline-size: fit-content |
| 43 | with margin-inline-start: auto pushes a block-level button to the far |
| 44 | edge without a flex wrapper around it, on the inline axis, so it |
| 45 | lands on the right in a left-to-right page and the left in Arabic |
| 46 | with no direction check anywhere in view.js. It reads the target |
| 47 | floor itself, because tokens.css floors a button only through |
| 48 | rst-btn; border-box so the floor is the whole button, not its text. */ |
| 49 | .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; } |
| 50 | .ds-copy:hover { background: var(--rst-accent-soft); color: var(--rst-text); } |
| 51 | .ds-copy:focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } |
| 52 | |
| 53 | /* ── A grouped widget's states ─────────────────────────────────────── |
| 54 | Under a grouped widget every state has a block of its own: the |
| 55 | state's name on a line of its own, and its note where the prose has |
| 56 | one. The name and the note are two elements with nothing written |
| 57 | between them, since a joiner would be copy no review saw. */ |
| 58 | .ds-group-note { margin-block-start: var(--rst-sp-3); } |
| 59 | .ds-note__state { display: block; font-weight: 600; } |
| 60 | .ds-group-note > .ds-note { margin: var(--rst-sp-1) 0 0; } |
| 61 | |
| 62 | /* ── The preview widget ────────────────────────────────────────────── |
| 63 | [Desktop] [Mobile] [HTML] over one frame. HTML is there only when |
| 64 | the example has code to show, so a widget has two or three tabs. |
| 65 | |
| 66 | No script anywhere in it. The tabs are radio inputs sharing a name, |
| 67 | hidden but focusable inside their labels, and :has() reads which one |
| 68 | is checked from the wrapper, so the panels switch on the browser's |
| 69 | own form behaviour and a reader with JavaScript off has the same |
| 70 | views as everyone else. |
| 71 | |
| 72 | None of them starts checked, and that is the whole of how the |
| 73 | opening view follows the reader. CSS cannot tell "the reader chose |
| 74 | Desktop" from "Desktop is what the markup shipped checked", so a |
| 75 | widget that opens on a checked radio can never let the width pick |
| 76 | the opening view and still take an explicit choice. With nothing |
| 77 | checked the width picks Desktop or Mobile, and any tab, once |
| 78 | clicked, overrides it at any size. |
| 79 | |
| 80 | Which width picks is the stage's, asked with a container query on |
| 81 | .ds-view, and not the viewport's. The viewport is the wrong ruler |
| 82 | because the gallery's rail arrives at 800px and takes its width out |
| 83 | of the column: a reader who widens their window makes the preview |
| 84 | narrower, so a viewport rule switches to the rendering that needs |
| 85 | the most room at exactly the width where it has the least. A |
| 86 | container query measures the box a reader actually gets. |
| 87 | |
| 88 | One number does the deciding, written twice because CSS cannot put a |
| 89 | custom property in a query condition: --ds-kmin, the least the frame |
| 90 | may be scaled to, and the stage width at which the desktop rendering |
| 91 | reaches it, --ds-wd × --ds-kmin. Keep them in step; the drives |
| 92 | assert the identity off each widget's own --ds-wd. |
| 93 | |
| 94 | Two width classes. --ds-wd is the width the desktop rendering is |
| 95 | laid out at: |
| 96 | |
| 97 | .ds-view--page 1200px a whole page: the layout documents, the |
| 98 | two shell idioms, the modal, the demo |
| 99 | application. A page frame wants a window |
| 100 | to be a page frame in. |
| 101 | .ds-view 900px everything else: a field, a pill, a row, |
| 102 | a menu. A component is something inside |
| 103 | a page, and never gets a window's width |
| 104 | in an app either. |
| 105 | |
| 106 | The default is the component one, because nearly every example is a |
| 107 | component. view.go's pageFrame() decides which is which. Laid out at |
| 108 | 1200px, every component was drawn at four-fifths size in the stage |
| 109 | the gallery's column allows; at 900px a field is a field. Not the |
| 110 | full column, because tokens.css switches the shells to their narrow |
| 111 | layout at 800px, and a "desktop" rendering under that width would be |
| 112 | the mobile layout under a lit Desktop tab. |
| 113 | |
| 114 | Where 0.72 comes from: the type in these samples is 14px, 12.5px and |
| 115 | 11.5px, and 12.5px carries most pages, so it is the one to protect; |
| 116 | 9px is about where rendered text stops being read and starts being |
| 117 | texture, and 12.5 × 0.72 = 9.0px exactly. That gives both the floor |
| 118 | and the thresholds: 1200 × 0.72 = 864px = 54rem for a page frame, |
| 119 | 900 × 0.72 = 648px = 40.5rem for a component. The arithmetic is exact |
| 120 | at a 16px root and only there; a reader who scales their root type |
| 121 | up moves the rem values and not the ratio, and the gap falls the safe |
| 122 | way. |
| 123 | |
| 124 | The pair is written as (min-width: Nrem) and its exact negation |
| 125 | rather than a min/max pair: a max-width one pixel under leaves a gap |
| 126 | a fractional width can land in, where neither rule applies and no |
| 127 | tab is lit. The highlight is driven by the same two queries, so the |
| 128 | tab a reader sees lit is always the rendering in front of them. An |
| 129 | engine with :has() but no container queries gets a third rule that |
| 130 | lights Desktop to match the rendering it will be showing. |
| 131 | |
| 132 | The scale: the frame is laid out at a virtual --ds-w (its class's |
| 133 | --ds-wd, or 390px on Mobile) and scaled to fit the reader's column, |
| 134 | so the desktop rendering is the desktop rendering on a phone too. |
| 135 | CSS divides two lengths into a plain number with tan(atan2(a, b)), |
| 136 | and 100cqw is the container's own width, so --ds-k is that fraction; |
| 137 | the 1 stops it scaling anything up and --ds-kmin stops it scaling |
| 138 | anything into illegibility. Where the trig functions are missing the |
| 139 | @supports block never applies, --ds-k stays 1, and the frame is a |
| 140 | virtual-width page clipped to the column: smaller, not broken. |
| 141 | |
| 142 | Below its class's threshold the desktop rendering stops shrinking and |
| 143 | starts panning: --ds-k holds at 0.72, the box stays as wide as the |
| 144 | column, and the scaled page inside it scrolls horizontally within |
| 145 | the box. The scroller is on the box itself, not on the state that |
| 146 | made it necessary: the Mobile rendering clamps at 0.72 too, and |
| 147 | under 281px of stage the opening view cropped 42px at a 320px window |
| 148 | with no way to reach it. A box that can clamp can overflow, and one |
| 149 | that can overflow has to scroll. |
| 150 | |
| 151 | The container is .ds-view and not .ds-view__stage, because the tabs |
| 152 | are the stage's siblings and a query on the stage cannot reach them. |
| 153 | .ds-view carries no padding and no border, so the two are the same |
| 154 | number of pixels wide; the drives measure both and require them |
| 155 | equal. |
| 156 | |
| 157 | --ds-h is the frame's virtual height, written per example by |
| 158 | previewStyle from the measured table in heights.go. It sizes the |
| 159 | box; the frame takes its height back off the box, 100% / --ds-k, |
| 160 | which is the same number until a reader drags the resize grip and |
| 161 | then is whatever they dragged it to. The box is a window on a |
| 162 | document, not a fit to it: a taller sample scrolls inside its frame, |
| 163 | and the grip is there for the ones a reader wants more of. |
| 164 | |
| 165 | min-block-size is insurance: block-size names --ds-k, and a --ds-k an |
| 166 | engine parses but cannot resolve drops that declaration to auto, |
| 167 | which for a box whose only child is absolutely positioned is zero. |
| 168 | Its value is the smallest height the healthy cascade can produce, so |
| 169 | it never binds when things work. The @supports guard tests the same |
| 170 | shape the declaration uses, 100cqw and all. */ |
| 171 | .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; } |
| 172 | .ds-view--page { --ds-wd: 1200px; } |
| 173 | /* A grouped row's frame sits flush with its tabs, not centred: its |
| 174 | states start at the inline start like the labels above them. */ |
| 175 | .ds-view--rows .ds-view__box { margin-inline-start: 0; } |
| 176 | /* Two pairs, the renderings and the source, that wrap as wholes: three |
| 177 | tabs in one row can be wider than a sample's column on a 320px phone |
| 178 | in a language with long words, and a label that may wrap breaks a |
| 179 | tab into two lines of text. */ |
| 180 | .ds-view__tabs { border: 0; display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); margin: 0 0 var(--rst-sp-2); padding: 0; } |
| 181 | .ds-view__pair { display: inline-flex; } |
| 182 | /* A tab is a target like any control: the floor in force, border-box so |
| 183 | the border and padding are inside it. */ |
| 184 | .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; } |
| 185 | .ds-view__tab + .ds-view__tab { border-inline-start: 0; } |
| 186 | .ds-view__tab:first-of-type { border-end-start-radius: var(--rst-radius-sm); border-start-start-radius: var(--rst-radius-sm); } |
| 187 | .ds-view__tab:last-of-type { border-end-end-radius: var(--rst-radius-sm); border-start-end-radius: var(--rst-radius-sm); } |
| 188 | .ds-view__tab input { block-size: 1px; clip-path: inset(50%); inline-size: 1px; margin: 0; overflow: hidden; position: absolute; } |
| 189 | .ds-view__tab:hover { background: var(--rst-accent-soft); color: var(--rst-text); } |
| 190 | .ds-view__tab:has(input:checked) { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } |
| 191 | /* One pair of queries per width class, and the two declarations each |
| 192 | pair drives are written inside it: which rendering opens, and which |
| 193 | tab is lit over it, are one decision, and two rules answering to two |
| 194 | copies of the same condition is the shape a divergence hides in. */ |
| 195 | @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; } } |
| 196 | @container ds-view not (min-width: 40.5rem) { |
| 197 | .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; } |
| 198 | .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); } |
| 199 | } |
| 200 | @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; } } |
| 201 | @container ds-view not (min-width: 54rem) { |
| 202 | .ds-view--page:not(:has(input:checked)) .ds-view__tab--m { background: var(--rst-accent-soft); color: var(--rst-accent); font-weight: 600; } |
| 203 | .ds-view--page:not(:has(.ds-view__tab--d input:checked)) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); } |
| 204 | } |
| 205 | @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; } } |
| 206 | .ds-view__tab:has(input:focus-visible) { outline: 2px solid var(--rst-accent); outline-offset: 2px; } |
| 207 | .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; } |
| 208 | .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; } |
| 209 | /* transform-origin has no logical keywords, so the side is a selector. |
| 210 | Anchored at the physical left, a right-to-left box could not scroll |
| 211 | to the frame's start. */ |
| 212 | [dir="rtl"] .ds-view__frame { transform-origin: top right; } |
| 213 | /* A frame shows nothing of its own until view.js marks it, once its |
| 214 | document is parsed and styled and has the page's scheme written on |
| 215 | it (load if that is missed), and again for each document a |
| 216 | navigation inside the frame loads. Before that, in every scheme: |
| 217 | - the frame holds the about:blank it was created with, which Safari |
| 218 | painted white on a dark page (see the color-scheme rule below); |
| 219 | - its document is parsing, or parsed without its stylesheets; |
| 220 | - with Light or Dark chosen, it has painted in the OS's scheme while |
| 221 | scrolling to a lazy frame, after a reload at a remembered scroll, |
| 222 | or on a click inside one. |
| 223 | Hidden, the box behind it shows this page's --rst-bg instead; a |
| 224 | hidden frame still lazy-loads. data-gallery-js because view.js is |
| 225 | what shows it: with scripts off nothing would, and the frames show |
| 226 | as they come. */ |
| 227 | :root[data-gallery-js] .ds-view__frame:not([data-gallery-painted]) { visibility: hidden; } |
| 228 | /* A browser compares a frame's color-scheme with its document's, and |
| 229 | Safari draws a document whose scheme differs on an opaque canvas. |
| 230 | about:blank declares none, so it is light, and with the frame |
| 231 | inheriting a dark page's scheme every frame was white until its |
| 232 | document came. Normal matches it. A preview declares its own scheme |
| 233 | and paints its whole canvas, so this changes nothing once it paints. |
| 234 | It is all a reader without scripts gets, since nothing hides their |
| 235 | frames. */ |
| 236 | .ds-view__frame { color-scheme: normal; } |
| 237 | @supports (block-size: calc(1px * clamp(0.5, tan(atan2(100cqw, 1200px)), 1))) { |
| 238 | /* Mobile is the stage's width up to 390px: below 390px of stage, |
| 239 | laying the page out at the stage's width is what that screen would |
| 240 | show, and min() keeps a 760px window's Mobile the 390px phone |
| 241 | rather than a 760px tablet. --ds-k is then at least 1, so Mobile is |
| 242 | never scaled. The cqw resolves where --ds-w is used, inside |
| 243 | .ds-view, its container. */ |
| 244 | .ds-view { --ds-wm: min(390px, 100cqw); } |
| 245 | .ds-view__box { --ds-k: clamp(var(--ds-kmin), tan(atan2(100cqw, var(--ds-w))), 1); } |
| 246 | } |
| 247 | /* An explicit Mobile beats the width, at either class and any size. */ |
| 248 | .ds-view:has(.ds-view__tab--m input:checked) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); } |
| 249 | /* A widget with a rest height (--ds-hr and --ds-hmr, written by |
| 250 | previewStyle for a frame whose menus open far below its page) is |
| 251 | marked data-ds-rest by view.js while no menu in its frame is open. |
| 252 | The stage then hands the box the rest heights under the names its |
| 253 | Desktop and Mobile rules read, so either rendering rests short. |
| 254 | Unmarked, as with scripts off, the box keeps the open height and a |
| 255 | menu opened in it is in view. */ |
| 256 | .ds-view[data-ds-rest] > .ds-view__stage { --ds-h: var(--ds-hr); --ds-hm: var(--ds-hmr); } |
| 257 | /* HTML replaces the frame with its panel, and only then: the radios |
| 258 | are one group, so one is checked at most. */ |
| 259 | .ds-view__code { display: none; margin-block-start: 0; } |
| 260 | .ds-view:has(.ds-view__tab--h input:checked) .ds-view__stage { display: none; } |
| 261 | .ds-view:has(.ds-view__tab--h input:checked) .ds-view__code--h { display: block; } |
| 262 | |
| 263 | /* On a phone the stage is the screen's width. The bleed is what sits |
| 264 | between the widget and the screen's edge: the page column's inline |
| 265 | padding. Written rather than measured, so a change to the column |
| 266 | shows up as a sideways scroll in the reflow and phone drives instead |
| 267 | of a quietly narrower stage. The tabs and the code stay in the text |
| 268 | column. */ |
| 269 | @media (max-width: 799.98px) { |
| 270 | .ds-view { --ds-bleed: var(--rst-sp-4); margin-inline: calc(-1 * var(--ds-bleed)); } |
| 271 | .ds-view__tabs, .ds-view__code { margin-inline: var(--ds-bleed); } |
| 272 | /* Content-box sizing under max-inline-size: 100%: a border here |
| 273 | would make a full-width box 2px wider than the screen. */ |
| 274 | .ds-view__box { border-inline-width: 0; border-radius: 0; } |
| 275 | } |
| 276 | |