Skip to content

Commit 020be3b

Browse files
Fixes
Signed-off-by: Kristin Brown <kristin.brown@solo.io>
1 parent 0b94e3c commit 020be3b

15 files changed

Lines changed: 575 additions & 12 deletions

File tree

assets/css/docs-theme-extras.css

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,50 @@
2727
.dark .toggle-light { display: block; }
2828
.dark .toggle-dark { display: none; }
2929

30+
/* ── Cards shortcode grid ────────────────────────────────────────────── */
31+
/* The module's `{{< cards >}}` shortcode emits
32+
<div class="hextra-cards hextra-cards-grid" style="--hextra-cols: N;">
33+
Hextra's bundled CSS styles `.hextra-cards` (grid-template-columns based
34+
on `--hextra-cards-grid-cols`) but not `.hextra-cards-grid`, and our
35+
shortcode uses the different `--hextra-cols` variable name. Without
36+
these rules the container is just a regular block element and cards
37+
stack full-width with no gap.
38+
Mobile-first responsive: 1 col by default, 2 at sm, --hextra-cols (3
39+
default) at lg. */
40+
.hextra-cards-grid {
41+
display: grid;
42+
gap: 1rem;
43+
margin-top: 1rem;
44+
grid-template-columns: 1fr;
45+
}
46+
@media (min-width: 640px) {
47+
.hextra-cards-grid {
48+
grid-template-columns: repeat(2, 1fr);
49+
}
50+
}
51+
@media (min-width: 1024px) {
52+
.hextra-cards-grid {
53+
grid-template-columns: repeat(var(--hextra-cols, 3), minmax(0, 1fr));
54+
}
55+
}
56+
57+
/* When a card is wrapped by another element — most commonly Goldmark's
58+
<p> around a card that itself lives inside a percent-form shortcode
59+
like `{{% version %}}{{< card >}}{{% /version %}}` — the wrapper
60+
becomes the direct grid child. CSS grid stretches the wrapper to the
61+
row's height, but the <a> card inside doesn't fill the wrapper.
62+
Result: a "short" card in an otherwise tall row.
63+
Force any single-card wrapper to be a transparent flex column so the
64+
card fills it. Scoped with :has() to single-child wrappers so plain
65+
text content inside <cards> isn't affected. */
66+
.hextra-cards-grid > p:has(> .hextra-card:only-child) {
67+
display: flex;
68+
margin: 0;
69+
}
70+
.hextra-cards-grid > p:has(> .hextra-card:only-child) > .hextra-card {
71+
flex: 1;
72+
}
73+
3074
/* ── Version banner ──────────────────────────────────────────────────── */
3175
/* Matches old alert-info banner styling */
3276
.version-banner {
@@ -348,6 +392,18 @@ html.sidebar-loading .hextra-sidebar-container .hextra-scrollbar {
348392
visibility: hidden;
349393
}
350394

395+
/* Same FOUC-prevention pattern for the body when the URL has a #hash.
396+
Without this, the browser starts painting the top of the page before
397+
our JS scrolls to the anchor — the user perceives a "load at top,
398+
then jump" flash. head-end.html adds .hash-loading synchronously in
399+
<head> (only when window.location.hash is set), then removes it on
400+
DOMContentLoaded after scrolling to the anchor. A 2s safety timeout
401+
in the same script also removes the class, so a malformed hash or
402+
missing target never traps the user in a blank page. */
403+
html.hash-loading body {
404+
visibility: hidden;
405+
}
406+
351407
/* Product logo in sidebar */
352408
.sidebar-product-logo {
353409
padding: 0.5rem 0 1rem;
@@ -403,6 +459,18 @@ html.sidebar-loading .hextra-sidebar-container .hextra-scrollbar {
403459
max-width: 100%;
404460
}
405461

462+
/* Skip the white-card wrapper for images that ARE the dark variant.
463+
reuse-image and reuse-image-dark wrap their dark output in
464+
`<div class="toggle-light">` (shown only in dark mode). Those images
465+
are already dark-themed, so the surrounding white card both clashes
466+
visually and steals space from the SVG viewport (cutting off labels
467+
like "MARKER_IMAGE_DARK"). */
468+
.dark .content .toggle-light img {
469+
background: transparent;
470+
padding: 0;
471+
border-radius: 0;
472+
}
473+
406474
/* ── Solo footer ─────────────────────────────────────────────────────── */
407475
.solo-footer {
408476
border-top: 1px solid #e5e7eb;

fixture/assets/conrefs/test/everything.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ In the local override at `layouts/shortcodes/callout.html`, the `callout` shortc
4141
{{< cards >}}
4242
{{% version include-if="v2" %}}{{< card link="../rebased/" title="Companion view" description="The same content rendered through the alternate render path." icon="document-duplicate" >}}{{% /version %}}
4343
{{< card link="https://github.com/solo-io/docs" title="solo-io/docs" description="Source repository for the docs framework." icon="github" >}}
44-
{{< card link="https://hugo.imfing.com/" title="Hextra theme" description="Upstream theme this module overlays. Uses the `code` icon, which renders both as a Heroicon SVG (module) and as a Material Icon (docs) present in both maps." icon="code" >}}
44+
{{< card link="https://hugo.imfing.com/" title="Hextra theme" description="Upstream theme this module overlays. Uses the code icon, which renders both as a Heroicon SVG (module) and as a Material Icon (docs); present in both maps." icon="code" >}}
4545
{{< /cards >}}
4646

4747
## Checklist
@@ -265,14 +265,30 @@ An inline SVG with `role="img"` and `aria-label` (so screen readers announce the
265265
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="80" viewBox="0 0 240 80" role="img" aria-label="MARKER_SVG_ALT. A diagram showing a Client box connected to a Server box by an arrow.">
266266
<title>MARKER_SVG_ALT. A diagram showing a Client box connected to a Server box by an arrow.</title>
267267
<defs>
268+
<!-- currentColor here resolves to the stroke color of the <line>
269+
that references this marker. The line itself uses currentColor,
270+
which inherits from the surrounding text color — so the arrow
271+
tip stays visible in both light and dark mode. -->
268272
<marker id="test-arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
269-
<polygon points="0,0 10,3 0,6" fill="#1a1a1a"/>
273+
<polygon points="0,0 10,3 0,6" fill="currentColor"/>
270274
</marker>
271275
</defs>
272-
<rect x="5" y="20" width="80" height="40" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
276+
<!-- Box stroke + connector line + arrow all use currentColor so they
277+
adapt to the page theme. Box fill stays white and the text inside
278+
stays dark — that contrast is intentional and works against both
279+
light and dark page backgrounds. -->
280+
<rect x="5" y="20" width="80" height="40" fill="#fff" stroke="currentColor" stroke-width="2"/>
273281
<text x="45" y="44" text-anchor="middle" font-family="-apple-system, system-ui, sans-serif" font-size="14" fill="#1a1a1a">Client</text>
274-
<line x1="85" y1="40" x2="155" y2="40" stroke="#1a1a1a" stroke-width="2" marker-end="url(#test-arrow)"/>
275-
<rect x="155" y="20" width="80" height="40" fill="#fff" stroke="#1a1a1a" stroke-width="2"/>
282+
<!-- x2 stops at 152 (not 155) so the marker's arrow tip lands on the
283+
page background just before the right box. If the tip overlaps the
284+
white box, `currentColor` makes it invisible in dark mode (light
285+
arrow on white box). With the line geometry:
286+
line ends at world x=152
287+
marker refX=9, markerWidth=10 → tip is at world x=153
288+
box starts at x=155
289+
→ 2-unit gap; tip is always on page bg, always visible. -->
290+
<line x1="85" y1="40" x2="152" y2="40" stroke="currentColor" stroke-width="2" marker-end="url(#test-arrow)"/>
291+
<rect x="155" y="20" width="80" height="40" fill="#fff" stroke="currentColor" stroke-width="2"/>
276292
<text x="195" y="44" text-anchor="middle" font-family="-apple-system, system-ui, sans-serif" font-size="14" fill="#1a1a1a">Server</text>
277293
</svg>
278294
</a>
@@ -388,6 +404,12 @@ The `reuse-image-dark` variant by itself (renders only when dark mode is active)
388404

389405
{{< reuse-image-dark srcDark="img/test/dark.svg" alt="MARKER_REUSE_IMAGE_DARK. Dark-only variant." caption="Visible only when dark mode is active." width="160" >}}
390406

407+
### Light variant only
408+
409+
The `reuse-image-light` variant by itself (renders only when light mode is active):
410+
411+
{{< reuse-image-light src="img/test/light.svg" alt="MARKER_REUSE_IMAGE_LIGHT. Light-only variant." caption="Visible only when light mode is active." width="160" >}}
412+
391413
### Version-gated image
392414

393415
A version-gated image variant (only renders on v2 after remap) — uses `reuse-image` inside a `version` block:

fixture/assets/img/test/dark.svg

Lines changed: 1 addition & 1 deletion
Loading

fixture/assets/img/test/light.svg

Lines changed: 1 addition & 1 deletion
Loading

layouts/partials/themeExtras/head-end.html

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,64 @@
6969
If JS is disabled the class is never added, so the sidebar stays visible. */}}
7070
<script>document.documentElement.classList.add('sidebar-loading');</script>
7171

72+
{{/* Land the viewport on the URL hash without a visible "top first, then
73+
jump" flash AND without a post-reveal jump from late-loading images
74+
pushing content around.
75+
Strategy: when the URL has a hash, synchronously mark <html> with
76+
`hash-loading` BEFORE any paint. CSS (in docs-theme-extras.css)
77+
hides the body while that class is present. We KEEP it hidden until
78+
window.load (not DOMContentLoaded) so images / fonts / Mermaid have
79+
all resolved and the anchor's final position is locked in before
80+
the user sees anything. Then we scroll once (instant, invisible)
81+
and reveal. A 2s safety timeout caps the worst-case blank time so
82+
a stuck resource never traps the user in a permanently blank page.
83+
Trade-off note: keeping body hidden until `load` makes the
84+
perceived load slower than revealing at DCL, but it eliminates the
85+
post-reveal "jump" entirely. Worth it on hash navigations; the
86+
hash-loading class is not added at all on hash-less loads, so
87+
regular page visits aren't affected. */}}
88+
<script>
89+
(function(){
90+
if (!window.location.hash) return;
91+
var id;
92+
try {
93+
id = decodeURIComponent(window.location.hash.slice(1));
94+
} catch (e) { return; /* malformed hash */ }
95+
if (!id) return;
96+
97+
document.documentElement.classList.add('hash-loading');
98+
99+
var revealed = false;
100+
function scroll() {
101+
var el = document.getElementById(id);
102+
if (el) el.scrollIntoView({ behavior: 'instant', block: 'start' });
103+
}
104+
function scrollAndReveal() {
105+
if (revealed) return;
106+
revealed = true;
107+
scroll();
108+
document.documentElement.classList.remove('hash-loading');
109+
}
110+
111+
// Safety: cap the blank-page time. If `load` doesn't fire within 2s
112+
// (slow image, hung script, etc.), reveal anyway. The user will see
113+
// a small jump in that case — better than a permanently blank page.
114+
setTimeout(scrollAndReveal, 2000);
115+
116+
if (document.readyState === 'complete') {
117+
scrollAndReveal();
118+
} else {
119+
window.addEventListener('load', function(){
120+
// Two rAFs so any synchronous post-load handlers (Mermaid init,
121+
// etc.) get a chance to mutate the DOM before we measure.
122+
requestAnimationFrame(function(){
123+
requestAnimationFrame(scrollAndReveal);
124+
});
125+
});
126+
}
127+
})();
128+
</script>
129+
72130
{{/* Tab switching logic for custom tabs/tab shortcodes */}}
73131
<script>
74132
document.addEventListener('DOMContentLoaded', function(){

layouts/shortcodes/card.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434
{{- $image := .Get "image" -}}
3535
{{- $alt := .Get "alt" | default $title -}}
3636
<style>
37-
.hextra-card-styled { display: flex; flex-direction: column; justify-content: start; overflow: hidden; border-radius: 0.5rem; border: 1px solid #d1d5db; padding: 1rem; min-height: 7rem; text-decoration: none; color: inherit; transition: all 0.2s ease; background: transparent; }
37+
.hextra-card-styled { display: flex; flex-direction: column; justify-content: start; overflow: hidden; border-radius: 0.5rem; border: 1px solid #d1d5db; padding: 1rem; min-height: 10rem; text-decoration: none; color: inherit; transition: all 0.2s ease; background: transparent; }
3838
.hextra-card-styled:hover { border-color: #9ca3af; background: #f8fafc; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.07); }
3939
.dark .hextra-card-styled { border-color: #374151; }
4040
.dark .hextra-card-styled:hover { border-color: #4b5563; background: rgba(255,255,255,0.03); }
4141
.hextra-card-styled .card-title { font-weight: 600; color: #374151; }
4242
.dark .hextra-card-styled .card-title { color: #e5e7eb; }
43-
.hextra-card-styled .card-subtitle { margin-top: 0.5rem; font-size: 0.875rem; line-height: 1.4; color: #6b7280; }
43+
.hextra-card-styled .card-subtitle { display: block; margin-top: 0.5rem; font-size: 0.875rem; line-height: 1.4; color: #6b7280; }
4444
.dark .hextra-card-styled .card-subtitle { color: #9ca3af; }
4545
</style>
4646
<a class="hextra-card hextra-card-styled" href="{{ $link }}">
@@ -51,5 +51,11 @@
5151
{{- end -}}
5252
{{- if $icon }}{{ partial "utils/icon.html" (dict "name" $icon "attributes" "style='width:1.5rem;height:1.5rem;margin-bottom:0.5rem;'") }}{{ end -}}
5353
{{- if $title }}<span class="card-title">{{ $title }}</span>{{ end -}}
54-
{{- if $subtitle }}<p class="card-subtitle">{{ $subtitle | markdownify }}</p>{{ end -}}
54+
{{- /* Subtitle is a <span> (with display:block via CSS), not a <p>.
55+
If the card is wrapped by markdownify (e.g., from a percent-form
56+
{{%% %%}} shortcode like `version`), Goldmark wraps the card output
57+
in a <p>. A nested <p> inside that triggers the browser's
58+
auto-close-the-outer-<p> behavior: the <a> closes early and the
59+
description floats out of the card as an orphan sibling. */ -}}
60+
{{- if $subtitle }}<span class="card-subtitle">{{ $subtitle | markdownify }}</span>{{ end -}}
5561
</a>

layouts/shortcodes/openapi.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,14 @@
148148
SwaggerUIBundle({
149149
url: el.dataset.specUrl,
150150
dom_id: '#swagger-ui',
151-
deepLinking: true,
151+
// deepLinking hijacks window.location.hash for Swagger's own routing
152+
// (#/operations/... and #/components/schemas/...). On pages that also
153+
// have a Hextra TOC, clicking a TOC entry sets the hash to e.g.
154+
// #cards, then Swagger's hashchange listener rewrites it to #/cards
155+
// — breaking the page's normal anchor navigation. Off by default;
156+
// pages that are primarily API reference can re-enable via the
157+
// `deepLinking=true` shortcode arg.
158+
deepLinking: {{ eq (.Get "deepLinking" | default "false") "true" }},
152159
presets: [
153160
SwaggerUIBundle.presets.apis,
154161
SwaggerUIStandalonePreset

layouts/shortcodes/rebase.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@
8080
{{ $clean = replaceRE `\{\{<\s*(steps)\s*>}}` "{{% $1 %}}" $clean }}
8181
{{ $clean = replaceRE `\{\{<\s*(/steps)\s*>}}` "{{% $1 %}}" $clean }}
8282

83+
{{/* Convert `include` back to percent-bracket. Hextra's include shortcode
84+
calls .RenderShortcodes on the target page and returns its raw markdown.
85+
With angle-bracket form, that markdown is inserted AFTER the outer
86+
markdown pass, so backticks, links, and headings appear as literal text.
87+
The percent form lets the outer markdown engine re-process the inserted
88+
content. Hextra explicitly documents include as a percent-form-only
89+
shortcode for this reason. */}}
90+
{{ $clean = replaceRE `\{\{<\s*(include\s[^>]*)>}}` "{{% $1%}}" $clean }}
91+
8392
{{/* Adjust any relative doc links if needed */}}
8493
{{ $clean = replaceRE `\((\/?)docs\/` "(/" $clean }}
8594

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{{/* Light-only image variant. Symmetric counterpart to reuse-image-dark.
2+
Renders a single image wrapped in <div class="toggle-dark">, which CSS
3+
shows in light mode and hides in dark mode (see docs-theme-extras.css
4+
`.toggle-dark`/`.dark .toggle-dark`).
5+
Use when you have a light-themed asset that should NOT appear in dark
6+
mode at all — e.g., a screenshot whose dark-mode counterpart isn't
7+
ready yet and the white-card wrapper would look wrong, or a diagram
8+
whose colors are reference-specific. For the common "show this image
9+
in both modes" case, use the reuse-image shortcode; for dark-only,
10+
use reuse-image-dark. */}}
11+
{{ $src := .Get "src" }}
12+
{{ $width := .Get "width" }}
13+
{{ $alt := .Get "alt" }}
14+
{{ $image := resources.Get $src }}
15+
{{ $caption := .Get "caption" }}
16+
17+
{{/* Fallback: if the image isn't at assets/img/<file>, look for it at
18+
assets/<product>/<version>/img/<file>. Mirrors the lookup logic in
19+
reuse-image-dark.html so authors get consistent path resolution
20+
regardless of which variant they use. */}}
21+
{{ if not $image }}
22+
{{ $version := "" }}
23+
{{ range $part := split .Page.RelPermalink "/" }}
24+
{{ if not $version }}
25+
{{ range $.Site.Params.versions }}
26+
{{ if eq .version $part }}{{ $version = .version }}{{ end }}
27+
{{ end }}
28+
{{ end }}
29+
{{ end }}
30+
{{ if $version }}
31+
{{ $product := $.Site.Params.currentProduct }}
32+
{{ $trimmed := strings.TrimPrefix "/" $src }}
33+
{{ $versioned := printf "%s/%s/%s" $product $version $trimmed }}
34+
{{ $r := resources.Get $versioned }}
35+
{{ with $r }}
36+
{{/* Strip product prefix so publishDir doesn't double it */}}
37+
{{ $targetPath := printf "%s/%s" $version $trimmed }}
38+
{{ $image = resources.Copy $targetPath $r }}
39+
{{ end }}
40+
{{ end }}
41+
{{ end }}
42+
43+
{{- with $image -}}
44+
<div style="text-align: center;" class="toggle-dark"><figure><img src="{{ .RelPermalink }}" width="{{ $width }}" alt="{{ $alt }}"/> <figcaption style="font-style:italic">{{ $caption }}</figcaption></figure></div>
45+
{{- end -}}

layouts/shortcodes/reuse-image.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
{{ end }}
4343
{{ end }}
4444

45+
{{/* Emit light + dark variants in their respective toggle wrappers.
46+
.toggle-dark is shown in light mode, .toggle-light in dark mode (see
47+
docs-theme-extras.css). If $srcDark wasn't provided OR couldn't be
48+
resolved, fall back to rendering the light image in the dark slot
49+
too — better to show the light image in dark mode than nothing. */}}
4550
{{- with $image -}}
4651
<div style="text-align: center;" class="toggle-dark"><figure><img src="{{ .RelPermalink }}" width="{{ $width }}" alt="{{ $alt }}"/> <figcaption style="font-style:italic">{{ $caption }}</figcaption></figure></div>
4752
{{- end -}}
53+
{{- $darkRender := $imageDark | default $image -}}
54+
{{- with $darkRender -}}
55+
<div style="text-align: center;" class="toggle-light"><figure><img src="{{ .RelPermalink }}" width="{{ $width }}" alt="{{ $alt }}"/> <figcaption style="font-style:italic">{{ $caption }}</figcaption></figure></div>
56+
{{- end -}}

0 commit comments

Comments
 (0)