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 @@
+
+
+
+
+
+
+
+
+
{{ panel.label }}
+
+ {{ roundPct(reachedPctNow) }}%
+ {{ reachedCount }} of {{ numStudents }} reached
+
+
+
+
+
+
+
+
+
+
+
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)
+})
diff --git a/web/src/components/ScenarioPanel.vue b/web/src/components/ScenarioPanel.vue
index ce6cdd7..3498b75 100644
--- a/web/src/components/ScenarioPanel.vue
+++ b/web/src/components/ScenarioPanel.vue
@@ -22,6 +22,7 @@ const failed = computed(() => store.state.failedPanelIds.has(props.panel.id))
const menuItems = computed(() => [
{ key: 'edit', label: 'Edit' },
{ key: 'rename', label: 'Rename' },
+ { key: 'focus', label: 'Focus' },
{ key: 'duplicate', label: 'Duplicate', disabled: store.state.panels.length >= MAX_PANELS },
{ key: 'remove', label: 'Remove', disabled: store.state.panels.length <= 1 },
])
@@ -34,6 +35,9 @@ function onMenuSelect(itemKey: string) {
focusLabelOnOpen.value = itemKey === 'rename'
store.state.editingPanelId = props.panel.id
break
+ case 'focus':
+ store.setFocusPanel(props.panel.id)
+ break
case 'duplicate':
store.duplicatePanel(props.panel.id)
kebabButton.value?.focus()
@@ -136,7 +140,14 @@ const dimmed = computed(() => store.state.runState === 'running' && result.value
{{ chip.text }}
-
+
@@ -273,6 +284,16 @@ const dimmed = computed(() => store.state.runState === 'running' && result.value
flex-wrap: wrap;
}
+.netbtn {
+ display: block;
+ width: 100%;
+ border: none;
+ background: none;
+ padding: 0;
+ cursor: zoom-in;
+ border-radius: 8px;
+}
+
.chip {
font-size: 11.5px;
font-weight: 550;