fix: close final portfolio quality gaps

This commit is contained in:
2026-09-02 17:38:11 +03:00
parent 0899b6b00f
commit b4728ceef0
24 changed files with 1822 additions and 889 deletions

View File

@@ -1,21 +1,31 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import IconGlyph, { type IconGlyphName } from './IconGlyph.vue';
import type { AudioUiState } from '@/audio/types';
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch,
} from "vue";
import IconGlyph, { type IconGlyphName } from "./IconGlyph.vue";
import type { AudioUiState } from "@/audio/types";
import type { MotionPolicy, QualityTier } from "@/journey/motion-policy";
const props = withDefaults(defineProps<{
policy: MotionPolicy;
maximumQuality: QualityTier;
sceneStatus: string;
audioState?: AudioUiState;
audioMuted?: boolean;
audioVolume?: number;
}>(), {
audioState: () => ({ kind: 'idle' }),
audioMuted: false,
audioVolume: 0.4,
});
const props = withDefaults(
defineProps<{
policy: MotionPolicy;
maximumQuality: QualityTier;
sceneStatus: string;
audioState?: AudioUiState;
audioMuted?: boolean;
audioVolume?: number;
}>(),
{
audioState: () => ({ kind: "idle" }),
audioMuted: false,
audioVolume: 0.4,
},
);
const emit = defineEmits<{
pause: [];
@@ -35,50 +45,80 @@ const settingsOpen = ref(false);
const volumeOpen = ref(false);
const action = computed(() => {
if (props.policy.reason === 'visitor-pause') {
return { label: 'Resume motion', event: 'resume' as const, icon: 'play' as IconGlyphName };
if (props.policy.reason === "visitor-pause") {
return {
label: "Resume motion",
event: "resume" as const,
icon: "play" as IconGlyphName,
};
}
if (props.policy.reason === 'saved-static') {
return { label: 'Enable motion', event: 'enable' as const, icon: 'play' as IconGlyphName };
if (props.policy.reason === "saved-static") {
return {
label: "Enable motion",
event: "enable" as const,
icon: "play" as IconGlyphName,
};
}
if (props.policy.reason === 'full') {
return { label: 'Pause motion', event: 'pause' as const, icon: 'pause' as IconGlyphName };
if (props.policy.reason === "full") {
return {
label: "Pause motion",
event: "pause" as const,
icon: "pause" as IconGlyphName,
};
}
return null;
});
const isAudioActive = computed(() => (
props.audioState.kind === 'running' || props.audioState.kind === 'suspended'
));
const muteLabel = computed(() => props.audioMuted ? 'Unmute sound' : 'Mute sound');
const muteIcon = computed<IconGlyphName>(() => props.audioMuted ? 'sound-muted' : 'sound');
const isAudioActive = computed(
() =>
props.audioState.kind === "running" ||
props.audioState.kind === "suspended",
);
const muteLabel = computed(() =>
props.audioMuted ? "Unmute sound" : "Mute sound",
);
const muteIcon = computed<IconGlyphName>(() =>
props.audioMuted ? "sound-muted" : "sound",
);
const statusText = computed(() => {
if (props.policy.reason === 'system-reduce') return 'Motion: static — reduced motion requested';
if (props.policy.reason === 'data-saver') return 'Motion: static — data saver is active';
if (props.policy.reason === 'visitor-pause') return 'Motion: paused';
if (props.policy.reason === 'saved-static') return 'Motion: static';
if (props.sceneStatus === 'failed' || props.sceneStatus === 'static') return 'Visuals unavailable';
if (props.sceneStatus === 'loading' || props.sceneStatus === 'idle') return 'Motion: preparing';
if (props.policy.reason === "system-reduce")
return "Motion: static; reduced motion requested";
if (props.policy.reason === "data-saver")
return "Motion: static; data saver is active";
if (props.policy.reason === "visitor-pause") return "Motion: paused";
if (props.policy.reason === "saved-static") return "Motion: static";
if (props.sceneStatus === "failed" || props.sceneStatus === "static")
return "Visuals unavailable";
if (props.sceneStatus === "loading" || props.sceneStatus === "idle")
return "Motion: preparing";
return null;
});
const runAction = (): void => {
const event = action.value?.event;
if (event === 'pause') emit('pause');
else if (event === 'resume') emit('resume');
else if (event === 'enable') emit('enable');
if (event === "pause") emit("pause");
else if (event === "resume") emit("resume");
else if (event === "enable") emit("enable");
};
const changeQuality = (event: Event): void => {
const value = (event.currentTarget as HTMLSelectElement).value;
if (value === 'high' || value === 'medium' || value === 'low' || value === 'static') {
emit('qualityChange', value);
if (
value === "high" ||
value === "medium" ||
value === "low" ||
value === "static"
) {
emit("qualityChange", value);
}
};
const changeVolume = (event: Event): void => {
emit('audioVolumeChange', Number((event.currentTarget as HTMLInputElement).value));
emit(
"audioVolumeChange",
Number((event.currentTarget as HTMLInputElement).value),
);
};
const toggleSettings = (): void => {
@@ -92,7 +132,9 @@ const toggleVolume = (): void => {
};
const closePanels = (restoreFocus = false): void => {
const focusTarget = settingsOpen.value ? settingsButton.value : volumeButton.value;
const focusTarget = settingsOpen.value
? settingsButton.value
: volumeButton.value;
settingsOpen.value = false;
volumeOpen.value = false;
if (restoreFocus) void nextTick(() => focusTarget?.focus());
@@ -104,7 +146,8 @@ const onDocumentPointerDown = (event: Event): void => {
};
const onDocumentKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'Escape' || (!settingsOpen.value && !volumeOpen.value)) return;
if (event.key !== "Escape" || (!settingsOpen.value && !volumeOpen.value))
return;
event.preventDefault();
closePanels(true);
};
@@ -115,13 +158,13 @@ watch(
);
onMounted(() => {
document.addEventListener('pointerdown', onDocumentPointerDown);
document.addEventListener('keydown', onDocumentKeyDown);
document.addEventListener("pointerdown", onDocumentPointerDown);
document.addEventListener("keydown", onDocumentKeyDown);
});
onBeforeUnmount(() => {
document.removeEventListener('pointerdown', onDocumentPointerDown);
document.removeEventListener('keydown', onDocumentKeyDown);
document.removeEventListener("pointerdown", onDocumentPointerDown);
document.removeEventListener("keydown", onDocumentKeyDown);
});
</script>
@@ -154,13 +197,24 @@ onBeforeUnmount(() => {
<span>STARTING…</span>
</button>
<p v-else-if="audioState.kind === 'failed'" class="drive-controls__audio-status" role="status">
<p
v-else-if="audioState.kind === 'failed'"
class="drive-controls__audio-status"
role="status"
>
Audio unavailable. Continue without sound.
</p>
<div v-else-if="isAudioActive" class="drive-controls__dock" role="group" aria-label="Drive controls">
<div
v-else-if="isAudioActive"
class="drive-controls__dock"
role="group"
aria-label="Drive controls"
>
<button
v-if="audioState.kind === 'suspended' && audioState.resumeRequiresGesture"
v-if="
audioState.kind === 'suspended' && audioState.resumeRequiresGesture
"
class="drive-control-button"
type="button"
aria-label="Resume sound"
@@ -208,6 +262,18 @@ onBeforeUnmount(() => {
</button>
</div>
<button
v-if="action && !isAudioActive && action.event !== 'pause'"
class="drive-control-button"
type="button"
:aria-label="action.label"
:title="action.label"
:data-tooltip="action.label"
@click="runAction"
>
<IconGlyph :name="action.icon" />
</button>
<button
v-if="audioState.kind !== 'starting'"
ref="settingsButton"
@@ -223,6 +289,14 @@ onBeforeUnmount(() => {
<IconGlyph name="settings" />
</button>
<p
v-if="statusText"
class="drive-controls__status drive-controls__announcer visually-hidden"
role="status"
>
{{ statusText }}
</p>
<div
v-if="volumeOpen && isAudioActive"
id="drive-volume-panel"
@@ -240,7 +314,7 @@ onBeforeUnmount(() => {
step="0.05"
:value="audioVolume"
@input="changeVolume"
>
/>
<output for="drive-volume">{{ Math.round(audioVolume * 100) }}%</output>
</div>
@@ -253,13 +327,18 @@ onBeforeUnmount(() => {
>
<strong>DRIVE SETTINGS</strong>
<label for="drive-quality">QUALITY</label>
<select id="drive-quality" :value="maximumQuality" aria-label="Quality control" @change="changeQuality">
<select
id="drive-quality"
:value="maximumQuality"
aria-label="Quality control"
@change="changeQuality"
>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
<option value="static">Static</option>
</select>
<p v-if="statusText" class="drive-controls__status" role="status">{{ statusText }}</p>
<p v-if="statusText" class="drive-controls__status">{{ statusText }}</p>
</div>
</div>
</template>

View File

@@ -1,13 +1,17 @@
<script setup lang="ts">
import type { InfrastructureCaseStudy } from '@/content/types';
import type { InfrastructureCaseStudy } from "@/content/types";
defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
</script>
<template>
<article class="infrastructure-case">
<h3 :id="`${infrastructure.id}-diagram-title`">{{ infrastructure.title }}</h3>
<p :id="`${infrastructure.id}-diagram-description`">{{ infrastructure.summary }}</p>
<h3 :id="`${infrastructure.id}-diagram-title`">
{{ infrastructure.title }}
</h3>
<p :id="`${infrastructure.id}-diagram-description`">
{{ infrastructure.summary }}
</p>
<figure class="infrastructure-flow-figure">
<div
class="infrastructure-flow"
@@ -17,14 +21,22 @@ defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
:aria-describedby="`${infrastructure.id}-diagram-description`"
>
<div class="infrastructure-flow__route" aria-hidden="true">
<div class="infrastructure-node infrastructure-node--visitor" data-infrastructure-node="visitor">
<div
class="infrastructure-node infrastructure-node--visitor"
data-infrastructure-node="visitor"
>
<span class="infrastructure-node__sequence">Request</span>
<strong>Visitor</strong>
<span class="infrastructure-node__detail">Inbound traffic</span>
</div>
<div class="infrastructure-node infrastructure-node--ingress" data-infrastructure-node="nginx-tls">
<span class="infrastructure-boundary" data-trust-boundary>Public edge</span>
<div
class="infrastructure-node infrastructure-node--ingress"
data-infrastructure-node="nginx-tls"
>
<span class="infrastructure-boundary" data-trust-boundary
>Public edge</span
>
<span class="infrastructure-node__connector"></span>
<span class="infrastructure-node__sequence">01 / edge</span>
<strong>Nginx and TLS</strong>
@@ -32,35 +44,78 @@ defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
<span class="infrastructure-node__guard">fail2ban guard</span>
</div>
<div class="infrastructure-node infrastructure-node--access" data-infrastructure-node="authentication">
<span class="infrastructure-boundary" data-trust-boundary>Access boundary</span>
<div
class="infrastructure-node infrastructure-node--access"
data-infrastructure-node="authentication"
>
<span class="infrastructure-boundary" data-trust-boundary
>Access boundary</span
>
<span class="infrastructure-node__connector"></span>
<span class="infrastructure-node__sequence">02 / verify</span>
<strong>Authentication</strong>
<span class="infrastructure-node__detail">Protected access</span>
</div>
<div class="infrastructure-node infrastructure-node--services" data-infrastructure-node="containerized-services">
<span class="infrastructure-boundary" data-trust-boundary>Service boundary</span>
<div
class="infrastructure-node infrastructure-node--services"
data-infrastructure-node="containerized-services"
>
<span class="infrastructure-boundary" data-trust-boundary
>Service boundary</span
>
<span class="infrastructure-node__connector"></span>
<span class="infrastructure-node__sequence">03 / run</span>
<strong>Containerized services</strong>
<span class="infrastructure-node__detail">Managed workloads</span>
</div>
<div class="infrastructure-node infrastructure-node--operations" data-infrastructure-node="operations">
<span class="infrastructure-node__connector infrastructure-node__connector--vertical"></span>
<div
class="infrastructure-node infrastructure-node--operations"
data-infrastructure-node="operations"
>
<span
class="infrastructure-node__connector infrastructure-node__connector--vertical"
></span>
<span class="infrastructure-node__sequence">04 / sustain</span>
<strong>Operations</strong>
<span class="infrastructure-node__detail">Lifecycle + maintenance</span>
<span class="infrastructure-node__detail"
>Lifecycle + maintenance</span
>
</div>
</div>
</div>
<figcaption>{{ infrastructure.title }}</figcaption>
</figure>
<dl
class="infrastructure-legend"
aria-label="Infrastructure diagram legend"
>
<div>
<dt>
<span
class="infrastructure-legend__boundary"
aria-hidden="true"
/>Trust boundary
</dt>
<dd>Where trust or access rules change</dd>
</div>
<div>
<dt>
<span class="infrastructure-legend__flow" aria-hidden="true" />Request
flow
</dt>
<dd>The direction traffic moves through the system</dd>
</div>
</dl>
<ul data-testid="infrastructure-text-equivalent">
<li>
<strong>Legend</strong>: Boundary labels mark changes in trust or access
rules. Connectors show request flow direction.
</li>
<li v-for="band in infrastructure.bands" :key="band.id">
<strong>{{ band.title }}</strong>: {{ band.summary }}
<strong>{{ band.title }}</strong
>: {{ band.summary }}
</li>
</ul>
</article>