Skip to content

Refactor UX/UI to the editorial "workshop" design#90

Draft
amargiovanni wants to merge 3 commits into
mainfrom
claude/nifty-bohr-gWQ09
Draft

Refactor UX/UI to the editorial "workshop" design#90
amargiovanni wants to merge 3 commits into
mainfrom
claude/nifty-bohr-gWQ09

Conversation

@amargiovanni
Copy link
Copy Markdown
Owner

What & why

Recreates the layout and the few added features from the Claude Design handoff bundle, ported into the existing Astro + SolidJS + Tailwind v4 codebase. Per the agreed scope, the current product name is kept — only the look & feel changes — and all new UI copy is translated across EN/IT/ES/FR/DE.

The approach is token-first: the editorial palette and fonts are mapped onto the existing Tailwind token names, so all 53 tools reskin automatically, while the page chrome (home, tool shell, command palette) is rebuilt structurally.

Highlights

Design foundationsrc/styles/global.css, src/lib/icons.ts

  • Warm paper palette, burnt-vermillion accent, Newsreader (serif display) + Hanken Grotesk + JetBrains Mono, per-category hues, full light/dark.
  • A coherent line-icon set replaces every emoji in the chrome (tool→icon and category→hue maps).
  • svg block-display lives in @layer base so Tailwind's .hidden utility still wins.

HomeHomeCatalog.astro

  • Editorial sticky topbar · serif hero · "workbench band" (Jump back in / recents + richer Favorites) · sticky scroll-spy category index · numbered sections · line-icon cards with hover-to-favorite.

Tool shellToolLayout.astro, Sidebar.astro

  • app grid · collapsible, favorite-aware sidebar · breadcrumb bar · editorial tool header (icon tile + serif title) · related tools · keyboard-shortcut footer · recents recorded per visit. Sidebar group toggles are keyboard-accessible disclosures.

Command paletteCommandPalette.tsx

  • cmdk styling · Actions group (home / theme / GitHub / clear recents) + tools · / quick-open · footer hints · line icons.

New features — recents (src/lib/recents.ts), richer favorites surfaces, related tools, copy-everywhere icon button. 48 new UI strings added with key parity preserved across all 5 locales.

Compatibility

All interactive hooks the test suite relies on are preserved (#favorite-toggle + star icons, #tool-heading, #sidebar-favorites, language dropdown, palette placeholder, exactly 53 home tool links). One favorites home-page assertion was updated to match the new always-visible workbench band.

Verification

  • astro build — 271 pages
  • vitest run — 725 unit tests
  • ✅ Playwright e2e — 362 tests

Notes / follow-ups

  • Per-tool pixel-perfect bodies (the design's 4 examples) inherit the new look via tokens + restyled primitives; deeper per-tool redesigns can follow in later stages.
  • BrandMark.astro and the custom-logo plumbing in BaseLayout are now unused (left in place as a harmless no-op); can be removed in a cleanup pass if the custom-logo feature isn't needed.

https://claude.ai/code/session_011oU6LdcwaFS7b5Gf7kwUmA


Generated by Claude Code

Recreates the design-bundle layout in the Astro/Solid/Tailwind app while
keeping the current product name (only the look changes).

Foundation
- global.css: editorial token system (warm paper, burnt-vermillion accent,
  Newsreader/Hanken Grotesk/JetBrains Mono, per-category hues, light+dark)
  mapped onto the existing Tailwind token names so all 53 tools reskin
  automatically; ported component classes (icon/tile/kbd/chip/panel/switch/
  cmdk/sidebar/hero/catalog…). svg block-display moved into @layer base so
  Tailwind's `.hidden` utility still wins.
- lib/icons.ts: coherent line-icon set replacing every emoji in the chrome
  (tool→icon, category→hue maps).

Home (HomeCatalog.astro)
- Editorial sticky topbar, serif hero, "workbench band" (recents +
  richer favorites), sticky scroll-spy category index, numbered sections,
  line-icon cards with hover-to-favorite.

Tool shell (ToolLayout.astro, Sidebar.astro)
- `app` grid with a collapsible, favorite-aware sidebar; breadcrumb bar;
  editorial tool header (icon tile + serif title); related tools; keyboard
  shortcut footer. Sidebar group toggles are keyboard-accessible disclosures.

Command palette (CommandPalette.tsx)
- cmdk styling, an Actions group (home/theme/GitHub/clear-recents) plus
  tools, `/` quick-open, footer hints, line icons.

Features & i18n
- New recents ("Jump back in") via lib/recents.ts; copy-everywhere icon
  button; related tools. 48 new UI strings translated across EN/IT/ES/FR/DE
  (key parity preserved).

All interactive hooks the test suite relies on are preserved; one favorites
home-page assertion updated to match the always-visible workbench band.
Build, 725 unit tests, and 362 e2e tests all pass.

https://claude.ai/code/session_011oU6LdcwaFS7b5Gf7kwUmA
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jun 5, 2026

Deploying tools-collection with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6178a43
Status: ✅  Deploy successful!
Preview URL: https://a63d93dc.tools-collection.pages.dev
Branch Preview URL: https://claude-nifty-bohr-gwq09.tools-collection.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a major editorial redesign of the application, replacing emoji icons with a custom SVG line-icon library, introducing a 'Jump back in' recents section, and updating the home catalog, sidebar, layouts, and localization files. Key feedback from the review highlights a memory leak in the back-to-top scroll listener due to Astro's View Transitions, invalid HTML nesting of a button inside an anchor tag, a type-safety bypass using as never, and missing ARIA attributes on collapsible sidebar group headers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 306 to +337
function initBackToTop() {
const btn = document.getElementById('home-back-to-top')
if (!btn || btn.dataset.bound === 'true') return
btn.dataset.bound = 'true'

const globalWindow = window as Window & { __homeBackToTopAbort?: AbortController }
globalWindow.__homeBackToTopAbort?.abort()
const controller = new AbortController()
globalWindow.__homeBackToTopAbort = controller

if (!btn || btn.dataset['bound'] === 'true') return
btn.dataset['bound'] = 'true'
const threshold = 320
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
let isVisible = false
let ticking = false

function setVisible(visible: boolean) {
const el = btn
const setVisible = (visible: boolean) => {
if (visible === isVisible) return
isVisible = visible
btn.classList.toggle('opacity-0', !visible)
btn.classList.toggle('translate-y-2', !visible)
btn.classList.toggle('pointer-events-none', !visible)
btn.tabIndex = visible ? 0 : -1
btn.setAttribute('aria-hidden', visible ? 'false' : 'true')
el.classList.toggle('opacity-0', !visible)
el.classList.toggle('translate-y-2', !visible)
el.classList.toggle('pointer-events-none', !visible)
el.tabIndex = visible ? 0 : -1
el.setAttribute('aria-hidden', visible ? 'false' : 'true')
}

function updateVisibility() {
setVisible(window.scrollY > threshold)
}

function onScroll() {
if (!btn.isConnected) {
controller.abort()
return
}
const onScroll = () => {
if (ticking) return
ticking = true
window.requestAnimationFrame(() => {
updateVisibility()
setVisible(window.scrollY > threshold)
ticking = false
})
}

btn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: reducedMotion ? 'auto' : 'smooth',
})
}, { signal: controller.signal })
window.scrollTo({ top: 0, behavior: reducedMotion ? 'auto' : 'smooth' })
})
window.addEventListener('scroll', onScroll, { passive: true })
setVisible(window.scrollY > threshold)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Astro with client-side routing (View Transitions), the global window object persists across page navigations. Every time a user navigates back to the homepage, initBackToTop is executed, adding a new 'scroll' event listener to window without removing the previous one. This creates a memory leak and degrades scroll performance over time.

