Skip to content

Commit 79ddeb6

Browse files
author
Ondrej Machala
committed
feat: add visual picker carousel to landing page
- Add demo screenshots E2E test capturing BBC element selection flow - Create PickerCarousel Vue component with 3-step demo - Update landing page sections: picker carousel, responsive variants showcase - Add orange glow effect to carousel images
1 parent 30bab9f commit 79ddeb6

File tree

9 files changed

+361
-2
lines changed

9 files changed

+361
-2
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue';
3+
4+
const steps = [
5+
{
6+
number: 1,
7+
image: '/demo-screenshots/demo-element-selected.png',
8+
alt: 'Pick any element on the page',
9+
label: 'Pick Screenshots Visually',
10+
desc: 'Visit any URL, select any element',
11+
},
12+
{
13+
number: 2,
14+
image: '/demo-screenshots/demo-element-with-padding.png',
15+
alt: 'Drag handles to add padding',
16+
label: 'Adjust Size',
17+
desc: 'Drag any handle to add padding around your selection',
18+
},
19+
{
20+
number: 3,
21+
image: '/demo-screenshots/demo-element-with-mask.png',
22+
alt: 'Polished screenshot with visual effects',
23+
label: 'Polish',
24+
desc: 'Fine-tune with visual effects to make it pop',
25+
},
26+
];
27+
28+
const currentStep = ref(0);
29+
30+
function goTo(index: number) {
31+
currentStep.value = index;
32+
}
33+
34+
function next() {
35+
currentStep.value = (currentStep.value + 1) % steps.length;
36+
}
37+
38+
function prev() {
39+
currentStep.value = (currentStep.value - 1 + steps.length) % steps.length;
40+
}
41+
</script>
42+
43+
<template>
44+
<div class="picker-carousel">
45+
<div class="carousel-container">
46+
<div class="carousel-image-row">
47+
<button class="carousel-arrow prev" @click="prev" aria-label="Previous step">
48+
<svg
49+
width="24"
50+
height="24"
51+
viewBox="0 0 24 24"
52+
fill="none"
53+
stroke="currentColor"
54+
stroke-width="2"
55+
>
56+
<path d="M15 18l-6-6 6-6" />
57+
</svg>
58+
</button>
59+
60+
<div class="carousel-image">
61+
<img :src="steps[currentStep].image" :alt="steps[currentStep].alt" />
62+
</div>
63+
64+
<button class="carousel-arrow next" @click="next" aria-label="Next step">
65+
<svg
66+
width="24"
67+
height="24"
68+
viewBox="0 0 24 24"
69+
fill="none"
70+
stroke="currentColor"
71+
stroke-width="2"
72+
>
73+
<path d="M9 18l6-6-6-6" />
74+
</svg>
75+
</button>
76+
</div>
77+
78+
<div class="carousel-info">
79+
<div class="carousel-label">{{ steps[currentStep].label }}</div>
80+
<p class="carousel-desc">{{ steps[currentStep].desc }}</p>
81+
<div class="carousel-dots">
82+
<button
83+
v-for="(step, index) in steps"
84+
:key="index"
85+
:class="['dot', { active: currentStep === index }]"
86+
@click="goTo(index)"
87+
:aria-label="`Go to step ${index + 1}: ${step.label}`"
88+
>
89+
{{ step.number }}
90+
</button>
91+
</div>
92+
</div>
93+
</div>
94+
</div>
95+
</template>
96+
97+
<style scoped>
98+
.picker-carousel {
99+
max-width: 900px;
100+
margin: 0 auto;
101+
}
102+
103+
.carousel-container {
104+
display: flex;
105+
flex-direction: column;
106+
align-items: center;
107+
}
108+
109+
.carousel-image-row {
110+
display: flex;
111+
align-items: center;
112+
gap: 16px;
113+
width: 100%;
114+
}
115+
116+
.carousel-arrow {
117+
flex-shrink: 0;
118+
width: 48px;
119+
height: 48px;
120+
border: none;
121+
border-radius: 50%;
122+
background: #ffffff !important;
123+
color: var(--vp-c-text-1);
124+
cursor: pointer;
125+
display: flex;
126+
align-items: center;
127+
justify-content: center;
128+
transition: all 0.2s;
129+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
130+
position: relative;
131+
z-index: 10;
132+
}
133+
134+
.dark .carousel-arrow {
135+
background: var(--navy-dark) !important;
136+
}
137+
138+
.carousel-arrow:hover {
139+
background: var(--vp-c-brand-1) !important;
140+
color: white;
141+
}
142+
143+
.carousel-image {
144+
flex: 1;
145+
max-width: 800px;
146+
border-radius: 12px;
147+
overflow: hidden;
148+
box-shadow:
149+
0 8px 32px rgba(0, 0, 0, 0.1),
150+
0 0 100px rgba(234, 88, 12, 0.25),
151+
0 0 200px rgba(251, 146, 60, 0.2),
152+
0 0 350px rgba(251, 146, 60, 0.12);
153+
}
154+
155+
.carousel-image img {
156+
width: 100%;
157+
height: auto;
158+
display: block;
159+
}
160+
161+
.carousel-info {
162+
margin-top: 24px;
163+
background: #ffffff;
164+
padding: 16px 24px;
165+
border-radius: 12px;
166+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
167+
width: 80%;
168+
max-width: 640px;
169+
text-align: center;
170+
}
171+
172+
.dark .carousel-info {
173+
background: var(--navy-dark);
174+
}
175+
176+
.carousel-label {
177+
font-size: 20px;
178+
font-weight: 600;
179+
color: var(--navy-base);
180+
margin-bottom: 8px;
181+
}
182+
183+
.carousel-desc {
184+
font-size: 16px;
185+
color: var(--vp-c-text-2);
186+
margin: 0;
187+
}
188+
189+
.carousel-dots {
190+
display: flex;
191+
justify-content: center;
192+
gap: 12px;
193+
margin-top: 16px;
194+
}
195+
196+
.dot {
197+
width: 36px;
198+
height: 36px;
199+
border: 2px solid var(--vp-c-divider);
200+
border-radius: 50%;
201+
background: var(--vp-c-bg-soft);
202+
color: var(--vp-c-text-2);
203+
font-size: 14px;
204+
font-weight: 600;
205+
cursor: pointer;
206+
transition: all 0.2s;
207+
}
208+
209+
.dot:hover {
210+
border-color: var(--vp-c-brand-1);
211+
color: var(--vp-c-brand-1);
212+
}
213+
214+
.dot.active {
215+
background: var(--vp-c-brand-1);
216+
border-color: var(--vp-c-brand-1);
217+
color: white;
218+
}
219+
220+
@media (max-width: 768px) {
221+
.carousel-arrow {
222+
width: 40px;
223+
height: 40px;
224+
}
225+
226+
.carousel-arrow svg {
227+
width: 20px;
228+
height: 20px;
229+
}
230+
231+
.step-badge {
232+
width: 40px;
233+
height: 40px;
234+
font-size: 20px;
235+
}
236+
237+
.carousel-label {
238+
font-size: 20px;
239+
}
240+
241+
.carousel-desc {
242+
font-size: 14px;
243+
}
244+
}
245+
</style>

