ui/phase-12: order composer skeleton

OrderDraftStore persists per-game command drafts in Cache; the
sidebar Order tab renders the list with a per-row delete control.
The layout passes a `historyMode` prop through Sidebar / BottomTabs
as a constant `false`, so Phase 26 only flips the source.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-08 23:26:58 +02:00
parent e5dab2a43a
commit 460591c159
18 changed files with 1022 additions and 53 deletions
+90 -10
View File
@@ -1332,17 +1332,97 @@ Goal: implement the empty order composer as a persistent vertical list
that survives navigation and reloads, ready to receive commands in
later phases.
Artifacts:
Decisions taken with the project owner during implementation:
- `ui/frontend/src/lib/sidebar/order-tab.svelte` vertical command list
with empty state copy
- `ui/frontend/src/sync/order-draft.ts` draft order store backed by
`Cache`, persisting across reloads
- `ui/frontend/src/sync/order-types.ts` typed command shape
(`OrderCommand` discriminated union)
- topic doc `ui/docs/order-composer.md` describing the
draft-replaces-server-order model, the local-validation invariant,
and command status semantics
1. **Store filename uses the runes extension.** PLAN.md originally
listed `ui/frontend/src/sync/order-draft.ts`. Svelte 5 runes only
compile inside `*.svelte` / `*.svelte.ts` files; the draft state
has to be reactive so `order-tab.svelte` re-renders on
add/remove/move. The artifact ships as
`ui/frontend/src/sync/order-draft.svelte.ts`, mirroring the
Phase 11 `lib/game-state.svelte.ts` pattern.
2. **Single `placeholder` variant in the discriminated union.** The
project compactness rule rejects defining surface for the next
phase. Phase 14 owns `planetRename` end-to-end (inspector UI,
command type, submit pipeline, server-result merging) and is the
right place to add the first real variant. Phase 12 ships exactly
one variant — `{ kind: "placeholder"; id: string; label: string }`
— sufficient for the add/remove/reorder/persist tests.
3. **Reorder API is `move(fromIndex, toIndex)`.** One canonical
operation; up/down at the call site is a one-line index
arithmetic. No `moveUp`/`moveDown` aliases.
4. **Write-on-every-mutation persistence.** `add`/`remove`/`move`
each call `Cache.put` with the full draft snapshot. Phase 25 may
profile the submit pipeline and batch writes if needed; until
then deterministic writes are easier to test.
5. **Per-game scoping via Svelte context.** One `OrderDraftStore`
instance per game is created in `routes/games/[id]/+layout.svelte`
alongside `GameStateStore`, exposed through
`ORDER_DRAFT_CONTEXT_KEY`, disposed on layout destroy.
6. **`historyMode` as a prop, not a module.** Layout passes
`historyMode={false}` (a constant in Phase 12) to `Sidebar` and
`BottomTabs`; both forward to their tab-bar children which omit
the order entry when the flag is true. Phase 26 introduces the
real `lib/history-mode.ts` module and replaces the constant in
one place.
7. **Empty-state copy is `order is empty` / `приказ пуст`.** The
`coming soon` placeholder text is replaced; per-row delete
button reads `delete` / `удалить`.
8. **e2e seeding via `__galaxyDebug.seedOrderDraft`.** The existing
debug surface in `routes/__debug/store/+page.svelte` is extended
with `seedOrderDraft(gameId, commands)` and
`clearOrderDraft(gameId)` helpers that write directly to the
`order-drafts` cache namespace. The store loads the seeded draft
on the next layout mount the same way it would after a real
reload.
9. **Race / disposal hygiene mirrors `GameStateStore`.** Mutations
are gated on `status === "ready"` so calls before `init`
resolves are no-ops, and `persist` checks a `destroyed` flag so
in-flight writes after `dispose` resolve into nothing.
Artifacts (delivered):
- `ui/frontend/src/sync/order-types.ts` — `OrderCommand`
discriminated union (single `placeholder` variant) and
`CommandStatus` lifecycle type.
- `ui/frontend/src/sync/order-draft.svelte.ts` —
`OrderDraftStore` runes class with
`init` / `add` / `remove` / `move` / `dispose`, plus
`ORDER_DRAFT_CONTEXT_KEY`. Persists the full draft on every
mutation under namespace `order-drafts`, key `{gameId}/draft`.
- `ui/frontend/src/lib/sidebar/order-tab.svelte` — replaces the
Phase 10 stub. Empty state from `game.sidebar.empty.order`;
ordered list with stable `data-testid="order-command-{i}"`
rows and a per-row delete button.
- `ui/frontend/src/lib/sidebar/sidebar.svelte`,
`tab-bar.svelte`, `bottom-tabs.svelte` — `historyMode` prop on
the sidebar forwards to `hideOrder` on tab-bar / bottom-tabs;
active-tab `order` is reset to `inspector` if the flag flips
on, and the `?sidebar=order` URL seed falls back to
`inspector` while the flag is true.
- `ui/frontend/src/routes/games/[id]/+layout.svelte` —
instantiates `OrderDraftStore`, sets context, runs
`init({ cache, gameId })` next to `gameState.init` through
one `Promise.all`, disposes on destroy, passes
`historyMode={false}` down.
- `ui/frontend/src/routes/__debug/store/+page.svelte` — extended
`DebugSurface` with `seedOrderDraft` / `clearOrderDraft`.
- `ui/frontend/src/lib/i18n/locales/{en,ru}.ts` — new
`game.sidebar.order.command_delete` key plus updated
`game.sidebar.empty.order` copy.
- `ui/docs/order-composer.md` — topic doc describing the
draft-replaces-server-order model, local-validation invariant,
command status state machine, persistence, history-mode wiring,
and test layout. Cross-references `storage.md` and
`navigation.md`.
- `ui/docs/storage.md` — namespace registry row for
`order-drafts`.
- `ui/docs/navigation.md` — describes the historyMode prop wiring
through Sidebar / BottomTabs.
- `ui/README.md` — new entry under topic docs for
`order-composer.md`.
- Vitest: `ui/frontend/tests/order-draft.test.ts`.
- Playwright: `ui/frontend/tests/e2e/order-composer.spec.ts`.
Dependencies: Phases 6, 10.