| 1 |
|
| 2 |
Carousel: the widget tier's first consumer. |
| 3 |
|
| 4 |
The markup is no longer written in a template. It comes from a description -- |
| 5 |
`crate::quasi::widgets::carousel` says "an ordered set of pictures, called a |
| 6 |
carousel" -- and quasi-webview turns that into a region carrying |
| 7 |
`data-widget="carousel"` with one `.picture` or `.picture-img` per frame. |
| 8 |
|
| 9 |
So these rules key on the widget name rather than on a hand-written class. |
| 10 |
That is what "a renderer that recognises the name draws it its own way" |
| 11 |
means on this host: the name is the hook, and the stylesheet plus the island |
| 12 |
are the browser's idea of a carousel. |
| 13 |
|
| 14 |
THE FALLBACK RUNS THE OTHER WAY NOW. This used to render frame one and hide |
| 15 |
the rest, because the controls that reach them are the scripted part -- so |
| 16 |
with JS off, two of three screenshots were unreachable. Now the unenhanced |
| 17 |
page is the whole gallery, stacked in order. |
| 18 |
|
| 19 |
THE CHROME IS DESCRIBED TOO, as of makeover-layout 0.23.0. The description |
| 20 |
says `Showing::One` -- an ordered set of pictures with one showing -- and |
| 21 |
quasi-webview derives prev/position/next from that, once, for every widget |
| 22 |
there will ever be. Nothing here or in any renderer matches on the name |
| 23 |
"carousel" to get it. The island stopped injecting chrome and now only binds |
| 24 |
what is already on the page; the dot strip and the overlaid arrows are gone. |
| 25 |
|
| 26 |
The controls are still not shipped live to a reader with no script: makeover |
| 27 |
keeps `.showing` hidden until something sets [data-ready], so the markup no |
| 28 |
longer has to be absent to avoid lying. |
| 29 |
========================================================================= |
| 30 |
|
| 31 |
|
| 32 |
.item-gallery, |
| 33 |
.project-gallery { |
| 34 |
margin: var(--gap-pane) 0; |
| 35 |
} |
| 36 |
|
| 37 |
|
| 38 |
element is what the browser upgrades automatically after an htmx swap, and |
| 39 |
the description emits a plain div because it does not know this host's tag |
| 40 |
prefix. Default display for an unknown element is inline, so say otherwise. |
| 41 |
mnw-carousel { |
| 42 |
display: block; |
| 43 |
} |
| 44 |
|
| 45 |
|
| 46 |
the page is settled from first paint and nothing rearranges on load. Showing |
| 47 |
all three and collapsing them was a 141px -> 58px jump on every load that |
| 48 |
everyone with JS paid; `no-js.css`, loaded from a <noscript>, is what opens |
| 49 |
the stack out for a visitor without scripting. |
| 50 |
|
| 51 |
This is the one half of the carousel that stays local, and it is host policy |
| 52 |
rather than a gap. makeover's generated sheet collapses the stack on |
| 53 |
[data-ready], which is the honest default for a renderer that cannot know |
| 54 |
whether this page has script: it ships every child and takes them away once |
| 55 |
something binds them. Trading a load-time jump for that is a decision about |
| 56 |
this site's landing page, and <noscript> is a mechanism a generated |
| 57 |
stylesheet has no way to reach. |
| 58 |
|
| 59 |
No padding-bottom and no position: relative any more. Both were for chrome |
| 60 |
that is gone -- the strip that had to be reserved and the arrows that were |
| 61 |
absolutely positioned over the frame. |
| 62 |
[data-widget="carousel"] > .showing-frame:not(.current) { |
| 63 |
|
| 64 |
what it cannot express is <noscript>. A renderer that cannot know whether a |
| 65 |
page has script has to ship every child and take them away once something |
| 66 |
binds them; this page collapses from first paint instead and lets no-js.css |
| 67 |
open it back, which is a trade about this landing page rather than about |
| 68 |
the vocabulary. |
| 69 |
display: none; |
| 70 |
} |
| 71 |
|
| 72 |
[data-widget="carousel"] .picture { |
| 73 |
margin: 0; |
| 74 |
} |
| 75 |
|
| 76 |
|
| 77 |
all come from makeover's own `.picture-img` rule, which layout.css puts on |
| 78 |
every page through crate::shell::head. Restating any of them here is a |
| 79 |
components-layer declaration quietly beating the design system, which is what the |
| 80 |
drift check in build.rs exists to catch -- and did, on the first attempt at |
| 81 |
this file. |
| 82 |
[data-widget="carousel"] .picture-img { |
| 83 |
border-radius: var(--radius-control); |
| 84 |
} |
| 85 |
|
| 86 |
|
| 87 |
--content-muted, and this said the same thing one layer louder. What is |
| 88 |
local is the centring and the mono face, which are this widget's look and |
| 89 |
not a claim about what a caption is. |
| 90 |
[data-widget="carousel"] .picture-caption { |
| 91 |
margin-top: var(--gap-group); |
| 92 |
font-family: var(--font-mono); |
| 93 |
font-size: var(--text-note); |
| 94 |
text-align: center; |
| 95 |
} |
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
The three landing frames are screenshots of this site, so the parchment they |
| 100 |
were shot on is the parchment they sit on, and a 1px border between two |
| 101 |
identical grounds is not an edge anyone sees. |
| 102 |
|
| 103 |
THE ELEVATION IS GONE, and that is the point of the port. This carried |
| 104 |
`box-shadow: 0 4px 10px var(--elevation), 0 14px 36px var(--elevation)`, |
| 105 |
which is the defect `c0b63ea9` names: in-flow depth is a bevel and elevation |
| 106 |
is for what sits *over* the page. It survived this long because it was |
| 107 |
invisible -- a declaration in a later layer on the same class makeover |
| 108 |
sets, so it beat the design system's bevel silently. The drift check in build.rs found it |
| 109 |
the moment the frame became a described `.picture-img`. |
| 110 |
|
| 111 |
So the frame is raised by makeover's bevel now, from `Depth::Raised`, and |
| 112 |
what is left here is the one thing that was never about depth: a stronger |
| 113 |
border colour, because a shadow falling downward does nothing for the top |
| 114 |
edge and the top edge is where a parchment screenshot dissolves into a |
| 115 |
parchment page. |
| 116 |
.landing-showcase .picture-img { |
| 117 |
border-color: var(--border-strong); |
| 118 |
} |
| 119 |
|
| 120 |
|
| 121 |
Enhanced. Everything below needs the island, and the island sets |
| 122 |
[data-ready] once it has upgraded and bound the controls. |
| 123 |
--------------------------------------------------------------------------- |
| 124 |
|
| 125 |
[data-widget="carousel"][data-ready]:focus-visible { |
| 126 |
outline: 2px solid var(--focus-ring); |
| 127 |
outline-offset: 3px; |
| 128 |
} |
| 129 |
|
| 130 |
|
| 131 |
than injected here, and it needs no rules: it is two `.button`s and a muted |
| 132 |
readout, all three of which the generated sheet already styles. What used to |
| 133 |
sit here was `.carousel-nav`, `.carousel-prev`, `.carousel-next`, |
| 134 |
`.carousel-dots` and `.carousel-dot` -- 60 lines for chrome this host drew by |
| 135 |
hand because nothing could describe it. |
| 136 |
|
| 137 |
The arrows were absolutely positioned over the frame, which Max called |
| 138 |
cluttered and which a terminal cannot honestly do at all; the dots had no |
| 139 |
form past a handful of frames, and two of the three galleries here are |
| 140 |
creator uploads of arbitrary length. Both halves of that were the same half. |
| 141 |
|
| 142 |
|
| 143 |
|
| 144 |
Custom page editor (dashboard/custom_page_editor.html) |
| 145 |
Moved out of an inline <style> block per the extract-shared-CSS rule. |
| 146 |
--------------------------------------------------------------------------- |
| 147 |
.cp-editor { max-width: 1200px; margin: 0 auto; padding: var(--gap-section); } |
| 148 |
.cp-editor-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--gap-section); flex-wrap: wrap; } |
| 149 |
.cp-editor-actions { display: flex; gap: var(--gap-section); } |
| 150 |
.cp-intro { color: var(--content-muted); margin: var(--gap-bound) 0 var(--gap-section); } |
| 151 |
|
| 152 |
that number is the pane's, not the viewport's, and the pair stacks itself. |
| 153 |
.cp-editor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: var(--gap-section); align-items: start; } |
| 154 |
.cp-editor-form { display: flex; flex-direction: column; gap: var(--gap-peer); } |
| 155 |
.cp-code { width: 100%; min-height: 220px; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--text-note); line-height: 1.4; resize: vertical; } |
| 156 |
.cp-editor-buttons { display: flex; align-items: center; gap: var(--gap-section); margin-top: var(--gap-peer); } |
| 157 |
.cp-preview-pane { border: 1px solid var(--border); border-radius: var(--radius-control); overflow: hidden; } |
| 158 |
.cp-preview-bar { font-size: var(--text-fine); padding: var(--gap-bound) var(--gap-group); background: var(--surface-sunken); border-bottom: 1px solid var(--border); } |
| 159 |
.cp-preview { width: 100%; height: 520px; border: 0; background: var(--primary-light); display: block; } |
| 160 |
.cp-blocked { margin-top: var(--gap-section); } |
| 161 |
.cp-blocked-title { font-weight: 600; margin-bottom: var(--gap-peer); } |
| 162 |
.cp-blocked-empty { color: var(--content-muted); } |
| 163 |
.cp-blocked-list { list-style: none; padding: 0; margin: 0; } |
| 164 |
.cp-blocked-list li { padding: var(--gap-peer) 0; border-bottom: 1px solid var(--border); font-size: var(--text-note); display: flex; gap: var(--gap-peer); flex-wrap: wrap; align-items: baseline; } |
| 165 |
.cp-blocked-kind { font-weight: 600; } |
| 166 |
.cp-blocked-loc { color: var(--content-muted); } |
| 167 |
.cp-blocked-val { background: var(--surface-sunken); padding: 0 var(--gap-bound); } |
| 168 |
.cp-status.cp-ok { color: var(--success); } |
| 169 |
.cp-status.cp-err { color: var(--warning); } |
| 170 |
.cp-reset { margin-top: var(--gap-pane); } |
| 171 |
.cp-locked-banner { margin: var(--gap-peer) 0 var(--gap-section); padding: var(--gap-section); border: 1px solid var(--warning); border-radius: var(--radius-control); background: color-mix(in oklch, var(--warning) 12%, var(--surface-page)); } |
| 172 |
|
| 173 |
whose class didn't already set an aspect-ratio / fixed size. |
| 174 |
.proj-image-preview img { width: 120px; height: 120px; object-fit: cover; } |
| 175 |
|
| 176 |
|
| 177 |
Two surfaces, doing two different jobs (db/discover.rs, migration 179). |
| 178 |
|
| 179 |
.results-tier-break is the single heading, and it sits exactly where the |
| 180 |
claim it makes turns true: above it every row holds at least one word the |
| 181 |
searcher typed, below it none of them do. .row-match-note is the per-row |
| 182 |
half, "Matched 3 of 5 words", because coverage varies row to row and one |
| 183 |
page can hold a 4-of-5 beside a 1-of-5. |
| 184 |
|
| 185 |
Both are deliberately quiet. They explain a result the reader is already |
| 186 |
looking at; competing with the title would make search feel like it is |
| 187 |
apologising. Muted colour and small type, no accent, no border of their own |
| 188 |
beyond the rule that separates the tiers. |
| 189 |
.results-tier-break { |
| 190 |
display: flex; |
| 191 |
align-items: baseline; |
| 192 |
gap: var(--gap-peer); |
| 193 |
flex-wrap: wrap; |
| 194 |
padding: var(--gap-section) var(--gap-section) var(--gap-peer); |
| 195 |
border-top: 1px solid var(--border); |
| 196 |
background: var(--surface-page); |
| 197 |
} |
| 198 |
|
| 199 |
|
| 200 |
the break supplies its own top border. Mid-list that sits against the |
| 201 |
preceding row's border-bottom and reads as one heavier 2px rule, which is |
| 202 |
wanted here: it is a section change, not another row. At the top of an |
| 203 |
all-fuzzy page there is no preceding row, and the same border supplies the |
| 204 |
table's missing top edge. |
| 205 |
.results-tier-label { |
| 206 |
font-family: var(--font-display); |
| 207 |
font-weight: bold; |
| 208 |
font-size: var(--text-fine); |
| 209 |
text-transform: uppercase; |
| 210 |
letter-spacing: 0.03em; |
| 211 |
color: var(--content); |
| 212 |
} |
| 213 |
|
| 214 |
.results-tier-hint { |
| 215 |
font-size: var(--text-fine); |
| 216 |
color: var(--content-muted); |
| 217 |
} |
| 218 |
|
| 219 |
|
| 220 |
heading takes one card's slot and the tiers interleave visually. |
| 221 |
.results-container.results-grid .results-tier-break { |
| 222 |
grid-column: 1 / -1; |
| 223 |
padding-left: 0; |
| 224 |
padding-right: 0; |
| 225 |
background: none; |
| 226 |
} |
| 227 |
|
| 228 |
|
| 229 |
another child of .table-row-link: that is a fixed five-column grid and a |
| 230 |
sixth child would invent a column and shift every row that carried one. |
| 231 |
.row-match-note { |
| 232 |
font-family: var(--font-mono); |
| 233 |
font-size: var(--text-fine); |
| 234 |
color: var(--content-muted); |
| 235 |
white-space: nowrap; |
| 236 |
overflow: hidden; |
| 237 |
text-overflow: ellipsis; |
| 238 |
} |
| 239 |
|
| 240 |
|
| 241 |
.grid-card .row-match-note { |
| 242 |
display: block; |
| 243 |
margin-top: var(--gap-bound); |
| 244 |
} |
| 245 |
|
| 246 |
|
| 247 |
column that survives. Keeping the note there would crowd the title on a |
| 248 |
phone, where the tier heading above still carries the important half. |
| 249 |
@media (max-width: 599px) { |
| 250 |
.table-row .row-match-note { |
| 251 |
generated sheet describes one `.table-row`; which of its children survive a |
| 252 |
narrow viewport is this page's call, not the design system's. display: none; } |
| 253 |
} |
| 254 |
|
| 255 |
|
| 256 |
|