Releases: stamat/poops-docs-theme
Release list
v4.2.0
A row of glyphs told a sighted reader nothing a screen reader was not already
being told. The npm mark, the package cube, an emoji someone pasted in for a chat
room — the name was in aria-label, where a mouse and a pair of eyes never reach
it.
Added
-
site.iconLinksand the GitHub button carry a tooltip, shown on hover and
on focus by
<tooltip-elemental>,
dismissed with Escape. The words are thetitleyou already write: the element
takes the attribute over so the browser's own tooltip cannot double up, and
writes those same words back asaria-label, which is where they were before.
Nothing to configure and no new key inpoops.json.A touch screen has no hover, and the element ignores pointer events coming from
one rather than half-handling them, so a tap opens a link instead of a bubble.
Nothing is lost: the name is on the link the whole time, which is what a screen
reader reads either way. With scripting off the browser draws the native tooltip
from thetitlethe element never came to claim.New DOM in the topbar. Each icon link is now
tooltip-elemental > a.icon-btn + span, and the glyph inside the link is
wrapped in<span aria-hidden="true">—iconmay be an emoji or a pasted
<svg>, and text the name computation can read would otherwise leave a link
called "💬" described as "Discord". That wrapper isdisplay: flex, so it is
the size of the glyph rather than a line box with the glyph on its baseline —
inline, it lands the icon a few pixels above the middle of its button. A site
styling.topbar-actions > .icon-btnshould drop the>;.icon-btnitself is
unchanged.New custom properties.
--tooltip-elemental-surface,
--tooltip-elemental-colorand--tooltip-elemental-border-colorare
re-pointed at--fgand--bg; the element's own default isCanvasTexton
Canvas, which follows the operating system rather than the switch in the bar.
The bubble is also lifted toz-index: 30, over the drawer it can be opened
inside.
Fixed
-
A quote in a page's
descriptionno longer truncates the description.
Poops renders withautoescapeoff, so the hand-written
content="{{ page.description or page.excerpt or site.description }}"shipped
front matter verbatim — and one"in a sentence closed the attribute, leaving
the page described by the words before it. Both layouts now use Poops'
descriptionfilter, which reads the same chain and escapes what it emits:{{ page | description(site) }}
-
Every other value written into an attribute is escaped. Same cause, same
silence, and none of it needed a filter — both engines shipescape.lang,
data-theme,robots, the edit-linkhref, the brand and nav linkhrefs and
titles, and the footer's package links all go through it now, as does the
<title>element, where a<opens a tag rather than ending a value. The
favicon'ssite.brandMarkgetsurlencodeinstead: it sits inside adata:
URI, which wants URI encoding, not entities.Site-owned front matter and config, so this was a footgun rather than a
vulnerability — unless a site takes docs pages by pull request, where front
matter is somebody else's input.
Changed
poops >=2.5.0is now the peer range, up from>=2.0.0. Thedescription
filter arrived in 2.5.0, and an older Poops raises an unknown-filter error
rather than skipping it quietly.
Full Changelog: v4.1.0...v4.2.0
v4.1.0
The edit-link row asked readers to fix the page and told them nothing about how
stale it was. A page nobody has revised since a rewrite two versions ago reads
exactly like one corrected this morning.
Added
-
docsrenders aLast updated:line at the left of the edit-link row
whenever a page hasupdatedin its data — hand-written in front matter, or
filled in for every page by Poops ≥ 2.4.0'smarkup.options.lastUpdated, which
dates pages from a committed index of content hashes rather than from git or an
mtime a clone destroys. A page withoutupdatedrenders no line, and the edit
link stays exactly where it was. The date prints asMMM D, YYYYbeside a
machine-readable<time datetime>; the format is the theme's, not a new config
key.New DOM and a new class. The row is now
.edit-link-row > p.last-updated + a.edit-link, and it renders for a page carryingupdatedbut no
repo/filePathto link to — a case that previously rendered nothing at all.
.edit-link-rowgainedalign-items,flex-wrapandgap; it still ends
flush right, with.last-updatedpushed away from it by its own auto margin.
Below40remthe row turns into a centred column instead — wrapped, the date
hugging the left edge and the button the right read as two unrelated things. A
site overriding either class should look at it once.
Changed
-
The icon links and the theme switch go into the drawer with the links. On a
phone the bar held a brand, a search field, everysite.iconLinksbutton, the
GitHub link, the theme switch and a hamburger — so the title truncated to a few
characters to pay for controls the drawer under it had room for. They now move
into the drawer as the row under the links, and move back onto the bar when
<navbar-elemental>measures its way out of stack mode. Search stays: it is
already an icon at that width and expanding it is the reason to keep it in reach.Moved, not copied. One
<switch-elemental>in the page at every width, so
there is never a second switch left saying the theme is off while the page is
dark. With scripting off there is no drawer either, and the controls stay on the
bar exactly as before.New DOM and a moved custom property. In stack mode the row gains a final
<li class="drawer-actions" data-navbar-stack>holding those controls —
data-navbar-stackkeeps it out of the element's measurement, so it never
competes with a link for room on the bar.--actions-insetis now declared on
.topbar > navbar-elementalrather than on.topbar-actions: read from the
group the switch has just left, the padding it sets would compute to nothing. A
site overriding it on.topbar-actionsstill gets what it asked for on the bar
and should set it on the bar instead.
Full Changelog: v4.0.2...v4.1.0
v4.0.2
Fixed
- Bumps
book-of-elementalsto^0.7.2to pick up a fix for listboxes pointer events not
working on iOS
Full Changelog: v4.0.1...v4.0.2
v4.0.1
Fixed
Issue where the search results would stack on a row and not wrap properly that was fixed in
book-of-elementals 0.7.1. This theme now depends on the version carrying the fix.
Full Changelog: v4.0.0...v4.0.1
v4.0.0
Arrow keys did nothing. Enter did nothing. The panel had no role, the field never said it had
a popup, and results appearing in it announced nothing at all — a change a sighted reader
watches happen and a screen reader user is told nothing about, which is
WCAG 2.2 4.1.3 unmet. The
only way through a hit was the pointer.
The index behind it was fetched on every docs page whether or not anybody searched, and if
the file was not there the failure was swallowed: from then on every query answered
No results, which is a true sentence about a search that never happened.
The search index poops writes is every page's front matter copied out verbatim, and the
theme dropped a result's title, description and url straight into innerHTML. A <img src=x onerror=…> in one page's title: therefore ran on every page of the site, since the
search box is in the topbar.
The default footer also credited Poops and stopped there, so the layout, styles and script a
visitor is actually looking at had no name anywhere on the page. The 💩 trailed after the
sentence as a full stop of its own, and a screen reader read it out as "pile of poo".
Added
-
The search field is
<search-elemental>and<suggest-elemental>, which is the whole
keyboard the panel never had: ↓ and ↑ walk the results and wrap,
Home and End reach the ends once a row is under the cursor and stay
with the caret until then, Enter follows the row, Escape closes. The
cursor isaria-activedescendantrather than focus, so typing carries on while you look,
and the field now reports itself as a combobox with a popup instead of as a plain text box.
A settled search says5 results,No resultsorSearch failedin arole="status"
region — the announcement the panel filling itself never made.Two more things the reader gets: Escape empties the field rather than only
closing the panel, and focus leaving empties it too — this field is in the topbar of
every page, so a query left in it outlives the results it fetched, and on a phone a field
with something in it will not fold back into its icon.The DOM changes, and a site with its own rules for it needs them:
#search-resultsis gone.
The panel issuggest-elemental[open]holdingul > li > a[role="option"], the wrapper is
search-elemental.searchcarryingdata-state, and the row classes.sr-title/.sr-desc
are unchanged inside it..sr-emptystill exists but has moved out of the panel and is now
p.sr-note.sr-empty(see below). The panel's look is the element's optional theme with
--suggest-elemental-surface,--suggest-elemental-active,--suggest-elemental-radius,
--suggest-elemental-insetand--suggest-elemental-max-heightpointed at this theme's
tokens; anything that styled.search-resultsdirectly wants those instead.Measured against the same build before the change,
docs.min.jsgrows by 8.4KB and
docs.min.cssby 3.5KB — minified, before compression, and everything in this entry
included. -
The search field has a clear button a keyboard can reach.
<input type="search">gets a
cross of its own in Chromium and Safari, none in Firefox, and the one it gets is drawn by
the non-standard::-webkit-search-cancel-button— mouse-only, no tab stop, and absent from
the accessibility tree. It is switched off withappearance: noneand replaced by
button.search-clear: the octicon x at the magnifier's weight and distance from its end of
the field, a 24px target around a 16px glyph,aria-label="Clear the search", and a focus
ring, because a control you can tab to and cannot see you have reached is
2.4.7 Focus Visible unmet.
It isvisibility: hiddenwhile the field is empty, which takes it out of the tab order with
the look. Pressing it empties the field and hands the caret back.The field is 2rem of padding at both ends now, and the pending spinner moved one control
inward to--search-elemental-spinner-inset-inline: 2remso the two never share a corner. -
The empty and failed searches are drawn as well as spoken.
p.sr-note.sr-emptysays
No results andp.sr-note.sr-errorsays Search failed, in a box under the field,
shown bydata-statealone with no script involved. The words areempty-textand
error-texton the element, so what a reader sees and what the live region reads out is one
sentence rather than two that can drift.They sit outside the panel, which is the one place this theme departs from the element's own
advice: alistboxmay only ownoptions, so a "No results" row inside the panel is
aria-required-children— critical, andscript/a11yfails the build on it. Measured, not
assumed: the row was written that way first and the sweep caught it. -
/and ⌘K / Ctrl+K put the cursor in the search field. The field was reachable by
pointer or by tabbing the length of the topbar, and every docs site a reader arrives from
answers to one of these. The slash is ignored while an<input>,<textarea>,<select>
or acontenteditableholds focus — it is a character somebody may be mid-word in — and the
modifier pair works from inside a field, where it selects what is already typed. Shift is
where it stops —Ctrl+Shift+Kopens the web console and stays the browser's. What the two
do cost is Firefox's quick-find and itsCtrl+Ksearch bar, and Chrome'sCtrl+K
address-bar search, on a docs page. Nothing was added to the markup: no hint sits beside the
field. -
A grouped section in the mock site, and poops 2.3 to build it with.
Kitchen sinkand
Live samplesnow carrynavGroup: Samples, poops 2.3's front-matter grouping — the pages
stay where they are, urls and breadcrumbs unchanged, and only the sidebar gains the heading.
Filler with a job: a group is a section node with no page of its own, so it is the first
thing in the mock to render thenavtree.htmlbranch that omits the Overview link —
every other section there has an index page. The sweep and the unit suite cover it now
rather than by claim.poopsmoved to^2.3.0as a devDependency only; the peer range
stays>=2.0.0, because the theme reads the tree as data and an older poops just leaves the
field unread.
Changed
-
search-index.jsonis fetched on the first query, not on page load. The field is in the
topbar of every docs page and most visits never type in it, so the request went out for
everybody and paid off for a few. The first search of a visit now waits for the network and
gets a spinner while it does; every one after it answers from an index already in memory. -
The search field is 2.2rem tall, set rather than left to fall out of its padding and the
16px iOS floor, which came to 2.6rem — the tallest thing in a bar standing next to 2.25rem
icon buttons. -
book-of-elementalsmoves to^0.7.0from^0.5.0, which is where the two search
elements arrive. Nothing this theme already used changed shape: the hover tint the 0.6
release evened out across the book is re-pointed at--bg-althere and always was. -
The default footer names poops-docs-theme beside Poops, and the 💩 moved in front of
the Poops link witharia-hidden="true"on it, so it reads as decoration rather than as a
word. Both layouts change; a site settingsite.footerrenders its own html and is
untouched.
Fixed
-
A missing search index says so instead of reporting no results.
fetchresolves on a
404, and the load was wrapped in acatchthat threw the error away — so a site that never
generatedsearch-index.json, or one whose file was briefly unreachable, had a search box
that answered every query with No results for the rest of the visit. The response is
checked, the failure reaches the element as a rejection, and the field says Search failed
in the box and in the live region. The failed load is dropped rather than remembered, so the
next keystroke tries again. -
The mobile drawer no longer slides itself shut on page load. Closed is the state the
drawer arrives in —<disclosure-elemental>writes it at upgrade — but the transform
transition was live from the first frame, so on a load where the script landed after the
first paint (a cold cache, a slow phone) the browser animated the difference between the
rail the stylesheet had already drawn and the closed drawer the script asked for. The
transition rules now key off asidebar-nav-readyclass thatdocs.jsputs on
#sidebar-navwhen the toggle is first tapped, so a drawer nobody has touched cannot
travel. That first tap still slides — the class goes on before the element writes the state,
with a reflow between them. Counting animation frames instead was tried and measured: a
closed panel painted for two frames still slid in from nothing when the rule arrived.
Crossing the breakpoint before any tap now snaps instead of sliding, which is what the
element already does with the state itself. A site overriding
#sidebar-nav { transition: … }needs the class in its selector now. -
Escape no longer closes the sidebar rail on a wide screen. Escape and the scrim are the
light dismiss a drawer over a scrim wants and the disclosure pattern does not owe it — but
the keyboard is on the page at every width, and<disclosure-elemental>writesopenfrom
itsmediaquery only when the query changes. A query that still matches changes nothing,
so Escape above 60rem closed the rail and nothing put it back: the navigation was gone for
the rest of the visit, with the toggle that would reopen itdisplay: noneat that width.
The close now returns early while the element reports `da...
v3.1.1
A nav nested three levels deep put a <ul> directly inside a <ul>, with no <li> between
them. Invalid HTML, and the counts a screen reader reads out of a list come from that
nesting — so the third level and everything under it was announced wrong. Two levels were
fine, which is why nothing caught it: the mock site stopped at two, so script/a11y never
rendered the markup and reported green over a branch it had never seen.
Fixed
navtree.htmlemits a valid list at any depth. The per-node<li>moved into its own
navnodemacro and the recursion goes through that, so a nested section arrives as an
<li>rather than as the<ul>the oldnavtreecall opened with. Only sites with three
or more nav levels produced the broken markup; their DOM changes, everything shallower is
byte-identical.
Added
- A third nav level in the mock site.
preview/src/docs/guide/deep/— a section inside
Guideand one leaf under it. Filler pages, but they are what makes the recursive branch
ofnavtree.htmlrender at all, and reverting the fix now fails the sweep with
list (serious)on every page instead of passing.
Full Changelog: v3.1.0...v3.1.1
v3.1.0
Nothing measured this theme's own accessibility. The unit suite runs in jsdom, which has no
layout and no colours, and the sweep in
book-of-elementals deliberately audits the
<code-preview> iframes and excludes the pages around them — because those pages are this
theme's markup, and reporting them there would file this repo's bugs against that one. The
hole was exactly the shape of the chrome: the topbar, the drawer, the nav tree, the prose
styles and every contrast ratio in the palette.
Added
-
script/a11y— axe over the preview site, in Chromium. Each page as served, then with
everything that says it is closed opened, then with the search panel showing hits and
again showing its empty state. The document-level rules stay on, which is the opposite
call from the sibling sweep and for the opposite reason: a landmark, a skip link, a title
and alangare a fragment's business nowhere and this theme's business exactly.Two viewports, not one. The bar folds into a drawer below
40remand the sidebar
toggle only exists below60rem, so the markup a phone gets is markup a desktop never
renders. A sweep at one width audits half the theme and reports as though it did all of
it. Two themes as well, seeded throughlocalStoragebefore the page loads rather than
set on the document after: the boot script reads that key and the topbar switch seeds
itself from what it chose, so settingdata-themeby hand would audit a dark page with a
switch still reporting itself off — a disagreement the sweep would have introduced and
then measured.It also fails on an
aria-controls,aria-labelledby,aria-describedbyor
aria-activedescendantnaming an id no element has. axe will not decide that one — a
collapsed toggle may legitimately point at a panel not in the document yet — so a typo in
one otherwise fails no run anywhere. None are dangling today.Contrast axe cannot compute, over a pseudo element or under something overlapping it, is
printed by rule and by reason rather than guessed at. New devDependencies:axe-coreand
playwright-core— the same pair the sibling repo uses,playwright-corebeing the one
that ships no browser, sonpm cidoes not download one.It found 161 violations across four rules on the first run. They are the Fixed list
below, and CI now runs the sweep, so the count stays at nought. -
A skip link, first in the body of both layouts. Reaching the prose from a keyboard meant
tabbing the whole topbar first — brand,docspill, search, everysite.linksentry, the
GitHub link, the theme switch — and on a docs page the entire nav tree after it: every
heading of every section, on every page, before the one you asked for. That is
WCAG 2.4.1 Bypass Blocks,
and the sweep above did not catch it: axe'sbypassrule is satisfied by a<main>
landmark, which both layouts have had all along.Off-screen until focused rather than
display: none— a hidden element is out of the tab
order, which is the one thing this link cannot be. No script::focusis the keyboard
intent, and a pointer never reaches the link to see it.DOM change: both layouts gain
<a class="skip-link" href="#content">as the first child
of<body>, and<main class="content">gainsid="content"andtabindex="-1". A site
already using#contenthas a duplicate id to resolve. Thetabindexis what makes the
jump land: a fragment pointing at an element that cannot take focus moves the viewport and,
in Safari, leaves focus on<body>— so the next Tab goes back to the topbar the link just
skipped. CSS change:.content:focusdrops the focus ring with it, because the theme's
2px outline drawn around the whole column reads as breakage rather than as focus arriving.
Fixed
-
Syntax highlighting and admonition titles now meet AA, in both themes. The light code
scheme ran 2.8:1 to 4.0:1 on--bg-code— a set of hues chosen against each other rather
than against the surface behind them. Each is now the same hue darkened until it clears
4.5:1, keeping 75% to 92% of what it was, so the scheme still reads as itself. The dark
scheme was already 6.2:1 to 11.5:1 and is untouched.Admonitions were worse and failed at both ends, because
--admis one value doing three
jobs: the border, the 7% tint behind the title, and the title's own text. One mid-range hue
cannot be text on a white tint and text on a dark one —importantread at 1.8:1 in
light,cautionat 3.7:1 in dark. Light now takes a darkened set and dark keeps the
original where it already cleared. Yellow is the one that could not keep its face: nothing
about a 7%-tinted white leaves room for#fab005, soimportantis a dark gold in light
now. CSS change: the--admvalues, and a site overriding them should recheck its own. -
The dark code scheme reaches a reader whose dark mode came from the OS. The token
colours were under:root[data-theme="dark"]alone, while_base.scsssets its own tokens
under that andprefers-color-scheme. With the script blocked the attribute never lands,
so the surface went dark and the syntax colours stayed light — the one pairing neither set
had been measured against. Both now come from one mixin applied in both places. -
A code block and a wide table can be scrolled from the keyboard. Both scroll sideways
and neither could be reached without a pointer: nothing inside apretakes focus, since
the copy button is a sibling in the wrapper rather than a child. Both now take
tabindex="0". Unconditionally, not from a measurement — whether the content is wider than
the column is a question the viewport answers, and an answer taken once is wrong at the
first resize. DOM change:.prose preand.prose tablegaintabindex="0". -
The phone search field no longer leaves focus underneath itself. Opening it expands it
across the whole topbar, by design — but the brand, the nav toggle and the icon links stayed
in the tab order behind it, so tabbing out of the field put focus on a GitHub link no one
could see. That is WCAG 2.2's Focus Not Obscured (2.4.11), and it was a tap target too: axe
measured 1px of the nav toggle left uncovered. Everything the open field covers is now
visibility: hiddenfor as long as it covers it, which takes it out of the tab order and
the accessibility tree together. -
The heading permalink is not a tab stop on nothing. Poops writes it
aria-hidden="true"
and focusable, so a keyboard landed on it once per heading with nothing to announce. Fixed
at the source in poops, butpoopsis a peer at
>=2.0.0and the versions this theme supports include the ones that write it — so the
theme repairs it on load as well, and is measured green against a build that has not got
the fix. That patch goes when the peer floor rises past the fixed release.
Full Changelog: v3.0.1...v3.1.0
v3.0.1
A page whose front matter title matched site.title — which a one-page site's landing
page usually does — rendered <title>Hydrargyri · Hydrargyri</title>. The suffix went on
whenever page.title was set at all, without asking what it was being appended to.
Fixed
- The site name is not appended to itself. Both layouts add the
· {{ site.title }}
suffix only whenpage.titlediffers from it; when the two match, the tab, the bookmark
and the search result read the name once. A page with notitleis unchanged, and so is
every reference page, whose title differs by definition.og:titlewas never affected —
poops emitspage.title or site.titlethere, with no suffix.
Full Changelog: v3.0.0...v3.0.1
v3.0.0
Added
-
<kbd>is styled in prose. Nothing painted it before, so a key name rendered as
body text and read as the word beside it. It takes the inline-code sizing —--bg-alt
fill,--font-mono,0.8em,nowrapso a key never wraps mid-name — plus a cap edge:
a 1px border and a0 2px 0shadow under it, both from--kbd-edge. That is a local
mix of--fginto--border, which darkens on light and lightens on dark; a fixed
color would have vanished into one theme or the other. A chord nests per the spec —
<kbd><kbd>Ctrl</kbd> + <kbd>C</kbd></kbd>— where the outer element groups rather than
names a key, so:has(kbd)takes the cap off it and leaves the inner two.CSS: the selector is
.prose kbd.--kbd-edgeis declared on that rule rather than
on:root, same as--admon admonitions, so a site retuning the edge sets it in its
own.prose kbdblock — setting it higher up will not reach.
Changed
-
The copy button on a code block is
<copy-elemental>now. The one the theme drew
itself did the visible half and none of the other. It swapped an icon to a tick and told a
screen reader nothing at all, which is
WCAG 2.2 SC 4.1.3 Status Messages
unmet — and a clipboard write that failed swapped nothing, so a refused copy and a button
nobody pressed looked exactly alike. Both halves are what the element exists for, so the
theme takes it rather than growing a live region of its own.book-of-elementalsis
^0.5.0(was^0.4.0), which is the release the element arrived in.What a reader gets: the tick is said out loud as well as drawn; a failed copy is red
and says Copy failed in the same corner tooltip the success uses; and on a page served
over plainhttp, wherenavigator.clipboarddoes not exist to be asked, there is no
button rather than one that quietly does nothing. What lands on the clipboard is also
trimmed now — leading newlines and trailing whitespace, so a pasted block does not run its
last command on arrival, and indentation is left alone.DOM this changes: the
.code-wraparound each.prose prestays. Inside it the
button is now
<copy-elemental for="…"><button data-tip data-tip-error></button></copy-elemental>, plus
the<span role="status">the element appends for the announcement. The button has lost
its.copy-btnclass and its two inline<svg>s — the icon is a CSS mask. A<pre>with
noidis givencode-block-N, sinceforis how the element finds what to copy; an id
the page already uses is stepped over rather than taken.CSS this changes: every
.copy-btnrule is gone, and a site overriding one is
overriding nothing. The button iscopy-elemental > button, its states are
copy-elemental[data-state="copied"]and[data-state="error"], and the stand-down inside
a live sample iscode-preview copy-elemental { display: none }. The look is the element's
own theme with four properties re-pointed:--copy-elemental-surfaceto--bg,
--copy-elemental-border-colorto--border,--copy-elemental-hoverto--bg-alt,
--copy-elemental-icon-sizeto1rem.Canvasis what the first of those had been —
the UA's page colour, which is a shade off this theme's own on a dark page. -
The sidebar drawer inherits 0.5's
<disclosure-elemental>fixes. Its region — which
here is#sidebar-nav— is nowdisplay: flow-root, so a region whose first or last child
carries a margin no longer slides open past where it sits and snaps back, and it carries
data-state="open"/"closed"alongsidehidden. Nothing in this theme moves: the rail's
inset is on.sidebar > .navand nothing in there was collapsing a margin out through the
edge. A site that gave the region adisplayof its own still wins — the rule is one
class — but one leaning on a child margin escaping it has that to put back.
Fixed
-
Tapping the search field no longer zooms iOS into the topbar. Safari zooms the page
whenever a field under 16px takes focus, and it does not zoom back out when the field is
blurred — so one tap on search left the reader scrolled sideways through a magnified page
with no way back but a pinch.#search-inputwas0.9rem, which is 14.4px at the default
root size. It ismax(16px, 1em)now: 16px is Safari's threshold verbatim, so it holds even
for a reader whose root size is smaller, which a plainremwould follow straight back under
it; the1emarm lets a scaled topbar carry the field up.It is not a phone-only bug and it is not gated behind a media query, because the two queries
that look like they would scope it both miss the same device: an iPad with a trackpad
attached clears the 40rem breakpoint and reportspointer: fine, and its screen still
gets tapped. A site retuning the field wants a floor of its own, not a barefont-size.
Changed
-
Code blocks carry the same 16px floor as the search field.
.prose prewas0.85rem
— 13.6px on a default root, small enough on a phone that reading a snippet meant pinching,
and pinching a block that scrolls sideways zooms the page instead. It ismax(16px, 1em)
now: body size in an ordinary article, and never under the size iOS zooms below. That
second half is not only about legibility — acode-previewpane can make a block editable,
and an editable block is a field Safari will zoom into on focus like any other.
The extra width goes to theoverflow-xthe block already had.Inline code and
<kbd>are unchanged: both are sized inem, so they follow whatever text
they sit in rather than the root, and neither can take focus.CSS: the selector is
.prose pre. A site that wants the old density should keep a floor
rather than replace the declaration outright — a barefont-size: 0.85remthere puts an
editable preview back under the threshold.
Full Changelog: v2.0.0...v3.0.0
v2.0.0
The theme carried its own navigation. A dark-mode button that left its state to an
icon. A row of links that folded into a hamburger at a width typed into a stylesheet,
with a focus trap and an arrow-key walker behind it. A sidebar drawer driven by a
matchMedia listener. Three patterns, none of them this theme's business, and each one
a place where the markup and the ARIA could drift apart. They are
book-of-elementals elements now —
<switch-elemental>, <navbar-elemental> and <disclosure-elemental> — and what is
left here is the layout around them.
The switch brought its own three bugs on a phone, all of them width: it is wider than
the icon button it replaced, and the bar it sits in had no room spare.
Changed
-
The
site.linksrow is<navbar-elemental>, and the topbar is that element. The
old row folded into a hamburger below 40rem and did nothing at all above it — so a
site with five links and a search field had them overlapping at 900px, and a site with
two short ones hid both on a tablet with room to spare. Neither is a width anybody can
type: it depends on the labels, the reader's font, and whether that font has arrived
yet. The element measures the row instead. Links move into a More panel one at a
time as the room goes, and when only one is left beside More the whole row is a drawer
— 40rem is still there as themediaattribute, but now it is the floor rather than the
whole story. That last stop ismin-bar-items="2": a single link beside an overflow button
is a drawer wearing a bar's clothes. The theme writes the attribute only for a site with
more than one link insite.links, because the threshold is read against the total as well
as against how many fit — set on a one-link site it would be a drawer at every width.DOM: the header is
<header class="topbar"><navbar-elemental>wrapping everything
in the bar. The links are<nav class="rail" aria-label="Site"><ul>between the brand
and the action group — the box takes the room the bar leaves, which is what the row is
measured against, and the links sit at its far end so they read as part of the controls
on the right rather than as a second brand. They end in a<li data-navbar-more>the element fills, and the
drawer's button is<button data-navbar-toggle aria-label="Site navigation">last in
the action group — empty, because the element writes the hamburger and the X it crosses
into. Gone:.topbar-nav,ul.topbar-links#topbar-links,.menu-toggleand its svg.
The sidebar's toggle is labelled "Documentation navigation" rather than "Toggle
navigation", now that a docs header has two navigation toggles in it.CSS:
.topbaris no longer the flex row — it is the sticky frame and the banner
landmark, and.topbar > navbar-elementalis the row, carrying the 1rem inset the
header used to have. That move is load-bearing: the drawer is positioned against the
element, so padding outside it would be a drawer floating clear of both edges of the
screen. The theme takes the element's two stylesheets and re-points
--navbar-elemental-surface,--navbar-elemental-border,--navbar-elemental-hover,
--navbar-elemental-shadowand--navbar-elemental-radiusat its own tokens.
A page styling.topbar-linkswants.topbar .railinstead. The
@media (max-width: 60rem)block that grew the search field is gone — the rail is the
flexible item now, and the field keeps its 16rem.Known limit: a sticky header is a positioned ancestor, so it is the containing
block for the overflow panel and the element'sposition-try-fallbackscannot fire
against the viewport. The panel stays under its own button, which is where it wants to
be; a bar whose More button ends up hard against the right edge is the case where
that shows.Script:
setupMenu()andfocusStep()are gone fromsrc/prose.ts— some seventy
lines, and the four tests that covered them with them, because what they implemented is
now the element's and jsdom cannot exercise a row that measures itself.prose.ts
importsbook-of-elementals/navbar, so both bundles register it. -
The sidebar drawer is
<disclosure-elemental>. It was amatchMedialistener, a
class on the panel and a focus trap; the panel is the element's region now and the
breakpoint is itsmediaattribute, declared once indocs.htmlrather than in the
markup and the stylesheet both. It is no longer modal — focus is not trapped and the
article is notinert, which is the APG disclosure pattern for what is, after all, a
list of links to the same site. What the theme still owns is the two ends the pattern
does not owe you: focus handed to the current page's link when the drawer opens, and
Escape or the scrim closing it.DOM:
<disclosure-elemental for="sidebar-nav" media="(min-width: 60rem)">wraps
the toggle; the element writesaria-expanded,aria-controlsand
hidden="until-found", so a closed drawer is reachable by find-in-page. CSS: the
drawer's rules key off[data-mode="free"]on the element and on the panel instead of
repeating 60rem, which also means none of them can apply before the script does. The
sidebar's own section toggles took the same caret while they were at it: the▸/▾text
markers are the chevron the elementals draw, as a mask onsummary::beforethat rotates
with[open]— so every caret on the page is one caret, and it takes its colour from the
text it sits beside. -
book-of-elementalsis^0.4.0(was^0.3.0). The row needs 0.4's two fixes to
be usable at all: the copy it measures is clipped, so a header whose links do not fit
no longer hands the whole page a horizontal scrollbar, and its items say
box-sizing: border-boxthemselves rather than assuming the page has. -
The theme toggle is
<switch-elemental>from
book-of-elementals, which is a new
dependency of this package. It writesrole="switch"andaria-checked, so the
state is announced rather than drawn, and the accessible name is "Dark mode" —
what the setting is, not what pressing it does, since "Toggle dark mode, switch,
on" says it twice.DOM: the topbar now emits
<switch-elemental class="switch-elemental-small switch-elemental-thin"><button data-theme-toggle>with a.switch-elemental-off
and a.switch-elemental-onspan inside it, in place of<button class="icon-btn" data-theme-toggle>with.theme-sunand.theme-moonsvgs. Both old classes are
gone, and so are the four rules that swapped them. The two icons are 14px, sized
to the knob thesmallpreset leaves rather than to the bar.CSS: the theme imports the element's own two stylesheets and re-points
--switch-elemental-knob-checkedto--bg, since the element's default there is
Canvasand a themed dark page keeps a light one. The size is the element's own
smallandthinpresets, 2.75rem × 1.5rem with a 1px border, taken as classes
rather than set here. The rest of the look is mixed out ofcurrentcolor, so a
page that themes the topbar's text colour themes the switch with it. Anything that
was styling.theme-sunor.theme-moonshould style the element's own two spans
instead. -
The topbar's right-hand group is spaced by inset, not by gap. An icon button
is a 2.25rem box around a 20px glyph, so it brings 8px to each edge and the switch
and the search field bring none — a singlegaplands on top of the first and on
nothing beside the second, and the icons read further apart than anything else in
the row. No number fixes that; the padding had to move.CSS:
.topbar-actionsisgap: 0and declares--actions-inset(0.5rem),
which.searchandswitch-elementaltake aspadding-inline—
the icon buttons already have it in their box. Any two neighbours are now 16px
apart.switch-elementalisdisplay: flexrather than the element's own
contents, so it has a box to pad, and.search-resultshangs off
--actions-insetrather than 0 so the panel still lines up with the field. A page
overriding the group's spacing wants--actions-inset, notgap.The group's outer edge moves in by that inset too: the last control's ink sits
24px from the viewport rather than 16px, against the brand's 16px on the left.
The docs layout already looked like that — its leftmost control is an icon
button. -
The site title truncates instead of pushing the bar wider.
.brandis a block
withtext-overflow: ellipsisrather than a flex row, and.brand-markkeeps its
place onvertical-alignand a margin instead ofalign-itemsandgap. A page
overriding either should check it still lands.
Added
-
--dangertoken,#e03131light and#ff8787dark, alongside--linkand
--focus. Nothing in the theme itself paints an error yet; the token exists so
elements embedded in a page can find one. It matches the name sulphuris already
emits through$color-aliases, so a component styled against one is styled
against the other. -
.prose code-preview > :is(pre, .code-wrap) { margin: 0 }, so a page using
<code-preview>no longer has to
write it. The element resets that margin itself, but itscode-preview > :is(pre, .code-wrap)is one class and one type against this theme's two-class
.prose :is(figure, .code-wrap)— so the theme's 1.75rem came back as a gap between
the frame and the code under it, doubled oncedocs.jshad wrapped theprefor its
copy button. Every site using the element was copying the same rule out of the
package's README; the specificity is the theme's, so the rule is now too.The package itself stays a non-dependency and is in neither bundle: a docs site with
no ...