Parent epic: #326 · Depends on: F2, F3, F4 (ModalShell + Dropdown atoms), F5 · Stories: US-003, US-018, US-020, US-021, US-023, US-026, US-028
Problem
The workspace has four cross-cutting overlay surfaces. They are referenced from V1, V3, V4, V5 — but the surfaces themselves are V9's responsibility so the verticals can dispatch a single open event and not own the overlay implementation.
Solution
NewIssueModal.vue
- Fields: Title (required) · Type (
feat | fix | research | chore | docs | req) · Scope (free text with suggestions: plugin-shell | tooling | ui | infra | …) · Description (textarea) · Assign Agent (optional, populated from useLiveStreamStore.agents)
- Submit →
CreateIssueUseCase (calls GitHubPort.createIssue) → on success: inject IssueRow at top of sidebar list (via useEntitiesStore.insertIssue); numbered toast "Issue #N created" (US-003)
- Esc / ✕ closes without creating
- Empty title → submit disabled; inline validation hint
EditIssueModal.vue
- Pre-fills all fields from current issue + a "Context & Notes" textarea (acceptance criteria, constraints) — US-017, US-018
- Submit →
UpdateIssueUseCase (calls GitHubPort.updateIssue); re-renders IssueHeader on success
- Same Esc / ✕ behaviour
DiffModal.vue
- Wide (~660px), centered
- File header: kind badge + path + line counts
- Line-numbered diff body using
DiffViewer atom (F4); hunk header in blue
- Two variants:
- Proposal: footer has Accept / Reject buttons; dispatch
AcceptProposalUseCase / RejectProposalUseCase (US-020, US-021)
- PR file: footer has Close + branch info (no actions) (US-026, US-028)
- Esc closes
AgentPicker.vue
- Fixed-position dropdown (not modal — must position relative to the trigger element)
- Lists all agents with gradient avatar + Name + Role + status dot (green/amber)
- Click an agent → resolves the dispatching promise with the agent id; consumer dispatches
AssignTaskToAgentUseCase
- Outside-click dismisses (use
useEventListener('click', …, { once: true }) or capture-phase listener; respect z-index)
- Esc dismisses
Cross-cutting modal infrastructure
- Modals open via a
useModalStack() composable (built on the ModalShell atom from F4): LIFO Esc handling, single overlay, focus trap, restore focus on close
- All four overlays expose a typed promise-returning API:
await openNewIssueModal() // resolves to Issue | undefined
await openEditIssueModal(issue)
await openDiffModal(source) // source: { kind: 'proposal'; proposal } | { kind: 'pr-file'; pr; file }
await openAgentPicker({ anchorEl })
Acceptance criteria
Out of scope
- "Assign Agent" field in NewIssueModal actually assigning (note as TODO; defer to Phase 2 since agent assignment is its own use case)
- Diff syntax highlighting language selection (just pass through
code tokens from F4's DiffViewer)
Affected files
| File |
Action |
src/ui/components/modals/NewIssueModal.vue |
New |
src/ui/components/modals/EditIssueModal.vue |
New |
src/ui/components/modals/DiffModal.vue |
New |
src/ui/components/modals/AgentPicker.vue |
New |
src/ui/composables/useModalStack.ts |
New |
src/application/issue/CreateIssueUseCase.ts |
New |
src/application/issue/UpdateIssueUseCase.ts |
New |
tests/ui/components/modals/*.test.ts + .po.ts |
New |
tests/application/issue/*.test.ts |
New |
stories/modals/*.stories.ts |
New |
References
inputs/specorator-mvp-design-2026-05/Specorator_Design_Brief.html §"Modals & Overlays"
inputs/specorator-mvp-design-2026-05/Specorator_Handoff.html §"Modals & Overlays" table
- User stories US-003, US-018, US-020, US-021, US-023, US-026, US-028
Parent epic: #326 · Depends on: F2, F3, F4 (ModalShell + Dropdown atoms), F5 · Stories: US-003, US-018, US-020, US-021, US-023, US-026, US-028
Problem
The workspace has four cross-cutting overlay surfaces. They are referenced from V1, V3, V4, V5 — but the surfaces themselves are V9's responsibility so the verticals can dispatch a single open event and not own the overlay implementation.
Solution
NewIssueModal.vuefeat | fix | research | chore | docs | req) · Scope (free text with suggestions:plugin-shell | tooling | ui | infra | …) · Description (textarea) · Assign Agent (optional, populated fromuseLiveStreamStore.agents)CreateIssueUseCase(callsGitHubPort.createIssue) → on success: inject IssueRow at top of sidebar list (viauseEntitiesStore.insertIssue); numbered toast"Issue #N created"(US-003)EditIssueModal.vueUpdateIssueUseCase(callsGitHubPort.updateIssue); re-rendersIssueHeaderon successDiffModal.vueDiffVieweratom (F4); hunk header in blueAcceptProposalUseCase/RejectProposalUseCase(US-020, US-021)AgentPicker.vueAssignTaskToAgentUseCaseuseEventListener('click', …, { once: true })or capture-phase listener; respect z-index)Cross-cutting modal infrastructure
useModalStack()composable (built on theModalShellatom from F4): LIFO Esc handling, single overlay, focus trap, restore focus on closeAcceptance criteria
ModalShell(modals) orDropdown(picker)issue.context(US-017, US-018)role="dialog"+aria-modal="true"+aria-labelledbyfor modals; focusable trigger / restorable focusnpm run typecheck,npm run lint,npm run test,npm run test:storybookpassOut of scope
codetokens from F4'sDiffViewer)Affected files
src/ui/components/modals/NewIssueModal.vuesrc/ui/components/modals/EditIssueModal.vuesrc/ui/components/modals/DiffModal.vuesrc/ui/components/modals/AgentPicker.vuesrc/ui/composables/useModalStack.tssrc/application/issue/CreateIssueUseCase.tssrc/application/issue/UpdateIssueUseCase.tstests/ui/components/modals/*.test.ts+.po.tstests/application/issue/*.test.tsstories/modals/*.stories.tsReferences
inputs/specorator-mvp-design-2026-05/Specorator_Design_Brief.html§"Modals & Overlays"inputs/specorator-mvp-design-2026-05/Specorator_Handoff.html§"Modals & Overlays" table