From 64ecb564a9bf0b9db337a54a2fe006a4840c6b43 Mon Sep 17 00:00:00 2001 From: leubeem Date: Wed, 17 Jun 2026 15:09:07 +0200 Subject: [PATCH] feat(pm): group scenario chooser by journey with a phase flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworked the scenario picker so it's clear which single-phase practice belongs to which chain. Scenarios are grouped by Departure / Arrival; each complete chain renders as a left-to-right flow of its phases with arrows (e.g. Clearance → Startup & Taxi → Tower → Departure). Tap any step to practise just that phase, or "Fly full" to run the whole chain. Co-Authored-By: Claude Opus 4.8 --- app/pages/pm.vue | 147 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 41 deletions(-) diff --git a/app/pages/pm.vue b/app/pages/pm.vue index 5f66a49..b8f3864 100644 --- a/app/pages/pm.vue +++ b/app/pages/pm.vue @@ -140,48 +140,57 @@ {{ error }} - -
-

- Complete scenarios + +

+

+ {{ grp.category }}

-
- - - -
{{ s.name }}
-
{{ s.subtitle }}
-
-
-
+ + + + +
+
+ +
+
{{ chain.complete.name }}
+
{{ chain.complete.subtitle }}
+
+
+ + Fly full + +
+ + +
+ +
+
+
- -
-

- Practice a single phase -

-
- -
-
+

+ Tap a step to practise just that phase · “Fly full” runs the whole chain. +

@@ -1922,8 +1931,35 @@ const SCENARIOS: Scenario[] = [ }, ] -const completeScenarios = SCENARIOS.filter(s => s.isComplete) -const individualScenarios = SCENARIOS.filter(s => !s.isComplete) +// --------------------------------------------------------------------------- +// Chooser layout: each complete chain is shown as a flow of its phases, so it's +// clear which single-phase practice maps to which segment of which journey. +// --------------------------------------------------------------------------- +interface ChainDef { + category: 'Departure' | 'Arrival' + completeId: string // the isComplete scenario that flies the whole chain + segmentIds: string[] // individual-phase scenario ids, in order +} + +const CHAIN_DEFS: ChainDef[] = [ + { category: 'Departure', completeId: 'ifr-departure', segmentIds: ['clearance', 'taxi', 'tower', 'departure'] }, + { category: 'Arrival', completeId: 'ifr-arrival', segmentIds: ['ifr-enroute', 'ifr-approach', 'ifr-landing', 'taxi-in'] }, + { category: 'Arrival', completeId: 'vfr-arrival', segmentIds: ['vfr-approach', 'circuit-landing', 'taxi-in'] }, +] + +const chainGroups = computed(() => { + const byCategory = new Map>() + for (const def of CHAIN_DEFS) { + const complete = SCENARIOS.find(s => s.id === def.completeId) + if (!complete) continue + const segments = def.segmentIds + .map(id => SCENARIOS.find(s => s.id === id)) + .filter((s): s is Scenario => !!s) + if (!byCategory.has(def.category)) byCategory.set(def.category, []) + byCategory.get(def.category)!.push({ id: def.completeId, complete, segments }) + } + return Array.from(byCategory, ([category, chains]) => ({ category, chains })) +}) /** The scenario the user just finished (used on the completion screen). */ const completedScenario = ref(null) @@ -4778,6 +4814,35 @@ onUnmounted(() => { .pm-readback-expected { color: #fff; font-weight: 600; } .pm-readback-forms { color: rgba(255, 255, 255, 0.4); } +/* Scenario chooser — phase flow with arrows */ +.pm-flow { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; +} +.pm-flow-node { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 9px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + font-size: 11.5px; + line-height: 1.2; + color: rgba(255, 255, 255, 0.8); + transition: border-color .15s, background .15s, color .15s; +} +.pm-flow-node:hover { + border-color: rgba(34, 211, 238, 0.6); + background: rgba(34, 211, 238, 0.1); + color: #fff; +} +.pm-flow-arrow { + color: rgba(255, 255, 255, 0.25); +} + .pm-bottomnav { flex: 0 0 auto; display: flex;