06 — Screen Specifications (Search Module)
- 1. Global Search Bar (in-shell)
- 2. Results Screen State Machine
- 3. Query contract (what the client sends)
- 4. Empty-query behavior (exact)
- 5. Stale-data posture
- 6. Keyboard / platform adaptations
The authoritative behavioral spec for the Search results surface. Every backend behavior below is verified against source with file:line references. Client behaviors are the forward-looking spec (PRD keeps mobile out of Phase 1 — 00-shared/12 A1). This is the largest file in the package by design: it pins the full state machine of the only full screen plus the in-shell bar.
1. Global Search Bar (in-shell)
1.1 Composition
[AppBar leading] SearchBarField [scan icon*] [clear ✕]
(hint, debounce, submit)
* forward-looking
- Height 48 dp; rounded 24 dp;
AppTextFieldvariant with search icon (shared00-shared/03). - Clear button appears only when text is non-empty; clears and re-shows
landing (zero-state,
05 §4).
1.2 Behavior contract
| Event | Client action | Server effect |
|---|---|---|
| Text change (composing, non-empty) | restart 300 ms debounce timer (proposed); fire request on expiry | GET /search?q=… — trimmed query; blank/whitespace q short-circuits server-side to empty result (search.service.ts:28-30) |
| Text change (empty) | cancel timer; emit idle; show landing | no request |
| Submit (enter / search action) | commit query immediately, cancel debounce | same endpoint; page resets to 1 |
| Tap "See all X" | set entityType param, page 1 | server filters fetched page by type (search.service.ts:44-49) |
| Tap result | navigate to owner module detail (/students/:id etc.) | none — result carries entityType + entityId (search.service.ts:11-12) |
1.3 Debounce specification
- Delay: 300 ms
(proposed)— tuned for a Mongo$textquery withskip/limit(search-index.repository.ts:27-33). - Rule: only the latest query fires; any earlier in-flight response for a
superseded query is discarded (stale-guard token,
13§3). - Exceptions: no debounce on submit; no debounce on pagination (fires immediately).
- Why: each keystroke would otherwise hit
$text+countDocuments+ optional fallback — two queries per search (search.service.ts:36-37).
1.4 Permission behavior
- Bar hidden entirely without
searchpermission (permissions.constants.ts:93). - If the token expires mid-typing: 401 → global session-expiry flow
(
00-shared/10 §3), state preserved, re-login resumes.
2. Results Screen State Machine
States (legend per 05): idle → loading → success | error | empty; the
machine is implemented as SearchCubit (13).
2.1 idle
- No query. Landing content only (
05 §4). No network.
2.2 loading
- Triggered: debounce expiry, submit, pagination fetch, retry.
- UI: previous results stay visible for refinement (reload, not
replace); first-ever query shows
SearchResultSkeleton(07§4). - Inline spinner in bar; list dimmed at 40% opacity.
- Minimum spinner display 250 ms to avoid flash on fast responses
(proposed).
2.3 success
data: SearchResult[](search.service.ts:10-16), rendered grouped.- Grouping: client groups by
entityType; server sends flat list (search.service.ts:51-57). Group order (proposed): Student, User, Teacher, Staff, Parent, Book, Organization, Lead, Announcement — stable, types absent from results are omitted. Header shows label + per-page count. - Row anatomy (
SearchResultTile):- Leading:
AppAvatar(initials fromtitle; entity-type icon fallback). - Title:
title(e.g. full name, admission number, book title — extraction rulessearch-indexer.service.ts:68-75). - Subtitle:
description(email, book description, announcement body —:77-81), ellipsized 2 lines. - Trailing:
AppTags fortags(role/department/grade —:83-86), max 2 + "+n". - A11y label:
"{title}, {entityType}, {description}".
- Leading:
- Empty description/tags: omit rows' subtitle/trailing — defaults are
''and[](search-index.schema.ts:18-22). - Pagination: scroll to 80% of the list → fetch
page+1(limit 20 default, max 50 —search-query.dto.ts:23-29) ifmeta.hasNext(pagination-query.dto.ts:41-55). Append; show bottom loading row. - Counts: header count is per fetched page, not a global total —
server computes
totalItemsfromcountSearch(q)(search.service.ts:37), but per-type totals are unknowable from the flat response (search.service.ts:44-49).
2.4 empty
- Server:
data: []withmeta.totalItems: 0. Two sources:- blank/whitespace
q—search.service.ts:28-30(client never sends it, by contract); - no matches in index (text index or fallback scan) —
search-index.repository.ts:20-50.
- blank/whitespace
- UI:
AppEmptyState: "No results for “q”", sub-line "Check the spelling or try fewer words." CTA: "Clear search" (returns to landing). - Do not show suggestions (no backend suggestions
(forward-looking)).
2.5 error
- Server auto-fails-over:
$textfailure → regex fallback (search.service.ts:35-41) — so most query-level failures never reach the client. Clienterrortriggers on: 401 (session), 403 (permission revoked mid-session), 429 (rate limited —00-shared/07 §4), network/offline. - UI:
AppErrorStatewith message + Retry (re-run same query) and "Back". - Offline:
AppOfflineBanner+ cached-last-results if any; retry on connectivity restore (00-shared/10 §2).
2.6 permission
- Not reached from the bar (hidden), but direct deep links
(forward-looking)must render a permission state: icon + "You don't have search access." (403→PERMISSION_DENIED,00-shared/07 §3).
3. Query contract (what the client sends)
| Param | Source | Rules |
|---|---|---|
q | search-query.dto.ts:6-9 | optional string; client trims; never send blank; recommended client cap 100 chars (proposed) — server has no max |
entityType | search-query.dto.ts:11-14 | optional string; client sends one of the 9 known types |
page | search-query.dto.ts:16-21 | ≥1, default 1 |
limit | search-query.dto.ts:23-29 | 1–50, default 20; validation errors → 400 VALIDATION_ERROR (00-shared/07 §3) |
4. Empty-query behavior (exact)
Server: !q?.trim() → { data: [], meta: buildPaginationMeta(page, limit, 0) }
(search.service.ts:28-30) — totalPages computes to 1, hasNext/hasPrevious
false (pagination-query.dto.ts:46-54). No index access. Client contract:
never send it; landing screen (05 §4) covers the idle state.
5. Stale-data posture
- Index freshness = event-driven only; no reindex job in code
(
search-indexer.service.ts:44-46). - Known staleness:
LeadandAnnouncementrows never update/delete (search-indexer.service.ts:29-31) → renamed/edited leads and edited announcements surface stale titles until(planned)reindex/event wiring. - UI: no staleness signal today; when a reindex job ships
(planned), surfaceupdatedAton tiles (base.schema.ts:31-33).
6. Keyboard / platform adaptations
- Tablet/desktop: two-column group layout;
/focuses bar;Escclears. - IME composing ignored until composition end (CJK-safe debounce).
- Landscape: same as portrait, more tiles per row.