Files
kandev/docs/specs/ui/system-design/task-navigation.md
T
2026-09-21 18:22:29 +01:00

121 lines
5.8 KiB
Markdown

---
status: current
system: ui
requirements:
- REQ-UI-TASK-NAVIGATION-001
---
# Task navigation system design
## Purpose and boundaries
This design extends existing URL and client-navigation primitives. It does not
introduce a router or route catalog. UI owns the reusable interaction; task
identity and dependency semantics remain in the task system.
## Requirement mapping
| Requirement | Design sections |
| --- | --- |
| REQ-UI-TASK-NAVIGATION-001 | URL authority, Link authority, Enforcement, Responsive behavior |
## URL authority
`apps/web/lib/links.ts::linkToTask` remains the only first-party workbench URL
builder. Keep the current `(taskId, layout?)` call shape compatible. Add an
options overload containing `layout?`, `sessionId?`, and `searchParams?`
(`URLSearchParams`) for current canvas and inbox contexts. Clone caller-owned
parameters; explicit layout/sessionId values replace matching keys. Preserve
other keys and repeated values. Encode the raw ID as one path segment exactly
once. Callers pass raw IDs, never pre-encoded IDs. Test both overloads.
Route matching can still recognize `/t/` and `/tasks/`. Compatibility redirects
use the builder for their destination. `/tasks` listings, `/office/tasks/`,
API paths, external URLs, and stored user-authored content are separate contracts.
Do not globally rewrite strings or historical content.
## Link authority
Add `components/routing/task-link.tsx::TaskLink`, a thin wrapper around existing
`components/routing/app-link.tsx`. Accept `taskId`, builder options, normal
anchor presentation props, and `onNavigated`; omit `href` from public props.
The wrapper calls `linkToTask`, forwards its ref, and owns no task/store state.
Existing compliant `Link href={linkToTask(id)}` consumers may remain; new direct
task anchors should prefer TaskLink. Programmatic navigation continues through
`useRouter().push(linkToTask(...))`.
Extend AppLink with an optional `onNavigated` callback called inside the
existing successful `pushNavigationState` callback, after the location event.
It must not run for cancelled guards, prevented clicks, external navigation,
modified clicks, or alternate targets. TaskLink delegates all click decisions
to AppLink. Do not duplicate the router's guard or browser-modifier logic.
DependencyRow uses TaskLink. The chip supplies a stable close callback through
DependencyLists, invoked only after committed same-tab navigation. Keep browser
Back semantics and anchor accessibility. Missing-task and access errors continue
through the existing destination route.
## Enforcement
Add a local ESLint rule and register it as an error on production TS/TSX in
`apps/web/eslint.config.mjs`. Test it using the existing local-rule pattern.
Reject hand-built root-relative task-detail URL expressions outside links.ts:
string literals with a nonempty task segment, interpolated templates, and string
concatenation beginning `/t/` or `/tasks/`. Permit bare prefix constants used
for route recognition; exclude tests, E2E, generated files, comments, API and
Office paths. Replace constructed comparison URLs with linkToTask too.
Also reject native JSX anchors whose href resolves to a task route or a
linkToTask call, and window.location assign/replace/href writes with such a
value. Resolve import aliases and simple local const aliases. Report a clear
replacement suggestion. Rule fixtures cover direct, aliased, concatenated,
and conditional forms plus benign route matching and API paths.
This is bounded static enforcement, not whole-program URL analysis. Unknown
runtime strings remain outside its proof. Do not claim all possible dynamic
links are statically verified. Add a wiring test that evaluates the real ESLint
configuration on a synthetic new production component so new directories are
covered. Start at zero violations by migrating current builders in the same
work order; no growing baseline or broad per-directory exemptions.
## Responsive behavior
Keep the current dependency popover on fine pointers and useTouchDrawer on
coarse pointers. Reuse shared Drawer safe-area handling and the single internal
list scroller. The nearest mobile navigation exemplar is
`components/task/mobile/session-task-switcher-sheet.tsx`; the dependency chip
already uses the appropriate brief-choice drawer. Task selection completes the
choice and goes to the workbench, so successful navigation dismisses the drawer.
Keep its header and close action, constrain long lists to the viewport, and
apply 44px row sizing only on touch surfaces. No new persisted state or copy.
## Validation
Component tests prove canonical hrefs, click interception, guard cancellation,
callback timing, modifier handling, and encoding. Desktop and Pixel 5 E2E
follow links in both dependency directions, verify target task content, dismiss
the disclosure, and exercise Back. Set a unique window property before clicking
and assert it survives; also observe main-frame document requests. A correct URL
alone is not evidence against reload. Check touch targets and drawer containment.
## Related contracts
[Mobile task navigation](../requirements/mobile-task-navigation.md) owns broader
task actions and mobile board navigation. This contract adds shared task-link
transport and URL construction across desktop and mobile consumers.
## Related decisions and alternatives
[Navigation manifest boundaries](../../../decisions/2026-08-04-navigation-manifest-boundaries.md)
retains top-level destination ownership.
[Settings save coordination](../../../decisions/0046-settings-route-save-coordinator.md)
retains dirty-navigation authority. No new route ownership registry is needed.
A URL helper alone cannot prevent raw anchors from reloading. A component alone
cannot cover imperative redirects. Reusing both existing authorities with a
thin wrapper and lint enforcement addresses these distinct failure modes.
## Implementation plans
- [Task link navigation](../../../plans/task-link-navigation/plan.md)