feat: finish portfolio title, navigation, and destination effects
Add the approved name animation, rounded docking navigation, localized keyboard focus cue, unified builds card, and destination copy hierarchy.
Introduce lightweight one-shot meteors and a sun ring that repeats every three seconds at Contact. Open the email action in a separate browsing context. Preserve reduced-motion behavior and keep private handoff and visual-check artifacts out of version control.
Validation: 649 tests passed; all 20 failures in world-models and SceneCanvas reproduce on baseline 103427f. Typecheck, artifact verification, and transfer budgets passed. Client/SSR preview build passed before this commit. Existing baseline test failures remain unchanged.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import type { SectionId } from "@/content/types";
|
||||
import type { JourneySnapshot } from "@/journey/types";
|
||||
|
||||
const props = defineProps<{
|
||||
items: ReadonlyArray<{ id: SectionId; label: string }>;
|
||||
journey: JourneySnapshot;
|
||||
allowMotion?: boolean;
|
||||
onSectionLinkClick: (event: MouseEvent, requestSign?: boolean) => void;
|
||||
}>();
|
||||
|
||||
@@ -21,8 +22,22 @@ const navigationLabels: Readonly<Record<SectionId, string>> = {
|
||||
};
|
||||
|
||||
const disclosure = ref<HTMLDetailsElement | null>(null);
|
||||
const enhanced = ref(false);
|
||||
const docked = ref(false);
|
||||
const restScale = ref(1);
|
||||
let compactQuery: MediaQueryList | null = null;
|
||||
|
||||
// Reuse the journey's existing scroll sample; no second scroll/render loop.
|
||||
watch(() => props.journey.scrollY, y => {
|
||||
if (y > 24) docked.value = true;
|
||||
else if (y <= 6) docked.value = false;
|
||||
}, { immediate: true });
|
||||
|
||||
const measureRestWidth = (): void => {
|
||||
const width = document.documentElement.clientWidth;
|
||||
if (width > 0) restScale.value = Math.min(1400, Math.max(0, width - 48)) / width;
|
||||
};
|
||||
|
||||
const syncDisclosureToViewport = ({
|
||||
matches,
|
||||
}: Pick<MediaQueryList, "matches">): void => {
|
||||
@@ -31,12 +46,16 @@ const syncDisclosureToViewport = ({
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
measureRestWidth();
|
||||
enhanced.value = true;
|
||||
window.addEventListener('resize', measureRestWidth, { passive: true });
|
||||
compactQuery = window.matchMedia("(max-width: 767px)");
|
||||
syncDisclosureToViewport(compactQuery);
|
||||
compactQuery.addEventListener("change", syncDisclosureToViewport);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', measureRestWidth);
|
||||
compactQuery?.removeEventListener("change", syncDisclosureToViewport);
|
||||
compactQuery = null;
|
||||
});
|
||||
@@ -49,7 +68,7 @@ const navigate = (event: MouseEvent): void => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="site-navigation" aria-label="Portfolio sections">
|
||||
<nav class="site-navigation" aria-label="Portfolio sections" :data-enhanced="enhanced" :data-docked="docked" :data-motion="allowMotion === true" :style="{ '--nav-rest-scale': restScale }">
|
||||
<details ref="disclosure" class="site-navigation__disclosure" open>
|
||||
<summary>Navigate</summary>
|
||||
<ul>
|
||||
|
||||
Reference in New Issue
Block a user