Skip to content

feat: put the skin switcher in the toolbox and drop the footer select - #351

Merged
lens0021 merged 7 commits into
mainfrom
claude/tools-portlet-skins
Aug 15, 2026
Merged

lens0021 merged 7 commits into
mainfrom
claude/tools-portlet-skins

Conversation

@lens0021

Copy link
Copy Markdown
Collaborator

Stacked on #346 (claude/tools-portlet), which it modifies — review that first.

The toolbox is emptied because everything makeToolbox() produces is a Special:
page or an index.php query. The other skins' copies of the current page are
neither: the bake writes them as real files, so they are honest static links, and
they give the portlet something to hold instead of nothing.

The crux: is the href computable at render time? Yes

targetUrl() works in the browser because it reads location.pathname and derives
the page's depth from wgPageName. A render-time href does not need any of that,
because it does not need to be depth-correct when it is written.

Every link in the export is written root-relative — Main::onGetLocalURL() emits
./Name.html — and Rename later moves subpage files into real directories and
calls RelativeUrl::reparent(), which adds a ../ per level to every href="./…"
and href="../…" in the page. The non-main-skin canonical already relies on exactly
this, writing "../$name.html" from Main::onOutputPageAfterGetHeadLinksArray().

So the switcher writes the same form — ./ from the main skin's output, ../ from
a skin subdirectory — and reparenting carries it with everything else. Confirmed on a
real bake with a Manual/Deep subpage:

page → main → citizen → minerva
Installation.html (current) ./citizen/Installation.html ./minerva/Installation.html
Manual/Deep.html (current) ../citizen/Manual/Deep.html ../minerva/Manual/Deep.html
citizen/Installation.html ../Installation.html (current) ../minerva/Installation.html
citizen/Manual/Deep.html ../../Manual/Deep.html (current) ../../minerva/Manual/Deep.html

Every link in the baked tree was resolved against the file it sits in: 12 pages
carry the switcher, 28 links, 0 broken
, none escaping dist/.

1. The non-clickable current entry

Core renders an entry with no href as plain escaped text (SkinComponentLink), so
the <li> is still there with the skin's own text-wrapper:

<li id="t-wikven-skin-vector-2022" class="mw-list-item active"><span>Vector (2022)</span></li>
<li id="t-wikven-skin-citizen" class="mw-list-item"><a href="./citizen/Installation.html"><span>Citizen</span></a></li>

Vector and Citizen both render it, and both ignore an icon key on a toolbox entry
entirely. Neither styles .active, so unstyled text next to styled links reads as
broken rather than selected; one rule in ext.Wikven.styles.less marks it instead.

Labels were coming out as raw keys (vector-2022, citizen, minerva) — the footer
<select> had the same bug — because getInstalledSkins() returns only an explicit
displayname from skin.json and never resolves the skinname-* message. Timeless
looked right purely because it declares one. Now read from the message first:
"Vector (2022)", "Citizen", "MinervaNeue".

3. Minerva — and a landmine

Minerva does not take entries the way the others do. DefaultOverflowBuilder
keeps only entries carrying an icon, so with plain entries Minerva renders zero
of them. Adding an icon so they appear, while the current entry has no href, fatals
the bake
:

TypeError: SingleMenuEntry::__construct(): Argument #3 ($url) must be of type string,
null given, called in DefaultOverflowBuilder.php on line 99
Wikven: build failed for skin 'minerva' (exit 255)

The return $href ? … : null on the next line is dead code — the constructor already
threw. This is the same $sidebar['TOOLBOX'] path that made unset() fatal in #346.

So Minerva gets icon + href on every entry, current skin included, and the entry
shapes that fatal are never constructed. The cost is that Minerva's current skin is a
clickable self-link — Minerva's SingleMenuEntry has no notion of active, so it
cannot be marked there.

Is it reachable? Yes, and I checked rather than assuming. The links land in the
"More" (ellipsis) menu of the page-actions bar, beside "Edit full page" — a visible
button, one tap, no scrolling — and the bar is present on every exported page,
main page and subpages included (checked across all four pages of the bake). It is
not the tidiest home for it: that menu is page actions, and the icon (listBullet)
is borrowed, because Minerva's overflow icon pack has nothing appearance-related.
Sensible enough to ship; a touch incongruous.

2. The footer <select> — removed

The precondition holds: Vector 2022 (Tools dropdown), Citizen ("More actions" card)
and Minerva (page-actions overflow) all carry the switcher, on every page. So the
control is gone, and with it:

  • resources/skin-switcher.js and its ext.Wikven.skinSwitcher module — the prize: the navigation logic stops existing rather than being maintained.
  • The wgWikvenMainSkin JS config var. The global stays; the build still needs it.
  • Adder::skinSwitcher() and the skin-switcher footer item.
  • The wikven-skin-switcher-label message from en.json/qqq.json.

Nothing reaches the module by a non-obvious path: buildScripts.php discovers
modules from what rendered pages queue, so dropping addModules() was enough —
targetUrl no longer appears in modules-static.js in a real bake.
rewriteScripts.php never referenced it.

4. What happens to #346

Keep it, and this branch changes how it applies. A filled toolbox must never be
hidden — it is now the only switcher there is — so the rules move out of the
always-loaded stylesheet into ext.Wikven.emptyToolbox, added only when fewer than
two skins are enabled.

That direction matters: if the gate is ever wrong, a single-skin site shows an empty
box (cosmetic). Hiding-by-default with a CSS escape hatch would fail the other way
and lose the switcher, so the gate is in PHP, mirroring the #p-search precedent.
Both paths verified on real bakes — multi-skin emits no such stylesheet and links
none; single-skin emits and links it with the rules intact.

Worth knowing: with stock default.yml a .wikven.yml adds to skins: [Vector],
so a single-skin export is always Vector-only. The Timeless and Citizen selectors in
that module are unreachable in practice; kept as they are cheap and mirror #346.

Tests

skin-switcher.spec.js is rewritten, not deleted — same journeys (main → non-main,
non-main → non-main, back to root, on a translated page and a source page), now by
opening each skin's tools control and clicking the link. It discovers the skin list
from the page instead of naming skins, so it survives Timeless leaving and Minerva
arriving. tools-portlet.spec.js becomes the reachability test: open each skin's
control, assert a switcher link is actually visible.

