Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/per-domain-element-hiding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'heroshot': minor
---

Add per-domain element hiding for screenshots
65 changes: 65 additions & 0 deletions .github/workflows/update-editor-snapshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Update Editor Snapshots

on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to update snapshots on (defaults to current branch)'
required: false
reason:
description: 'Reason for updating snapshots'
required: false
default: 'Update editor snapshots'

jobs:
update-snapshots:
runs-on: [self-hosted, din]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
clean: false

- uses: pnpm/action-setup@c5ba7f7862a0f64c1b1a05fbac13e0b8e86ba08c # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20

- name: Install dependencies
run: pnpm install --prefer-offline

- name: Build editor
run: pnpm build:editor

- name: Install Playwright browsers
run: pnpm exec playwright install chromium

- name: Update editor snapshots
run: pnpm test:editor:e2e -- --update-snapshots
env:
CI: true

- name: Check for snapshot changes
id: changes
run: |
if [ -n "$(git status --porcelain editor/tests/snapshots/)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "Snapshot changes detected:"
git status --porcelain editor/tests/snapshots/
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "No snapshot changes"
fi

- name: Commit and push updated snapshots
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add editor/tests/snapshots/
git commit -m "test: update editor e2e snapshots

Reason: ${{ github.event.inputs.reason }}
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
git push
6 changes: 3 additions & 3 deletions docs/docs/actions-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,9 @@ Resize the browser viewport mid-flow.

Hide elements from screenshot. Use to remove cookie banners, chat widgets, ads.

| Property | Type | Required | Description |
| ----------- | -------- | -------- | ----------------------------------------- |
| `selectors` | string[] | yes | Element selectors to hide (display: none) |
| Property | Type | Required | Description |
| ----------- | -------- | -------- | ---------------------------------------------- |
| `selectors` | string[] | yes | Element selectors to hide (visibility: hidden) |

```json
{
Expand Down
4 changes: 3 additions & 1 deletion docs/docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Back to [Configuration overview](./config).
| ↳ `screenshots[].borderColor` | string | - | Border color (hex, default "#000000") |
| ↳ `screenshots[].borderRadius` | number | - | Corner radius in pixels — rounds the screenshot corners with transparency (PNG only) |
| ↳ `screenshots[].actions` | any[] | - | Ordered list of actions to execute before capturing. Actions run sequentially. |
| `hiddenElements` | Record | - | Elements to hide per domain (hostname → CSS selectors) |

## Example

Expand All @@ -47,6 +48,7 @@ Back to [Configuration overview](./config).
"jpegQuality": 80,
"browser": {},
"workers": 4,
"screenshots": []
"screenshots": [],
"hiddenElements": {}
}
```
15 changes: 15 additions & 0 deletions docs/docs/guide/screenshot-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,21 @@ Dev tools, debug panels, feature flags - hide them:

Or capture against a production-like environment.

### Hide Elements Per Domain

Some elements are distracting on every screenshot from a domain - cookie banners, chat widgets, navigation bars. Instead of adding hide actions to each screenshot, hide them once at the domain level:

```json
{
"hiddenElements": {
"app.example.com": [".cookie-banner", ".chat-widget"],
"docs.example.com": [".announcement-bar"]
}
}
```

These elements are hidden automatically for all screenshots captured from that domain. You can manage them visually in the editor using the eraser tool in the sidebar.

## Highlighting with Annotations

Sometimes a screenshot needs more than just showing the UI - you need to draw attention to a specific button, field, or area. That's what annotations are for.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ features:
- icon:
src: /icons/paintbrush.svg
title: Visual Editor
details: Adjust padding, style borders, change background colors, and edit text - all visually. What you see is what you screenshot.
details: Adjust padding, style borders, change backgrounds, hide distracting elements, and edit text - all visually. What you see is what you screenshot.
link: /docs/guide/screenshot-design
linkText: Open the editor
- icon:
Expand Down
2 changes: 1 addition & 1 deletion editor/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
// Update snapshots with: npx playwright test --update-snapshots
updateSnapshots: 'none',

// Skip visual regression on CI (Chromium renders differently than local Chrome)
// Skip visual regression on CI (Docker container renders fonts differently than host)
ignoreSnapshots: !!process.env.CI,

use: {
Expand Down
80 changes: 78 additions & 2 deletions editor/src/components/EditorBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
import AnnotateIcon from '../icons/AnnotateIcon.svelte';
import ChevronDownIcon from '../icons/ChevronDownIcon.svelte';
import ChevronUpIcon from '../icons/ChevronUpIcon.svelte';
import EraserIcon from '../icons/EraserIcon.svelte';
import GripIcon from '../icons/GripIcon.svelte';
import PickerIcon from '../icons/PickerIcon.svelte';
import SettingsIcon from '../icons/SettingsIcon.svelte';
import TrashIcon from '../icons/TrashIcon.svelte';
import { queryElements } from '../lib/selectorGenerator';
import type { ScreenshotItem } from '../types';
import ScreenshotItemComponent from './ScreenshotItem.svelte';

type Props = {
screenshots: ScreenshotItem[];
/** Whether picker mode is active */
pickerActive: boolean;
/** Whether hide mode is active */
isHideMode: boolean;
/** Whether the screenshot list is expanded */
expanded: boolean;
/** Whether settings modal is visible */
Expand All @@ -21,7 +26,10 @@
selectedId: string | null;
/** Active annotation tool type (null = not annotating) */
annotationTool: string | null;
/** Hidden element selectors for current domain */
hiddenSelectors: string[];
onTogglePicker: () => void;
onToggleHideMode: () => void;
onToggleExpanded: () => void;
onToggleSettings: () => void;
onDone: () => void;
Expand All @@ -31,18 +39,22 @@
onEditingComplete: () => void;
onDraftConfirm: (id: string) => void;
onToggleAnnotationTool: (tool: string) => void;
onUnhideElement: (selector: string) => void;
}

let {
screenshots,
pickerActive,
isHideMode,
expanded,
settingsVisible,
editingId,
draftId,
selectedId,
annotationTool,
hiddenSelectors,
onTogglePicker,
onToggleHideMode,
onToggleExpanded,
onToggleSettings,
onDone,
Expand All @@ -52,11 +64,13 @@
onEditingComplete,
onDraftConfirm,
onToggleAnnotationTool,
onUnhideElement,
}: Props = $props();

let localEditingId = $state<string | null>(null);
let editValue = $state('');
let annotationDropdownOpen = $state(false);
let hiddenExpanded = $state(false);

// Whether an element is selected (annotation button only visible when selected)
let hasSelectedElement = $derived(selectedId !== null);
Expand Down Expand Up @@ -235,7 +249,7 @@
role="complementary"
aria-label="Heroshot editor panel"
>
<div class="w-64 max-h-[calc(100vh-32px)] bg-slate-800 rounded-xl shadow-2xl font-sans text-white flex flex-col overflow-hidden">
<div class="w-72 max-h-[calc(100vh-32px)] bg-slate-800 rounded-xl shadow-2xl font-sans text-white flex flex-col overflow-hidden">
<!-- Header with drag handle and toolbar buttons -->
<div
class="flex items-center justify-between px-2 py-2 border-b border-slate-700"
Expand All @@ -254,7 +268,7 @@
<div class="flex items-center gap-1">
<button
type="button"
class="w-8 h-8 rounded-md flex items-center justify-center transition-colors {pickerActive ? 'bg-green-500' : 'bg-slate-700 hover:bg-slate-600'}"
class="w-8 h-8 rounded-md flex items-center justify-center transition-colors {pickerActive && !isHideMode ? 'bg-green-500' : 'bg-slate-700 hover:bg-slate-600'}"
onclick={onTogglePicker}
onpointerdown={(event) => event.stopPropagation()}
title="Pick element"
Expand All @@ -264,6 +278,16 @@
<PickerIcon size={18} />
</button>

<button
type="button"
class="w-8 h-8 rounded-md flex items-center justify-center transition-colors {isHideMode ? 'bg-red-500' : 'bg-slate-700 hover:bg-slate-600'}"
onclick={onToggleHideMode}
onpointerdown={(event) => event.stopPropagation()}
title="Hide element"
>
<EraserIcon size={16} />
</button>

{#if hasSelectedElement}
<div class="relative">
<div class="flex">
Expand Down Expand Up @@ -381,6 +405,58 @@
</ul>
{/if}
</div>

<!-- Hidden Elements section -->
{#if hiddenSelectors.length > 0}
<div class="border-t border-slate-700/50">
<button
type="button"
class="flex items-center gap-1.5 px-3 py-2 w-full text-left"
onclick={() => { hiddenExpanded = !hiddenExpanded; }}
onpointerdown={(event) => event.stopPropagation()}
>
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">Hidden</h3>
<span class="bg-red-600/50 text-white text-xs font-bold min-w-5 h-5 px-1 rounded-full flex items-center justify-center">{hiddenSelectors.length}</span>
<span class="ml-auto text-slate-400">
{#if hiddenExpanded}
<ChevronUpIcon size={14} />
{:else}
<ChevronDownIcon size={14} />
{/if}
</span>
</button>
{#if hiddenExpanded}
<ul class="px-2 pb-2 space-y-1">
{#each hiddenSelectors as selector (selector)}
<li
class="flex items-center gap-1 p-1.5 rounded-lg bg-slate-700/50 group"
onmouseenter={() => {
for (const element of queryElements(selector)) {
if (element instanceof HTMLElement) element.style.removeProperty('visibility');
}
}}
onmouseleave={() => {
for (const element of queryElements(selector)) {
if (element instanceof HTMLElement) element.style.setProperty('visibility', 'hidden', 'important');
}
}}
>
<span class="flex-1 text-xs text-slate-300 truncate font-mono" title={selector}>{selector}</span>
<button
type="button"
class="w-5 h-5 rounded flex items-center justify-center text-slate-400 opacity-60 group-hover:opacity-100 hover:text-red-400 transition-colors"
onclick={() => onUnhideElement(selector)}
onpointerdown={(event) => event.stopPropagation()}
title="Remove from hidden"
>
<TrashIcon size={14} />
</button>
</li>
{/each}
</ul>
{/if}
</div>
{/if}
</div>
{/if}
</div>
Expand Down
18 changes: 15 additions & 3 deletions editor/src/components/ElementPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
type Props = {
/** Whether picker mode is active */
active: boolean;
/** Whether hide mode is active (clicking hides instead of selects) */
hideMode: boolean;
/** Screenshots list (for loading saved padding) */
screenshots: ScreenshotItem[];
/** Active annotation tool (null = not annotating) */
Expand All @@ -22,6 +24,8 @@
onToggle: () => void;
/** Callback when new element is picked (creates draft) */
onNewElement: (selector: string) => void;
/** Callback when element is picked for hiding */
onHideElement: (selector: string) => void;
/** Callback when existing screenshot padding is updated */
onPaddingUpdate: (id: string, padding: Padding) => void;
/** Callback when existing screenshot scroll position is updated */
Expand Down Expand Up @@ -50,7 +54,7 @@
onExpandedRectChange: (rect: { top: number; left: number; width: number; height: number } | null) => void;
}

const { active, screenshots, annotationTool, onToggle, onNewElement, onPaddingUpdate, onScrollUpdate, onPaddingFillUpdate, onElementFillUpdate, onTextOverrideUpdate, onAnnotationsUpdate, onAnnotationToolDeactivate, onCancel, onDeselect, onAnnotationSelectionChange, onTextEditChange, onEditingScreenshotChange, onExpandedRectChange }: Props = $props();
const { active, hideMode, screenshots, annotationTool, onToggle, onNewElement, onHideElement, onPaddingUpdate, onScrollUpdate, onPaddingFillUpdate, onElementFillUpdate, onTextOverrideUpdate, onAnnotationsUpdate, onAnnotationToolDeactivate, onCancel, onDeselect, onAnnotationSelectionChange, onTextEditChange, onEditingScreenshotChange, onExpandedRectChange }: Props = $props();

// Default padding
const defaultPadding: Padding = { top: 0, right: 0, bottom: 0, left: 0 };
Expand Down Expand Up @@ -239,6 +243,14 @@
if (currentElement) {
const selector = getSelector(currentElement);

// Hide mode: hide element and return
if (hideMode) {
currentElement = null;
tooltipData = null;
onHideElement(selector);
return;
}

selectedElement = currentElement;
selectedPadding = { ...defaultPadding };
originalPadding = { ...defaultPadding };
Expand Down Expand Up @@ -857,9 +869,9 @@
/>
{/if}
{:else}
<!-- Picker mode: simple cyan border -->
<!-- Picker mode: green border / Hide mode: red border -->
<div
class="fixed border-[3px] pointer-events-none box-border border-heroshot-primary bg-heroshot-primary/10"
class="fixed border-[3px] pointer-events-none box-border {hideMode ? 'border-red-500 bg-red-500/10' : 'border-heroshot-primary bg-heroshot-primary/10'}"
style="top:{overlayRects.highlight.top}px;left:{overlayRects.highlight.left}px;width:{overlayRects.highlight.width}px;height:{overlayRects.highlight.height}px;"
></div>
{/if}
Expand Down
Loading