From 8929eeddb0cc6d8be2922510177a48afbe472b17 Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Wed, 10 Jun 2026 17:55:36 +0200 Subject: [PATCH] web: focus modal, slice 8 of M3 Clicking a panel's network (cursor zoom-in, a real button for keyboard users) or its menu's Focus item opens the modal: the same SVG at the same global round rendered up to 90vw/85vh over the spec scrim, with a second PlayerBar docked in the footer sharing the store. Only the page PlayerBar owns the global keyboard map, so keys are never handled twice. role=dialog with aria-modal, labelled by the panel name, focus moves in and is trapped, Esc/X/scrim close, and focus returns to the opener. focusPanelId already lives in the URL, so shared links open pre-focused; on mobile the modal becomes a full-screen sheet. --- web/src/App.vue | 2 + web/src/components/FocusModal.vue | 226 +++++++++++++++++++++++++++ web/src/components/PlayerBar.vue | 12 +- web/src/components/ScenarioPanel.vue | 23 ++- 4 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 web/src/components/FocusModal.vue diff --git a/web/src/App.vue b/web/src/App.vue index 0f4a719..1c67fa4 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -3,6 +3,7 @@ import { onMounted } from 'vue' import AppBar from '@/components/AppBar.vue' import ControlsCard from '@/components/ControlsCard.vue' import ErrorBanner from '@/components/ErrorBanner.vue' +import FocusModal from '@/components/FocusModal.vue' import FooterDisclaimer from '@/components/FooterDisclaimer.vue' import HeroHeadline from '@/components/HeroHeadline.vue' import LegendRow from '@/components/LegendRow.vue' @@ -45,6 +46,7 @@ onMounted(async () => { /> +
{{ store.state.announcement }}
diff --git a/web/src/components/FocusModal.vue b/web/src/components/FocusModal.vue new file mode 100644 index 0000000..60a49e1 --- /dev/null +++ b/web/src/components/FocusModal.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/web/src/components/PlayerBar.vue b/web/src/components/PlayerBar.vue index b895e23..03b0f87 100644 --- a/web/src/components/PlayerBar.vue +++ b/web/src/components/PlayerBar.vue @@ -9,6 +9,10 @@ import { useSimStore } from '@/composables/useSimStore' // and Home/End work globally except in text inputs; on the scrubber the // browser handles them natively. +// The modal docks a second PlayerBar over the same store; only the page +// instance owns the global keyboard map so keys are not handled twice. +const props = withDefaults(defineProps<{ globalKeys?: boolean }>(), { globalKeys: true }) + const store = useSimStore() const playback = usePlayback() @@ -62,8 +66,12 @@ function onGlobalKeydown(event: KeyboardEvent) { } } -onMounted(() => document.addEventListener('keydown', onGlobalKeydown)) -onBeforeUnmount(() => document.removeEventListener('keydown', onGlobalKeydown)) +onMounted(() => { + if (props.globalKeys) document.addEventListener('keydown', onGlobalKeydown) +}) +onBeforeUnmount(() => { + if (props.globalKeys) document.removeEventListener('keydown', onGlobalKeydown) +})