How this was verified

Docker Hub layer blobs are egress-blocked here, so the bake was reproduced without
Docker (MediaWiki 1.46 + mediawiki-vendor, Vector / Citizen / MinervaNeue at pinned
refs, WikvenSettings.php, maintenance/build.php) over a source tree shaped like
docs/, with a Manual/Deep subpage added to exercise depth.

  • Baked Vector 2022 + Citizen + Minerva and read the output per skin, not the stylesheets.
  • Resolved all 28 switcher links against the filesystem: 0 broken.
  • Baked the failure variants too: plain entries (Minerva renders none) and iconed-without-href (fatal).
  • Laid pages out with WeasyPrint (real cascade + layout, screen media): all three skin names render on all three skins' pages, at depth 0 and depth 1.
  • Confirmed the footer control and targetUrl are absent from the baked output.
  • composer phpcs, mago format --check, mago lint, biome ci pass.

Not verified

  • No browser here (cdn.playwright.dev blocked, Ubuntu chromium is a snap stub), so the Playwright specs have not been run. They are the only check on the opening of each skin's control — the disclosure selectors are read from baked markup, not exercised. CI runs them.
  • WeasyPrint does not evaluate min-width media features, so the Vector rule in ext.Wikven.emptyToolbox is unverified locally; and a single-skin export is always Vector, so that module's effect could not be laid out at all here.
  • Minerva came from a locally assembled tree, not the image — see #claude/add-minerva.

Generated by Claude Code

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 7.69231% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
includes/Hooks/Adder.php 7.69% 24 Missing ⚠️

📢 Thoughts on this report? Let us know!

claude and others added 6 commits August 16, 2026 01:17
The toolbox is emptied because everything in it is a Special: page. The other
skins' copies of the current page are not: the bake writes them, so they are
honest static links and give the portlet something real to hold.

