fix: close final portfolio quality gaps
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user