SPEC_ANSWERHUB_CRAFT.md
AnswerHub — navigation, craft, and the features that never landed — spec
What this covers: giving AnswerHub a navigation it does not currently have, executing its existing visual language properly instead of crudely, and finishing three features that were migrated as shells.
For what is actually built, deployed or reachable, see
ClaudeDocs/HANDOFF.md — status deliberately does not live here.
This spec does not change the family's visual direction.
ClaudeDesign/APP_FAMILY.md stands unamended: same tokens, same
radius, same paper/ink and sage green, same Notion/Linear restraint.
That was established during the interview and is worth stating twice,
because the owner's complaint sounded like a design-direction problem
and was not one. See Further Notes.
It follows SPEC_ANSWERHUB_SHELL.md, which built the page frame this
one navigates between, and sits alongside SPEC_ANSWERHUB_LIVE.md,
whose browser walk is now partly done — sign-in works.
No issue tracker exists for this project, so this is filed as a
ClaudeDocs/SPEC_* document, the same precedent SPEC_APPS.md set.
Problem Statement
The owner opened the deployed product and said three things: the interface still looks dated, most pages do not really work, and moving between pages is a problem. Investigating each turned up something different from what the words suggested.
There is almost no navigation. Crawling the deployed site, every link reachable from every page resolves to exactly two destinations: the homepage and sign-in. Search, settings, bulk upload and the moderation queue have no entry point anywhere in the product. They answer 200 if you type the URL, and there is no way to type it unless you already know it. This is why the owner said those pages "aren't live" — from inside the product, they are not. It is also a defect that page-by-page review cannot catch: each page is individually fine.
Three features were migrated as shells. The profile page is seventy-seven lines and never queries the submissions belonging to the person whose profile it is — so "我的解答" leads to a bio and three links. The old app's equivalent was two hundred and eleven lines with a paginated list of that person's answers and a small set of counts. Search works, but has no in-progress state, no result count and an empty state that says nothing and offers nothing. Listing cards show a title, a description and a like count while the database holds the language, the comment count and a view count for each submission.
The view count is a field nobody writes to. It exists on three models and no code path anywhere increments it.
The interface is badly executed, not badly directed. This is the part that took a wrong turn during the interview and needs stating plainly. "太方太平" — too boxy, too flat — reads as a complaint about the visual language, and the interview initially treated it as one, to the point of preparing to change shared tokens and drag SiaoHub along. The owner corrected it: the tone is the same, what is written now is just badly done. SiaoHub uses the same tokens and the same radius and draws no complaint. What is wrong is spacing chosen ad hoc rather than on a scale, a type hierarchy where headings and body read at nearly the same weight, uniform density regardless of what a region contains, and hover states that only change a border colour. Notion and Linear are not flat and boxy; restraint was implemented as absence.
Solution
Three separate pieces of work, none of which touches the design reference.
Give the product a navigation. A horizontal bar: the product name, then home, search and new answer; on the right, the theme control and a user menu holding my answers, settings, moderation (for administrators) and sign out. It collapses to a single control at phone width. Every destination in the product acquires an entry point, and a structural test fails if a future page does not — the same shape of guard this project already uses for content visibility, because this is the same class of bug: invisible in review, obvious in use.
Make transitions not jar. Both halves of what the owner meant by "跳轉問題": having somewhere to go, and getting there without flicker, white flashes or the layout jumping.
Execute the existing tone properly. A spacing scale instead of ad-hoc values, a type hierarchy with real contrast between levels, density considered per region, and hover and focus states that were decided rather than defaulted. Tokens, radius, colours and the design reference are untouched.
Finish the three shells, using the old application's field list as the reference the owner asked for. The profile page gets its owner's answers, paginated, with the counts that can be computed honestly. Search gets an in-progress state, a result count and an empty state that offers a way forward. Listing cards show the fields the database actually holds. The view count starts being written, so the number it displays means something.
The old application's trust badge is deliberately not carried over: it rendered "已驗證 / 請謹慎參考" from a field that does not exist in either schema. It was decoration shaped like information.
User Stories
Getting around
- As a visitor, I want a navigation bar on every page, so that I can reach the product's features without knowing their URLs.
- As a visitor, I want to reach search from anywhere, so that looking something up does not require going back to the homepage first.
- As a visitor, I want to reach the upload page from anywhere, so that contributing is always one click away.
- As a signed-in user, I want a menu holding my own things — my answers, my settings — so that account actions are grouped and predictable.
- As an administrator, I want the moderation queue in that menu, so that I do not have to remember its address.
- As a signed-in user, I want bulk upload to be reachable, so that a feature that exists is a feature I can find.
- As a visitor on a phone, I want the same destinations behind one control, so that the product is not desktop-only.
- As a visitor, I want the current section indicated in the navigation, so that I know where I am.
- As a developer adding a page later, I want the test suite to fail if it has no entry point, so that the product cannot grow another unreachable corner.
- As the owner, I want deliberate exceptions — blog and knowledge base are hidden on purpose — to be stated rather than to look like the same bug.
Moving between pages
- As a visitor, I want the page not to flash white when I navigate, so that the product feels like one application.
- As a visitor, I want the layout not to jump as content arrives, so that I do not lose my place or misclick.
- As a visitor, I want navigation to acknowledge my click immediately, so that I do not press again.
- As a visitor who prefers reduced motion, I want transitions to respect that, so that the interface does not make me unwell.
How it reads
- As a visitor, I want headings to look like headings, so that I can scan a page instead of reading it.
- As a visitor, I want related things grouped closely and unrelated things separated, so that the layout tells me the structure.
- As a visitor, I want dense regions to be dense and airy regions to be airy, so that a list of twenty items and a single form do not get the same treatment.
- As a visitor, I want hovering something clickable to feel like more than a border changing colour, so that the product feels responsive.
- As a keyboard user, I want focus to be clearly visible everywhere, so that I can see where I am.
- As the owner, I want the product to stop reading as dated without the palette or the radius changing, so that the family stays one family.
My profile
- As a signed-in user, I want my profile to list the answers I have posted, so that "我的解答" leads to my answers.
- As a visitor, I want anybody's profile to list their answers, so that I can see what a contributor has produced.
- As a visitor, I want that list paginated, so that a prolific contributor's profile stays usable.
- As a visitor, I want to see how many answers somebody has posted and how many likes they have received, so that I have some sense of them.
- As a visitor, I want those numbers to be real, so that the profile is not decorated with figures nobody computes.
- As a visitor to an empty profile, I want it to say so, so that a blank area is not mistaken for a failure.
Finding things
- As a visitor searching, I want to see that a search is running, so that I do not think nothing happened.
- As a visitor searching, I want to know how many results there are, so that I can judge whether to refine.
- As a visitor whose search found nothing, I want to be offered something to do next, so that a dead end is not the end.
- As a visitor, I want each result to show enough to choose between them — language, engagement, author — so that I do not open five pages to find one.
What a card says
- As a visitor, I want a listing card to show the language, so that I can skip answers in a language I do not use.
- As a visitor, I want to see likes, comments and views on a card, so that I can tell what other people found useful.
- As a visitor, I want those numbers to reflect reality, so that they are worth reading.
- As a visitor, I want the same card everywhere it appears — homepage, search, profile — so that I learn to read it once.
- As the owner, I do not want a badge that renders a field the schema does not have, so that the interface does not imply verification nobody performs.
Views
- As a contributor, I want a view counted when somebody opens my answer, so that the number on my profile means something.
- As the owner, I want that write to be cheap and non-blocking, so that counting views does not slow down reading them.
- As the owner, I want it clear that this adds a write on a path that previously only read, so that the change is not discovered later.
Implementation Decisions
ClaudeDesign/APP_FAMILY.mdis not amended. No token, radius, colour or tone decision is reopened. The work is executing what it already says. SiaoHub is not touched at all this round.A horizontal navigation bar owned by the shared shell, so a page cannot fail to have one. Primary destinations sit inline; account and administrative destinations sit behind a user menu. This introduces a Radix dropdown, which clears the bar
APP_FAMILY.mdsets for adding a primitive: a real page in current scope needs it.A drawer was considered and rejected. The old application used one, but only because Liquid Glass wanted a large panel to render into — and Liquid Glass is gone. With this many destinations, making a desktop user open a drawer before every navigation is friction with no purchase behind it.
The current section is indicated in the navigation, derived from the active path.
Reachability becomes a structural test. The route list is walked and compared against what the navigation links to; anything unreachable must be named in an exemption list with a defended reason. Blog and knowledge base go there, since they are hidden deliberately. Same mechanism, same discipline, as the existing visibility and frame guards: an empty exemption list is the default and every entry is a claim.
Spacing comes from a scale, not from taste per component. The current values were chosen individually and do not relate to each other. Regions get density appropriate to their content rather than one uniform padding.
Type hierarchy gets real contrast between page title, section heading, body and secondary text — currently they differ by very little, which is why pages read as undifferentiated.
Interaction states are decided rather than defaulted: hover, active and focus each do something considered, and focus is visible everywhere for keyboard use. Motion stays restrained and honours a reduced-motion preference.
The profile page gains a paginated list of that person's submissions, filtered through the existing visibility seam — a profile is a listing like any other and does not get to bypass it. Two counts are shown: submissions posted and likes received, both computed from real rows.
View counting is added on the submission detail page, so the third number is honest. It is a write on a path that previously only read; it must not block rendering, and it is called out here rather than discovered later. The counter is deliberately naive — no deduplication, no session tracking — because a rough number is the honest shape of the thing being measured and anything cleverer needs a decision nobody has made.
One card component, used by the homepage, search and profile. Three places currently render similar-but-different markup. It shows title, description, author, language, likes, comments and views — every one a field the schema holds.
The trust badge is not carried over. The old component rendered a
verifiedprop against a field neither schema has. Reproducing it would mean inventing the data or hardcoding the answer.Search gains an in-progress state, a result count and an empty state that offers a next action. The old implementation is the reference for what it says, not for how it is built — it had no filtering, sorting or pagination, and none is added here.
Field selection stays explicit on every new query, per the standing decision from the previous round: a listing fetches what it renders and not the submission files blob.
Navigation is client-side, so moving between pages does not re-fetch the shell or flash. Route-level loading states remain confined to segments that never call
notFound()— aloading.tsxopts its subtree into streaming, and a streamed response has already sent200beforenotFound()runs. That trap is documented inAPP_FAMILY.mdand must not be re-sprung while adding transitions.
Testing Decisions
Tests assert what a visitor can observe: what a page renders, where a link goes, what a checker reports, what a row contains afterwards. Never which component produced it.
Prior art is this repository's existing suite — the shell, accessibility, visual and structural specs added in the previous round, plus the older behavioural specs.
Navigation is tested through pages, not through the nav component. Asserting that the component renders a link to search would prove the component works while a page that failed to render it went unnoticed — the same distinction that made the shell tests meaningful.
Reachability gets a structural test that walks the routes and fails on any page the navigation does not link to. Like the guards before it, its matcher is pinned: it must be shown to flag a genuinely unreachable page, or it is a test that passes by matching nothing.
The profile page gets behavioural coverage: a user with submissions sees them, a user without sees an empty state that says so, another person's profile shows theirs, pagination works past the page size, and a hidden submission does not appear on its author's own profile — that last one because a profile is a listing and this project has twice shipped a listing that forgot the visibility seam.
View counting is tested by its effect: opening a submission increases the stored count; the page still renders if the write fails.
Search states are tested as states: in-progress appears, the count matches the results, the empty state renders for a query that matches nothing.
Accessibility runs across every page again, including the new navigation and user menu — a dropdown is the component most likely to have keyboard and focus problems, and the checker reports them.
Every visual snapshot is retaken. The layout changes substantially, so the existing baselines are worthless; they are regenerated once and then meaningful again. The suite still runs against a development server in one browser, and still proves local regression rather than cross-platform correctness.
The done bar is typecheck, unit, end-to-end and a production build, all measured in the session that claims them.
Out of Scope
- Any change to
ClaudeDesign/APP_FAMILY.md— tokens, radius, colour, tone. This spec executes it, it does not amend it. - SiaoHub. Untouched this round.
- Filtering, sorting or faceted search. The old application had none; adding it is a product decision nobody has made.
- Deduplicated or session-aware view counting.
- A verification or trust system. Not carrying the old badge over is not a promise to build the thing behind it.
- Blog and knowledge base. Still unreachable by design; they go in the reachability exemption list rather than getting navigation.
- The browser walk, credential rotation, and the handoff rewrite —
those belong to
SPEC_ANSWERHUB_LIVE.mdand remain outstanding. - Public reachability. No DNS, tunnel or certificate work.
- Renaming routes or identifiers.
Further Notes
The interview took a wrong turn worth recording. "太方太平" was read as a complaint about the visual direction, and the questioning went as far as choosing a new radius and asking whether SiaoHub should follow. The owner's correction — the tone is the same, this is just badly written — was the actually useful diagnosis, and the evidence was available the whole time: SiaoHub ships the same tokens and the same radius and draws no complaint. The generalisable lesson is that when an aesthetic complaint arrives about one of two surfaces built from one design system, the design system is unlikely to be the variable. Check what differs before offering to change what does not.
The unreachable-pages defect is the interesting one. Four pages had no entry point anywhere in the product. Every page passed its own tests, rendered correctly, and was individually reviewable. Nothing short of asking "what can you actually get to from here?" would find it, which is why it becomes a structural test rather than a fix.
The old application is a reference for what, not how. Its interface logic — which destinations exist, which fields a card carries, what a profile shows — is worth copying. Its implementation is built on Liquid Glass, which this family dropped entirely, and its trust badge renders a field that never existed.
"Migrated" turned out to mean three different things across these features: fully working (search), present but hollow (profile), and present but unreachable (settings, bulk upload, moderation). Only the first is what the word normally implies. Worth being precise about the next time a migration is called done.