Skip to content

feat(#385): replace the chrome emoji with the icon set - #405

Merged
TortoiseWolfe merged 2 commits into
mainfrom
feat/385-chrome-icons
Jul 28, 2026
Merged

feat(#385): replace the chrome emoji with the icon set#405
TortoiseWolfe merged 2 commits into
mainfrom
feat/385-chrome-icons

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Summary

18 of 20 emoji replaced with the icon set, across the seven surfaces outside the page children. Seven new icons: cookie, copy, pin, ban, tip, eye, eye-off.

The grid test in icons.ts validated all seven unprompted — which is what documenting the grid bought. I rendered them anyway, because that test cannot see whether a drawing reads as the thing it names; that is exactly how settings and theme were caught in #377.

Every replacement is decorative — decided per site, not defaulted

In each case an accessible name already sat beside the glyph, so adding a second one would repeat rather than inform:

surface the name that already existed
CookieConsent aria-label="Cookie" next to "We use cookies to enhance your experience" — it announced "Cookie, We use cookies…"
A11yDevOverlay buttons already labelled "Accessibility violations: N. Open panel." and "Collapse accessibility panel"
LocationButton text reads "Location blocked" / "Update location" / "Get my location"
SEOAnalysisPanel <h4>Strengths (N)</h4> and <h4>Weaknesses (N)</h4> above the lists — the ✓/✗ was never the only non-colour signal
schedule "Prepare for the meeting:" follows it
messages/setup button already labelled "Show password" / "Hide password" — and the two eye emoji were near-identical at button size

Two are deliberately left, because they are text and not chrome

A11yDevOverlay:252    🎉  inside the sentence "No accessibility violations 🎉"
SEOAnalysisPanel:42   ✗   in CLIPBOARD EXPORT text

An SVG cannot go in clipboard text, and the 🎉 is prose. Replacing either would be swapping an icon system into a sentence.

BlogContent needed different handling

Its copy button is built as an HTML string with an inline onclick, not JSX, so <Icon> cannot be used. An iconSvg() helper emits the markup with single-quoted attributes — the surrounding onclick is a double-quoted HTML attribute and double quotes would terminate it. The button also swaps two pre-rendered spans by display rather than rewriting innerHTML, because injecting SVG through an attribute-embedded JS string is precisely where quoting breaks.

That surfaced a real defect. With the emoji gone the button had no text content at all, leaving title as its only accessible name. It now carries sr-only "Copy code".

The existing test asserted toHaveTextContent('📋'). I fixed the button rather than the assertion — it now asserts the real label, which is a stronger check than the glyph was.

Also

Refreshed two doc comments in Icon.tsx and its a11y spec that cited CookieConsent's aria-label as a live example. It no longer is — same body-is-truth discipline (#358) applied to code comments.

Testing

  • 35 E2E greenaccessibility, depth-tokens, container-width, blog-touch-targets, color-contrast AAA
  • 239 unit green across every touched component
  • type-check, lint clean

Related Issues

Refs #385
Part of epic #376

🤖 Generated with Claude Code

TurtleWolfe and others added 2 commits July 28, 2026 06:54
18 of 20 emoji across the seven surfaces outside the page children. Seven
new icons drawn to the grid in icons.ts: cookie, copy, pin, ban, tip, eye,
eye-off. The grid test validated all seven unprompted; I rendered them
anyway, because it cannot see whether a drawing reads as the thing it
names -- which is how `settings` and `theme` were caught in #377.

Every replacement is DECORATIVE, and that is a per-site judgement rather
than a default. In each case the accessible name already existed beside
the glyph:

  CookieConsent    aria-label="Cookie" next to "We use cookies to enhance
                   your experience" -- the name repeated the copy
  A11yDevOverlay   button already labelled "Accessibility violations: N"
                   and "Collapse accessibility panel"
  LocationButton   text reads "Location blocked" / "Update location"
  SEOAnalysisPanel <h4>Strengths (N)</h4> / <h4>Weaknesses (N)</h4> above
                   the lists, so the marker was never the only non-colour
                   signal
  schedule         "Prepare for the meeting:" follows
  messages/setup   button already labelled "Show/Hide password" -- and the
                   two eye emoji were near-identical at button size

TWO ARE DELIBERATELY LEFT, because they are text and not chrome:

  A11yDevOverlay:252   🎉 inside the sentence "No accessibility violations"
  SEOAnalysisPanel:42  ✗ in CLIPBOARD EXPORT text, where an SVG cannot go

BlogContent needed different handling. Its copy button is built as an HTML
string with an inline onclick, so <Icon> cannot be used; an `iconSvg()`
helper emits markup with single-quoted attributes, because the surrounding
onclick is a double-quoted HTML attribute and double quotes would
terminate it. The button also swaps two pre-rendered spans by `display`
rather than rewriting innerHTML -- injecting SVG through an
attribute-embedded JS string is exactly where quoting breaks.

That change surfaced a real defect: with the emoji gone the button had NO
text content, leaving `title` as its only accessible name. It now carries
sr-only "Copy code". The test asserted toHaveTextContent('📋'); it now
asserts the real label, which is a stronger check than the glyph was.

Also refreshed two doc comments in Icon.tsx and its a11y spec that cited
CookieConsent's aria-label as a live example -- it no longer is.

  35 E2E green; 239 unit green

Refs #385
Part of epic #376

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate was very nearly inert. Its selector matched 60 elements at 390px
and exactly ONE was visible -- the hamburger itself. Everything else sat
inside a closed dropdown, isVisible() skipped it, and the test reported
green having checked a single 44px button. It had been that way for as
long as the mobile nav has been a dropdown.

#378 flagged this as something its own nav regrouping WOULD cause. It had
already happened.

Opening the menu found a real defect on the first run: **Sign Out at
144x26px**, 18px under the standard, in both the desktop and mobile menus.
Neither had a className at all, so both inherited DaisyUI's 26px menu
default. Never measured, never reported. Fixed with min-h-11.

Two changes to the gate:

  click(), not focus() -- a DaisyUI dropdown is held open by :focus-within
  and a programmatic focus on the <label> does not reliably establish it
  (measured: focus 3 targets, click 6).

  A COVERAGE FLOOR. Every check is conditional on isVisible(), so zero
  measured is indistinguishable from zero failures. The floor makes a drop
  visible instead of silent.

6 is measured, not chosen, and the comment is explicit about what it does
NOT mean: the selector deliberately exempts inline text links, so the
menu's <a> items are out of scope by design. 6 is full coverage for this
selector, not proof every nav target is checked.

  MUTATION: leave the menu closed -> "Only 5 nav touch targets were
  measured, down from 6", naming the cause.

This is the prerequisite that makes #378 safe to attempt: regrouping the
nav into `Demos ▾` and `Display ▾` now has a gate that notices if targets
get hidden rather than fixed.

Refs #378, #396

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TortoiseWolfe
TortoiseWolfe merged commit 404bfd2 into main Jul 28, 2026
19 checks passed
@TortoiseWolfe
TortoiseWolfe deleted the feat/385-chrome-icons branch July 28, 2026 11:50
TortoiseWolfe added a commit that referenced this pull request Jul 30, 2026
…nd a correction to my own justification (#464)

* feat(#431): the messaging surface joins the depth vocabulary, and /80 turns out to fail AAA

Presentational only. No changes to key handling, RLS, or the authorization
contract, and the MessageThread scroll container is untouched - `parentRef` at
:369 is `absolute inset-0 overflow-y-auto` with `data-testid="message-thread"`,
and four sites in tests/e2e/messaging/ dispatch scroll events against it because
WebKit does not reliably fire them for programmatic scrollTop. Restyling it would
have put that at risk for no visual gain.

Two tab strips become rails - ConversationList's All/Unread/Archived and
UnifiedSidebar's Chats/Connections. `.tab` gets no depth from #427, and DaisyUI's
`tabs-boxed` and `tabs-bordered` were a fifth vocabulary alongside the rails
already used by GlobalNav, /blog, /docs, /payment and the admin console.

Two hand-rolled modals - ReAuthModal ("Enter Your Messaging Password", which is
what /messages actually shows a returning user) and ConsentModal - carried
`shadow-xl` as their only elevation. They are not `.modal-box`, so #427's plate
rule never reached them, and simply deleting the shadow would have left them
flat. They now carry `sh-plate` explicitly.

CHECKED WHAT ASSERTS ON THESE TABS BEFORE TOUCHING THEM, AND MY FIRST CHECK WAS
INCOMPLETE.

I grepped tests/e2e and found `toHaveClass(/tab-active/)` belonging to
ConnectionManager (friend-requests.spec.ts:307,310) and to PaymentButton's
stripe/paypal tabs (three payment specs) - both left alone, deliberately.

Then the vitest run failed: UnifiedSidebar.test.tsx:70 asserts `tab-active` too.
Colocated unit tests are as much a consumer of these classes as the E2E suite is,
and grepping only tests/e2e missed them. The assertion is updated to
`sh-rail-active` WITH a negative case on the inactive tab - as written it would
have passed on a component that marked every tab active - and a note that
`aria-selected` on the line below is the real contract, since a class being
present says nothing about whether it renders (#430).

/80 AND /85 ARE NOT SAFE, AND NOW THERE ARE NUMBERS.

#411 measured /60 and /70 failing and removed them; /80 and /85 were left as
presumed safe. Measured properly by canvas readback, compositing the dimmed
foreground over each surface, on scripthammer-light:

                /100    /85     /80     /70
  base-100     12.96   8.30    7.08    5.20
  base-200     11.73   7.73    6.62    4.98
  base-300     10.10   6.88    6.05    4.57

/80 on base-200 is 6.62:1, under the 7:1 gate - and MessagingGate's card is
`bg-base-200` with dimmed text inside it. /85 on base-300 is 6.88:1. The dark
theme is comfortable everywhere except /70 on base-300. Full tables in #462.

So the nine dim levels removed here were a real AAA failure, not just an
unmeasured risk. ConversationListItem:205 keeps its `/90` deliberately: it is the
read/unread distinction, it measures ~9:1 on base-100 and ~8.6:1 on base-200, and
it passes. Removing it would have cost meaning for nothing.

VERIFIED AGAINST THE STATE THAT SHOWS THE PAGE.

/messages/setup redirects to /messages for any user who already has messaging
keys, which the standing test user does - so the first run measured /messages
twice and reported /messages/setup clean without ever loading it. The same shape
as #454. Created a throwaway keyless user, minted a session for it by
service-role grant, measured the real page (h1 "Set Up Encrypted Messaging",
redirects=0), then deleted the user.

3 routes x 2 house themes: contrast fails 0, horizontal overflow 0 at
320/390/428/768/1024/1280/1440, shadow-xl 0 on the messaging surface. 82 unit
tests pass across 10 files, type-check clean.

Closes #431. Refs #462.

* docs(#463): three comments justified a clamp that was already fixed

AdminGate.tsx:105, game/page.tsx:25 and schedule/page.tsx:20 each said, in the
present tense, that Tailwind's `container` clamps to the previous registered
breakpoint and throws away up to 43.9% of the viewport at 767px, citing #373 —
as the reason those call sites had been given an explicit measure instead.

None of it is true of this stylesheet, and it was not true when those comments
were written. #373's §A1 was fixed by the `@utility container` override in
globals.css:56-59. Measured live on /blog:

  viewport   container max-width   rendered   margin-left
    390px          1280px            390         0px
    428px          1280px            428         0px
    767px          1280px            767         0px
   1023px          1280px           1023         0px
   1440px          1280px           1280        80px

Full width at every tier below 1280, nothing wasted. #373's table describes the
state before its own fix landed.

The code is unchanged — the explicit measures are fine, they are just a
readability choice rather than a bug fix, and the comments now say so. Verified
that removing `container`'s cap did not matter above 1280px either: /account and
/profile hold a 672px box with gutters 384/384, 624/624 and 944/944 at
1440/1920/2560, /contact and /accessibility hold 896px, and horizontal overflow
is zero throughout.

globals.css:40 keeps the 43.9% figure on purpose: it sits inside the
`@utility container` block documenting why the override exists. Historical
context for a fix is not a stale claim.

Why it happened, which is the part worth keeping: I read #373's headline and its
measurement table and skipped its own resolution note, which is the FIRST LINE of
the body. That ticket is a mix of fixed and open sections — §A1 fixed, §A2 onward
open — and I quoted a figure from the fixed part as if it were current. A ticket
that documents its own partial resolution has to be read to the end before any
number in it is quoted.

Folded into this branch rather than sent as its own PR because the E2E
concurrency group allows one waiting run (#444), and an hour of queue for a
three-comment correction is not a good trade.

Closes #463.

* fix(#385): drop the last decorative emoji from the a11y overlay, and un-dim its empty state

`A11yDevOverlay.tsx:252` read "No accessibility violations 🎉". The emoji is gone
— it cannot take `currentColor` and renders differently on every OS, which #385
lists as a real problem regardless of which direction that ticket settles on. The
sentence carries the meaning without it.

The same line carried `text-base-content/60`, which measures 5.20:1 on base-100 in
scripthammer-light — under the 7:1 gate. #411 removed /60 and /70 elsewhere; this
one survived because the overlay is dev chrome that no route sweep visits. Full
per-surface opacity table is in #462.

Scope kept deliberately narrow. #385 is BLOCKED on an owner decision about whether
design-specified surfaces move from icons to typographic marks, and its body now
records that plus the measured state: five of its seven surfaces already carry no
decorative glyph after PR #405. `SEOAnalysisPanel.tsx:42` keeps its `✗` on
purpose — that string is built for the CLIPBOARD, not for markup, and a plain-text
marker is the right instrument in text a user pastes into a document.

The JSX comment for this change went in as a child of the `<ul>`, not inside the
ternary branch. Placed between `? (` and its element it is a second root node in a
parenthesised expression, which is a syntax error — the same trap recorded in
#429's commit, walked into again and caught by type-check.

Refs #385. Refs #462.

---------

Co-authored-by: TurtleWolfe <TurtleWolfe@users.noreply.github.com>
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