From 2bc38d531745d84db2d8a945d19faf0efa1503a0 Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:16:09 +0900 Subject: [PATCH] web: the Inside filter's own files, left out of the previous commit. BuildFilters.tsx, its stylesheet and the BuildIndex test were untracked when 94cfd2e was made from tracked changes only; GlobeViewer imports the filter, so the web build on main failed on a missing module. Nothing else changes. Co-Authored-By: Claude Fable 5.1 --- .../InsideGlobe/BuildFilters.module.css | 49 ++++++++++++++ .../sections/InsideGlobe/BuildFilters.tsx | 23 +++++++ .../sections/InsideGlobe/BuildIndex.test.tsx | 64 +++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 web/src/components/sections/InsideGlobe/BuildFilters.module.css create mode 100644 web/src/components/sections/InsideGlobe/BuildFilters.tsx create mode 100644 web/src/components/sections/InsideGlobe/BuildIndex.test.tsx diff --git a/web/src/components/sections/InsideGlobe/BuildFilters.module.css b/web/src/components/sections/InsideGlobe/BuildFilters.module.css new file mode 100644 index 00000000..26ae5ca9 --- /dev/null +++ b/web/src/components/sections/InsideGlobe/BuildFilters.module.css @@ -0,0 +1,49 @@ +.filters { + position: absolute; + top: 4px; + left: 50%; + transform: translateX(-50%); + z-index: 4; + display: flex; + gap: 2px; + padding: 2px; + background: var(--pf-cream); +} + +.filters button { + appearance: none; + border: 0; + background: transparent; + color: var(--pf-ink-muted); + padding: 6px 12px; + font-family: var(--pf-mono); + font-size: 11px; + line-height: 16px; + white-space: nowrap; + cursor: pointer; +} + +.filters button:hover { + color: var(--pf-ink); +} + +.filters button[aria-pressed='true'] { + background: var(--pf-ink); + color: var(--pf-cream); +} + +.filters button:focus-visible { + outline: 2px solid var(--pf-led); + outline-offset: 2px; +} + +@media (max-width: 900px) { + .filters { + top: auto; + bottom: 8px; + } + .filters button { + min-height: 44px; + padding: 8px 12px; + } +} diff --git a/web/src/components/sections/InsideGlobe/BuildFilters.tsx b/web/src/components/sections/InsideGlobe/BuildFilters.tsx new file mode 100644 index 00000000..8c11e6d8 --- /dev/null +++ b/web/src/components/sections/InsideGlobe/BuildFilters.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { BUILD_FILTERS } from './builds'; +import { useBuildSelection } from './useBuildSelection'; +import styles from './BuildFilters.module.css'; + +export default function BuildFilters() { + const { filter, setFilter } = useBuildSelection(); + return ( +
+ {BUILD_FILTERS.map(({ value, label }) => ( + + ))} +
+ ); +} diff --git a/web/src/components/sections/InsideGlobe/BuildIndex.test.tsx b/web/src/components/sections/InsideGlobe/BuildIndex.test.tsx new file mode 100644 index 00000000..958366bf --- /dev/null +++ b/web/src/components/sections/InsideGlobe/BuildIndex.test.tsx @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; +import { useAppStore } from '@/store/useAppStore'; +import BuildFilters from './BuildFilters'; +import BuildIndex from './BuildIndex'; +import BuildRouteSync from './BuildRouteSync'; +import { builds } from './builds'; + +beforeEach(() => { + useAppStore.setState({ selectedBuildId: null, insideFilter: 'all' }); + window.history.replaceState(null, '', '/inside'); +}); + +function mount(buildId: string | null = null) { + render(<>); + return within(screen.getByRole('list', { name: 'Patternflow around the world' })); +} + +describe('Inside browsing', () => { + it('shows every entry as a text link and filters to projects', () => { + const list = mount(); + expect(list.getAllByRole('link')).toHaveLength(builds.length); + expect(list.queryByRole('img')).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Projects' })); + expect(list.getAllByRole('link')).toHaveLength(2); + expect(list.getByRole('link', { name: /MOTIFLOW/ })).toHaveAttribute('href', '/inside/azmano-iran'); + expect(list.getByRole('link', { name: /Raspberry Pi port/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Projects' })).toHaveAttribute('aria-pressed', 'true'); + }); + + it('preserves a matching selection and clears a hidden pin and its URL', () => { + const list = mount(); + fireEvent.click(list.getByRole('link', { name: /MOTIFLOW/ })); + fireEvent.click(screen.getByRole('button', { name: 'Projects' })); + expect(useAppStore.getState().selectedBuildId).toBe('iran-azmano'); + expect(window.location.pathname).toBe('/inside/azmano-iran'); + fireEvent.click(screen.getByRole('button', { name: 'In use' })); + expect(useAppStore.getState().selectedBuildId).toBeNull(); + expect(window.location.pathname).toBe('/inside'); + expect(list.queryAllByRole('link')).toHaveLength(0); + expect(screen.getByText('No entries in this category yet.')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Builds' })); + expect(list.getByRole('link', { name: /In the DJ booth/ })).toBeInTheDocument(); + }); + + it('reveals a directly linked entry even after browsing another category', () => { + useAppStore.setState({ insideFilter: 'builds' }); + const list = mount('iran-azmano'); + expect(screen.getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'true'); + expect(list.getByRole('link', { name: /MOTIFLOW/ })).toHaveAttribute('aria-current', 'true'); + }); + + it('reveals a pin restored by browser history without pushing another URL', () => { + const list = mount(); + fireEvent.click(screen.getByRole('button', { name: 'In use' })); + act(() => { + window.history.replaceState(null, '', '/inside/day-france'); + window.dispatchEvent(new PopStateEvent('popstate')); + }); + expect(window.location.pathname).toBe('/inside/day-france'); + expect(list.getByRole('link', { name: /Raspberry Pi port/ })).toHaveAttribute('aria-current', 'true'); + expect(screen.getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'true'); + }); +});