diff --git a/AGENTS.md b/AGENTS.md index cc594b3..b9c7e0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -238,3 +238,16 @@ site behavior for the next person. for the full story). Truncate strings in PHP instead, guarded with `function_exists('mb_substr')` falling back to `substr()`, and pass the already-truncated value into the template. +- Same class of bug, different filter: don't use Twig's `|escape('js')` (or + the `'js'` arg to `|e`) either — it calls `Twig\Runtime\mb_ord()` + (`novaconium/vendor/twig/src/Extension/EscaperExtension.php`), which + hard-requires `mbstring` the same way `|slice` does, and fatals + identically without it. Hit for real when + `novaconium/pages/_layout/code-copy.twig` used it to pass SVG icon + markup into an inline ` diff --git a/novaconium/pages/_layout/icons.twig b/novaconium/pages/_layout/icons.twig index 65f6ef1..8761aa7 100644 --- a/novaconium/pages/_layout/icons.twig +++ b/novaconium/pages/_layout/icons.twig @@ -6,9 +6,9 @@ surrounding link/text color, including on :hover, with no extra CSS. Available: home, git, book, link, sitemap, email, search, rss, tag, - lock, trash, external_link, menu, back_to_top, sun, moon. Some - (search, rss, tag) are ahead of the features that will use them (see - novaconium/ISSUES.md) — added now so those features don't need an + lock, trash, external_link, menu, back_to_top, sun, moon, copy, check. + Some (search, rss, tag) are ahead of the features that will use them + (see novaconium/ISSUES.md) — added now so those features don't need an icons.twig change later. #} {% macro home(class) %} @@ -144,3 +144,16 @@ {% endmacro %} + +{% macro copy(class) %} + +{% endmacro %} + +{% macro check(class) %} + +{% endmacro %} diff --git a/novaconium/pages/_layout/layout.twig b/novaconium/pages/_layout/layout.twig index 36ec68f..497bae2 100644 --- a/novaconium/pages/_layout/layout.twig +++ b/novaconium/pages/_layout/layout.twig @@ -36,5 +36,6 @@ + {% include '_layout/code-copy.twig' %} diff --git a/novaconium/pages/admin/docs/styling/index.twig b/novaconium/pages/admin/docs/styling/index.twig index a1b151b..c2da398 100644 --- a/novaconium/pages/admin/docs/styling/index.twig +++ b/novaconium/pages/admin/docs/styling/index.twig @@ -57,4 +57,8 @@ docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app novaconium-sass \

The toggle button in novaconium/pages/_layout/nav.twig flips a data-theme attribute on <html> at runtime and persists the choice to localStorage. novaconium/pages/_layout/theme-init.twig, included early in <head> before the stylesheet, re-applies a saved choice before first paint on every later page load, so switching to light doesn't flash dark first. The sun/moon icon swap inside the button is pure CSS reacting to the attribute — no JS involved there — so it works correctly even on sidecar-less pages that get statically cached.

To customize the light theme the same way you'd customize the dark one, edit the -light variables in App/sass/_colors.sass and recompile.

+ +

Copy-to-clipboard on code blocks

+ +

novaconium/pages/_layout/code-copy.twig, included once from _layout/layout.twig's footer, injects a hover-revealed copy button into every <pre> containing a <code> on the page — no per-page markup needed. It copies via code.textContent (not innerHTML), so HTML-entity-escaped samples like &lt;h1&gt; in the SEO starter template come out as literal characters, not escaped markup. Uses the same event-delegation pattern as the theme toggle in _layout/nav.twig: one document-level click listener rather than a listener per button.

{% endblock %} diff --git a/novaconium/sass/main.sass b/novaconium/sass/main.sass index 6f8c846..b189442 100644 --- a/novaconium/sass/main.sass +++ b/novaconium/sass/main.sass @@ -131,6 +131,7 @@ code font-size: 0.9em pre + position: relative background: var(--surface) border: 1px solid var(--border-color) border-radius: 6px @@ -142,6 +143,35 @@ pre padding: 0 color: var(--text-color) + &:hover .copy-code-button, .copy-code-button:focus-visible + opacity: 1 + +.copy-code-button + position: absolute + top: 0.5rem + right: 0.5rem + display: inline-flex + align-items: center + gap: 0.3rem + background: var(--bg) + border: 1px solid var(--border-color) + border-radius: 4px + color: var(--muted-color) + font-size: 0.75rem + padding: 0.25rem 0.5rem + cursor: pointer + opacity: 0 + transition: opacity 0.15s ease + + &:hover + background: var(--bg) + color: var(--text-color) + border-color: var(--accent) + + &.copied + color: var(--accent) + border-color: var(--accent) + hr border: none border-top: 1px solid var(--border-color) diff --git a/public/css/main.css b/public/css/main.css index cb55de6..99e1081 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -133,6 +133,7 @@ code { } pre { + position: relative; background: var(--surface); border: 1px solid var(--border-color); border-radius: 6px; @@ -144,6 +145,36 @@ pre code { padding: 0; color: var(--text-color); } +pre:hover .copy-code-button, pre .copy-code-button:focus-visible { + opacity: 1; +} + +.copy-code-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: inline-flex; + align-items: center; + gap: 0.3rem; + background: var(--bg); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--muted-color); + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; +} +.copy-code-button:hover { + background: var(--bg); + color: var(--text-color); + border-color: var(--accent); +} +.copy-code-button.copied { + color: var(--accent); + border-color: var(--accent); +} hr { border: none;