Restore the AbortController cleanup pattern (which was present in the previous implementation) to clean up the event listeners when the component is re-initialized.

  function initBackToTop() {
    const btn = document.getElementById('home-back-to-top')
    if (!btn || btn.dataset['bound'] === 'true') return
    btn.dataset['bound'] = 'true'

    const globalWindow = window as Window & { __homeBackToTopAbort?: AbortController }
    globalWindow.__homeBackToTopAbort?.abort()
    const controller = new AbortController()
    globalWindow.__homeBackToTopAbort = controller

    const threshold = 320
    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
    let isVisible = false
    let ticking = false
    const el = btn
    const setVisible = (visible: boolean) => {
      if (visible === isVisible) return
      isVisible = visible
      el.classList.toggle('opacity-0', !visible)
      el.classList.toggle('translate-y-2', !visible)
      el.classList.toggle('pointer-events-none', !visible)
      el.tabIndex = visible ? 0 : -1
      el.setAttribute('aria-hidden', visible ? 'false' : 'true')
    }
    const onScroll = () => {
      if (ticking) return
      ticking = true
      window.requestAnimationFrame(() => {
        setVisible(window.scrollY > threshold)
        ticking = false
      })
    }
    btn.addEventListener('click', () => {
      window.scrollTo({ top: 0, behavior: reducedMotion ? 'auto' : 'smooth' })
    }, { signal: controller.signal })
    window.addEventListener('scroll', onScroll, { passive: true, signal: controller.signal })
    setVisible(window.scrollY > threshold)
  }

Comment thread src/components/HomeCatalog.astro Outdated
Comment on lines +195 to +205
<a class="tool-card" href={`/${lang}/tools/${tool.id}/`} data-id={tool.id}>
<span class="tile" style={`--tint:${hueOf(tool.category)}`} set:html={toolIconSvg(tool.id)} />
<span class="body">
<span class="name">{t(lang, getToolNameKey(tool.id))}</span>
<span class="desc">{t(lang, getToolDescKey(tool.id))}</span>
</span>
<button class="favbtn" type="button" data-fav={tool.id} aria-label={t(lang, 'favorites_add')} title={t(lang, 'favorites_title')}>
<Fragment set:html={iconSvg('star', 'icon-sm')} />
</button>
<span class="open-hint">{t(lang, 'home_cardOpen')} <span class="kbd">↵</span></span>
</a>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Nesting a <button> element inside an <a> (anchor) element is invalid HTML according to the HTML5 specification. Interactive content must not be nested. This can cause issues with screen readers, keyboard navigation, and unpredictable click event bubbling across different browsers.

