feat: add Night Shift layer (boot intro, ops strip, headlight, chapter numerals, film grain)

This commit is contained in:
2026-10-10 14:13:26 +03:00
parent 475e494675
commit d16ce88639
8 changed files with 389 additions and 0 deletions

View File

@@ -2,10 +2,12 @@
import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import ContactDestination from './ContactDestination.vue';
import DriveControls from './DriveControls.vue';
import BootSequence from './BootSequence.vue';
import ExperienceTimeline from './ExperienceTimeline.vue';
import HeroJourney from './HeroJourney.vue';
import InfrastructureDiagram from './InfrastructureDiagram.vue';
import JourneyHud from './JourneyHud.vue';
import OperationsStrip from './OperationsStrip.vue';
import PortfolioSection from './PortfolioSection.vue';
import ProjectCard from './ProjectCard.vue';
import ProjectCaseStudy from './ProjectCaseStudy.vue';
@@ -16,6 +18,7 @@ import { useAdaptiveQuality } from '@/composables/useAdaptiveQuality';
import { useAudioEngine } from '@/composables/useAudioEngine';
import { useAudioIntent } from '@/composables/useAudioIntent';
import { useJourneySections } from '@/composables/useJourneySections';
import { useHeadlight } from '@/composables/useHeadlight';
import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation';
import { useMotionPreferences } from '@/composables/useMotionPreferences';
import { assetUrl } from '@/content/asset-url';
@@ -59,6 +62,7 @@ const sectionLabel = (id: SectionId): string => {
const canonicalNavigation = computed(() => canonicalSectionIds.map((id) => ({ id, label: sectionLabel(id) })));
const motion = useMotionPreferences();
useHeadlight(computed(() => motion.policy.value.allowSceneRequest));
const navigation = useKeyboardNavigation(canonicalSectionIds);
const journeyBinding = useJourneySections(
canonicalSectionIds,
@@ -211,6 +215,8 @@ onUnmounted(() => removePageListeners());
:sample-frame="sampleSceneFrame"
@state-change="updateSceneState"
/>
<BootSequence :allow-motion="motion.policy.value.allowSceneRequest" />
<OperationsStrip />
<JourneyHud
:journey="journey"
:section-label="activeSectionLabel"

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef } from 'vue';
const props = defineProps<{ allowMotion: boolean }>();
const SEEN_KEY = 'portfolio.boot.seen';
const LINES = [
'night-shift v1.0 bucharest 03:00',
'ingress ........ tls ok',
'identity ....... access boundaries enforced',
'nextcloud ...... migrated, rollback ready',
'backups ........ verified',
'all systems quiet. starting route.',
] as const;
const visible = shallowRef(false);
const shown = shallowRef(0);
const leaving = shallowRef(false);
const timers: number[] = [];
const finish = (): void => {
if (!visible.value || leaving.value) return;
leaving.value = true;
timers.push(window.setTimeout(() => { visible.value = false; }, 600));
};
const onKey = (): void => finish();
onMounted(() => {
let seen = false;
try { seen = window.sessionStorage.getItem(SEEN_KEY) === '1'; } catch { seen = false; }
if (!props.allowMotion || seen || navigator.webdriver) return;
try { window.sessionStorage.setItem(SEEN_KEY, '1'); } catch { /* optional */ }
visible.value = true;
LINES.forEach((_, index) => {
timers.push(window.setTimeout(() => { shown.value = index + 1; }, 260 + index * 280));
});
timers.push(window.setTimeout(finish, 260 + LINES.length * 280 + 500));
window.addEventListener('keydown', onKey, { once: true });
});
onUnmounted(() => {
timers.forEach((id) => window.clearTimeout(id));
window.removeEventListener('keydown', onKey);
});
</script>
<template>
<div v-if="visible" class="boot-sequence" :data-leaving="leaving || undefined" aria-hidden="true" @click="finish">
<ol class="boot-sequence__log">
<li v-for="(line, index) in LINES.slice(0, shown)" :key="index">{{ line }}</li>
</ol>
<span class="boot-sequence__skip">click or press any key to skip</span>
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import { onMounted, onUnmounted, shallowRef } from 'vue';
import { bucharestClock, uptimeSince } from '@/lib/operations';
/** Start of the current hosting-admin role (content file: Trupa lui Răzuș, Aug 2025). */
const SHIFT_START = new Date('2025-08-01T00:00:00Z');
const clock = shallowRef<string | null>(null);
const shift = shallowRef<string | null>(null);
let timer = 0;
const tick = (): void => {
const now = new Date();
const { months, days } = uptimeSince(SHIFT_START, now);
clock.value = bucharestClock(now);
shift.value = `${months} mo ${days} d`;
};
onMounted(() => {
tick();
timer = window.setInterval(tick, 30_000);
});
onUnmounted(() => window.clearInterval(timer));
</script>
<template>
<aside class="ops-strip" aria-hidden="true">
<span class="ops-strip__dot" />
<span>Bucharest <strong>{{ clock ?? '--:--' }}</strong></span>
<span class="ops-strip__sep">/</span>
<span>On shift <strong>{{ shift ?? '-- mo' }}</strong></span>
</aside>
</template>