docs/.vitepress/theme/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import DefaultTheme from 'vitepress/theme';
22
import type { Theme } from 'vitepress';
33
import Heroshot from '../../../integrations/vue/src/components/Heroshot.vue';
44
import IntegrationTabs from './components/IntegrationTabs.vue';
5+
import PickerCarousel from './components/PickerCarousel.vue';
56
import { setManifest } from '../../../integrations/shared/manifestStore';
67
// @ts-expect-error - virtual module provided by heroshot plugin
78
import manifest from 'virtual:heroshot-manifest';
@@ -16,5 +17,6 @@ export default {
1617
enhanceApp({ app }) {
1718
app.component('Heroshot', Heroshot);
1819
app.component('IntegrationTabs', IntegrationTabs);
20+
app.component('PickerCarousel', PickerCarousel);
1921
},
2022
} satisfies Theme;

docs/.vitepress/theme/showcase.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@
127127
background: var(--vp-c-brand-2);
128128
}
129129

130+
/* Visual Element Picker section */
131+
.picker-section {
132+
max-width: 1152px;
133+
margin: 48px auto 0;
134+
padding: 32px 0 48px;
135+
}
136+
137+
@media (max-width: 768px) {
138+
.picker-section {
139+
margin: 32px 16px 0;
140+
padding: 24px 16px 32px;
141+
}
142+
}
143+
130144
/* Integrations section */
131145
.integrations-section {
132146
max-width: 1152px;

docs/index.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,13 @@ features:
6767
linkText: Padding & masking
6868
---
6969

70+
<div class="picker-section">
71+
<PickerCarousel />
72+
</div>
73+
7074
<div class="showcase">
71-
<h2>See It In Action</h2>
72-
<p class="subtitle">This screenshot is captured by heroshot with responsive variants (desktop, tablet, mobile) and color scheme support (light/dark).<br>Toggle the theme or resize your browser to see it switch automatically.</p>
75+
<h2>One Config, Many Variants</h2>
76+
<p class="subtitle">This screenshot of the hero section above is captured by Heroshot - desktop, tablet, mobile, light and dark.<br>Resize your browser or toggle the theme - the matching variant loads automatically.</p>
7377

7478
<div class="screenshot-showcase">
7579
<Heroshot name="Hero" alt="Heroshot landing page screenshot" class="hero-screenshot" />

docs/public/demo-screenshots

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../editor/tests/snapshots/demo-screenshots.test.ts
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Demo Screenshots
3+
*
4+
* NOT a real test - captures screenshots of heroshot in action for documentation.
5+
* Uses BBC homepage as a realistic example of picking elements.
6+
*
7+
* Run with: pnpm test:editor:e2e
8+
* Update snapshots with: pnpm test:editor:e2e --update-snapshots
9+
*/
10+
11+
import { expect, test } from 'playwright/test';
12+
import { createMockScreenshot, injectToolbar } from './utils';
13+
14+
const BBC_URL = 'https://www.bbc.co.uk';
15+
16+
// Selector for the second promo card (smaller article card in the second LI)
17+
const PROMO_SELECTOR = 'li:nth-child(2) [data-testid="promo"]';
18+
19+
test('demo: pick element on BBC homepage with padding', async ({ page }) => {
20+
test.setTimeout(60000); // BBC takes a while to load
21+
// Navigate to BBC and wait for full load
22+
await page.goto(BBC_URL, { waitUntil: 'networkidle' });
23+
24+
// Dismiss cookie banner - wait for it and click
25+
try {
26+
const rejectCookiesButton = page.getByRole('button', { name: 'Reject additional cookies' });
27+
await rejectCookiesButton.waitFor({ state: 'visible', timeout: 5000 });
28+
await rejectCookiesButton.click();
29+
await page.waitForTimeout(1000); // Wait for banner to fully disappear
30+
} catch {
31+
// Cookie banner may not appear if already dismissed
32+
}
33+
34+
// Scroll to top to ensure consistent positioning
35+
await page.evaluate(() => window.scrollTo(0, 0));
36+
await page.waitForTimeout(300);
37+
38+
// Create a mock screenshot that targets the promo element
39+
const demoScreenshot = createMockScreenshot({
40+
id: 'demo-shot',
41+
name: 'BBC Promo Card',
42+
url: BBC_URL,
43+
selector: PROMO_SELECTOR,
44+
});
45+
46+
// Inject toolbar with pending highlight job - this will automatically select and highlight the element
47+
await injectToolbar(page, {
48+
screenshots: [demoScreenshot],
49+
pendingJob: {
50+
type: 'highlight',
51+
selector: PROMO_SELECTOR,
52+
},
53+
selectedId: 'demo-shot',
54+
sidebarVisible: true,
55+
});
56+
57+
await page.waitForTimeout(500);
58+
59+
// Screenshot 1: Element selected (before resize) - full viewport
60+
await expect(page).toHaveScreenshot('demo-element-selected.png', { fullPage: false });
61+
62+
// Step 2: Get element rect and drag corner to add padding (~80px for more visible effect)
63+
const promoElement = page.locator(PROMO_SELECTOR).first();
64+
const elementRect = await promoElement.boundingBox();
65+
if (!elementRect) throw new Error('Could not get element bounding box');
66+
67+
// Bottom-right corner handle position
68+
const handleX = elementRect.x + elementRect.width;
69+
const handleY = elementRect.y + elementRect.height;
70+
71+
// Drag corner handle to add more padding (80px)
72+
await page.mouse.move(handleX, handleY);
73+
await page.mouse.down();
74+
await page.mouse.move(handleX + 80, handleY + 80, { steps: 10 });
75+
await page.mouse.up();
76+
await page.waitForTimeout(500);
77+
78+
// Screenshot 2: Element with padding after resize
79+
await expect(page).toHaveScreenshot('demo-element-with-padding.png', { fullPage: false });
80+
81+
// Step 3: Click in the padding area to activate the mask (white background)
82+
const paddingClickX = elementRect.x + elementRect.width + 40;
83+
const paddingClickY = elementRect.y + elementRect.height / 2;
84+
await page.mouse.click(paddingClickX, paddingClickY);
85+
await page.waitForTimeout(300);
86+
87+
// Move mouse outside the padding area to hide the tooltip
88+
await page.mouse.move(0, 0);
89+
await page.waitForTimeout(300);
90+
91+
// Screenshot 3: Element with white mask visible
92+
await expect(page).toHaveScreenshot('demo-element-with-mask.png', { fullPage: false });
93+
});
370 KB
Loading
338 KB
Loading
377 KB
Loading

0 commit comments

Comments
 (0)