To fix this, structure the card so that the favorite button is a sibling of the anchor link rather than a child, wrapping both in a relative container.

                {tools.map((tool) => (
                  <div class="relative group">
                    <a class="tool-card block" href={`/${lang}/tools/${tool.id}/`} data-id={tool.id}>
                      <span class="tile" style={`--tint:${hueOf(tool.category)}`} set:html={toolIconSvg(tool.id)} />
                      <span class="body">
                        <span class="name">{t(lang, getToolNameKey(tool.id))}</span>
                        <span class="desc">{t(lang, getToolDescKey(tool.id))}</span>
                      </span>
                      <span class="open-hint">{t(lang, 'home_cardOpen')} <span class="kbd">↵</span></span>
                    </a>
                    <button class="favbtn absolute z-10" type="button" data-fav={tool.id} aria-label={t(lang, 'favorites_add')} title={t(lang, 'favorites_title')}>
                      <Fragment set:html={iconSvg('star', 'icon-sm')} />
                    </button>
                  </div>
                ))}

Comment thread src/components/HomeCatalog.astro Outdated
Comment on lines +25 to +34
const categoryBlurbKey: Record<string, string> = {
'text-processing': 'categoryBlurb_textProcessing',
generators: 'categoryBlurb_generators',
extraction: 'categoryBlurb_extraction',
analysis: 'categoryBlurb_analysis',
security: 'categoryBlurb_security',
converters: 'categoryBlurb_converters',
development: 'categoryBlurb_development',
utilities: 'categoryBlurb_utilities',
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using as never to cast the translation key is a type-safety bypass. We can leverage TypeScript's Parameters utility to type categoryBlurbKey strictly and safely, ensuring all keys are valid translation keys.

const categoryBlurbKey: Record<string, Parameters<typeof t>[1]> = {
  'text-processing': 'categoryBlurb_textProcessing',
  generators: 'categoryBlurb_generators',
  extraction: 'categoryBlurb_extraction',
  analysis: 'categoryBlurb_analysis',
  security: 'categoryBlurb_security',
  converters: 'categoryBlurb_converters',
  development: 'categoryBlurb_development',
  utilities: 'categoryBlurb_utilities',
}

Comment thread src/components/Sidebar.astro Outdated
Comment on lines +38 to +42
<div class="sb-group-head" data-toggle tabindex="0">
<span class="ghue"></span>{t(lang, 'favorites_title')}
<span class="gcount" data-fav-count>0</span>
<Fragment set:html={chevSvg} />
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The collapsible sidebar group headers are keyboard-focusable via tabindex="0", but they lack appropriate ARIA roles and attributes. Screen readers will not recognize them as buttons, nor will they announce whether the group is currently expanded or collapsed.

Add role="button" and aria-expanded="true" to the .sb-group-head elements, and update the aria-expanded attribute dynamically in the toggle script.

    <div class="sb-group-head" data-toggle tabindex="0" role="button" aria-expanded="true">
      <span class="ghue"></span>{t(lang, 'favorites_title')}
      <span class="gcount" data-fav-count>0</span>
      <Fragment set:html={chevSvg} />
    </div>

claude added 2 commits June 5, 2026 15:40
- Back-to-top: restore AbortController cleanup so the window scroll
  listener is not leaked across Astro view transitions.
- Home tool cards: replace the invalid <button>-inside-<a> nesting with a
  stretched-link pattern (card is a <div> with an absolutely-positioned
  <a> hit area + the favorite <button> as a sibling above it).
- HomeCatalog: type categoryBlurbKey with Parameters<typeof t>[1] instead
  of the `as never` bypass.
- Sidebar: make collapsible group headers real <button>s with
  aria-expanded/aria-controls (keyboard + screen-reader accessible);
  the toggle script keeps aria-expanded in sync.
- e2e: add an opt-in `exact` flag to the shared tool-test helper and use
  exact button-name matching in the convert-case/list-generator/
  timestamp-converter specs so the "Convert" action no longer collides
  with the sidebar "Converters" group button.
- Bump product version to 1.6.0 and add a changelog entry.

https://claude.ai/code/session_011oU6LdcwaFS7b5Gf7kwUmA
Per design feedback the warm editorial palette felt too retro. This is a
pure token swap — layout, components and logic are unchanged:

- Palette: cool slate surfaces with an electric-indigo accent, in light
  and dark; cooler, crisper per-category hues; tighter radii; an
  indigo focus ring.
- Type: Space Grotesk (geometric display) + Inter (body) + JetBrains Mono
  (code), replacing Newsreader/Hanken Grotesk. Display weights bumped to
  suit Space Grotesk; the hero emphasis is now a coloured bold word
  instead of a serif italic.
- Favicon mark recoloured to indigo.
- Changelog wording updated to match the shipped theme.

https://claude.ai/code/session_011oU6LdcwaFS7b5Gf7kwUmA
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