The href is computable at render time. Links are written root-relative ("./x"
from the main skin's output, "../x" from a skin subdirectory) and rename.php
reparents them by the page's own depth afterwards, exactly as it already does
for the non-main-skin canonical. A subpage two levels down therefore gets
"../../minerva/Manual/Deep.html" without anyone computing that here.

Server-rendered hrefs need no JS, so skin-switcher.js and its module go, along
with the footer <select> they drove. Removing the module is enough:
buildScripts.php discovers modules from what the rendered pages queue, so it
drops out of the bundle on its own.

Minerva is the awkward one. It feeds TOOLBOX into its page-actions menu, whose
builder keeps only entries with an icon and hands each to SingleMenuEntry, whose
$url is a typed string -- so an icon without an href fatals the bake. There every
entry carries both and the current skin is a self-link; elsewhere the current
skin is marked and not a link.

The empty-box rules move to their own style module, loaded only when one skin is
enabled and the toolbox really does stay empty. They cannot then hide a box with
the switcher in it, which is now the only one there is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qiZSGWHrxkjvFxA8swynp
Vector 2022 draws the dropdown as a checkbox with a label beneath it, and
the checkbox is what a click lands on -- so clicking the label timed out
against the checkbox intercepting every attempt.

Citizen's opener is a <details> summary and Minerva's toggle is its own
control, so only Vector was naming the affordance instead of the thing
that responds. Minerva is not enabled on this branch, so its opener is
untested here either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qiZSGWHrxkjvFxA8swynp
The sidebar skins wrap the link in a list item wider than the link, so a
click aimed at the entry landed beside it and no navigation followed --
only from a skin whose list item is wider, which is why the journeys
starting at the main skin passed and the others did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qiZSGWHrxkjvFxA8swynp
Two holes in the new specs. The current skin's entry was asserted to carry no
href, on the list item, which never carries one -- so it passed however the
entry rendered, and the one skin whose current entry is deliberately a self-link
(Minerva) would not have been caught.

And beforeAll read `.find(...).skin` off a list that a single-skin export leaves
empty, so every test in the file failed with a TypeError instead of skipping.
docs/ went from three skins to two last week; two to one is the same edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJtGZbbjBAHWmnC4QQyRUj
Vector 2022 labels that menu "General" and Citizen labels it "Tools", so a
reader opening it found two product names under a heading that explained
neither. The footer switcher had a "Skin" label; this restores one.

Vector ignores core's `toolbox` message and substitutes its own key
(VectorComponentPageTools::getMenus), so both keys are overridden, through
MessageCacheFetchOverrides. Only when there is a skin list to name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJtGZbbjBAHWmnC4QQyRUj
#349 brought this journey back on main while this branch was rewriting the
helper it calls, so the merge produced the same test twice, one of them using
the footer <select> API that no longer exists. Keep the discovered-skins
version and take the return trip to the root copy from the restored one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJtGZbbjBAHWmnC4QQyRUj
@lens0021
lens0021 force-pushed the claude/tools-portlet-skins branch from cdbb7f0 to 719d643 Compare August 15, 2026 16:18
@lens0021
lens0021 marked this pull request as ready for review August 15, 2026 16:20
@lens0021
lens0021 enabled auto-merge (squash) August 15, 2026 16:20
Same shape as Vector: a transparent checkbox is laid over the label that draws
the button, so a click aimed at the label is intercepted by the checkbox and
never lands. Confirmed on the deployed preview, where both elements occupy the
same 88x44 box and clicking the checkbox reveals the entries.

Minerva only started running these specs when #349 put it in docs/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJtGZbbjBAHWmnC4QQyRUj
@github-actions
github-actions Bot temporarily deployed to preview August 15, 2026 16:30 Destroyed
@lens0021
lens0021 merged commit 2f2551e into main Aug 15, 2026
20 of 21 checks passed
@lens0021
lens0021 deleted the claude/tools-portlet-skins branch August 15, 2026 16:32
lens0021 added a commit that referenced this pull request Aug 15, 2026
…ox (#363)

#351 named the toolbox after the skin list it fills, by overriding
core's `toolbox` message and Vector's `vector-page-tools-general-label`.
That was the wrong instrument, twice over, and this replaces it with the
one MediaWiki already provides.

## What was wrong with the override

**It renamed more than a heading.** Vector labels its tab-row dropdown
button with `msg( 'toolbox' )` (`SkinVector22.php:476`), so the button
read "Skins". On the live site the panel became a "Skins" button over a
"Tools" title over a "Skins" section.

**It assumed what the toolbox holds.** `Hider` empties it because
everything `makeToolbox()` produces is unservable, and this extension
refills it. That does not make the toolbox the skin list: a site whose
own extensions put static-servable entries there would find them under a
heading naming something else. The docs site has nothing else in its
toolbox, which is what the change was verified against, and wikven
cannot assume that of its users.

## What replaces it

A sidebar section of our own. Core turns any sidebar key into a portlet
whose heading is the message of the same name
(`SkinComponentMenu::getMenuLabel`, falling back to the key), so
`$sidebar['wikven-skins']` renders as `p-wikven-skins` headed by
`wikven-skins`. Nothing another skin owns is touched, and Vector's
button stays "Tools":

```
[Tools ⌄]              tab-row button, msg( 'toolbox' )
  Tools                panel title, vector-page-tools-label
  Skins                our section, msg( 'wikven-skins' )
    Vector (2022)
    Citizen
    MinervaNeue
```

**Vector 2022 only.** `SkinVector22::extractPageToolsFromSidebar()`
splices from the toolbox index to the end (`array_splice( $rest, $i )`,
no length), so every section appended after the toolbox joins the
page-tools menu. Core drops `SEARCH` and `LANGUAGES` from that list,
which leaves the toolbox last and ours immediately after it.

The other two skins keep the toolbox, and nothing about them changes:

| skin | why |
| --- | --- |
| Citizen | splices the toolbox alone (`array_splice( $rest, $i, 1 )`),
so a section of ours would leave the "More actions" card for the sidebar
drawer |
| Minerva | reads only `$sidebar['navigation']` and
`$sidebar['TOOLBOX']` (`SkinMinerva.php:149`,
`MainMenuDirector.php:75`); every other section is dropped |

Vector's toolbox is then empty, and core hides an empty portlet, so no
stray "General" heading is left behind. `ext.Wikven.emptyToolbox` still
gates on the skin count and still applies to exactly the case it was
written for: a single-skin export, where there is no list to show in any
skin.

## Also

The `.active` rule marking the current skin gains the new portlet, since
for Vector the entries are no longer inside `.mw-portlet-tb`.

Two tests cover the routing: the section a skin's entries land in, and
the href written for a skin at the export root versus one in a
subdirectory.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lens0021 added a commit that referenced this pull request Aug 15, 2026
Closes the gap #353 collected. #352 declared Vector, Minerva and Citizen
the supported skins; Citizen was the one whose support lived in the docs
site's `MediaWiki:Citizen.css` rather than in wikven, so every wikven
site enabling it had to rediscover the same workarounds. That file is
deleted, and what it stood for now happens in the build.

Three of the four items in the issue are fixed rather than relocated,
and two more turned up while checking.

## The trigger: the skin name in the Tools portlet

Citizen hangs a menu item's box off the `<a>`. The current skin is text
rather than a link (`Hooks\Adder`), so its name sat flush against the
card edge beside two padded links. One rule in `ext.Wikven.styles.less`
gives it the same box, in Citizen's custom properties, the ones that
follow its light/dark theme, where its Less tokens stay at static Codex
values.

It could not go in the skin's own module. `ResourceModuleSkinStyles` is
keyed by skin name and `ExtensionProcessor` **assigns** rather than
merges, so `"citizen": { "+skins.citizen.styles": … }` from wikven would
replace the ~200-entry map Citizen declares for itself (or be replaced
by it, depending on load order. Citizen loads second, so wikven's
silently vanished). Verified by baking it: the rules never reached the
output. `ext.Wikven.styles.css` is linked after the skin's, so equal
specificity is enough there.

## Preferences: it loads now

`skins.citizen.preferences` is seeded into the bundle in
`buildScripts.php`, exactly as the lazy search module already is. The
bundle only *implements* untriggered modules, so Citizen's own
`mw.loader.using` on first open is what executes it, with the DOM
present, which matters: the bundle runs in `<head>`, and the module
mounts at import time.

That is a real feature, not just a hidden error message:
light/dark/auto, text size, content width, pure black, image dimming and
performance mode all work on the static site, and the choice survives
navigation through `clientPrefs`.

## Search: it works now

Citizen's own search is the REST-backed command palette, which an export
has no backend for. Underneath it the skin renders an ordinary form for
readers without JavaScript, and `commandPalette.js` deletes that form
the moment it finds its trigger **by id**, so `rewriteScripts.php` drops
the id and the working search is left standing. SifterSearch's
`ext.sifter.retarget` already points it at the results page; submitting
reaches `Search.html?search=…` with real Pagefind results.

Gated on `Search::hasResultsPage()`, not on SifterSearch being loaded. A
results page is not configured by default, and the skins that keep a
typeahead do not need one (`ext.sifter` sends the submit to the top
Pagefind result instead). Citizen, whose box nothing else wires up, has
only this path. Without one the whole box is removed, the way Vector's
is. Both branches were baked.

## Two more, found while checking

**The manifest carried the build host.** `$wgCitizenEnableManifest` left
`<link rel="manifest"
href="http://localhost:4000/api.php?action=appmanifest">` in every
Citizen page. Turned off in `WikvenSettings.php`, beside the ULS
webfonts precedent.

**A service worker 404 per page.** Citizen registers one from `load.php`
whenever the client-side script path is the wiki root, which is what the
build installs with. There is no script path in a static export, so
Citizen pages now say so and the registration returns early on its own
guard. Scoped to Citizen: it is the only thing that reads the value for
a decision, and the two other readers in the bundle
(`mw.util.wikiScript` building `api.php` and `rest.php` URLs) are dead
in an export either way.

**And the typeface.** `AssetLocalizer` copied direct `/skins/…` paths
for images only, so Citizen's eight `@font-face` files 404ed and the
site rendered in a fallback font. It now copies `woff/woff2/ttf/otf`
too, for stylesheets only, since inlining a typeface into the JS bundle
would carry the whole thing, in base64, in every page's JavaScript.
`localizeImages` is renamed `localizeAssets` to match. +440 KB in
`dist/citizen`, and the skin looks like itself.

## The docs site

`MediaWiki:Citizen.css` is gone. Its `!important` (the `biome ci`
warning the issue notes) goes with it.

Its external-link rule went the other way. `MediaWiki:Common.css`
recolours the arrow by swapping a background image, which assumes core's
`content-links-external` feature; Citizen opts out of it and draws its
own arrow as a `currentColor` mask, already teal from the token above,
so the image tiled across the link and `Citizen.css` had to suppress
*both*. The rule is now scoped away from Citizen, which gets its
indicator back instead of losing it.

## Verification

Docker Hub was reachable this session, so this is the real pipeline
rather than a reconstruction: the image built and `docs/` baked with it,
three skins, then read in Chromium.

- **The four new tests** in `tests/e2e/citizen.spec.js` (search,
preferences, nothing missing or backend-bound, the toolbox entry lining
up) were each run against a bake of `main` first: **all four fail
there**, so they are testing the change and not the weather. The
alignment one measures where the label text actually starts, not a CSS
property.
- **PHPUnit passes** against MediaWiki REL1_46.
- **A second bake** with Citizen alone and no results page: its search
box is removed cleanly, Vector's is kept.
- The smoke workflow's own shell assertions re-run by hand: main page
present, no `load.php` in dumped CSS, webfonts intact, 324/324
language-bar entries.
- `biome ci`, `mago format --check`, `mago lint`, `composer phpcs`,
`yamllint --strict`, `typos`, `rumdl`, `taplo` all clean.

Rebased onto `main` after #363 landed, and re-verified from the image
up. Citizen keeps the skin list in the toolbox there, so that change and
this one compose.

## Not fixed

`/` and `Ctrl`+`K` still call Citizen's palette loader, which 404s twice
and then fails to load the notification module too, so the keypress is
inert rather than visibly broken. Unchanged by this PR; it does the same
on `main`. Making those open the search box needs either a wikven client
module for the shortcut or an upstream way to feed the palette a
Pagefind search client; its `SearchProvider` is constructed inline, and
the `citizen.commandPalette.register` hook only takes prefix-triggered
modes.

## The panel becomes where the skin is chosen, and the chrome around it
clears

Review feedback on the preview, and what checking each item turned up.

### The skin list moves into the preferences panel

Which skin to read in is a choice about how the page looks, so on
Citizen it now sits beside the theme, the text size and the content
width rather than behind "More actions". Citizen's panel is a public
extension point for exactly this (`citizen.preferences.register` /
`.changed`, documented and stable since 3.14.0, under the skin's
deprecation policy), which is the half of #342's comparison that made
Citizen worth extending rather than working around.

`resources/citizen-skins.js` **reads the toolbox entries wikven already
renders** rather than building its own list. Those carry hrefs the bake
has reparented for the page's own depth, so the module has no idea where
in the export it is running and no URL arithmetic to keep correct. The
thing #351 was pleased to delete stays deleted. It removes them once
read, which leaves a reader with **no JavaScript** the plain list of
links, still working, rather than nothing. The empty "More actions" card
goes with them, unless something else on the site put an entry in it.

Two details the skin dictates: Citizen stores a preference value under
`/^[a-zA-Z0-9]+$/` (the `clientPrefs.polyfill.js` rule #353 notes), so
`vector-2022` becomes `vector2022` and the entry it came from carries
the real target; and the panel reads its selection off the `<html>`
class, which the inline client-preferences script may have restored from
the page before, so the module restates the skin actually being
rendered.

### Search suggests as you type

Citizen sets `SkinPageReadyConfig`'s `search` to false, because its own
search is the command palette. With the palette gone and the plain form
standing, that flag is the only thing between the box and SifterSearch's
Pagefind typeahead, so wikven turns it back on for Citizen. It has to be
registered through `HookContainer::register()` rather than declared in
`extension.json`: handlers declared there run in load order, wikven
loads before the skins, and Citizen would overrule it. A runtime
registration appends after every extension and skin handler.

The widget behind it is core's `jquery.suggestions`, whose panel is a
hardcoded white box. Citizen's palette is in custom properties that
follow its theme, so in dark mode the panel stayed white while the text
in it turned light, the trap #353 describes, in the flesh. Restated in
the skin's own properties.

### The drawer counted nothing

The page count was empty because `$wgArticleCountMethod` defaults to
`link`, which counts only pages carrying an internal link, and a
translated site links through `Special:MyLanguage/`, which is a special
page and so is never recorded in `pagelinks`. Every page in `docs/` was
uncountable. `ArticleCountMethod: any` in `default.yml` fixes the count
wherever it shows: the drawer, and `{{NUMBEROFARTICLES}}`. It reads 40
now.

The user and edit counts are hidden. One is the build's own admin
account, the other is how many revisions building the site happened to
take; neither says anything about the site being read.

### Four dead requests, and one dead link

- **Upload file.** Citizen moves the entry out of the toolbox (which
`Hider` empties) into the sidebar, pointing at a `Special:` page the
export does not have. Source images are uploaded by the pass that
populates the wiki, so by the time a skin renders there is nothing left
to upload: the per-skin pass now sets `$wgEnableUploads = false` and the
entry is never built.
- **Help about MediaWiki.** Not a docs-site choice: it came in with the
sidebar's first commit, copied from MediaWiki's default, and points
readers of a wikven site at mediawiki.org's help. Removed from
`docs/MediaWiki:Sidebar.wikitext`.
- **Input methods.** Focusing any text input had the browser fetch
`ext.uls.ime` and `jquery.ime` from `load.php`. Nothing in an export
takes typed input anywhere, so `$wgULSIMEEnabled` joins the webfonts
switch beside it.
- **TabberNeue's icons.** `mw.loader.load( 'ext.tabberNeue.icons' )` the
moment it finds a tabber, so nothing in the rendered HTML names it and
the bundle never carried it. Seeded like the other run-time modules.

### The footer

`docs/MediaWiki:Citizen-footer-desc` and `-tagline` are created, so the
footer stops reading "Edit this text on MediaWiki:Citizen-footer-desc".
Both are rendered `inContentLanguage()`, so a `/ko` variant would never
be shown and none is added. **The tagline states the licence**. That is
a claim about the project, so please read it rather than take my word:
it says the software and this documentation are under GPL-3.0-or-later,
which is what `LICENSE` at the repository root covers.

### Verification

Rebuilt, re-baked and re-read, same as before. The suite grew a case for
the chooser (followed from `citizen/Getting_Started/ko.html`, two
directories down, to check the href it inherits is depth-correct), and
the alignment case above becomes a **JavaScript-disabled** one, which is
the only place that list still renders and exactly the fallback the
design rests on. `skin-switcher.spec.js`'s journeys now leave Citizen
through the panel.

A Citizen page, and the pages the other two skins render, request
nothing from a backend and get everything they ask for.

### Not fixed, and worth knowing

- **Minerva's search is broken on this site**, and always was. It sets
`searchModule` to `skins.minerva.search`, which SifterSearch does not
recognise (it swaps `mediawiki.searchSuggest` and
`skins.vector.search`), so typing in Minerva's box queries
`rest.php/v1/search/title` and gets a 404. Not touched here: the fix
belongs in SifterSearch, as an `ext.sifter.minerva` beside the Vector
one. Filed as #381.
- `/` and `Ctrl`+`K` still call Citizen's palette loader, as on `main`.
Filed as #384.

## A diff link goes to the repository, where what changed actually is

Citizen's "last modified" button asks for the latest diff (`getLocalURL(
[ 'diff' => '' ] )`), and
`GetLocalURL` was resolving it to the page it was already on, since only
`action=edit` and
`action=history` were special-cased. A diff is history: the export holds
one revision of a page, so
what changed is only in the repository, and the button now goes where
the history link goes. Keyed
on the query rather than on the button, so any link asking for a diff
lands there.

## Covering the new hook handling, and one thing the typeahead does not
do

`codecov/patch` was the one check that failed once the chooser landed:
the search wiring and the chooser's load conditions were new code in
`includes/` with nothing asserting them. What decides the wiring moves
out of a closure into `Main::enableCitizenSearch()`, so the decision can
be asserted rather than only the registration that carries it, and the
chooser's four conditions get a case each.

Writing the typeahead's test turned up what it actually does, which is
worth stating plainly: **a suggestion does not link to its page.**
Core's `jquery.suggestions` builds each suggestion's href from the
search form, and SifterSearch has pointed that form at the results page,
so clicking "Standalone binary" lands on `Search.html?search=Standalone
binary`, which does list the page first, but is a hop more than Vector's
typeahead, where `ext.sifter.vector` builds real page links. Any skin on
the `ext.sifter` path has this, not just Citizen. The fix belongs beside
`rewriteSpecialSuggestionLinks()` in SifterSearch, which already
rewrites the "containing…" row for exactly this reason. Filed as #381.

### Where `codecov/patch` still stands

Every other check passes. The patch figure Codecov reports is **28.3%
(15 hit, 38 missed)**, and that is the whole diff, `maintenance/`
included, which is where the misses are: `rewriteScripts.php`'s Citizen
branch and `buildScripts.php`'s seeding, neither of which the unit suite
reaches. `.codecov.yml` says as much and scopes the status to
`includes/` for exactly this reason.

In `includes/` the misses are down to two things, and both are inherent
rather than skipped:

- **`WikvenSettings.php`, four lines.** It is LocalSettings-time code;
PHPUnit never loads it. Anything added there counts as missed.
- **`Main::restoreCitizenSearchWiring()`'s registration.** Its guard and
the handler it registers are both tested; the
`HookContainer::register()` call between them only runs when
SifterSearch is loaded, which it is not in this suite. It could be made
reachable by moving the `Search::isActive()` check into the handler, but
then the test would be asserting that Citizen's search stays off, the
opposite of the behaviour, and not worth one line.

#351 merged in this state; flagging it rather than deciding it.

## Verified after rebasing onto `main`

Rebased once more once `main` moved past the merge base CI had tested,
and re-run from the image up: **225 PHPUnit tests and 25 Playwright
tests pass**, along with the smoke workflow's own shell assertions and
all eight linters.

Three things this leaves are tracked rather than carried: #380 (two menu
keys reaching assistive technology unresolved), #381 (search reaching
Vector properly and the other skins only partly) and #384 (the command
palette's shortcuts).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01R6DSxPcqj4BAejWJQ88X8J)_

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lens0021 pushed a commit that referenced this pull request Aug 31, 2026
🤖 I have created a release *beep* *boop*
---


## 1.0.0 (2026-08-31)

The first release. There is no earlier version to compare against or
upgrade from, so
what follows is not a list of changes to one but the work this release
is made of, as
the commits recorded it.


### Features

* **action:** default to a pinned image version and cache it between
runs ([#188](#188))
([f7fcb40](f7fcb40))
* **actions:** offer a guiding pull request comment on translation
findings ([#520](#520))
([1ab0175](1ab0175))
* add a "View source" tab linking to the repo source
([#119](#119))
([24e4d16](24e4d16))
* add a {{WIKVENVERSION}} variable
([#604](#604))
([a702b9f](a702b9f))
* add a footer skin switcher
([#114](#114))
([1867ea6](1867ea6))
* add a mark command to number translation units
([#216](#216))
([0fcde73](0fcde73))
* add a serve subcommand for local preview
([#155](#155))
([8d97799](8d97799))
* add translate scaffold to generate translation skeletons
([#223](#223))
([48ce2ab](48ce2ab))
* bake ULS webfonts into the static export (opt-in)
([#286](#286))
([074a103](074a103))
* bake with the standalone binary in CI, and fix the three things that
found ([#481](#481))
([8cf3bc3](8cf3bc3))
* base the image on mediawiki:1.46-fpm-alpine
([#332](#332))
([f3fdcee](f3fdcee))
* **build:** list the site's extensions and skins, with their licenses
([#523](#523))
([42d75d2](42d75d2))
* **build:** name wikven in the User-Agent of every request it makes
([#542](#542))
([7d6d1ce](7d6d1ce))
* bundle the native search closure so SifterSearch search works offline
([#98](#98))
([cf6b910](cf6b910)),
closes [#97](#97)
* derive page files from content model, dropping the .css.wikitext
suffix ([#62](#62))
([ef645c4](ef645c4))
* derive the footer project link label from the repo host
([#118](#118))
([e14c059](e14c059))
* **docs:** give the landing page symbols, an output it can show, and
some warmth
([#524](#524))
([3b07995](3b07995))
* **docs:** give the note template a teal icon
([#341](#341))
([05a7ea2](05a7ea2))
* **docs:** keep the rendered page behind the tree instead of trading
places ([#530](#530))
([83fc9ee](83fc9ee))
* **docs:** put a copy button on the code blocks
([#377](#377))
([3beee91](3beee91))
* **docs:** stop the sidebar linking to the page you are reading
([#499](#499))
([1bf9e2b](1bf9e2b))
* **docs:** turn the diagram's output pane over on scroll where the
browser can
([#526](#526))
([6c7bcc8](6c7bcc8))
* fold the version page into an About page
([#426](#426))
([af61267](af61267))
* gate translation checks on broken source pages, not on stale
translations
([#385](#385))
([aacfbfc](aacfbfc))
* generate a Special:Version-style page linked from the footer
([#117](#117))
([d2dfa8a](d2dfa8a))
* give Minerva a main menu the export can serve
([#366](#366))
([fc59117](fc59117))
* give the standalone binary the translate commands
([#507](#507))
([25ba28e](25ba28e))
* **i18n:** translate the footer, skin switcher and version page strings
([#123](#123))
([9849b36](9849b36))
* index each language separately, and assert it stays that way
([#447](#447))
([b4b01cc](b4b01cc))
* keep non-main skin pages out of search indexes
([#113](#113))
([0d8d94e](0d8d94e))
* keep Vector's appearance menu, which works statically
([#387](#387))
([ce30fea](ce30fea))
* let a site choose the skin it is read in
([#589](#589))
([5f96e9e](5f96e9e))
* let a skin author turn off the chrome wikven imposes
([#551](#551))
([ea78cab](ea78cab))
* let a stamp mean someone read the translation
([#572](#572))
([12d3292](12d3292))
* load .wikven.yaml through MediaWiki's settings system and validate it
([#77](#77))
([0952b3b](0952b3b))
* localize translated-page chrome and route Special:Translate links to
the edit host
([#229](#229))
([3afa7c4](3afa7c4))
* make page titles translatable, and translate them into Korean
([#256](#256))
([acdd975](acdd975))
* make the Translating page translatable
([#248](#248))
([6122151](6122151))
* name a setting that belongs to no one
([#578](#578))
([3df5b0e](3df5b0e))
* name the setting core cannot accept
([#574](#574))
([56f2d80](56f2d80))
* offer the Citizen skin (search hidden)
([#115](#115))
([b83717b](b83717b))
* pin and verify fetched extensions/skins
([#74](#74))
([8ccd5e7](8ccd5e7))
* point non-main skin pages' canonical at the main copy
([#116](#116))
([5bf1d6a](5bf1d6a))
* publish images on releases and nightlies, not on every commit to main
([#488](#488))
([18fa018](18fa018))
* publish the image to quay.io as well, with no token to keep
([#485](#485))
([f387e5e](f387e5e))
* put everything the build generates in one directory
([#584](#584))
([d8b8f43](d8b8f43))
* put the generated templates under one name, and keep the old ones
([#559](#559))
([2cd766a](2cd766a))
* put the pictures pages carry where the rest of the output goes
([#592](#592))
([78fdf9f](78fdf9f))
* put the skin switcher in the toolbox and drop the footer select
([#351](#351))
([2f2551e](2f2551e))
* render each enabled skin into its own output directory
([#111](#111))
([b0adc94](b0adc94))
* require MediaWiki 1.46, dropping 1.45 support
([#236](#236))
([993e62c](993e62c))
* resolve Special:MyLanguage/ links in the static export
([#225](#225))
([f4904ea](f4904ea))
* say what every built site redistributes, on a page the footer links
([#560](#560))
([3fe4eb6](3fe4eb6))
* say what wikven does about Lua, and stop guessing at it
([#473](#473))
([ea10b7d](ea10b7d))
* select the build skin via WIKVEN_BUILD_SKIN
([#110](#110))
([126a021](126a021))
* serve logos from a shared upload instead of inlining them per page
([#56](#56))
([ad4017a](ad4017a))
* serve the search results at Search and exclude it from indexing
([#104](#104))
([85d35e2](85d35e2))
* settle the actions' inputs before 1.0.0 freezes them
([#583](#583))
([2bee0c1](2bee0c1))
* settle what a page is called in the output
([#605](#605))
([a2860f7](a2860f7))
* ship a composite action and dogfood it in the docs deploy
([#150](#150))
([19392b7](19392b7))
* ship SifterSearch as a built-in, on-by-default search extension
([#195](#195))
([7b19be8](7b19be8))
* stop the build on three things it used to publish around
([#590](#590))
([9c0a0b3](9c0a0b3))
* support Citizen in wikven instead of per-site workarounds
([#369](#369))
([bced0dd](bced0dd))
* translate page content into other languages
([#214](#214))
([28ff2b2](28ff2b2))
* use the core viewsource message for the View source tab
([#120](#120))
([24230c0](24230c0))


### Bugfixes

* abort the build when a page fails to import
([#146](#146))
([cf0a007](cf0a007))
* **action:** use a locally-built image instead of always pulling
([#194](#194))
([8046aa4](8046aa4))
* answer symlinks where a source tree is read, not where a path is
bounded ([#591](#591))
([d3be13e](d3be13e))
* build the image when a Translate dev dependency has an advisory
([#277](#277))
([00838b9](00838b9))
* build the search index once, at the end
([#283](#283))
([09a5324](09a5324))
* **build:** put wikven in front of the User-Agent core builds for
Commons ([#543](#543))
([4c774a3](4c774a3))
* bump bundled SifterSearch to v0.6.0 (stop the double search box)
([#200](#200))
([cd38aa7](cd38aa7))
* bundle the modules core loads by looking at the rendered page
([#484](#484))
([c96c107](c96c107))
* **check-translations:** keep the comment to what the change touches
([#541](#541))
([8288229](8288229))
* clear the output directory before each build
([#72](#72))
([b4cf416](b4cf416))
* clear the title cache once the translation units exist
([#466](#466))
([562ea9e](562ea9e))
* create directories with wfMkdirParents
([#289](#289))
([5157174](5157174))
* date each page at the commit that changed it, not at the bake
([#424](#424))
([4cd31f4](4cd31f4)),
closes [#406](#406)
* declare the build's directory settings in extension.json
([#262](#262))
([65432b5](65432b5))
* decode JS-bundle URL escapes with json_decode in AssetLocalizer
([#297](#297))
([ccbf702](ccbf702))
* **docs:** cross the output pane's two states gradually instead of at a
line ([#540](#540))
([85fb9c9](85fb9c9))
* **docs:** cut the hero's light at the screen, and turn the pane over
mid-screen ([#531](#531))
([ea482af](ea482af))
* **docs:** keep sidebar links in the reader's language
([#382](#382))
([f2b27f4](f2b27f4))
* **docs:** keep the hero's light inside the hero
([#525](#525))
([aaeef7a](aaeef7a))
* **docs:** keep the logo in the reader's language
([#391](#391))
([749f507](749f507))
* **docs:** let the hero's light run off the frame so it reads as ground
([#529](#529))
([3d40660](3d40660))
* **docs:** let the hero's light run past the text column again
([#539](#539))
([2fbee0e](2fbee0e))
* **docs:** let the output pane settle back on the page as the diagram
leaves ([#544](#544))
([9d0785a](9d0785a))
* **docs:** light the hero from its corners, and drop the clip
[#531](#531) needed
([#536](#536))
([186421c](186421c))
* **docs:** make the brand teal and the note box legible in dark mode
([#489](#489))
([a03a76c](a03a76c))
* **docs:** make the hero's light read as ground rather than as two
spheres ([#528](#528))
([e31f7a5](e31f7a5))
* **docs:** paint the landing page's marks on its translations, and its
buttons anywhere
([#498](#498))
([1ff3a50](1ff3a50))
* **docs:** stop the diagram clipping its own file names on a narrow
screen ([#527](#527))
([74c35fb](74c35fb))
* **docs:** translate the Minerva section of Skins into Korean
([#415](#415))
([2ac133a](2ac133a))
* don't treat a documented &lt;translate&gt; example as a translation
source ([#239](#239))
([cdcc388](cdcc388))
* drain translation jobs by hand again, reverting
[#311](#311)
([#326](#326))
([4170fd7](4170fd7))
* drop dead footer/search affordances from the static export
([#160](#160))
([c08a5d2](c08a5d2))
* drop the .git the bundled extension clones leave in the image
([#330](#330))
([d19dbf4](d19dbf4))
* drop the category footer's dead Special:Categories link
([#173](#173))
([2df6bf8](2df6bf8))
* drop the discussion tab in every skin, Minerva included
([#493](#493))
([cd0fccd](cd0fccd))
* dump ResourceLoader modules without load.php's HTTP response
([#259](#259))
([3a39aac](3a39aac))
* end a source translation unit where its &lt;translate&gt; block ends
([#379](#379))
([50aebd8](50aebd8))
* export subpage titles as real subdirectories
([#213](#213))
([1db8e0d](1db8e0d))
* fail on un-localized images and hand output back to the host user
([#161](#161))
([1bb8c86](1bb8c86))
* fail the build when a stylesheet never reaches the disk
([#565](#565))
([6ad1b41](6ad1b41))
* fail the build when the webfonts a site asked for cannot be copied
([#566](#566))
([6fbd87a](6fbd87a))
* fail when a composer package lands where nothing loads it
([#581](#581))
([263fee5](263fee5))
* fetch a third-party extension again when its pin moves
([#546](#546))
([44938c2](44938c2))
* fetch the bundled extensions as tarballs, not clones
([#331](#331))
([9f60efa](9f60efa))
* freeze page_touched so wiki-page modules hash the same every bake
([#271](#271))
([853c06f](853c06f))
* give a fetch that reaches the network more than one go
([#469](#469))
([ba9fb1d](ba9fb1d))
* give Citizen's "View source" tab an icon
([#440](#440))
([f0b4e02](f0b4e02))
* give Citizen's search shortcuts the search the export has
([#397](#397))
([27eeebf](27eeebf))
* give every source image its own File: page
([#603](#603))
([623b866](623b866))
* give the per-skin build its own environment and working directory
([#298](#298))
([cfb0243](cfb0243))
* give the skin list its own section instead of renaming the toolbox
([#363](#363))
([aa13b7b](aa13b7b))
* grant the release binary job the provenance permissions it needs
([#177](#177))
([be9f283](be9f283))
* hide dead skin chrome left by the static export in Timeless and
Citizen ([#196](#196))
([1b4c9e1](1b4c9e1))
* hide edit/history/source links on generated pages
([#130](#130))
([fb6df1e](fb6df1e))
* hide the category footer on the static export
([#121](#121))
([939c435](939c435))
* hide the empty tools box each skin draws around the toolbox
([#346](#346))
([5afb909](5afb909))
* index a translated page once, not once per title it has
([#458](#458))
([e934e00](e934e00))
* keep $wgCacheEpoch out of module versions
([#270](#270))
([7795bf9](7795bf9))
* keep a search inside the skin copy the reader is in
([#434](#434))
([32e7f6d](32e7f6d))
* keep a skin pass out of the other skins' pages
([#430](#430))
([e84cd11](e84cd11))
* keep the build's chrome off a licenses page the site wrote
([#568](#568))
([e45596e](e45596e))
* keep the paths the build derives out of a site's hands
([#573](#573))
([e21b16b](e21b16b))
* keep the site's own styles linked from a style directory
([#563](#563))
([5dfea21](5dfea21))
* leave a page of its own where a translation would go
([#597](#597))
([09934b7](09934b7))
* link the search toggle to the results page, not to Special:Search
([#421](#421))
([3b3b2d5](3b3b2d5))
* list directories with FilesystemIterator, not glob
([#294](#294))
([f8188fa](f8188fa))
* make edit/history links correct for all valid titles
([950c0b2](950c0b2)),
closes [#67](#67)
* make storeImages downloads status-aware and stream to disk
([#307](#307))
([b75afc3](b75afc3))
* make the exported HTML the same in every bake
([#282](#282))
([caf6661](caf6661))
* make the first listed skin the default skin
([#109](#109))
([19e3fae](19e3fae))
* make the main page configurable and fail if it was not imported
([#71](#71))
([4399b8f](4399b8f))
* make verbatimRanges() comment-aware
([#312](#312))
([59117f7](59117f7))
* match &lt;translate&gt; with its attributes, as Translate does
([#292](#292))
([243bbb0](243bbb0))
* move Vector's skin list into the appearance menu
([#423](#423))
([68917e0](68917e0)),
closes [#405](#405)
* name the project namespace Wikven instead of clashing with MediaWiki
([#105](#105))
([4695978](4695978))
* never read a translation file as a base page
([#390](#390))
([6adb270](6adb270))
* obtain GadgetRepo from the service, not the removed singleton()
([288c733](288c733))
* point prevnext navigation at the reader's-language pages
([#245](#245))
([a3c8411](a3c8411))
* probe openssl's own compiled-in CA bundle location first
([#309](#309))
([c82d5b1](c82d5b1))
* propagate the child's exit code from wikven build and serve
([#302](#302))
([5c75797](5c75797))
* read a source page the way Translate segments it
([#388](#388))
([ff95548](ff95548))
* read a subpage as a translation only where it says it is one
([#580](#580))
([e967fb5](e967fb5))
* read units the way Translate does
([#358](#358))
([4642fc8](4642fc8))
* read Vector feature classes through classList
([#301](#301))
([9dfebd1](9dfebd1))
* rebase the printfooter link on a page exported into a subdirectory
([#422](#422))
([5e5b905](5e5b905))
* refuse an extension or skin name that points outside the image
([#570](#570))
([35c0678](35c0678))
* refuse an image or asset path that climbs out of its directory
([#588](#588))
([8fe34a1](8fe34a1))
* refuse images the source tree only points at
([#596](#596))
([38669f5](38669f5))
* render every translated page after all pages are marked
([#243](#243))
([0d987d3](0d987d3))
* reparent the local URLs a page carries in its JavaScript config
([#445](#445))
([beba746](beba746))
* replace fetchExtensions' curl subprocess with core's HTTP client
([#308](#308))
([9be62ec](9be62ec))
* replace hand-rolled balanced-div matcher with RemexHtml
([#314](#314))
([3173c2d](3173c2d))
* report a vetoed File:/MediaWiki: import as a failure
([#291](#291))
([8140261](8140261))
* require real tag context before rewriting a relative reference
([#313](#313))
([0f04ab6](0f04ab6))
* require UniversalLanguageSelector explicitly and disable its webfonts
([#218](#218))
([1b3e1b7](1b3e1b7))
* resolve $wgWikvenLogos through Title/RepoGroup, not by hand
([#315](#315))
([6f4de14](6f4de14))
* resolve phan findings and target MediaWiki 1.45
([be87a48](be87a48))
* resolve translate mark/stamp file paths against the source directory
([#220](#220))
([0aa95ea](0aa95ea))
* retry and cache the build's Wikimedia Commons lookups
([#257](#257))
([2ac2cce](2ac2cce))
* run the job queue in a fixed order
([#281](#281))
([6aa694e](6aa694e))
* say what language the licenses page's copies are in
([#562](#562))
([4242977](4242977))
* send a Special:MyLanguage link to the file it means
([#611](#611))
([1aad459](1aad459))
* set the maintenance user without StubGlobalUser
([8128dc5](8128dc5)),
closes [#93](#93)
* settle the search bundle's language order between bakes
([#451](#451))
([e7fc96c](e7fc96c))
* show the search box when SifterSearch provides static search
([#99](#99))
([a9cd689](a9cd689))
* small build-script and config papercuts
([#76](#76))
([1302e23](1302e23))
* split translation units only on adjacent newlines, as Translate does
([#295](#295))
([0feeebf](0feeebf))
* stop a new unit inheriting a deleted one's translations
([#564](#564))
([5fdd228](5fdd228))
* stop suppressing real categories; drop Version __NOINDEX__
([#122](#122))
([a3686bf](a3686bf))
* stop the build when an image of yours did not import
([#569](#569))
([c78b572](c78b572))
* stop the RLPAGEMODULES rewrite from editing article prose
([#296](#296))
([9ef6b8a](9ef6b8a))
* stop three things going quiet
([#599](#599))
([9103f0c](9103f0c))
* stop ULS reaching for input methods the export cannot serve
([#402](#402))
([af60e0e](af60e0e))
* strip the clocks ImageMagick writes into exported thumbnails
([#276](#276))
([a1bb8d0](a1bb8d0))
* switch skins correctly from a translated page
([#258](#258))
([6f85fe6](6f85fe6))
* take the DISPLAYTITLE spellings from the magic word registry
([#293](#293))
([7c88a7a](7c88a7a))
* take the translations comment down rather than turn it into an
all-clear ([#595](#595))
([6b88a10](6b88a10))
* **tf:** skip the vulnerability-alerts import when the resource is
absent ([#265](#265))
([b5d3ba3](b5d3ba3))
* translate a page whose file name is not spelled like its title
([#567](#567))
([6ec596a](6ec596a))
* turn the parser cache off so a bake reads its own finished state
([#335](#335))
([91036c3](91036c3))
* use CSSMin's data: URI encoder in AssetLocalizer
([#310](#310))
([6fe63d5](6fe63d5))
* validate .wikven.yaml value shapes and flag warnings clearly
([#163](#163))
([021f819](021f819))
* wrap a word too long for the screen instead of scrolling the page
([#545](#545))
([818a0e9](818a0e9))


### Performance

* freeze page_touched once, in the orchestrator
([#431](#431))
([a2d878b](a2d878b))
* render the skins beside each other
([#437](#437))
([33bbbfb](33bbbfb))
* stop rendering the history pages the export throws away
([#432](#432))
([39fe30c](39fe30c))


### Refactoring

* group the composite actions under actions/
([#359](#359))
([01f0f3d](01f0f3d))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 20, 2026
This was referenced Oct 2, 2026

This branch was successfully deployed

No deployments
preview — 58784ac1 Deployed Aug 15, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants