diff --git a/.changeset/per-domain-element-hiding.md b/.changeset/per-domain-element-hiding.md new file mode 100644 index 00000000..e77c241f --- /dev/null +++ b/.changeset/per-domain-element-hiding.md @@ -0,0 +1,5 @@ +--- +'heroshot': minor +--- + +Add per-domain element hiding for screenshots diff --git a/.github/workflows/update-editor-snapshots.yml b/.github/workflows/update-editor-snapshots.yml new file mode 100644 index 00000000..5b8b7e53 --- /dev/null +++ b/.github/workflows/update-editor-snapshots.yml @@ -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 diff --git a/docs/docs/actions-reference.md b/docs/docs/actions-reference.md index 4a8afad7..8d96c5b9 100644 --- a/docs/docs/actions-reference.md +++ b/docs/docs/actions-reference.md @@ -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 { diff --git a/docs/docs/config-reference.md b/docs/docs/config-reference.md index e649a90e..db39949c 100644 --- a/docs/docs/config-reference.md +++ b/docs/docs/config-reference.md @@ -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 @@ -47,6 +48,7 @@ Back to [Configuration overview](./config). "jpegQuality": 80, "browser": {}, "workers": 4, - "screenshots": [] + "screenshots": [], + "hiddenElements": {} } ``` diff --git a/docs/docs/guide/screenshot-design.md b/docs/docs/guide/screenshot-design.md index 5640b61c..cebcf25d 100644 --- a/docs/docs/guide/screenshot-design.md +++ b/docs/docs/guide/screenshot-design.md @@ -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. diff --git a/docs/index.md b/docs/index.md index 319b0f6a..b3355e96 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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: diff --git a/editor/playwright.config.ts b/editor/playwright.config.ts index 53146dd1..7d07a18c 100644 --- a/editor/playwright.config.ts +++ b/editor/playwright.config.ts @@ -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: { diff --git a/editor/src/components/EditorBar.svelte b/editor/src/components/EditorBar.svelte index 24fa1d46..bb90a6a6 100644 --- a/editor/src/components/EditorBar.svelte +++ b/editor/src/components/EditorBar.svelte @@ -2,9 +2,12 @@ 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'; @@ -12,6 +15,8 @@ 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 */ @@ -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; @@ -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, @@ -52,11 +64,13 @@ onEditingComplete, onDraftConfirm, onToggleAnnotationTool, + onUnhideElement, }: Props = $props(); let localEditingId = $state(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); @@ -235,7 +249,7 @@ role="complementary" aria-label="Heroshot editor panel" > -
+
+ + {#if hasSelectedElement}
@@ -381,6 +405,58 @@ {/if}
+ + + {#if hiddenSelectors.length > 0} +
+ + {#if hiddenExpanded} +
    + {#each hiddenSelectors as selector (selector)} +
  • { + 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'); + } + }} + > + {selector} + +
  • + {/each} +
+ {/if} +
+ {/if}
{/if}
diff --git a/editor/src/components/ElementPicker.svelte b/editor/src/components/ElementPicker.svelte index 9eebb99c..e1e47843 100644 --- a/editor/src/components/ElementPicker.svelte +++ b/editor/src/components/ElementPicker.svelte @@ -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) */ @@ -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 */ @@ -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 }; @@ -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 }; @@ -857,9 +869,9 @@ /> {/if} {:else} - +
{/if} diff --git a/editor/src/components/SettingsModal.svelte b/editor/src/components/SettingsModal.svelte index ea593f8b..70e06c66 100644 --- a/editor/src/components/SettingsModal.svelte +++ b/editor/src/components/SettingsModal.svelte @@ -16,6 +16,10 @@ let lightSelected = $state(true); let darkSelected = $state(true); let deviceScaleFactor = $state(); + let outputDirectory = $state('heroshots'); + let outputFormat = $state<'png' | 'jpeg'>('png'); + let jpegQuality = $state(80); + let workers = $state(); // Update local state when settings prop changes or modal opens $effect(() => { @@ -23,6 +27,10 @@ width = props.settings.viewport.width; height = props.settings.viewport.height; deviceScaleFactor = props.settings.deviceScaleFactor; + outputDirectory = props.settings.outputDirectory ?? 'heroshots'; + outputFormat = props.settings.outputFormat ?? 'png'; + jpegQuality = props.settings.jpegQuality ?? 80; + workers = props.settings.workers; // Convert colorScheme to selection state const scheme = props.settings.colorScheme; @@ -81,6 +89,10 @@ viewport: { width, height }, colorScheme: getColorSchemeValue(), deviceScaleFactor, + outputDirectory: outputDirectory || undefined, + outputFormat: outputFormat === 'png' ? undefined : outputFormat, + jpegQuality: outputFormat === 'jpeg' ? jpegQuality : undefined, + workers, }); props.onClose(); } @@ -178,7 +190,7 @@
-
+
Color Scheme
+ +
+ + +
+ + +
+ + +
+ Output Format +
+ + +
+
+ + + {#if outputFormat === 'jpeg'} +
+ + +
+ {/if} + + +
+ + +
+