Date: 2026-05-14 Status: v0 scaffold
This document captures the architectural decisions made on day one, informed by competitive research on BugHerd and the OSS annotation / visual-feedback landscape.
travisEATSbugs is not a feature for one product. It's the tool that sits on top of every product it's installed into. Same widget, same npm package, drops into any host app and lights up immediately.
Every design decision optimizes for installability across multiple downstream products, not for any single host.
- A BugHerd clone. They have 13 years of edge cases handled. Don't fight that war.
- A SaaS-only product. Self-hostable from day one.
- A real-time-first product. Polling-on-focus in v1, Yjs over Cloudflare Durable Objects in v2.
- A canvas-drawing tool. Sticky-note pins, not freeform drawing. Excalidraw / tldraw are out of scope.
No existing tool ships visual feedback + agency project rollups + hours tracking + client lists in one product. travisEATSbugs is the feedback layer that, paired with agency-platform tooling, makes the bundle defensible.
Pre-1.0 install paths (see install.md for the full walkthrough):
- Script tag (alpha demo):
<script src="https://travismakes.org/travis-eats-bugs/widget.js"></script>
- Vendored package:
import { init } from '@travisbreaks/travisEATSbugs' init({ /* opts */ })
A dedicated CDN endpoint (e.g. eats.travisfixes.com/v1.js) and a public npm publish both land at v1.0; see the strategic plan for the bundle-versioning + tenant-pin design.
The widget injects into a Shadow DOM root. Required for installability: host pages have unpredictable CSS, and the widget must not leak into or absorb from host styles. The Shadow DOM also makes the widget invisible to host-page event handlers unless we explicitly bubble events out.
Every pin stores three selectors and falls back gracefully:
- CSS selector via
@medv/finder. Shortest unique selector. Primary. - XPath. Stable across class-name churn (Emotion / styled-components / Tailwind JIT).
- W3C text-quote selector. Surface text + prefix + suffix. Survives DOM restructuring as long as the text stays.
Stored using the W3C Web Annotation Data Model. Free interop with Hypothesis, Annotorious, and any future consumer.
The position within the anchor element is stored as a fraction of the bounding box ({x: 0.3, y: 0.7}) so the pin survives responsive resize. Algorithm borrowed from SitePing.
modern-screenshot over html2canvas. Faster, smaller, better on modern CSS. Optional server-side fallback via headless Chrome for sites that fail client-side capture.
interface Adapter {
create(annotation: Annotation): Promise<{ id: string }>
list(filter: Filter): Promise<Annotation[]>
update(id: string, patch: Partial<Annotation>): Promise<void>
resolve(id: string, resolution: Resolution): Promise<void>
delete(id: string): Promise<void>
onCreate(handler: (a: Annotation) => void): void
}Two adapters ship in this monorepo:
@travisbreaks/travisEATSbugs-cloudflare: D1 + R2, default hosted backend ateats.travisfixes.com. The path for hosts that don't want to run their own server.@travisbreaks/travisEATSbugs-http: BYO endpoint. POST/GET/PATCH/DELETE against a host-supplied URL. For self-hosted deploys with their own backend.
Two-tier, copied from BugHerd because it's the right pattern:
- Members: paid seats, dashboard access, can manage / resolve / delete.
- Reporters / Guests: unlimited, free. Comment via share link or project token. No signup required. Identity is a name prompt on first comment, stored in localStorage on their browser.
No-auth reporter access is non-negotiable. Largest feature gap between the existing page-notes seed and the BugHerd category.
The adapter's onCreate event fires a webhook to a Cloudflare Worker. The worker runs Claude over:
{
title: string
comment_text: string
screenshot_url: string
page_url: string
element_selector: string
}And returns:
{
severity: 'critical' | 'high' | 'medium' | 'low'
category: 'visual' | 'copy' | 'data' | 'perf' | 'a11y' | 'other'
suggested_assignee?: string
dupe_of?: string // id of existing annotation
}The response is written back to the annotation as a W3C body field. Hosts can disable AI triage by not setting AI_TRIAGE_URL on the adapter.
No existing OSS project ships "yellow paper, tilt, lift, drag" well. Build from primitives:
- CSS
transform: rotate(<deterministic-from-pin-id>deg)for individual tilt - Paper-texture SVG noise overlay
- Motion
whileHoverfor lift-and-tilt - Motion
whileDrag+dragMomentumfor physics-based reposition - Color-coded by pin kind: bug (yellow/amber), idea (blue), question (purple), praise (green)
Themeable so each downstream product can override colors, fonts, paper texture. Per-product visual canon means each host's pins read as that host's brand.
travisEATSbugs is part of the Travis design family. Every surface (widget, playground, marketing, docs) adheres to the canon at memory/design-canon.md. Full per-product theming guide and audit checklist at docs/design.md.
Non-negotiable rules:
- No-Shift Principle: no interaction shifts host-page layout. Composite-only transforms (translate, scale, rotate).
prefers-reduced-motionkill switch: MANDATORY on every animated property.- No em dashes anywhere (use colons, periods, parens).
- Easing:
cubic-bezier(0.45, 0, 0.55, 1)for ambient,cubic-bezier(0.16, 1, 0.3, 1)for entrance reveals. - Mobile-first: 44px minimum touch target, 48-52px on narrow viewports.
Color is role-based, not fixed palette. Widget exposes theme tokens (--teb-signal, --teb-fg, --teb-shadow, etc.) as CSS custom properties on :host. Host pages override at :root. CSS custom properties pierce Shadow DOM boundaries automatically.
Per-product visual canon. Each downstream brand recolors the widget independently. The widget recolors per host; it does NOT impose any single brand identity. The marketing surface at eats.travisfixes.com is a travisFIXES property and follows the travisFIXES signature.
- Language: TypeScript everywhere
- Workspaces: pnpm
- Bundler: tsup (ESM + CJS + IIFE)
- Test runner: Vitest + happy-dom
- Lint / format: Biome
- Playground: Next.js 15 + React 19
- Hosted backend: Cloudflare Workers + D1 + R2
- Targeting: ES2022, modern browsers (no IE11, no transpile-everything-to-ES5)
SitePing is the closest thing on GitHub to "OSS BugHerd" and has the multi-selector anchoring we want. We considered three paths:
(a) Adopt: Write a CF D1 adapter for SitePing, ship travisEATSbugs as a SitePing distribution. (b) Fork: Customize SitePing with our brand and the sticky-note layer. (c) Rebuild from primitives: Use SitePing as reference for the anchoring algorithm only. Build the rest fresh.
We picked (c). The sticky-note interaction is enough of a differentiator that anchoring our API surface to SitePing's is the wrong long-term move. SitePing stays as the reference impl for the bounding-box-fraction algorithm and the @medv/finder + XPath + text-quote fallback chain.
v0 (this scaffold):
- Floating bug button injected via Shadow DOM
- pnpm workspace, tsup build, Vitest tests, Next.js playground
v0.1:
- Click-to-mark mode toggle
- Triple-selector anchoring via
@medv/finder - Screenshot capture via
modern-screenshot - Sticky-note pin UI (Motion + paper texture)
- localStorage adapter (no backend needed)
v0.2:
- HTTP adapter
- Tokenized share-link mode for unauthenticated reporters
- W3C Web Annotation Data Model on the wire
v0.3:
- Cloudflare D1 + R2 adapter
- Hosted backend at eats.travisfixes.com
- Admin dashboard (separate package)
v0.4:
- AI triage webhook
- Tadao classifies, suggests assignee, finds dupes
v0.5:
- Two-way sync: GitHub Issues, Jira, Linear
- Resolved-PR link-back
v1.0:
- Public npm release
- Documentation site at eats.travisfixes.com
- Marketing site