rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

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. */
36ds-t { color: var(--rst-accent); }
37ds-a { color: var(--rst-tone-negative-fg); }
38ds-v { color: var(--rst-tone-positive-fg); }
39ds-x { color: var(--rst-text-muted); }
40ds-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