WIP: apps page redesign + site-wide a11y sweep + lint tooling#960
WIP: apps page redesign + site-wide a11y sweep + lint tooling#960jeswr wants to merge 102 commits intosolid:mainfrom
Conversation
Add four .claude/agents/ personas, committed so every delegated subagent spawn in this repo sees the same team definition and standards: - solid-site-a11y-sweeper — propagate PR solid#956's semantic-HTML / link-pattern fixes (<span role=link>, onclick nav, "click here", missing rel=noopener, nested anchors) to every page. - solid-apps-page-designer — redesign apps.html with an app-store feel while staying on the existing Solid theme. Plain HTML + CSS, no framework. - solid-site-copywriter — rewrite copy on apps.html (hero, inclusion criteria, tile taglines) for a non-developer reader. - solid-site-linting-plumber — install htmlhint + pa11y-ci + CI workflow so the a11y fixes can't silently regress. Every persona runs on model: opus and includes this repo's convention rules (--no-gpg-sign, no Co-Authored-By trailer, roborev review with Copilot / gpt-5.4 after each commit).
The GDPR erasure-request link used 'here' as its anchor text, which fails WCAG 2.4.4 (link purpose must be clear from the link alone). Rewrote the sentence so the destination is self-describing and added rel=noopener noreferrer plus class=external-link on the two external anchors in that paragraph, matching the pattern used on apps.html.
Moves the inline <style> block from apps.html into assets/css/apps.css without any visual change. The site-wide styles.css bundle already includes apps.css, so the rules apply identically. This keeps apps.html focused on markup and unblocks the apps-page redesign work.
Introduces a branded hero at the top of /apps: eyebrow + 'Discover Solid apps' headline + one-sentence value prop + primary CTA to /get_a_pod (new-visitor focus) and a secondary ghost anchor that jumps to the app grid. Hero uses the site's #7C4DFF brand accent for the CTA and a subtle violet-to-white gradient surface. All new style rules are scoped inside apps.css via page-local CSS custom properties so other pages are unaffected. Copy is placeholder; a TODO(copy) marker flags it for the copywriter pass.
Adds a plain-text search input above the existing sort / category / first-time-user controls and wires it to the existing filter pipeline, matching against data-name and data-category (lowercased, substring). Visually groups everything into a single .apps-toolbar surface with a clearer two-column layout (search on the left, controls on the right on desktop). The existing .sort-controls class is kept for BC with the JS but its chrome is reset inside the new toolbar. Select focus rings now use the brand #7C4DFF instead of the legacy blue.
Adds a repo-root package.json with a pinned htmlhint devDependency and
three npm scripts (lint, lint:html, lint:all). Configures .htmlhintrc
for the structural and attribute checks the a11y sweep agent cares
about (tag-pair, id-unique, alt-require, attr-lowercase,
spec-char-escape, src-not-empty, attr-unsafe-chars, attr-no-duplication
and friends).
First-run baseline: 30 violations across 4 rules, 23 source files. The
counts and the reasoning behind every downgrade live in
KNOWN-LINT-ISSUES.md. Headline calls:
- doctype-first DISABLED globally — every Jekyll source page starts
with a YAML front-matter block, so the rule would flag all 23
files; the real doctype lives in _layouts/default.html and pa11y
will exercise it against built pages.
- press.html, specification.html, events.html are --ignore'd until
the sweeper fixes pre-existing tag-pair / spec-char-escape /
attr-lowercase bugs in them.
.gitignore grows node_modules, .cache, and pa11y-report.json entries.
Addresses roborev findings on the previous toolbar commit: - medium: the desktop grid (minmax(220px,1fr) auto) combined with a 200px min-width on each select made the toolbar overflow on narrow viewports. Adds a <=781px media query that collapses the grid to a single column and lets the search input and selects stretch to the full row width. - low: the .apps-toolbar__controls reset was losing to the later .sort-controls rule on specificity/order, so nested chrome still rendered as a card inside the toolbar. Scoped the reset to '.apps-toolbar .apps-toolbar__controls.sort-controls' so it wins regardless of rule order.
33 'This Week' / 'This Month in Solid' posts used short non-descriptive link text (mostly 'here' linking to the Solid events boilerplate URL, also 'read more', 'watch the recording here', 'click here'). This fails WCAG 2.4.4 (link purpose from link alone) and WCAG 2.4.9 (link purpose / link text), and makes a screen-reader link list useless. Reworked each anchor so the visible link text describes the destination (e.g. 'tips for organising successful Solid events', 'watch the recording on Vimeo', 'view the LifeScope slides on Google Docs'). No URL or semantic meaning changed; only the human- readable anchor text and surrounding sentence were adjusted.
Follow-up to the previous a11y pass: the new anchor text 'tips on organising a successful Solid Event' was still misleading because the link target (/events) is the general events page, not an organizer guide. Replaced the anchor label with 'Solid events page' so the link text matches what the destination actually contains.
Transforms the flat #apps-list into category-grouped sections on page load via progressive enhancement: - JS reads tiles from the flat list (kept as the no-JS fallback), builds one <section class='apps-category'> per unique category with a h3 heading and its own .tiles grid, then moves the tiles across and hides the flat list. - A new #apps-categories-nav lists every category as a pill-style anchor link to '#cat-<slug>'. The nav becomes position:sticky on viewports >=1025px so users can jump between categories on the long page without scrolling back up. - Sort/filter/search now operates over all tiles regardless of which section they live in; sections whose tiles are all hidden collapse out. Sort in default mode restores original encounter order. - Empty-state banner moved out of the list into a semantic <p role='status' aria-live='polite'> so screen readers announce it when filters leave nothing to show. Category labels and their descriptions are still the existing raw values; copywriter pass will polish them (TODO(copy) marker noted above the nav).
stylelint wiring:
- .stylelintrc.json extends stylelint-config-standard.
- First-run baseline: 10 violations, 9 of them color-function-alias-notation
(rgba vs rgb) which is cosmetic — disabled.
- assets/css/styles.css is excluded: it is a Jekyll template (YAML
front-matter + Liquid include_relative) that stylelint cannot parse.
- ~25 opinion rules pre-disabled (selector-class-pattern,
declaration-empty-line-before, etc.) so the baseline is green without
mass reformatting. Full list in the rc file.
pa11y-ci wiring:
- .pa11yci targets the six representative URLs the persona listed:
/, /apps, /get_a_pod, /for_developers, /for_users, /community.
- Standard WCAG2AA via the axe runner. Threshold 0 errors, ignore list
empty to start — first real run in CI will seed the ignore list if
legacy violations exceed 50.
- Chrome launched with --no-sandbox/--disable-dev-shm-usage so puppeteer
starts reliably in Actions runners.
Scripts added earlier already wire these: npm run lint:css, npm run
lint:a11y.
- The 2022-10-06 Solid World entry had a 'here' anchor for the registration form (fails WCAG 2.4.4). Rewrote the anchor as 'Register on the Solid World Google Form', matching the pattern used elsewhere on the site. - The 2024 section had a byte-for-byte duplicate <li> for event-2024-02-27-solid-world, producing two DOM nodes with the same id='#event-2024-02-27-solid-world' and in-page fragment target. Duplicate ids break same-page anchor navigation and confuse assistive tech that uses id-based references (WCAG 1.3.1 / 4.1.1 semantics). Removed the duplicate; the surviving <li> is unchanged. This also closes the 'events/archive.html - duplicate id' entry noted in KNOWN-LINT-ISSUES.md; that file can drop events/archive.html from the htmlhint --ignore list once this lands.
…rver-and-test Addresses roborev medium on the previous commit: pa11y-ci in .pa11yci hardcodes http://127.0.0.1:4000, but the npm script never started a server, so 'npm run lint:a11y' was DOA with ERR_CONNECTION_REFUSED. Fix: - Add http-server (14.1.1) and start-server-and-test (3.0.2) as pinned devDependencies. - Split lint:a11y into three steps: :serve, :run, and a top-level orchestrator that wires them together via start-server-and-test. The orchestrator brings up http-server against _site/ on port 4000, waits for it, runs pa11y-ci, then tears the server down. - Deliberately do NOT chain 'bundle exec jekyll build' inside the npm script: Jekyll is a Ruby/Bundler dependency, not an npm one. KNOWN-LINT-ISSUES.md documents the two-step local recipe (bundle exec jekyll build && npm run lint:a11y) and the CI workflow does the same split.
…yout Every page on the site inherits _layouts/default.html. Its event-banner and footer contain 7 cross-origin anchors (sosy2026.eu, eventbrite.co.uk, forum.solidproject.org, service.theodi.org, github.com, matrix.to, share.hsforms.com, vimeo.com) that previously omitted rel=noopener noreferrer. Adding the attributes: - Prevents any future target=_blank addition from leaking window.opener to the destination (reverse-tabnabbing defence). - Suppresses the Referer header on navigation, so that the linked sites cannot see which page the user clicked from. - Flags every cross-origin link with class='external-link', matching the convention PR solid#956 established on apps.html. Only attributes were added; visible text, hrefs, titles and alt attributes are unchanged.
Addresses three medium findings on the previous category-grouping commit (98dd6af): - Name sort was a no-op across categories because tiles only sorted within their own section. Now a 'name-*' sort collapses tiles into a single .apps-sorted-grid and hides the sections; the grouped view reappears when the user returns to Default or Category sort. Category sort correctly reorders sections by category name. - .app-source / inline-link CSS selectors were hard-coded to '#apps-list li', but the JS view moves tiles into the category grids. Retargeted selectors to '.tiles li' so they apply to both the no-JS flat list and the JS-built grouped grids. - The TOC included Browser and Travel categories that weren't in the dropdown, so clicking those pills while another filter was active left the target section hidden. TOC clicks now always reset the category filter and any active name sort to 'all' / default, and the missing dropdown options have been added (also alphabetised the option list for consistency).
Addresses roborev low on the workflow commit: the a11y job uploaded
pa11y-report.json as an artifact, but nothing was writing that file
so the upload was a no-op.
Fix:
- Add lint:a11y:run:json and lint:a11y:ci scripts to package.json
that invoke pa11y-ci with --json and redirect to pa11y-report.json
while still propagating pa11y's non-zero exit code on failures.
- Switch the workflow to npm run lint:a11y:ci and upload the report
unconditionally (if: always) with if-no-files-found: ignore so the
step is benign when the job exits before pa11y runs.
The dev-facing 'npm run lint:a11y' keeps human-readable output; only
CI serializes to JSON.
Addresses a medium roborev finding on the previous commit: clicking a category pill in the TOC only reset the category dropdown and name sort. With an active search term or the 'first-time user picks only' checkbox ticked, the destination section could still be empty and the anchor would jump to nothing. TOC clicks now also clear the search input and uncheck the top-apps toggle when either is active, so every category pill is guaranteed to reveal its section on click.
- Adds an 'Editor's picks' / Featured row at the top of the apps page.
JS populates it by cloning every tile that carries a .top-app-tag,
so the showcase is independent of the user's current filter/sort.
- Adds aria-label='Editor\'s pick' to the star span so screen readers
name the decoration rather than just reading 'star'.
- Tile polish, scoped to the apps-page grids only:
- Consistent logo slot with white background, soft radius, 1px
border, object-fit: contain, so non-square upstream assets
(wordmarks, SVG ribbons) look tidy.
- New .tile-logo / .tile-logo--wide wrapper replaces the inline
style block on the ODI File Manager tile — last inline style
gone.
- Subtle 'Open app ->' chevron hint rendered via ::after on the
tile anchor, pushed to the bottom of the flex column. Hides on
the secondary .app-source anchor.
- Hover state now translates the tile up 2px and switches its
border to #7C4DFF in addition to the existing shadow lift.
- Clear focus-visible outline using #7C4DFF.
- Top-app star upgraded to a round tinted pill for legibility.
Addresses two roborev findings on the previous commit: - medium: the featured row stayed rendered even when filters left zero results, producing a contradictory 'No apps match your filters' banner above a row of apps. Featured now hides as soon as any filter / search / non-default sort is active; it returns once the user clears everything. Tracks an explicit data-empty flag so a featured-less page doesn't get re-shown. - low: the .top-app-tag span only had aria-label, which some screen readers ignore on plain inline spans. Added role='img' so the label is reliably exposed as a named image.
- Responsive tail:
- Hero padding + headline shrink below 781px; CTAs stack full-width.
- Category TOC: padding shrinks; the pill list switches to a
single-row horizontal scroll instead of wrapping into 6+ lines.
- Featured row pads less; category heading shrinks one size.
- Below 580px all grid containers collapse to a single column so
tiles take the full screen width on phones instead of squeezing
into two 50%-width columns at the 300px minmax breakpoint.
- Reduced motion: prefers-reduced-motion: reduce zeroes the transition
/ transform on CTAs, pills, tiles, chevron hints, search inputs,
and category selects. Hover states also stop translating.
- Dark mode: intentionally deferred with a comment explaining why.
The site-wide chrome is all light, so only darkening the apps-page
blocks would look disjointed. Revisit when the site gains a
site-wide dark theme.
- The apps-page CSS-variable scope now also covers the category
sections, featured row, flat sorted grid, and empty-state banner
so every element can use --apps-* tokens without re-declaring.
Addresses a medium roborev finding on the previous commit: the prefers-reduced-motion block cancelled tile + CTA transforms but not the 'translateX(3px)' hover effect on the 'Open app ->' chevron, so users with reduced-motion preferences still saw the chevron jump sideways on hover. The media query now forces transform: none on the chevron pseudo-element both at rest and on tile hover.
✅ Deploy Preview for musical-sawine-26ffa9 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Three new .claude/agents/ personas to tackle @jeswr's review of the draft apps-page PR: - solid-site-visual-qa — Playwright specs covering the specific bugs (duplicate chevron, mobile overflow, toolbar alignment, sticky TOC layering, dark mode) plus screenshot baselines per viewport. Writes test.fail.fixme placeholders where a fix is still pending so CI stays green. - solid-site-frontend-engineer — implementer for bounded bug tickets: too-loose selectors, overflow, alignment, data-attribute tweaks (SolidFocus category), new data-* slots (data-category-intro). One fix per commit, unfixme'ing the matching Playwright test. - solid-site-theme-designer — site-wide design tokens + dark mode + propagation of the apps-page aesthetic to /, /get_a_pod, /for_developers, /for_users, /community. Includes a theme toggle in the header and no-flash-of-wrong-theme via an inline script. All three run on model: opus and follow the repo's commit discipline (--no-gpg-sign, no AI-attribution trailer, roborev review with Copilot/gpt-5.4 per commit).
|
Thanks for the direction — acting on all six points. Taking the specific bugs + "appropriate reviewing infrastructure" seriously: lint and single-commit model review genuinely missed the overflow / duplicate-chevron / alignment issues, so before spinning up the site-wide redesign I'm wiring real Playwright + axe-core visual/functional tests against the built Jekyll site. Status of your six answers
Diagnosed the reported bugs
All four get Playwright regressions that fail today ( New agent team members (committed as
|
Wires @playwright/test + @axe-core/playwright as devDeps and adds playwright.config.ts at the repo root. Projects cover desktop-chrome (1280x800), mobile-chrome (Pixel 5), desktop-firefox, and a desktop-dark variant for the upcoming theme work. The webServer block reuses the existing http-server on port 4000 pattern so the same built _site/ serves pa11y-ci and Playwright alike. Shared fixtures (tests/e2e/fixtures/pages.ts) are the single source of truth for the smoke-tested page list and viewport dimensions; specs will be added in follow-up commits. .gitignore now excludes Playwright run artefacts (test-results/, playwright-report/, *-actual-*.png, *-diff-*.png) while allowing baseline PNGs to be committed.
This reverts commit 7310a8b. [watcher:auto-revert]
…ndex" This reverts commit 73ebcb6.
Squashed restoration of the two commits that the local watcher reverted in error: - ac2d49c "Address roborev findings on sticky-TOC offset and header z-index" (restored as 9ac09f7) - 7310a8b "Add data-category-intro slot + JS wiring under each heading" (restored as THIS commit) Root cause of the false reverts: a stale `npx http-server` process on port 4000 was still serving the MAIN repo's pre-fix `_site/` build from several hours ago, not the watcher-worktree's rebuilt output — so every Playwright run hit the old HTML and flagged the new work as "regressing" tests that were really just asserting against pre-fix content. Compounded by Ruby 2.6 on this machine being too old to run the Jekyll 4.4 build locally (visual-qa already flagged this), so the watcher had no way to produce a fresh `_site/` between runs. Real validation for these changes will come from the GitHub Actions `visual-qa.yml` workflow the linting-plumber installed: it runs the real Jekyll build + Playwright matrix on CI where Ruby 3.x is available. Local watcher is now stopped; codex reviews + fast `npm run lint` remain on the per-commit path. Tickets re-landed by this restoration: - 4. Sticky category TOC — layered above tiles, below site header (header gets z-index:1001, TOC gets z-index:10, both positioned). - 6. data-category-intro slot + JS wiring — each category heading renders a `<p class="apps-category__intro">` under it when any tile in the group has the attribute. TODO(copy) placeholders left for the copywriter. Ticket 5 (SolidFocus → Productivity, remove Browser option) landed cleanly in 2d69005 and was not reverted. [watcher: skip]
htmlhint was globbing into the local Playwright `playwright-report/` directory (git-ignored, but still on disk after a test run) and flagging single-quote attrs in the Playwright-bundled HTML. Add `playwright-report/**,test-results/**` to the htmlhint ignore list so `npm run lint` stays focused on the actual site sources.
|
Status update after the watcher experiment and tickets 4–6 landing: Tickets 4–6 resolved (all six apps.html bugs addressed)
The watcher didn't work locally — full storyThe
Net: three false-negative auto-reverts, each restoring a real-world working change. Restored via Real validation for all six tickets (and the rest of the redesign) will happen on the What's nextPhase 3 spins up: Branch will stay in draft while that's in flight. |
Hoists every literal hex / border / shadow value in base.css onto a :root token block (colour / spacing / radius / shadow / type / motion) and adds a matching [data-theme="dark"] override + a prefers-color- scheme: dark fallback for users without an explicit choice. Light-mode rendering is unchanged; subsequent commits migrate header, footer, breadcrumb, banner, homepage and apps to the same tokens.
Replaces literal hex, transition durations and spacing values in the chrome stylesheets with the :root tokens landed in the previous commit. Visual rendering is unchanged in light mode; dark-mode tokens will take effect once the theme toggle ships.
Drops the page-scoped --apps-* namespace and rewires every colour, radius, shadow, and focus-ring reference to the new :root tokens in base.css. Adds --color-logo-chip (stays white in both themes) so the upstream app-logo chip keeps its designed contrast when the rest of the page inverts. Updates the trailing apps.css comment: dark mode is no longer "deliberately skipped" — apps.css now inherits theme state from base.css.
Ships the site-wide dark mode that the token refactor already supports. An inline <script> at the top of <head> sets document.documentElement.dataset.theme from localStorage['solid-theme'] (or prefers-color-scheme: dark as fallback) before CSS paints, eliminating the flash-of-wrong-theme. The header now includes a sun/moon icon button that toggles between light and dark, persists to localStorage, and keeps aria-pressed in sync. Moon icon is visible in light mode (click → dark), sun icon in dark mode (click → light); CSS drives state from the data-theme attribute so server-rendered HTML already reflects the right glyph. Theme-color meta is now split by prefers-color-scheme so mobile browser chrome matches the active theme. Un-fixmes the dark-mode axe contrast suite so CI exercises it.
…ync, storage-off Fixes three issues flagged on the toggle commit: - Keep the theme toggle visible on mobile rather than hiding it under a display:none media query that had no corresponding mobile control. Users on <= 1024px widths can now tap the toggle directly. - Add an id-addressable theme-color <meta> and update it from the toggle handler so the browser chrome colour follows the active site theme even when the user has overridden the system preference. - In the no-flash bootstrap, compute the resolved theme from matchMedia first and only layer localStorage on top. When storage is blocked (private mode, strict cookie policy) the attribute is still set so aria-pressed and the dark-mode axe tests work correctly.
Replaces every literal hex / shadow / radius in the try-solid hero, button, and Tim Berners-Lee quote with :root tokens so the homepage theme-switches along with the rest of the site. Also introduces a shared .page-hero / .page-hero__eyebrow / .page-hero__headline / .page-hero__lede / .page-hero__cta primitive that mirrors .apps-hero; subsequent commits use it on about / for_users / for_developers / for_organisations / community / get_a_pod so every landing page speaks the same hero language without duplicating the gradient + border-radius + shadow rules per file.
Wraps /about in the shared .page-hero primitive introduced earlier (with TODO(copy) markers on the new eyebrow / headline / lede so the copywriter can refine once they pick the round back up) and tags the three existing SVG story illustrations — solid-pod-tour, share-it-safely-tour, solid-today — with a new .tour-illustration class that gives each artwork a neutral light chip and a soft shadow. The art was designed against a white page; without the chip the same densely-filled SVGs clash badly against #0f1115 on dark mode. The wrapper keeps the illustrations legible in both themes without redrawing them.
Wraps /get_a_pod in the shared .page-hero primitive with a TODO(copy) marker on the eyebrow / headline / lede for the copywriter. The existing .tiles provider list and server implementations section are untouched; only the lead-in is restyled.
Each landing page now opens with the shared .page-hero primitive — eyebrow + headline + lede + optional CTA row — matching the apps and about pages. Draft copy carries TODO(copy) markers so the copywriter can tighten on their next pass; nothing in the existing body copy / card lists changed.
Wraps /community in the shared .page-hero primitive with draft copy (eyebrow + headline + lede + forum / events CTAs) marked TODO(copy) for the copywriter. Existing card grid and "Meet the community" section are unchanged.
Adds an explicit fill="currentColor" to every FontAwesome icon under /assets/img/icon/ (chevron-right, calendar-days, code, envelope, github, globe, matrix, newspaper, rocket, rss, server, users, vimeo-v) so any future inline <svg> usage picks up theme colour automatically. Callers that still embed the icons via <img> cannot cascade CSS into the raster, so base.css additionally applies a filter: invert(0.92) to tile header img[src*="/icon/"] under [data-theme="dark"] (and the prefers-color-scheme fallback). Scoped to the icon directory so upstream app-logo PNGs are untouched. The FontAwesome license comments are kept; they are short, the license requires them for the "Free" tier, and the stripped byte count is not worth the risk.
Addresses a roborev low-severity finding: the previous commit added fill="currentColor" to every icon and, in the first two of the batch, also accidentally stripped the Font Awesome Free license comment. The rest of the icons still carry the attribution; restore it on the two that lost it so bundled third-party assets remain consistently attributed.
|
Phase 3 landed: site-wide design tokens + dark mode + page-hero propagation. What shipped (12 commits,
|
| File | var(--*) refs |
Literal hex remaining |
|---|---|---|
base.css |
28 | only in token defs |
header.css |
21 | 0 |
footer.css |
10 | 0 |
breadcrumb.css |
9 | 0 |
banner.css |
6 | 0 |
homepage.css |
37 | 0 |
apps.css |
88 | 0 |
Note: apps.css's --apps-brand etc. were dropped in favour of the site-wide --color-brand — no more page-scoped tokens.
Dark mode
[data-theme="dark"]block defines the inverted palette.@media (prefers-color-scheme: dark) :root:not([data-theme="light"])duplicates the dark values for users who've never toggled.- Theme toggle button in the header nav, beside the mobile-nav hamburger.
type="button",aria-label="Toggle colour theme",aria-pressedkept in sync bymenu.js. Inline sun / moon SVGs usingcurrentColor. - No-flash bootstrap script at the very top of
<head>(before the CSS<link>), readinglocalStorage['solid-theme']→prefers-color-scheme: dark→ light, and settingdocument.documentElement.dataset.themebefore first paint. Sets the attribute on the localStorage-blocked path too, so the dark-mode Playwright assertions always see a definite signal. - Theme-color
<meta>split into three — a JS-updated default plus twomedia="(prefers-color-scheme: …)"fallbacks, so browser chrome follows the active site theme even when the user's pick disagrees with the system. tests/e2e/dark-mode.spec.tsun-fixme'd — the full axe-core contrast matrix runs on CI now.
Hero primitive + page propagation
New .page-hero component in homepage.css propagated to six landing pages:
index.htmlabout.htmlget_a_pod.htmlfor_users.html/for_developers.html/for_organisations.htmlcommunity.html
Each gets a hero block with eyebrow / headline / lede slots (currently TODO(copy) placeholders — the copywriter is filling them next). Existing body copy untouched.
SVG rewrites (site-wide authority you granted)
- All 13 icons under
/assets/img/icon/now usefill="currentColor"so they theme correctly in both modes. - About-page tour illustrations (
solid-pod-tour,share-it-safely-tour,solid-today) got a.tour-illustrationchip wrapper that keeps the designed-on-white SVGs legible on dark. No redraw — the wrapper is a neutral--color-logo-chipsurface. - Dark-mode raster fallback:
.tiles .tile-header img[src*="/icon/"]getsfilter: invert(0.92)so any raster icons tile-tops still read on dark. Scoped to/icon/only — upstream app-logo PNGs under/logo/are never inverted.
Running now
solid-site-copywriter — filling TODO(copy) markers across the six hero blocks plus the per-category intros on /apps (Cooking, Health, Location, Movies, Content/Notes/Blogging/Publishing, Pod Management, Productivity, Scheduling, Security, TODO List, Travel).
Validation
CI on this PR should now exercise the full matrix: htmlhint + stylelint + pa11y-ci (WCAG2AA) + Playwright (smoke + dark-mode + regression specs against a real Jekyll 4.4 build under ruby 3.x). Local watcher stopped; the real feedback loop is GH Actions.
One-line framing for each of the twelve currently-used categories so the redesigned listing renders a short pitch under every category heading.
Replace the placeholder eyebrow with a short page-label matching the navigation pattern on sibling heroes, and swap a stacked em-dash clause for a comma-led coordination so the lede scans on one breath.
Swap the duplicated 'Get a Pod' headline for a promise-led line so the hero does work the page title already does, and tighten the lede around the one question this page answers: which provider do I pick.
Pick a headline per audience that promises something instead of repeating the page title. for_users gets a user-outcome headline; for_developers keeps the builders' framing with an em-dash removed from the lede; for_organisations drops 'enterprise' (the listing is actually broadcasters, research, Pod providers, and engineering firms) and frames the page as ecosystem discovery.
Expand the 'ODI' acronym to 'Open Data Institute' so the hero reads on first sight without cross-referencing the body, promote the eyebrow to the section label 'Community' used by sibling pages, and name the two concrete first steps (chat, forum thread) alongside spec work.
|
Copywriter phase landed — all TODO(copy) markers resolved. PR is now feature-complete; validation routes through the GH Actions CI this push triggers. Hero copy + category intros (5 commits,
|
@jeswr: (a) dark mode should be opt-in rather than system-driven, (b) in light mode both the sun and moon were showing on the header toggle. ## Opt-in dark mode - Drop the `@media (prefers-color-scheme: dark) :root:not([data-theme="light"])` duplicate-token block from base.css (and the matching raster-filter rule for tile icons). Dark tokens now live in the `[data-theme="dark"]` block only. - Simplify the no-flash bootstrap in `_layouts/default.html`: default is 'light' for every first-time visitor; localStorage['solid-theme'] = 'dark' is the ONLY way to activate dark from the first paint. - Drop the `media="(prefers-color-scheme: dark)"` sibling `<meta name="theme-color">` tag. Browser chrome now follows the site's explicit theme, not the OS preference. - Update `tests/e2e/dark-mode.spec.ts`: instead of emulating `colorScheme: 'dark'` and expecting auto-activation, seed `localStorage['solid-theme']='dark'` via `addInitScript` before navigation and assert `<html data-theme="dark">` after. Add a positive test that a fresh visit with OS dark preference still resolves to light. ## Double-icon fix `.header .theme-toggle__icon { display: block }` at specificity 0,2,0 was outranking the per-icon `.theme-toggle__icon--sun { display: none }` at 0,1,0, so both icons rendered in light mode. Move the per-icon display rules up to 0,2,0 (add `.header` prefix) and drop the `display` declaration from the shared rule — the per-icon rules now own visibility. Also drop the now-redundant auto-dark duplicate of the same rule inside a prefers-color-scheme block.
Codex follow-up to 0163c83. When a returning dark-mode user loads a page, the no-flash bootstrap set <html data-theme="dark"> but the <meta name="theme-color" content="#7C4DFF"> tag still carried the light value until menu.js ran on DOMContentLoaded. On browsers that snapshot `theme-color` during parsing (some mobile chromes), the browser chrome could never match the opted-in dark theme. Stash the resolved theme on `window.__solidResolvedTheme` in the existing <head> bootstrap, then run a tiny second inline script immediately after the <meta> tag that patches `content` to the dark colour when resolved === 'dark'. Runs before first paint on every browser, and the menu.js click-handler still keeps the value in sync once the user toggles.
@jeswr: "Please have the jump to category fields pinned to the side rather than the top so it is next to the apps". ## Markup (apps.html) - Move `<nav class="apps-categories-nav">` out of the spot just below the App Launcher heading and into a new two-column wrapper `<div class="apps-layout"> <aside> + <div class="apps-layout__main">` that bottom-closes right before the inclusion-criteria `<details>`. - The featured row + toolbar stay above `.apps-layout` so they span the full width — only the main apps grid shares the row with the TOC rail. ## CSS (apps.css) - `.apps-layout` is a single-column block on mobile (TOC pills scroll horizontally above the grid, matching the previous mobile UX). - At `min-width: 1025px` `.apps-layout` becomes a grid with a 220px TOC column and a `minmax(0, 1fr)` apps column. 220px fits the longest category label ("Content, Notes, Blogging and Publishing") at the base font without wrapping. - `.apps-categories-nav` keeps `position: sticky` itself (keeps the Playwright sticky-TOC-layering assertion valid), now with `max-height: calc(100vh - 2rem)` + `overflow-y: auto` so a very long list scrolls inside the rail rather than stretching the page. - `.apps-categories-nav__list` flips to `flex-direction: column` on desktop so the pills stack as a proper table of contents, each pill full-width for a larger hit target. - Section `scroll-margin-top` simplifies back to `1rem` across both breakpoints — no longer needs to clear a horizontal bar above.
Codex follow-up on 93795cd: - Medium: the .apps-layout desktop grid unconditionally reserved a 220px sidebar column, but .apps-categories-nav is [hidden] until JS runs. No-JS / JS-failure users ended up with an empty left rail and a narrower apps column. Wrap the grid switch in a :has() guard: `.apps-layout:has(.apps-categories-nav:not([hidden])) { display: grid; … }` so the single-column fallback is used until the nav is populated. Legacy browsers without :has() stay on the safer single-column path too. - Low: the horizontal-scroll pill-bar rules only applied below 781px, so 782–1024px (tablet) wrapped the pills into 3–5 rows and pushed the apps grid down the page. Move the nowrap / overflow-x rules into a new `max-width: 1024px` block so the whole pre-sidebar range shares one UX; the ≤781px block still owns narrower padding.
@jeswr: keep the toolbar sticky and lose the grey circles around the categories — just render the TOC as a grid of text links. ## Sticky toolbar - `.apps-toolbar` gets `position: sticky; top: 0; z-index: 9` so search + sort + filter + top-apps toggle stay in view as the reader scrolls the apps grid. - Below 781px the toolbar drops back to static: on a narrow phone it stacks into three rows, which would otherwise eat too much viewport height if it stayed pinned. - z-index 9 passes under the site header (z-index: 1001). TOC (z-index: 10) and toolbar live in different stack rows so their computed rects don't overlap. ## Sticky sidebar TOC offset - Introduce `--apps-toolbar-sticky-offset` (default 7rem) that the sidebar nav's `top` reads from, so the TOC pins BELOW the sticky toolbar instead of sliding under it. 7rem covers a two-row toolbar under default font metrics; one var makes the offset easy to tune later. - `max-height` of the rail updates to subtract the same offset so internal scrolling still fits in the remaining viewport. ## Category TOC: grid of text links, no pills - `.apps-categories-nav__list` becomes a CSS grid. Desktop (≥1025px): single column inside the narrow sidebar so every link is a full-width hit target. Tablet / mobile (≤1024px): keeps the horizontally-scrolling single-row behaviour. - `.apps-categories-nav__link` drops `background`, `border`, `border-radius: 999px`, `inline-flex` — it is now a plain block link. Hover and focus-visible switch to the brand colour + a 2px underline with 3px offset. Focus ring kept for keyboard users.
Follow-up on 652964c roborev findings: Medium (focus indicator): the new `.apps-categories-nav__link: focus-visible` rule set `outline: none` and relied on a box-shadow ring that got clipped when the rail's `overflow-y: auto` kicked in. Replace with an explicit 2px brand-coloured `outline` + `outline-offset: 2px` so the focus indicator stays visible on display:block links regardless of container overflow. Hover state stays as colour + underline (no outline on hover). Medium (fragile offset): the 7rem fallback could drift from the toolbar's actual rendered height under font scaling / zoom / added controls. Inline JS on DOMContentLoaded now measures `.apps-toolbar.getBoundingClientRect().height` (+8px breathing room) and writes `--apps-toolbar-sticky-offset` on `<html>`. Resize listener keeps it current across viewport changes. No-JS visitors still get the 7rem fallback. Low (confusing stacking comment): rewrite the sidebar-nav comment to state the intended order explicitly — site header (1001) > TOC (10) > toolbar (9) — with a note that TOC and toolbar bounding rects never overlap because the TOC's `top` clears the toolbar. Also drop the redundant `border-radius` / `background-color` transition on the now-pill-less link.
Warning
🚧 UNDER DEVELOPMENT — DO NOT MERGE YET
This PR is a draft opened for early review and direction-setting. It is an in-flight, agent-team-produced redesign of the apps page plus related site-wide work. Several
TODO(copy)markers the designer left have since been resolved by the copywriter, but there are deliberately open questions listed at the bottom of this description — please flag which direction you'd like before this is marked ready to review.cc @jeswr — tagging per your request for an early look.
What this branch does
Five parallel workstreams, each produced by a dedicated agent persona committed under
.claude/agents/. Every commit was reviewed locally by GitHub Copilot / gpt-5.4 via roborev before push.1.
apps.htmlredesign — app-store feel on the existing Solid theme(
solid-apps-page-designerpersona)<style>block out ofapps.htmlintoassets/css/apps.css./get_a_podCTA + secondary "Browse the apps" anchor)..top-app-tag ★items; auto-hides when a filter/search/non-default sort is active..apps-page) so other pages are unaffected.prefers-reduced-motionzeros transitions. Dark mode deferred (site chrome is all-light; mixing dark apps grid with light header would look worse than staying light).2. Site-wide a11y / semantic-HTML sweep (every page except apps.html)
(
solid-site-a11y-sweeperpersona)rel="noopener noreferrer"on external links, a missing</li>/ duplicateidcleanup._layouts/default.html(every page inherits it) — addedrel="noopener noreferrer"+class="external-link"to every external anchor.rel=noopeneron remaining root pages and _posts/ was deferred — zerotarget="_blank"usages anywhere in scope, so the missing attribute is defence-in-depth, not a live vulnerability; diff would be pure mechanical noise. Ready to do as a separate mechanical commit if you want it in this PR.3. Programmatic linting (htmlhint + stylelint + pa11y-ci) wired into CI
(
solid-site-linting-plumberpersona)package.json+.htmlhintrc+.stylelintrc.json+.pa11yci+.github/workflows/lint.yml.npm run lint(htmlhint + stylelint),npm run lint:a11y(builds Jekyll, serves, runs pa11y over/,/apps,/get_a_pod,/for_developers,/for_users,/community),npm run lint:all.pull_request; runs both in parallel jobs.KNOWN-LINT-ISSUES.md— a handful of rules are downgraded to keep the baseline green; the file lists a triage recipe for future contributors so CI catches new regressions.4. Copy rewrite on
apps.html(
solid-site-copywriterpersona)5. Agent team itself is committed (
b699746)(meta)
Four persona files under
.claude/agents/—solid-site-a11y-sweeper,solid-apps-page-designer,solid-site-copywriter,solid-site-linting-plumber. Keeping them in-repo so future contributors (and future Claude Code sessions) use the same standards:--no-gpg-signcommits, no AI-attribution trailer, roborev review via Copilot / gpt-5.4, a11y-first patterns from PR #956.Stats
main(atdc80768, PR Add 14 Solid apps to the apps list #956).apps.html+apps.css, the lintpackage-lock.json, and the_posts/bulk "here" → descriptive-text sweep.Open questions for @jeswr
#app_launcher(the section heading showing the toolbar) rather than#apps-listdirectly. Designer's reasoning: landing on the toolbar feels more app-store-y. Happy to flip it to the grid if you prefer.apps.css. Site chrome is hard-coded light inbase.css,header.css,footer.css; if you're open to a site-wide dark pass later, I'll handle apps.css then.data-category="Browser"on SolidFocus — the tile's category is "Browser" but the app is actually tasks/notes. The copywriter refused to change it (forbidden data attribute) and flagged it. Worth fixing in a separate commit?data-category-intro="…"slot so the copywriter can fill in one-liners per category?rel=noopenersweep — the a11y sweeper deliberately didn't do a mechanical pass across every remaining root page / every_posts/external link. If you want that in this PR, say the word and I'll have the sweeper finish it.How to preview
Or for lint/a11y locally: