feat: Refactored SearchAutocomplete from heroSize boolean to variant st…
- "frontend/src/components/SearchAutocomplete.tsx" - "frontend/src/App.tsx" - "frontend/src/App.css" - "frontend/src/pages/Home.tsx" - "frontend/src/pages/SearchResults.tsx" GSD-Task: S03/T01
This commit is contained in:
parent
adc86446f1
commit
50675db557
15 changed files with 684 additions and 10 deletions
|
|
@ -37,6 +37,11 @@ Ten milestones complete. The system is deployed and running on ub01 at `http://u
|
|||
- **Related techniques cross-linking** — Every technique page shows up to 4 related techniques scored by creator overlap, topic match, and shared tags. Curated links take priority; dynamic results fill remaining slots.
|
||||
- **Topic color coding & page transitions** — Per-category accent colors on sub-topic pages and search results. CSS-only fade-in page enter animation on all 7 public pages.
|
||||
- **Search autocomplete** — Shared SearchAutocomplete component with popular suggestions on empty focus and debounced typeahead on 2+ characters. Used on Home and SearchResults pages.
|
||||
- **Interaction delight** — Card hover animations (scale + shadow), staggered entrance animations on card grids, featured technique glow treatment, Random Technique discovery button.
|
||||
- **Topics collapse/expand** — Topics page loads with all categories collapsed. Smooth CSS grid-template-rows animation on expand/collapse.
|
||||
- **Creator stats pills** — Creator detail page topic stats rendered as colored pill badges matching category colors.
|
||||
- **Tag overflow** — Shared TagList component caps visible tags at 4 with "+N more" overflow pill. Applied across all 5 tag-rendering sites.
|
||||
- **Empty subtopic handling** — Subtopics with 0 techniques show "Coming soon" badge instead of dead-end links.
|
||||
|
||||
### Stack
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ Transform Chrysopedia from functionally adequate to engaging and accessible. Add
|
|||
| ID | Slice | Risk | Depends | Done | After this |
|
||||
|----|-------|------|---------|------|------------|
|
||||
| S01 | Interaction Delight & Discovery | medium | — | ✅ | Cards animate on hover with scale+shadow. Card grids stagger entrance. Featured technique has glow treatment. Random Technique button navigates to a random page. |
|
||||
| S02 | Topics, Creator Stats & Card Polish | medium | — | ⬜ | Topics page loads collapsed, expands with animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N more. Empty subtopics show Coming soon. |
|
||||
| S02 | Topics, Creator Stats & Card Polish | medium | — | ✅ | Topics page loads collapsed, expands with animation. Creator stats show colored topic pills. Cards limit tags to 4 with +N more. Empty subtopics show Coming soon. |
|
||||
| S03 | Global Search & Mobile Navigation | medium | — | ⬜ | Compact search bar in nav on all pages. Cmd+K focuses it. Mobile viewport shows hamburger menu. |
|
||||
| S04 | Accessibility & SEO Fixes | low | — | ⬜ | Every page has single H1, skip-to-content link, AA contrast text, and descriptive browser tab title. |
|
||||
|
|
|
|||
96
.gsd/milestones/M011/slices/S02/S02-SUMMARY.md
Normal file
96
.gsd/milestones/M011/slices/S02/S02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
id: S02
|
||||
parent: M011
|
||||
milestone: M011
|
||||
provides:
|
||||
- TagList component available for any future tag-rendering sites
|
||||
- Collapse/expand animation pattern reusable for other accordion-style UI
|
||||
requires:
|
||||
[]
|
||||
affects:
|
||||
[]
|
||||
key_files:
|
||||
- frontend/src/components/TagList.tsx
|
||||
- frontend/src/pages/TopicsBrowse.tsx
|
||||
- frontend/src/pages/CreatorDetail.tsx
|
||||
- frontend/src/pages/Home.tsx
|
||||
- frontend/src/pages/SearchResults.tsx
|
||||
- frontend/src/pages/SubTopicPage.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Used CSS grid-template-rows 0fr/1fr for smooth collapse/expand — no JS height measurement needed
|
||||
- Added pillClass prop to TagList to preserve existing pill--tag styling in Home.tsx
|
||||
- Kept single dot separator between video count and topic pills in CreatorDetail
|
||||
patterns_established:
|
||||
- Shared TagList component as single source for tag rendering with overflow — all 5 tag sites now use it
|
||||
- CSS grid-template-rows 0fr/1fr pattern for animating variable-height content without JS
|
||||
observability_surfaces:
|
||||
- none
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M011/slices/S02/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M011/slices/S02/tasks/T02-SUMMARY.md
|
||||
- .gsd/milestones/M011/slices/S02/tasks/T03-SUMMARY.md
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-31T08:36:24.689Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# S02: Topics, Creator Stats & Card Polish
|
||||
|
||||
**Topics page loads collapsed with smooth CSS grid animation, creator stats use colored topic pills, tags capped at 4 with +N overflow via shared TagList component, empty subtopics show Coming soon badge.**
|
||||
|
||||
## What Happened
|
||||
|
||||
Three tasks delivered four UI improvements across the frontend.
|
||||
|
||||
**T01 — Topics collapse/expand animation.** Changed TopicsBrowse to initialize with an empty expanded set (all categories collapsed on load). Replaced the conditional render (`{isExpanded && ...}`) with an always-rendered wrapper using CSS `grid-template-rows: 0fr/1fr` animation. The wrapper transitions over 300ms, the inner div uses `overflow: hidden; min-height: 0` for smooth clipping. No JS measurement needed.
|
||||
|
||||
**T02 — Creator stats colored pills.** Replaced the run-on dot-separated topic stats in CreatorDetail with `badge badge--cat-{slug}` spans wrapped in a flex container. Reuses the `catSlug()` utility and existing badge color classes from the Topics page, so colors are consistent across the app.
|
||||
|
||||
**T03 — Shared TagList + empty subtopic handling.** Created `TagList.tsx` component with configurable `max` (default 4) and optional `pillClass` prop. Applied it across all 5 tag-rendering sites: Home (featured + recent cards), SearchResults, SubTopicPage, CreatorDetail. Added `pill--overflow` styling for the "+N more" badge. In TopicsBrowse, subtopics with `technique_count === 0` now render as non-clickable spans with a "Coming soon" pill instead of dead-end links. Added `topic-subtopic--empty` and `pill--coming-soon` CSS.
|
||||
|
||||
## Verification
|
||||
|
||||
TypeScript type check (`npx tsc --noEmit`) passes with zero errors. Production build (`npm run build`) succeeds — 51 modules transformed, built in 777ms. All key source artifacts confirmed: collapsed init in TopicsBrowse, grid-template-rows CSS rules, badge--cat classes in CreatorDetail, TagList component with overflow logic, Coming soon badge rendering for empty subtopics, TagList imported in all 4 consumer pages.
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R019 — Topics page initializes collapsed, expand/collapse uses smooth 300ms CSS grid animation
|
||||
- R026 — Creator stats rendered as colored badge pills using catSlug-based classes
|
||||
- R027 — TagList component caps visible tags at 4 with +N more overflow badge, applied across all 5 sites
|
||||
- R028 — Empty subtopics render as non-clickable spans with Coming soon pill badge
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
None.
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
None.
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
None.
|
||||
|
||||
## Deviations
|
||||
|
||||
T03 added a `pillClass` prop to TagList (not in plan) to preserve the existing `pill--tag` class used in Home.tsx. Minor additive change, no impact.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
None.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/components/TagList.tsx` — New shared component — renders up to max tags with +N overflow pill
|
||||
- `frontend/src/pages/TopicsBrowse.tsx` — Collapsed-by-default init, grid animation wrapper, empty subtopic Coming soon badge
|
||||
- `frontend/src/pages/CreatorDetail.tsx` — Topic stats as colored badge pills in flex container, TagList for technique tags
|
||||
- `frontend/src/pages/Home.tsx` — Replaced inline tag maps with TagList component (featured + recent cards)
|
||||
- `frontend/src/pages/SearchResults.tsx` — Replaced inline tag map with TagList component
|
||||
- `frontend/src/pages/SubTopicPage.tsx` — Replaced inline tag map with TagList component
|
||||
- `frontend/src/App.css` — Added grid animation rules, topic-pills flex container, pill--overflow, pill--coming-soon, topic-subtopic--empty styles
|
||||
72
.gsd/milestones/M011/slices/S02/S02-UAT.md
Normal file
72
.gsd/milestones/M011/slices/S02/S02-UAT.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# S02: Topics, Creator Stats & Card Polish — UAT
|
||||
|
||||
**Milestone:** M011
|
||||
**Written:** 2026-03-31T08:36:24.689Z
|
||||
|
||||
# S02 UAT — Topics, Creator Stats & Card Polish
|
||||
|
||||
## Preconditions
|
||||
- Chrysopedia frontend running (http://ub01:8096 or local dev server)
|
||||
- At least one creator with multiple topic categories in the database
|
||||
- At least one technique with more than 4 topic tags
|
||||
- At least one subtopic with 0 techniques
|
||||
|
||||
---
|
||||
|
||||
## Test 1: Topics Page Loads Collapsed
|
||||
|
||||
1. Navigate to `/topics`
|
||||
2. **Expected:** All 7 category cards are visible but no subtopic lists are shown
|
||||
3. Click any category card header
|
||||
4. **Expected:** Subtopics expand with a smooth ~300ms slide animation (not an instant pop)
|
||||
5. Click the same category header again
|
||||
6. **Expected:** Subtopics collapse with the same smooth animation
|
||||
7. Click two different categories in sequence
|
||||
8. **Expected:** Both expand independently; expanding one does not collapse the other
|
||||
|
||||
## Test 2: Empty Subtopic Coming Soon Badge
|
||||
|
||||
1. Navigate to `/topics`
|
||||
2. Expand a category that contains a subtopic with 0 techniques
|
||||
3. **Expected:** The empty subtopic shows its name with a "Coming soon" pill badge
|
||||
4. **Expected:** The empty subtopic text is dimmed (opacity ~0.5) and the cursor is `default` (not pointer)
|
||||
5. Click the empty subtopic
|
||||
6. **Expected:** Nothing happens — it is not a link, no navigation occurs
|
||||
7. Hover over the empty subtopic
|
||||
8. **Expected:** No background highlight (unlike active subtopics)
|
||||
|
||||
## Test 3: Creator Stats Colored Pills
|
||||
|
||||
1. Navigate to `/creators` and click a creator with multiple topic categories
|
||||
2. **Expected:** The stats line shows topic categories as colored pill badges (not plain text with dots)
|
||||
3. **Expected:** Each pill has a distinct color corresponding to its topic category (same colors as Topics page category borders)
|
||||
4. **Expected:** Pills wrap naturally if the container is narrow (flex-wrap behavior)
|
||||
|
||||
## Test 4: Tag Overflow on Cards
|
||||
|
||||
1. Navigate to the homepage (`/`)
|
||||
2. Find a technique card (featured or recent) that has more than 4 tags in the database
|
||||
3. **Expected:** Exactly 4 tag pills are visible, followed by a "+N more" pill in muted/italic style
|
||||
4. Navigate to `/search` and search for a term that returns a technique with >4 tags
|
||||
5. **Expected:** Same overflow behavior — 4 tags + "+N more"
|
||||
6. Navigate to a subtopic page with a technique that has >4 tags
|
||||
7. **Expected:** Same overflow behavior
|
||||
8. Navigate to a creator detail page with a technique that has >4 tags
|
||||
9. **Expected:** Same overflow behavior
|
||||
|
||||
## Test 5: Tag Overflow Edge Cases
|
||||
|
||||
1. Find a technique card with exactly 4 tags
|
||||
2. **Expected:** All 4 tags shown, no "+N more" pill
|
||||
3. Find a technique card with fewer than 4 tags (e.g., 2)
|
||||
4. **Expected:** All tags shown, no "+N more" pill
|
||||
5. Find a technique card with exactly 5 tags
|
||||
6. **Expected:** 4 tags shown + "+1 more" pill
|
||||
|
||||
## Test 6: Collapse Animation is CSS-Based (Not Conditional Render)
|
||||
|
||||
1. Navigate to `/topics`
|
||||
2. Open browser DevTools → Elements panel
|
||||
3. Expand a category
|
||||
4. **Expected:** The subtopics wrapper div is always present in the DOM (not conditionally rendered). Its `data-expanded` attribute toggles between true/absent. The `grid-template-rows` CSS property transitions between `0fr` and `1fr`.
|
||||
|
||||
30
.gsd/milestones/M011/slices/S02/tasks/T03-VERIFY.json
Normal file
30
.gsd/milestones/M011/slices/S02/tasks/T03-VERIFY.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T03",
|
||||
"unitId": "M011/S02/T03",
|
||||
"timestamp": 1774946107163,
|
||||
"passed": false,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 7,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "npx tsc --noEmit",
|
||||
"exitCode": 1,
|
||||
"durationMs": 821,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "npm run build",
|
||||
"exitCode": 254,
|
||||
"durationMs": 84,
|
||||
"verdict": "fail"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
|
|
@ -1,6 +1,60 @@
|
|||
# S03: Global Search & Mobile Navigation
|
||||
|
||||
**Goal:** Make search accessible from any page and improve mobile navigation
|
||||
**Goal:** Compact search bar in nav on all non-home pages with Cmd+K focus. Mobile viewport (<768px) shows hamburger menu with stacked nav links and generous touch targets.
|
||||
**Demo:** After this: Compact search bar in nav on all pages. Cmd+K focuses it. Mobile viewport shows hamburger menu.
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Refactored SearchAutocomplete from heroSize boolean to variant string prop and wired compact nav search bar with Cmd+K shortcut into App.tsx header on all non-home routes** — Refactor SearchAutocomplete from heroSize boolean to variant string prop ('hero' | 'inline' | 'nav'), add nav-variant CSS (compact sizing, hidden submit button, high z-index dropdown), add globalShortcut prop for Cmd+K/Ctrl+K focus, wire nav search into App.tsx conditionally on non-home routes, update existing callers in Home.tsx and SearchResults.tsx.
|
||||
|
||||
This task delivers R020 (Global Search in Navigation).
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `SearchAutocomplete.tsx`: replace `heroSize?: boolean` with `variant?: 'hero' | 'inline' | 'nav'` (default `'inline'`). Update the className logic: `search-form--hero` when variant=hero, `search-form--inline` when variant=inline, `search-form--nav` when variant=nav. Same pattern for `search-input--*`.
|
||||
2. When `variant === 'nav'`: hide the submit button (don't render `<button>` — Enter submits the form). Add `search-container--nav` class to the container div for positioning context.
|
||||
3. Add `globalShortcut?: boolean` prop. When true, register a `useEffect` with a `keydown` listener for Cmd+K (metaKey) / Ctrl+K (ctrlKey) that calls `e.preventDefault()` and focuses `inputRef.current`. Clean up on unmount.
|
||||
4. In `App.css`: add `.search-form--nav` styles (max-width: 16rem, compact padding), `.search-input--nav` (smaller font, reduced padding), `.search-container--nav .typeahead-dropdown` (z-index: 200 to overlay page content). Add a Cmd+K hint badge (`.search-nav__shortcut`) styled as a subtle kbd element inside the input area.
|
||||
5. In `App.tsx`: import `useLocation` and `useNavigate` from react-router-dom, import `SearchAutocomplete`. Render `<SearchAutocomplete variant="nav" globalShortcut onSearch={...} placeholder="Search… ⌘K" />` between brand and right section, conditionally when `location.pathname !== '/'`.
|
||||
6. Update `Home.tsx` caller: change `heroSize` to `variant="hero"`.
|
||||
7. Update `SearchResults.tsx` caller: remove heroSize (already falsy, so `variant="inline"` default works, but be explicit: add `variant="inline"`).
|
||||
8. Verify: `cd frontend && npm run build` succeeds. Visually confirm in browser at desktop width.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] SearchAutocomplete accepts variant prop ('hero' | 'inline' | 'nav')
|
||||
- [ ] Nav variant: compact input, no submit button, high z-index dropdown
|
||||
- [ ] Cmd+K / Ctrl+K focuses nav search input (globalShortcut prop)
|
||||
- [ ] Nav search bar appears on all pages except homepage
|
||||
- [ ] Existing callers (Home.tsx, SearchResults.tsx) updated and unbroken
|
||||
- [ ] Build succeeds with zero errors
|
||||
- Estimate: 45m
|
||||
- Files: frontend/src/components/SearchAutocomplete.tsx, frontend/src/App.tsx, frontend/src/App.css, frontend/src/pages/Home.tsx, frontend/src/pages/SearchResults.tsx
|
||||
- Verify: cd frontend && npm run build 2>&1 | tail -5
|
||||
- [ ] **T02: Add mobile hamburger menu with touch targets and auto-close behavior** — Add a hamburger menu button visible below 768px that toggles a mobile nav panel. Nav links stack vertically with 44×44px minimum touch targets. Menu closes on route change, Escape, and outside click. The nav search bar (from T01) repositions inside the mobile panel on small screens.
|
||||
|
||||
This task delivers R021 (Mobile Hamburger Menu).
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `App.tsx`: add `menuOpen` state via `useState(false)`. Add a `<button className="hamburger-btn" onClick={() => setMenuOpen(v => !v)} aria-label="Toggle navigation" aria-expanded={menuOpen}>` inside `app-header__right`, before the nav element. Use a hamburger icon (three-line SVG or unicode ☰).
|
||||
2. Add `className` toggle on `app-nav`: when `menuOpen` is true, add `app-nav--open` class.
|
||||
3. Add `useEffect` watching `location.pathname` to close menu on navigation: `setMenuOpen(false)` when pathname changes.
|
||||
4. Add `useEffect` for Escape key: listen for `keydown` event, close menu when key is 'Escape' and menuOpen is true.
|
||||
5. Add outside-click close: `useRef` on the header element, check if click target is outside header when menuOpen, close if so.
|
||||
6. In `App.css`: add `.hamburger-btn` styles — hidden above 768px (`display: none`), visible below as a 44×44px button with no border/background, color matching nav text. Add `@media (max-width: 768px)` block: hide `.app-nav` by default, show `.app-nav--open` as a full-width column below the header row (position: absolute or flex column, stacked links). Each nav link gets `min-height: 44px; display: flex; align-items: center; padding: 0.75rem 1.5rem` for touch targets. The `.search-container--nav` inside the mobile panel gets `max-width: 100%` and appropriate positioning.
|
||||
7. Add open/close transition: `max-height` transition or `transform: scaleY` for smooth reveal.
|
||||
8. Verify: `cd frontend && npm run build` succeeds. Test at mobile viewport in browser.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Hamburger button visible only below 768px
|
||||
- [ ] Tapping hamburger toggles nav panel with stacked links
|
||||
- [ ] All mobile nav touch targets ≥ 44×44px
|
||||
- [ ] Menu closes on route change
|
||||
- [ ] Menu closes on Escape key
|
||||
- [ ] Menu closes on outside click
|
||||
- [ ] Nav search bar accessible in mobile menu
|
||||
- [ ] Build succeeds with zero errors
|
||||
- Estimate: 45m
|
||||
- Files: frontend/src/App.tsx, frontend/src/App.css
|
||||
- Verify: cd frontend && npm run build 2>&1 | tail -5
|
||||
|
|
|
|||
148
.gsd/milestones/M011/slices/S03/S03-RESEARCH.md
Normal file
148
.gsd/milestones/M011/slices/S03/S03-RESEARCH.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# S03 Research: Global Search & Mobile Navigation
|
||||
|
||||
## Summary
|
||||
|
||||
This slice adds two features to the app shell in `App.tsx`: (1) a compact search bar in the nav on all non-home pages with Cmd+K focus, and (2) a hamburger menu for mobile viewports (<768px). Both are well-understood patterns. The existing `SearchAutocomplete` component already supports `heroSize` toggling — a new compact/nav mode is a straightforward addition. No mobile nav exists yet; it's greenfield.
|
||||
|
||||
**Requirements owned:** R020 (Global Search in Navigation), R021 (Mobile Hamburger Menu)
|
||||
|
||||
## Recommendation
|
||||
|
||||
Straightforward implementation — no novel tech, no risky integration. Build the nav search first (it affects desktop and mobile layout), then add the hamburger menu. Both modify `App.tsx` and `App.css` primarily.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### Current App Shell Structure (`App.tsx`)
|
||||
|
||||
```
|
||||
<header className="app-header">
|
||||
<Link className="app-header__brand"><h1>Chrysopedia</h1></Link>
|
||||
<div className="app-header__right">
|
||||
<nav className="app-nav">
|
||||
<Link>Home</Link>
|
||||
<Link>Topics</Link>
|
||||
<Link>Creators</Link>
|
||||
<AdminDropdown />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
- No `useLocation` imported yet — will need it to conditionally render nav search (hide on `/`)
|
||||
- `app-header` is `display: flex; justify-content: space-between; padding: 0.75rem 1.5rem`
|
||||
- `app-header__right` is `display: flex; align-items: center; gap: 1.5rem`
|
||||
- At `max-width: 640px`: header becomes `flex-direction: column` and right section wraps
|
||||
- At `max-width: 768px`: no header changes currently (only technique columns go single-col)
|
||||
|
||||
### Existing SearchAutocomplete Component
|
||||
|
||||
File: `frontend/src/components/SearchAutocomplete.tsx`
|
||||
|
||||
- Props: `onSearch`, `placeholder`, `heroSize`, `initialQuery`, `autoFocus`
|
||||
- `heroSize` boolean toggles between `search-form--hero` and `search-form--inline` CSS classes
|
||||
- Input class toggles between `search-input--hero` and `search-input--inline`
|
||||
- Typeahead dropdown is absolutely positioned from `.search-container`
|
||||
- Has popular suggestions on empty focus + debounced search on 2+ chars
|
||||
- The component is self-contained with outside-click close and Escape handling
|
||||
|
||||
**For nav mode:** Need a third variant (beyond hero/inline). Options:
|
||||
1. Add a `navCompact` boolean prop that applies `search-form--nav` and `search-input--nav` classes
|
||||
2. Change `heroSize` to a `variant: 'hero' | 'inline' | 'nav'` string prop
|
||||
|
||||
Option 2 is cleaner. The nav variant needs: smaller padding, no submit button (Enter submits), narrower max-width (~16rem), and the typeahead dropdown needs `z-index` high enough to overlay page content.
|
||||
|
||||
### Nav Search Layout Plan
|
||||
|
||||
- Add `SearchAutocomplete` between brand and nav links in `app-header__right`
|
||||
- Conditionally render: show when `location.pathname !== '/'` (home has its own hero search)
|
||||
- Cmd+K handler: global `keydown` listener in App.tsx, calls `inputRef.current?.focus()`. Need to expose a ref or imperative handle from SearchAutocomplete — or add the listener inside SearchAutocomplete when variant is 'nav'.
|
||||
|
||||
**Cmd+K implementation:** The simplest approach is a `useEffect` in `App.tsx` that listens for `Cmd+K` (or `Ctrl+K` on non-Mac) and focuses a ref. SearchAutocomplete already has an internal `inputRef` — need to either:
|
||||
- Forward the ref via `React.forwardRef` so App.tsx can call `.focus()`
|
||||
- Or put the global keydown listener inside SearchAutocomplete when `variant === 'nav'`
|
||||
|
||||
The second is simpler — the component already manages its own input ref. Add `globalShortcut?: boolean` prop, and when true, register `keydown` listener for Cmd+K/Ctrl+K that focuses the input and prevents default browser behavior.
|
||||
|
||||
### Mobile Hamburger Menu Plan
|
||||
|
||||
No mobile nav exists. Current behavior at 640px: header stacks vertically, nav links wrap. At 768px: no header changes.
|
||||
|
||||
**Implementation:**
|
||||
- Add a hamburger button (☰) visible only below 768px via CSS `display: none` / media query
|
||||
- Hide `.app-nav` links below 768px by default, show in an overlay/dropdown when hamburger is active
|
||||
- State: `menuOpen` boolean in App.tsx
|
||||
- Close on: route change (watch `location` with useEffect), outside click, Escape key
|
||||
- Touch targets: 44×44px minimum per R021
|
||||
- The nav search bar should move inside the mobile menu panel on small screens
|
||||
|
||||
**CSS approach:**
|
||||
- `.hamburger-btn` — hidden on desktop, visible below 768px
|
||||
- `.app-nav` — hidden below 768px when menu closed
|
||||
- `.app-nav--open` — full-width dropdown below header with stacked links
|
||||
- Transition: `max-height` or `transform: translateX` for slide-in
|
||||
|
||||
**AdminDropdown in mobile:** It's already a dropdown component. In the mobile menu, it should render inline (expanded) rather than as a nested dropdown. Simplest: just let it render as-is inside the mobile panel — it works fine as a sub-dropdown.
|
||||
|
||||
### Existing CSS Custom Properties (relevant)
|
||||
|
||||
```css
|
||||
--color-bg-header (header background)
|
||||
--color-text-on-header (nav link color)
|
||||
--color-text-on-header-hover
|
||||
--color-bg-input (search input background)
|
||||
--color-border (input border)
|
||||
--color-accent (focus ring)
|
||||
--color-bg-surface (dropdown background)
|
||||
--color-shadow-heavy (dropdown shadow)
|
||||
```
|
||||
|
||||
All needed colors already exist as custom properties.
|
||||
|
||||
### Existing Responsive Breakpoints
|
||||
|
||||
- `max-width: 640px` — header columns, action bar stacking, small card adjustments
|
||||
- `max-width: 768px` — technique columns single-col
|
||||
- `min-width: 600px` — key moment grid
|
||||
|
||||
The 768px breakpoint for hamburger aligns with the existing 768px media query. The 640px breakpoint can handle further compacting if needed.
|
||||
|
||||
## Natural Task Decomposition
|
||||
|
||||
### T01: Nav Search Bar with Cmd+K (~45 min)
|
||||
1. Refactor `SearchAutocomplete` to accept `variant: 'hero' | 'inline' | 'nav'` (backward-compatible — default to hero/inline based on existing heroSize for now, or deprecate heroSize)
|
||||
2. Add nav-variant CSS: compact sizing, no submit button, appropriate z-index
|
||||
3. Add `globalShortcut` behavior inside SearchAutocomplete
|
||||
4. In `App.tsx`: import `useLocation`, conditionally render `<SearchAutocomplete variant="nav" ... />` when not on homepage
|
||||
5. Wire `onSearch` to `useNavigate` for `/search?q=...`
|
||||
|
||||
**Files:** `SearchAutocomplete.tsx`, `App.tsx`, `App.css`
|
||||
**Verify:** Build succeeds; search bar appears in nav on `/topics`, `/creators`, `/techniques/*`; hidden on `/`; Cmd+K focuses input; typing + Enter navigates to search results
|
||||
|
||||
### T02: Mobile Hamburger Menu (~45 min)
|
||||
1. Add hamburger button markup in `App.tsx` header
|
||||
2. Add `menuOpen` state with close-on-navigate (useEffect watching location), close-on-escape, close-on-outside-click
|
||||
3. CSS: hide hamburger above 768px, hide nav links below 768px unless open
|
||||
4. Mobile menu panel: stacked links with 44×44px touch targets
|
||||
5. Nav search bar repositioned inside mobile menu panel
|
||||
|
||||
**Files:** `App.tsx`, `App.css`
|
||||
**Verify:** Build succeeds; at viewport <768px, nav links hidden, hamburger visible; tapping hamburger shows nav; navigating closes menu; touch targets ≥44×44px
|
||||
|
||||
## Risks
|
||||
|
||||
- **Typeahead z-index in nav:** The dropdown positions absolutely from `.search-container`. In the nav, this needs to overlay page content below the header. Setting `z-index: 100` on the typeahead dropdown (already at 50) should handle it, but verify against sticky elements.
|
||||
- **SearchAutocomplete prop refactor:** Changing from `heroSize` boolean to `variant` string is a breaking change for Home.tsx and SearchResults.tsx callers. Need to update both call sites. Low risk but must be thorough.
|
||||
- **Mobile menu + search interaction:** When the mobile menu is open and the user focuses the search input, the typeahead dropdown needs to render within the mobile panel, not escape it. Verify positioning.
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
```bash
|
||||
# Build check
|
||||
cd frontend && npm run build
|
||||
|
||||
# Browser verification (manual)
|
||||
# Desktop: visit /topics, /creators — search bar in nav, Cmd+K focuses
|
||||
# Homepage: no nav search bar
|
||||
# Mobile viewport (<768px): hamburger visible, nav hidden, tap shows menu
|
||||
# Navigate from mobile menu: menu closes
|
||||
```
|
||||
51
.gsd/milestones/M011/slices/S03/tasks/T01-PLAN.md
Normal file
51
.gsd/milestones/M011/slices/S03/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
estimated_steps: 18
|
||||
estimated_files: 5
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Refactor SearchAutocomplete variant prop and add nav search bar with Cmd+K
|
||||
|
||||
Refactor SearchAutocomplete from heroSize boolean to variant string prop ('hero' | 'inline' | 'nav'), add nav-variant CSS (compact sizing, hidden submit button, high z-index dropdown), add globalShortcut prop for Cmd+K/Ctrl+K focus, wire nav search into App.tsx conditionally on non-home routes, update existing callers in Home.tsx and SearchResults.tsx.
|
||||
|
||||
This task delivers R020 (Global Search in Navigation).
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `SearchAutocomplete.tsx`: replace `heroSize?: boolean` with `variant?: 'hero' | 'inline' | 'nav'` (default `'inline'`). Update the className logic: `search-form--hero` when variant=hero, `search-form--inline` when variant=inline, `search-form--nav` when variant=nav. Same pattern for `search-input--*`.
|
||||
2. When `variant === 'nav'`: hide the submit button (don't render `<button>` — Enter submits the form). Add `search-container--nav` class to the container div for positioning context.
|
||||
3. Add `globalShortcut?: boolean` prop. When true, register a `useEffect` with a `keydown` listener for Cmd+K (metaKey) / Ctrl+K (ctrlKey) that calls `e.preventDefault()` and focuses `inputRef.current`. Clean up on unmount.
|
||||
4. In `App.css`: add `.search-form--nav` styles (max-width: 16rem, compact padding), `.search-input--nav` (smaller font, reduced padding), `.search-container--nav .typeahead-dropdown` (z-index: 200 to overlay page content). Add a Cmd+K hint badge (`.search-nav__shortcut`) styled as a subtle kbd element inside the input area.
|
||||
5. In `App.tsx`: import `useLocation` and `useNavigate` from react-router-dom, import `SearchAutocomplete`. Render `<SearchAutocomplete variant="nav" globalShortcut onSearch={...} placeholder="Search… ⌘K" />` between brand and right section, conditionally when `location.pathname !== '/'`.
|
||||
6. Update `Home.tsx` caller: change `heroSize` to `variant="hero"`.
|
||||
7. Update `SearchResults.tsx` caller: remove heroSize (already falsy, so `variant="inline"` default works, but be explicit: add `variant="inline"`).
|
||||
8. Verify: `cd frontend && npm run build` succeeds. Visually confirm in browser at desktop width.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] SearchAutocomplete accepts variant prop ('hero' | 'inline' | 'nav')
|
||||
- [ ] Nav variant: compact input, no submit button, high z-index dropdown
|
||||
- [ ] Cmd+K / Ctrl+K focuses nav search input (globalShortcut prop)
|
||||
- [ ] Nav search bar appears on all pages except homepage
|
||||
- [ ] Existing callers (Home.tsx, SearchResults.tsx) updated and unbroken
|
||||
- [ ] Build succeeds with zero errors
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``frontend/src/components/SearchAutocomplete.tsx` — existing search component to refactor`
|
||||
- ``frontend/src/App.tsx` — app shell to add nav search bar into`
|
||||
- ``frontend/src/App.css` — global styles for nav search variant`
|
||||
- ``frontend/src/pages/Home.tsx` — existing caller using heroSize prop`
|
||||
- ``frontend/src/pages/SearchResults.tsx` — existing caller using default props`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/components/SearchAutocomplete.tsx` — refactored with variant prop and globalShortcut behavior`
|
||||
- ``frontend/src/App.tsx` — nav search bar rendered conditionally on non-home routes`
|
||||
- ``frontend/src/App.css` — nav search variant styles added`
|
||||
- ``frontend/src/pages/Home.tsx` — updated to use variant='hero'`
|
||||
- ``frontend/src/pages/SearchResults.tsx` — updated to use variant='inline'`
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build 2>&1 | tail -5
|
||||
84
.gsd/milestones/M011/slices/S03/tasks/T01-SUMMARY.md
Normal file
84
.gsd/milestones/M011/slices/S03/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S03
|
||||
milestone: M011
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/components/SearchAutocomplete.tsx", "frontend/src/App.tsx", "frontend/src/App.css", "frontend/src/pages/Home.tsx", "frontend/src/pages/SearchResults.tsx"]
|
||||
key_decisions: ["Kept heroSize prop as deprecated fallback — variant prop takes precedence when both set", "Nav variant hides submit button entirely (Enter submits form)", "Kbd hint badge positioned absolutely inside input area"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "cd frontend && npm run build succeeds with zero errors — all 51 modules transformed, production bundle built in 770ms."
|
||||
completed_at: 2026-03-31T08:42:08.935Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Refactored SearchAutocomplete from heroSize boolean to variant string prop and wired compact nav search bar with Cmd+K shortcut into App.tsx header on all non-home routes
|
||||
|
||||
> Refactored SearchAutocomplete from heroSize boolean to variant string prop and wired compact nav search bar with Cmd+K shortcut into App.tsx header on all non-home routes
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S03
|
||||
milestone: M011
|
||||
key_files:
|
||||
- frontend/src/components/SearchAutocomplete.tsx
|
||||
- frontend/src/App.tsx
|
||||
- frontend/src/App.css
|
||||
- frontend/src/pages/Home.tsx
|
||||
- frontend/src/pages/SearchResults.tsx
|
||||
key_decisions:
|
||||
- Kept heroSize prop as deprecated fallback — variant prop takes precedence when both set
|
||||
- Nav variant hides submit button entirely (Enter submits form)
|
||||
- Kbd hint badge positioned absolutely inside input area
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-31T08:42:08.936Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Refactored SearchAutocomplete from heroSize boolean to variant string prop and wired compact nav search bar with Cmd+K shortcut into App.tsx header on all non-home routes
|
||||
|
||||
**Refactored SearchAutocomplete from heroSize boolean to variant string prop and wired compact nav search bar with Cmd+K shortcut into App.tsx header on all non-home routes**
|
||||
|
||||
## What Happened
|
||||
|
||||
Replaced heroSize boolean with variant prop ('hero' | 'inline' | 'nav'), added globalShortcut prop for Cmd+K/Ctrl+K focus, created nav-variant CSS with compact sizing and high z-index dropdown, wired nav search into App.tsx conditionally on non-home routes, updated Home.tsx and SearchResults.tsx callers.
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build succeeds with zero errors — all 51 modules transformed, production bundle built in 770ms.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd frontend && npm run build 2>&1 | tail -15` | 0 | ✅ pass | 2900ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Kept heroSize prop as deprecated fallback for backwards compat. Added min-width: 20rem to nav typeahead dropdown.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/components/SearchAutocomplete.tsx`
|
||||
- `frontend/src/App.tsx`
|
||||
- `frontend/src/App.css`
|
||||
- `frontend/src/pages/Home.tsx`
|
||||
- `frontend/src/pages/SearchResults.tsx`
|
||||
|
||||
|
||||
## Deviations
|
||||
Kept heroSize prop as deprecated fallback for backwards compat. Added min-width: 20rem to nav typeahead dropdown.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
47
.gsd/milestones/M011/slices/S03/tasks/T02-PLAN.md
Normal file
47
.gsd/milestones/M011/slices/S03/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
estimated_steps: 20
|
||||
estimated_files: 2
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Add mobile hamburger menu with touch targets and auto-close behavior
|
||||
|
||||
Add a hamburger menu button visible below 768px that toggles a mobile nav panel. Nav links stack vertically with 44×44px minimum touch targets. Menu closes on route change, Escape, and outside click. The nav search bar (from T01) repositions inside the mobile panel on small screens.
|
||||
|
||||
This task delivers R021 (Mobile Hamburger Menu).
|
||||
|
||||
## Steps
|
||||
|
||||
1. In `App.tsx`: add `menuOpen` state via `useState(false)`. Add a `<button className="hamburger-btn" onClick={() => setMenuOpen(v => !v)} aria-label="Toggle navigation" aria-expanded={menuOpen}>` inside `app-header__right`, before the nav element. Use a hamburger icon (three-line SVG or unicode ☰).
|
||||
2. Add `className` toggle on `app-nav`: when `menuOpen` is true, add `app-nav--open` class.
|
||||
3. Add `useEffect` watching `location.pathname` to close menu on navigation: `setMenuOpen(false)` when pathname changes.
|
||||
4. Add `useEffect` for Escape key: listen for `keydown` event, close menu when key is 'Escape' and menuOpen is true.
|
||||
5. Add outside-click close: `useRef` on the header element, check if click target is outside header when menuOpen, close if so.
|
||||
6. In `App.css`: add `.hamburger-btn` styles — hidden above 768px (`display: none`), visible below as a 44×44px button with no border/background, color matching nav text. Add `@media (max-width: 768px)` block: hide `.app-nav` by default, show `.app-nav--open` as a full-width column below the header row (position: absolute or flex column, stacked links). Each nav link gets `min-height: 44px; display: flex; align-items: center; padding: 0.75rem 1.5rem` for touch targets. The `.search-container--nav` inside the mobile panel gets `max-width: 100%` and appropriate positioning.
|
||||
7. Add open/close transition: `max-height` transition or `transform: scaleY` for smooth reveal.
|
||||
8. Verify: `cd frontend && npm run build` succeeds. Test at mobile viewport in browser.
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] Hamburger button visible only below 768px
|
||||
- [ ] Tapping hamburger toggles nav panel with stacked links
|
||||
- [ ] All mobile nav touch targets ≥ 44×44px
|
||||
- [ ] Menu closes on route change
|
||||
- [ ] Menu closes on Escape key
|
||||
- [ ] Menu closes on outside click
|
||||
- [ ] Nav search bar accessible in mobile menu
|
||||
- [ ] Build succeeds with zero errors
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``frontend/src/App.tsx` — app shell with nav search bar from T01`
|
||||
- ``frontend/src/App.css` — global styles to extend with mobile nav rules`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/App.tsx` — hamburger button, menuOpen state, auto-close behavior added`
|
||||
- ``frontend/src/App.css` — mobile nav styles with hamburger, panel, touch targets, transitions`
|
||||
|
||||
## Verification
|
||||
|
||||
cd frontend && npm run build 2>&1 | tail -5
|
||||
|
|
@ -1042,6 +1042,53 @@ a.app-footer__repo:hover {
|
|||
border-radius: 0.625rem;
|
||||
}
|
||||
|
||||
/* ── Nav search variant ───────────────────────────────────────────────────── */
|
||||
|
||||
.search-container--nav {
|
||||
position: relative;
|
||||
max-width: 16rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-form--nav {
|
||||
gap: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input--nav {
|
||||
padding: 0.375rem 2.75rem 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
border-radius: 0.375rem;
|
||||
background: var(--color-bg-input);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.search-input--nav::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.search-nav__shortcut {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.625rem;
|
||||
font-family: inherit;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-page);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.0625rem 0.3125rem;
|
||||
line-height: 1.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-container--nav .typeahead-dropdown {
|
||||
z-index: 200;
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
.btn--search {
|
||||
background: var(--color-btn-search-bg);
|
||||
color: var(--color-btn-search-text);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Link, Navigate, Route, Routes } from "react-router-dom";
|
||||
import { Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||
import Home from "./pages/Home";
|
||||
import SearchResults from "./pages/SearchResults";
|
||||
import TechniquePage from "./pages/TechniquePage";
|
||||
|
|
@ -11,14 +11,27 @@ import AdminPipeline from "./pages/AdminPipeline";
|
|||
import About from "./pages/About";
|
||||
import AdminDropdown from "./components/AdminDropdown";
|
||||
import AppFooter from "./components/AppFooter";
|
||||
import SearchAutocomplete from "./components/SearchAutocomplete";
|
||||
|
||||
export default function App() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const showNavSearch = location.pathname !== "/";
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<Link to="/" className="app-header__brand">
|
||||
<h1>Chrysopedia</h1>
|
||||
</Link>
|
||||
{showNavSearch && (
|
||||
<SearchAutocomplete
|
||||
variant="nav"
|
||||
globalShortcut
|
||||
placeholder="Search… ⌘K"
|
||||
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
|
||||
/>
|
||||
)}
|
||||
<div className="app-header__right">
|
||||
<nav className="app-nav">
|
||||
<Link to="/">Home</Link>
|
||||
|
|
|
|||
|
|
@ -19,18 +19,26 @@ import {
|
|||
interface SearchAutocompleteProps {
|
||||
onSearch: (query: string) => void;
|
||||
placeholder?: string;
|
||||
/** @deprecated Use variant="hero" instead */
|
||||
heroSize?: boolean;
|
||||
variant?: 'hero' | 'inline' | 'nav';
|
||||
initialQuery?: string;
|
||||
autoFocus?: boolean;
|
||||
/** When true, Cmd+K / Ctrl+K focuses the input globally */
|
||||
globalShortcut?: boolean;
|
||||
}
|
||||
|
||||
export default function SearchAutocomplete({
|
||||
onSearch,
|
||||
placeholder = "Search techniques…",
|
||||
heroSize = false,
|
||||
variant: variantProp,
|
||||
initialQuery = "",
|
||||
autoFocus = false,
|
||||
globalShortcut = false,
|
||||
}: SearchAutocompleteProps) {
|
||||
// Resolve variant: explicit prop wins, then legacy heroSize, then default 'inline'
|
||||
const variant = variantProp ?? (heroSize ? 'hero' : 'inline');
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [searchResults, setSearchResults] = useState<SearchResultItem[]>([]);
|
||||
const [popularSuggestions, setPopularSuggestions] = useState<SuggestionItem[]>([]);
|
||||
|
|
@ -64,6 +72,19 @@ export default function SearchAutocomplete({
|
|||
if (autoFocus) inputRef.current?.focus();
|
||||
}, [autoFocus]);
|
||||
|
||||
// Global Cmd+K / Ctrl+K shortcut to focus input
|
||||
useEffect(() => {
|
||||
if (!globalShortcut) return;
|
||||
function handleGlobalKey(e: KeyboardEvent) {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleGlobalKey);
|
||||
return () => document.removeEventListener("keydown", handleGlobalKey);
|
||||
}, [globalShortcut]);
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
|
|
@ -145,15 +166,15 @@ export default function SearchAutocomplete({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="search-container" ref={dropdownRef}>
|
||||
<div className={`search-container${variant === 'nav' ? ' search-container--nav' : ''}`} ref={dropdownRef}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className={`search-form ${heroSize ? "search-form--hero" : "search-form--inline"}`}
|
||||
className={`search-form search-form--${variant}`}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
className={`search-input ${heroSize ? "search-input--hero" : "search-input--inline"}`}
|
||||
className={`search-input search-input--${variant}`}
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
|
|
@ -161,9 +182,14 @@ export default function SearchAutocomplete({
|
|||
onKeyDown={handleKeyDown}
|
||||
aria-label="Search techniques"
|
||||
/>
|
||||
<button type="submit" className="btn btn--search">
|
||||
Search
|
||||
</button>
|
||||
{variant !== 'nav' && (
|
||||
<button type="submit" className="btn btn--search">
|
||||
Search
|
||||
</button>
|
||||
)}
|
||||
{variant === 'nav' && (
|
||||
<kbd className="search-nav__shortcut">⌘K</kbd>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{showDropdown && (showPopular || showSearch) && (
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export default function Home() {
|
|||
</p>
|
||||
|
||||
<SearchAutocomplete
|
||||
heroSize
|
||||
variant="hero"
|
||||
autoFocus
|
||||
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export default function SearchResults() {
|
|||
<div className="search-results-page">
|
||||
{/* Inline search bar */}
|
||||
<SearchAutocomplete
|
||||
variant="inline"
|
||||
initialQuery={q}
|
||||
onSearch={(newQ) =>
|
||||
navigate(`/search?q=${encodeURIComponent(newQ)}`, { replace: true })
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue