@safecanvas/feedback-widget
v0.1.1
Published
In-page feedback collector that exports change requests as Safecanvas prompts.
Maintainers
Readme
@safecanvas/feedback-widget
A tiny, zero-dependency, framework-agnostic in-page widget that lets a human walk through a staging environment, click elements they want changed, describe the change, and then copy all of it as one Safecanvas-ready prompt.
Paste the output into Safecanvas and the agent has everything it needs to find and apply each change: the page URL, a stable CSS selector, the element's text, viewport size, and the reviewer's message.
Framework-agnostic — works with React, Vue, Svelte, Angular, or plain HTML. When the reviewer picks an element, the widget walks the framework's runtime internals to extract component chain + source file so the agent can open the right file directly rather than grepping CSS selectors.
UX features
- Click an element to attach feedback
- Shift+click to keep accumulating elements without finishing
- Drag an area to select many elements at once
- Shift+drag to add a marquee's worth on top of the running selection
- Live preview of which elements will be picked while dragging
- Disabled buttons are pickable too (pointer events bypass the form-control event suppression)
- Text selection captured alongside the element when present
- Dark / light theme toggle (persists; follows system on first mount)
- Drag the launcher anywhere on screen — position is saved across reloads
- Drag the panel header to reposition while the panel is open
- Hide widget for the current page — refresh restores it
- In-place undo when deleting a draft (move away from the row to commit)
- Inline confirm for "Clear all" (no native popup)
Install
Two ways, both one-line:
1. CDN / <script> tag (no build step, works anywhere)
<script
src="https://unpkg.com/@safecanvas/feedback-widget/dist/feedback-widget.iife.js"
data-safecanvas-feedback
data-project="acme-marketing"
data-position="bottom-right"
defer
></script>Pin a specific version for production:
<script src="https://unpkg.com/@safecanvas/[email protected]/dist/feedback-widget.iife.js" defer></script>jsDelivr works too if you prefer it:
<script src="https://cdn.jsdelivr.net/npm/@safecanvas/feedback-widget/dist/feedback-widget.iife.js" defer></script>The script auto-mounts the widget and exposes a window.SafecanvasFeedback
global. Configure via data-* attributes on the <script> tag:
| data-* attribute | maps to |
| ------------------------- | ---------------------- |
| data-project | projectLabel |
| data-position | position |
| data-prompt-footer | promptFooter |
| data-storage-key | storageKey |
| data-hide-launcher | hideLauncher |
| data-capture-console-errors | captureConsoleErrors |
| data-max-console-errors | maxConsoleErrors |
| data-theme | theme (light / dark; omit to follow system) |
| data-auto-mount="false" | skip auto-mount; call window.SafecanvasFeedback.mount(...) yourself |
2. npm (for apps that already use a bundler)
pnpm add @safecanvas/feedback-widget
# or: npm install @safecanvas/feedback-widgetimport { mountFeedbackWidget } from "@safecanvas/feedback-widget";
if (import.meta.env.MODE !== "production") {
mountFeedbackWidget({
projectLabel: "acme-marketing",
promptFooter: "Open a PR titled 'staging-feedback: batch' when done.",
});
}Options
| option | default | notes |
| ---------------------- | ------------------------ | -------------------------------------------------------- |
| storageKey | safecanvas:feedback | localStorage key for persisted drafts |
| position | bottom-right | bottom-right / bottom-left / top-right / top-left|
| projectLabel | "" | Appears in the exported prompt header |
| promptFooter | "" | Appended to the end of the exported prompt |
| hideLauncher | false | Hide floating button; drive via .open() / .close() |
| captureConsoleErrors | true | Capture window.onerror + unhandled rejections |
| maxConsoleErrors | 20 | Bounded buffer |
| theme | (system) | light / dark. If unset, follows prefers-color-scheme on first mount, then stores the user's manual override. 'auto' is accepted for backward compatibility but treated as unset. |
API
const widget = mountFeedbackWidget();
widget.open();
widget.close();
widget.show(); // un-hide if previously hidden via the header eye-off
widget.hide(); // memory-only hide; refresh restores the widget
widget.startPicker(); // enter element-pick mode immediately
widget.setTheme('dark');
widget.destroy();
unmountFeedbackWidget();What gets captured per feedback item
Designed so the agent has multiple independent angles to locate the element in source. The exported prompt tells the agent to try them in priority order.
Page context
- Page URL — full
window.location.href - Route — pathname + query + hash
- Page title, viewport (w×h @dpr), captured timestamp
Element identity (primary locators)
- Human name — e.g.
button "Upgrade now",h1 "Pricing",email input [username] - CSS selector — validated unique on the live page (id → stable
data-*→tag.class:nth-of-type) - DOM path — readable breadcrumb (
body > div.container > section > h1#hero) - outerHTML snippet — truncated literal chunk for grep
- All CSS classes — not just stable ones, so the agent can grep any of them
- Test IDs —
data-testid,data-qa,data-cy,data-e2esurfaced verbatim - Element id,
role,aria-label,aria-describedby,title
Element-kind specific
<a>: href<img>: src, alt<input>/<textarea>: type, name, placeholder, value (sensitive types stripped), etc.
Component tracking (the big differentiator)
- Framework — auto-detected: React / Vue / Svelte / Angular / unknown
- Component chain — innermost component + its ancestors (up to 8)
- Source file + line + column when available:
- React dev builds →
_debugSourcevia fiber tree (__reactFiber$*) - Vue 3 dev builds →
__vueParentComponent.type.__file, orvite-plugin-vue-inspector'sdata-v-inspectorattr - Vue 2 →
$options.__file - Svelte → any
data-source/data-svelte-sourceattr set by a build-time inspector plugin - Angular → component class names via
window.ng.getComponent()
- React dev builds →
- Production builds typically strip debug info — the selector and outerHTML snippet remain reliable fallbacks.
Visual / context
- Bounding rect (page coords)
- Computed styles (color, background, font size/weight/family, display, position) — detailed mode only
- Nearby text — parent text context so same-selector elements can be disambiguated
- Parent selector — fallback anchor when the primary selector breaks
Runtime signals
- Console errors captured since mount (bounded) — appears alongside the feedback
Example exported prompt
# Staging feedback to apply
**Project:** acme-marketing
Collected 2 change requests from a live staging session. For each item, open
the **Page URL**, locate the element via **selector**, and apply the
**requested change**. Treat the selector as a hint — confirm the element in
the source before editing.
---
### Feedback 1
**Requested change:** Make the heading larger and use the brand orange.
- **Page URL:** https://staging.example.com/
- **Route:** `/`
- **Element selector:** `[data-testid="headline"]`
- **Element tag:** `<h1>`
- **Element text:** "Product landing page"
...Build
pnpm --filter @safecanvas/feedback-widget build
# → dist/feedback-widget.iife.js (for <script> tag / CDN)
# → dist/feedback-widget.esm.js (for bundler / npm consumers)
# → dist/feedback-widget.esm.d.ts (bundled TypeScript declarations)Bundler: tsdown — config in tsdown.config.ts.
Demo
# From the monorepo root
pnpm --filter @safecanvas/feedback-widget exec python3 -m http.server -d . 8787
# open http://localhost:8787/example/Design notes
- Shadow DOM isolates the widget's styling from the host page.
- Zero deps, zero network calls — everything stays in the browser.
- localStorage persistence survives page reloads during a review session.
- The exported prompt is plain markdown, designed to be pasted directly into a Safecanvas chat.
License
MIT — see LICENSE.md.
