Skip to content

Commit 3a642b6

Browse files
authored
1 parent 12abba5 commit 3a642b6

5 files changed

Lines changed: 334 additions & 14 deletions

File tree

docs/.vitepress/theme/components/home/FeatureRun.vue

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import runTerminal from '@local-assets/terminal-features/run.svg';
2+
import FeatureRunTerminal from './FeatureRunTerminal.vue';
33
</script>
44

55
<template>
@@ -23,16 +23,9 @@ import runTerminal from '@local-assets/terminal-features/run.svg';
2323
</ul>
2424
</div>
2525
</div>
26-
<div class="flex flex-col">
27-
<div
28-
class="bg-viteplus h-full overflow-clip flex justify-end items-center py-36 px-5 md:pl-10 md:pr-0"
29-
>
30-
<img
31-
loading="lazy"
32-
:src="runTerminal"
33-
alt="Monorepo caching that just works"
34-
class="w-full md:w-auto"
35-
/>
26+
<div class="flex flex-col min-h-[22rem] sm:min-h-[30rem]">
27+
<div class="bg-viteplus pl-5 sm:pl-10 h-full overflow-clip flex flex-col justify-center py-6">
28+
<FeatureRunTerminal />
3629
</div>
3730
</div>
3831
</section>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<script setup lang="ts">
2+
import { TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
3+
import { computed, onMounted, onUnmounted, ref } from 'vue';
4+
5+
import { featureRunTranscripts } from '../../data/feature-run-transcripts';
6+
import TerminalTranscript from './TerminalTranscript.vue';
7+
8+
const AUTO_ADVANCE_DELAY = 2400;
9+
10+
const activeStep = ref(featureRunTranscripts[0].id);
11+
const autoPlayEnabled = ref(true);
12+
const prefersReducedMotion = ref(false);
13+
const hasEnteredViewport = ref(false);
14+
const sectionRef = ref<HTMLElement | null>(null);
15+
16+
let autoAdvanceTimeout: ReturnType<typeof setTimeout> | null = null;
17+
let observer: IntersectionObserver | null = null;
18+
let mediaQuery: MediaQueryList | null = null;
19+
20+
const activeTranscript = computed(
21+
() =>
22+
featureRunTranscripts.find((transcript) => transcript.id === activeStep.value) ??
23+
featureRunTranscripts[0],
24+
);
25+
26+
const clearAutoAdvance = () => {
27+
if (autoAdvanceTimeout) {
28+
clearTimeout(autoAdvanceTimeout);
29+
autoAdvanceTimeout = null;
30+
}
31+
};
32+
33+
const goToNextStep = () => {
34+
const currentIndex = featureRunTranscripts.findIndex(
35+
(transcript) => transcript.id === activeStep.value,
36+
);
37+
const nextIndex = (currentIndex + 1) % featureRunTranscripts.length;
38+
activeStep.value = featureRunTranscripts[nextIndex].id;
39+
};
40+
41+
const onAnimationComplete = () => {
42+
if (!autoPlayEnabled.value || prefersReducedMotion.value) {
43+
return;
44+
}
45+
46+
clearAutoAdvance();
47+
autoAdvanceTimeout = setTimeout(() => {
48+
goToNextStep();
49+
}, AUTO_ADVANCE_DELAY);
50+
};
51+
52+
const onStepChange = () => {
53+
clearAutoAdvance();
54+
if (!prefersReducedMotion.value) {
55+
autoPlayEnabled.value = true;
56+
autoAdvanceTimeout = setTimeout(() => {
57+
goToNextStep();
58+
}, AUTO_ADVANCE_DELAY);
59+
}
60+
};
61+
62+
const syncReducedMotionPreference = () => {
63+
prefersReducedMotion.value = mediaQuery?.matches ?? false;
64+
if (prefersReducedMotion.value) {
65+
autoPlayEnabled.value = false;
66+
clearAutoAdvance();
67+
}
68+
};
69+
70+
onMounted(() => {
71+
if (typeof window !== 'undefined' && 'matchMedia' in window) {
72+
mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
73+
syncReducedMotionPreference();
74+
if ('addEventListener' in mediaQuery) {
75+
mediaQuery.addEventListener('change', syncReducedMotionPreference);
76+
} else {
77+
mediaQuery.addListener(syncReducedMotionPreference);
78+
}
79+
}
80+
81+
if (!sectionRef.value || typeof IntersectionObserver === 'undefined') {
82+
hasEnteredViewport.value = true;
83+
return;
84+
}
85+
86+
observer = new IntersectionObserver(
87+
(entries) => {
88+
entries.forEach((entry) => {
89+
if (entry.isIntersecting && !hasEnteredViewport.value) {
90+
hasEnteredViewport.value = true;
91+
observer?.disconnect();
92+
}
93+
});
94+
},
95+
{
96+
threshold: 0.35,
97+
rootMargin: '0px',
98+
},
99+
);
100+
101+
observer.observe(sectionRef.value);
102+
});
103+
104+
onUnmounted(() => {
105+
clearAutoAdvance();
106+
observer?.disconnect();
107+
if (!mediaQuery) {
108+
return;
109+
}
110+
if ('removeEventListener' in mediaQuery) {
111+
mediaQuery.removeEventListener('change', syncReducedMotionPreference);
112+
} else {
113+
mediaQuery.removeListener(syncReducedMotionPreference);
114+
}
115+
});
116+
</script>
117+
118+
<template>
119+
<div ref="sectionRef" class="feature-run-terminal">
120+
<TabsRoot v-model="activeStep" @update:modelValue="onStepChange">
121+
<div
122+
class="px-4 sm:px-5 py-5 sm:py-6 relative bg-slate rounded-tl rounded-bl outline-1 outline-offset-[2px] outline-white/20"
123+
>
124+
<TerminalTranscript
125+
v-if="hasEnteredViewport"
126+
:key="activeTranscript.id"
127+
:transcript="activeTranscript"
128+
:animate="!prefersReducedMotion"
129+
@complete="onAnimationComplete"
130+
/>
131+
</div>
132+
<TabsList
133+
aria-label="Vite Task cache examples"
134+
class="run-step-picker flex items-center p-1 rounded-md border border-white/10 bg-[#111]"
135+
>
136+
<TabsTrigger
137+
v-for="transcript in featureRunTranscripts"
138+
:key="transcript.id"
139+
:value="transcript.id"
140+
>
141+
{{ transcript.label }}
142+
</TabsTrigger>
143+
</TabsList>
144+
</TabsRoot>
145+
</div>
146+
</template>
147+
148+
<style scoped>
149+
.feature-run-terminal {
150+
width: 100%;
151+
margin-right: 0;
152+
}
153+
154+
.run-step-picker {
155+
width: fit-content;
156+
gap: 0.5rem;
157+
margin: 0.9rem auto 0;
158+
}
159+
160+
:deep(.terminal-copy) {
161+
min-height: 12.5rem;
162+
font-size: 0.8125rem;
163+
line-height: 1.35rem;
164+
}
165+
166+
:deep(.terminal-spacer) {
167+
height: 0.75rem;
168+
}
169+
170+
@media (min-width: 640px) {
171+
.feature-run-terminal {
172+
margin-right: 2.5rem;
173+
}
174+
175+
:deep(.terminal-copy) {
176+
font-size: 0.875rem;
177+
line-height: 1.5rem;
178+
}
179+
}
180+
</style>

docs/.vitepress/theme/components/home/TerminalTranscript.vue

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue';
33
44
import type {
55
TerminalLine,
6+
TerminalSegment,
67
TerminalTone,
78
TerminalTranscript,
89
} from '../../data/terminal-transcripts';
910
1011
const props = defineProps<{
1112
transcript: TerminalTranscript;
13+
animate?: boolean;
1214
}>();
1315
1416
const emit = defineEmits<{
@@ -31,6 +33,13 @@ const clearTimers = () => {
3133
3234
const restartAnimation = () => {
3335
clearTimers();
36+
if (props.animate === false) {
37+
visibleLineCount.value = props.transcript.lines.length;
38+
renderedPrompt.value = promptText.value;
39+
promptFinished.value = true;
40+
return;
41+
}
42+
3443
visibleLineCount.value = 0;
3544
renderedPrompt.value = '';
3645
promptFinished.value = false;
@@ -64,7 +73,10 @@ const restartAnimation = () => {
6473
};
6574
6675
const lineClass = (line: TerminalLine) => toneClass(line.tone ?? 'base');
67-
const segmentClass = (tone?: TerminalTone) => toneClass(tone ?? 'base');
76+
const segmentClass = (segment: TerminalSegment) => [
77+
toneClass(segment.tone ?? 'base'),
78+
segment.bold ? 'font-bold' : '',
79+
];
6880
6981
const toneClass = (tone: TerminalTone) => {
7082
switch (tone) {
@@ -84,7 +96,7 @@ const toneClass = (tone: TerminalTone) => {
8496
};
8597
8698
watch(
87-
() => props.transcript.id,
99+
() => [props.transcript.id, props.animate],
88100
() => restartAnimation(),
89101
{ immediate: true },
90102
);
@@ -110,7 +122,7 @@ onBeforeUnmount(() => clearTimers());
110122
v-for="(segment, segmentIndex) in line.segments"
111123
:key="`${transcript.id}-${index}-${segmentIndex}`"
112124
>
113-
<span :class="segmentClass(segment.tone)">{{ segment.text }}</span>
125+
<span :class="segmentClass(segment)">{{ segment.text }}</span>
114126
</template>
115127
</div>
116128
</TransitionGroup>

0 commit comments

Comments
 (0)