Skip to content

πŸ• v2.0: full rewrite β€” MV3, TypeScript + React, Shadow DOM, FAB, cross-env tooling#2

Merged
jjpaulino merged 15 commits into
masterfrom
feat/modernize-clay-devtools
May 13, 2026
Merged

πŸ• v2.0: full rewrite β€” MV3, TypeScript + React, Shadow DOM, FAB, cross-env tooling#2
jjpaulino merged 15 commits into
masterfrom
feat/modernize-clay-devtools

Conversation

@jjpaulino
Copy link
Copy Markdown
Member

@jjpaulino jjpaulino commented May 13, 2026

Summary

This PR is a full rewrite of clay-devtools (Clay Slip), delivered as a series of focused commits on top of the original 2018 Manifest V2 vanilla-JS extension.

It ships a modern, fully typed, fully tested Chrome extension built on Manifest V3 + TypeScript 6 + React 19 + Vite 8 + Vitest 4 + Zustand 5, runs on Node 24 LTS, fixes the original bugs, and adds a long list of new features tailored to both devs and product managers.

  • βœ… 84 unit tests passing, 0 lint warnings, 0 type errors, 0 format issues
  • βœ… GitHub Actions CI runs typecheck / lint / format / test / build on every PR
  • βœ… Production build (npm run build) is load-unpacked-ready in dist/
  • βœ… Every shortcut and capability of v1 is preserved

What's new (v2)

Floating button + panel

  • Floating Clay button (FAB) in the bottom-right corner is the new default surface β€” one click opens the full panel, escape / collapse returns to the FAB. Lives in a Shadow DOM so the page can never style it.
  • Resizable + dockable panel β€” drag the inner edge for width, the bottom edge for height, the corner for both; clamped to sensible bounds and persisted per user. Pick any of four corners or full-height left/right side dock.
  • Auto / light / dark themes that respond to OS theme changes live.
  • Toolbar badge with the count of [data-uri] elements on the current page (cleared on navigation).

Inspect & navigate

  • Manifest V3 Chrome extension with a service worker.
  • Shadow-DOM panel β€” React mounted in attachShadow({ mode: 'open' }) so host-page CSS never bleeds in.
  • Component tree + breadcrumbs, click any row or page element to select & scroll.
  • Inline JSON preview with syntax highlighting and per-(host, URI) caching.
  • Click-through-aware selection β€” selects the component but lets real <a> / <button> / <input> clicks go through.
  • Find-on-page β€” Tree search dims non-matches on the page, shows match counter, Enter cycles next, Shift+Enter previous, Esc clears.
  • Recently viewed components persisted across sessions (configurable cap).
  • Toggleable component outlines (eye icon, h shortcut) with configurable opacity slider.

Edit, share, screenshot

  • Open in Clay editor β€” Edit button on the Page section opens the parent page in editor mode at the unpublished version (strips @published so the editor can actually save).
  • Sticky-note annotations pinned to component URIs (chrome.storage.local); show as an orange dot on the page and a Notes tab listing every note across pages.
  • Shareable selection links β€” Share button copies a ?clay-slip-select=… URL that auto-opens Slip and selects the same component on someone else's machine. Reads location.href at click time so SPA nav can never produce a stale link.
  • Component screenshot to clipboard β€” captureVisibleTab + canvas crop scaled by devicePixelRatio, panel auto-hides during capture.

Cross-environment tooling

  • Configurable site host mappings in Settings β€” declare prod / staging / qa hostnames per brand.
  • "View on …" pills on the Page section jump to the equivalent URL on another env (preserving path / query / hash).
  • Split Share button β€” when mappings are configured, a β–Ύ menu adds one entry per cross-env target, copying a share link rewritten to that env's host.
  • Environment switcher pill + per-env host config in Settings; every link, fetch, cURL, and shortcut routes through the active env's host.
  • Cross-environment diff β€” pick another configured env from the Compare: select to diff the same URI across hosts.

Compare & diff

  • Published vs Draft diff for any @published selection.
  • Cross-environment diff as above.

SEO & audit (PMs love these)

  • SEO tab β€” <title> / description / canonical / robots / og:* / twitter:* / JSON-LD with Twitter + Facebook/Slack card previews and lints (length, missing og:image, duplicate <h1>, etc.); live updates via MutationObserver.
  • Page audit export β€” Export β–Ύ button on the Inspect tab β†’ JSON / CSV / Markdown of every component on the page, copied to clipboard (no file downloads β€” paste straight into a ticket).

Ergonomics

  • Copy-as menu: URI / cURL / fetch() snippet / CSS selector β€” all env-host aware.
  • Keyboard shortcut overlay triggered by ?. Shortcuts are gated by composedPath() so typing t in a note input doesn't switch tabs.
  • Smart popup: friendly "Not a Clay page" popup on non-Clay pages, gets out of the way on Clay pages so the icon click toggles the panel.
  • Full Options page for theme, dock side + width + height, env hosts, site host mappings, highlight intensity, recents history size, and shortcut toggle.

What it looks like

Inspect tab

Inspect tab

Tree tab

Tree tab

Options page

Options page

Bugs fixed from the original Slip + audit

  • Implicit globals in evaluateKeystroke (e.g. uri, opts) β€” entirely rewritten as a typed React hook.
  • Duplicate hiddenInput declaration β€” gone, clipboard goes through navigator.clipboard.write[Text] with a tested legacy fallback.
  • getPageInstance(...).replace(...) could throw on null β€” every URI utility is now a total function.
  • Highlight palette only generated 5 of 6 colors β€” fixed.
  • Imperative chrome.tabs.executeScript β€” replaced by an MV3-native content script + message-passing model.
  • Outlines weren't painted on first page load β€” now applied during bootstrap.
  • Auto-theme listener was a no-op when the OS theme changed β€” now actually re-renders.
  • Click-to-select hijacked all link clicks β€” now respects <a> / <button> / <input> / contenteditable.
  • Tree-row hover did nothing β€” now uses the highlighter's hover outline.
  • Toolbar badge wasn't cleared on SPA / non-Clay navigation β€” tabs.onUpdated clears it now.
  • Settings button was a no-op β€” chrome.runtime.openOptionsPage() isn't callable from a content script; routed through the service worker.
  • Collapsed panel had no way to re-expand β€” replaced by the FAB, which is always available and one click away from the full panel.
  • Edit button opened the published URL β€” now strips @published so the editor opens the editable version.
  • Keyboard shortcuts fired while typing in notes β€” composedPath() now correctly detects inputs inside the Shadow DOM.
  • FAB icon failed to load on host pages β€” image is now inlined as a base64 data URL via Vite's ?inline, so no cross-origin asset fetch.

Architecture

src/
β”œβ”€β”€ manifest.ts             # MV3 manifest in TypeScript
β”œβ”€β”€ background/
β”‚   └── service-worker.ts   # Open tabs, badge, screenshot capture, popup toggling
β”œβ”€β”€ content/
β”‚   β”œβ”€β”€ index.ts            # Bootstrap + deep-link selection + message handling
β”‚   β”œβ”€β”€ highlighter.ts      # Outline / hover / selected / annotated / match styling
β”‚   β”œβ”€β”€ shadow-host.ts      # Mounts React app inside a Shadow DOM
β”‚   β”œβ”€β”€ page-info.ts        # Reads Clay metadata
β”‚   └── panel/              # The React panel UI
β”‚       β”œβ”€β”€ App.tsx
β”‚       β”œβ”€β”€ store.ts        # Zustand store
β”‚       β”œβ”€β”€ theme.ts        # Light / dark tokens
β”‚       β”œβ”€β”€ styles.css      # Shadow-scoped styles
β”‚       β”œβ”€β”€ components/     # Fab, Tabs, Tree, JSON viewer, Diff, Breadcrumb,
β”‚       β”‚                   # SEO, Notes, Recents, Resize, Export, ShareMenu, ...
β”‚       └── hooks/          # Drag, theme, shortcuts, selection
β”œβ”€β”€ popup/                  # "Not a Clay page" popup
β”œβ”€β”€ options/                # Full options page (incl. site host mappings UI)
β”œβ”€β”€ assets/
β”‚   └── clay-icon.png       # FAB + header logo (inlined as data URL at build time)
└── lib/                    # Pure utilities
    β”œβ”€β”€ clay-uri.ts         # parsing + buildUrl/buildEditorUrl/buildShareLink + copy-as helpers
    β”œβ”€β”€ clipboard.ts        # Modern + legacy clipboard
    β”œβ”€β”€ site-host.ts        # findMappingForHost / rewriteUrlToEnv / availableEnvsFor
    β”œβ”€β”€ storage.ts          # User preferences in chrome.storage.sync
    β”œβ”€β”€ annotations.ts      # Sticky notes per URI in chrome.storage.local
    β”œβ”€β”€ recents.ts          # Recently viewed history
    β”œβ”€β”€ exporter.ts         # Page manifest β†’ JSON / CSV / Markdown
    β”œβ”€β”€ seo.ts              # Document head extractor + linter
    β”œβ”€β”€ screenshot.ts       # captureVisibleTab + crop β†’ clipboard PNG
    └── types.ts            # Shared types + DEFAULT_PREFERENCES

Configuration: site host mappings

Open the Options page β†’ Site host mappings to declare brand β†’ env hostnames, e.g.:

Label Prod Staging QA
Cut www.thecut.com stg.thecut.com qa.thecut.com
Vulture www.vulture.com stg.vulture.com qa.vulture.com

Once configured:

  • PageInfo shows "View on: prod / staging / qa" pills that swap the host of the current URL.
  • Share menu offers cross-env share links.
  • Diff offers cross-env diffs.

Migration notes (1.0 β†’ 2.0)

This release is a full rewrite, but no end-user features were dropped. The keyboard model (y+p / y+c / o+p / o+c) is preserved verbatim and now lives in the ? overlay alongside new bindings.

  • Node 24 LTS (.nvmrc pinned, engines.node = ">=24")
  • MV2 β†’ MV3 (service worker, chrome.action, chrome.scripting)
  • Vanilla JS β†’ TypeScript 6 + React 19 inside Shadow DOM
  • No build β†’ npm install && npm run build
  • 0 tests β†’ 84 Vitest tests

Test plan

CI runs all of these on every push and PR:

  • npm run typecheck (tsc --noEmit, strict)
  • npm run lint (--max-warnings=0, react-hooks v7 strict rules)
  • npm run format:check (Prettier)
  • npm run test β€” 84 tests passing
  • npm run build β€” valid dist/ with MV3 manifest

Manual smoke test (load unpacked from dist/):

  • Visit a non-Clay page β†’ click the icon β†’ "Not a Clay page" popup
  • Visit a Clay page β†’ outlines appear immediately, badge shows count, FAB appears in bottom-right
  • Click FAB β†’ panel opens; click Collapse in the header β†’ panel closes back to FAB
  • Press h β†’ outlines hide; press again β†’ return
  • Click an interactive <a> inside a component β†’ navigation still works
  • Tree tab β†’ search dims non-matches on the page; Enter cycles
  • Type letters into the Notes textarea β†’ shortcuts do not fire (no tab switching)
  • JSON tab β†’ fetched, syntax-highlighted, copy works
  • Diff tab β†’ Published vs Draft on a @published selection
  • Diff tab β†’ Compare: switches to a configured env diff
  • Page section Edit β†’ opens the page in editor mode at the unpublished version (no @published)
  • Component section: no Edit button (intentional)
  • Component Share β†’ toast confirms copy + correct URL on the clipboard
  • With site mappings configured: Share β–Ύ shows cross-env entries; PageInfo shows View-on pills
  • Screenshot button β†’ PNG in clipboard; panel was hidden during capture
  • Annotation note β†’ saved β†’ orange dot appears on the page β†’ Notes tab lists it
  • Export β–Ύ β†’ JSON / CSV / MD all copy to clipboard
  • SEO tab β†’ previews populate, lint warnings are accurate
  • Recently viewed β†’ past selections persist across reloads
  • Drag the panel inner edge β†’ width resizes; drag bottom edge β†’ height; drag corner β†’ both
  • Settings β†’ dock to "Right side (full height)" β†’ panel becomes a sidebar
  • Settings β†’ add site host mappings β†’ cross-env pills + share entries appear
  • OS theme switch in auto β†’ panel re-themes live

Made with Cursor

jjpaulino and others added 2 commits May 12, 2026 22:20
…ts, CI

Replaces the original Manifest V2 vanilla-JS extension with a modern,
fully typed, fully tested Chrome extension covering every item in the
upgrade plan.

Foundation
- Manifest V3 with a service worker (replaces the persistent MV2
  background page and `browserAction` API).
- TypeScript (strict, noUncheckedIndexedAccess, noUnusedLocals).
- Vite + @crxjs/vite-plugin for HMR-friendly builds.
- ESLint 9 (flat config) + Prettier + EditorConfig + .nvmrc.
- GitHub Actions CI: typecheck, lint, format check, tests, build,
  artifact upload of dist/.
- Pre-rendered icon set at 16 / 32 / 48 / 128 from a single SVG source.

Bug fixes from the legacy code
- Removed implicit globals in keystroke handler.
- Removed duplicate `hiddenInput` declaration.
- Null-guarded all URI parsing utilities (no more crashes when
  `getPageInstance()` returns null).
- Fixed the color hierarchy cycle so all 6 palette colors are used.
- Replaced deprecated `document.execCommand('copy')` with the
  Clipboard API (with a graceful fallback retained for legacy contexts).
- Migrated from `chrome.tabs.executeScript` to a content-script-based
  message-passing model that does not require ad-hoc injection.

UX overhaul
- React 18 panel mounted inside a Shadow DOM so host page CSS can
  never bleed into the panel.
- Light / dark / auto theme tied to `prefers-color-scheme`.
- Draggable, collapsible panel.
- Breadcrumb trail showing the nesting path of the selected
  component, with click-to-navigate.
- Keyboard shortcut overlay (`?`) with full binding documentation.
- Toolbar badge showing the count of `[data-uri]` elements on the
  current page.

New features (Phase 4)
- Component tree tab with depth-aware indentation and click-to-scroll.
- Live search / filter by component name, instance, or URI.
- Inline JSON preview that fetches the component's data without
  opening a new tab; results are syntax-highlighted and cached.
- Diff view comparing the published instance against the unpublished
  draft side by side.
- Environment switcher pill (local / dev / staging / prod) persisted
  in `chrome.storage.sync`.
- Full options page for theme, panel position, default environment,
  highlight intensity, and shortcut toggles.
- Copy-as-cURL button on the selected component.

Tests
- 33 Vitest unit tests covering URI parsing, clipboard fallbacks, and
  preference storage. happy-dom DOM environment.

Docs
- README rewritten with install instructions, full shortcut table,
  architecture overview, scripts catalog, and migration notes.
- Three UI screenshots (inspect / tree / options) under
  docs/screenshots/ and embedded in the README.

Co-authored-by: Cursor <cursoragent@cursor.com>
Aligns the toolchain with the current Node.js LTS line and brings every
runtime / dev dependency up to its current major.

Runtime
- Node 24.x LTS (`.nvmrc`, `engines.node = ">=24"`).
  CI already uses `node-version-file: '.nvmrc'`, so it picks this up
  automatically.

Major bumps
- TypeScript 5 β†’ 6 (6.0.3)
- React 18 β†’ 19 (19.2.x)
- Vite 5 β†’ 8 (8.0.x)
- Vitest 2 β†’ 4 (4.1.x)
- Zustand 4 β†’ 5 (5.0.x)
- happy-dom 15 β†’ 20 (20.9.x)
- @vitejs/plugin-react 4 β†’ 6
- @types/node 22 β†’ 24
- prettier 3.3 β†’ 3.8
- @types/chrome β†’ 0.1.x

Held back (still on 9.x because eslint-plugin-react has not yet declared
ESLint 10 as a peer):
- ESLint 9.39.x (latest 9.x line)
- @eslint/js 9.39.x

Compatibility fixes triggered by the bumps
- tsconfig: removed deprecated `baseUrl` (TypeScript 6 deprecation
  warning); `paths` now uses `./src/*`.
- vite.config.ts: dropped the embedded `test` config and switched
  `__dirname` to `import.meta.dirname` (Vite 8 ESM).
- New `vitest.config.ts` β€” Vitest 4 wants its own config file.
- Zustand 5 typed-store syntax: `create<T>()(impl)` instead of
  `create<T>(impl)`.
- React 19 ref typing: `useDraggable` now accepts
  `RefObject<HTMLElement | null>`.
- eslint-plugin-react-hooks 7.x added a strict
  `react-hooks/set-state-in-effect` rule. `useDraggable`,
  `JsonPreview`, and `DiffView` were refactored to use the
  documented "adjust state during render" pattern (store the
  previous prop and reset state inline) instead of `setState` inside
  a `useEffect`. Async branches keep using `setState` in `.then()`
  callbacks, which the rule allows.
- `scripts/build-icons.mjs` comment bumped to suggest sharp@0.34.

Validation (Node 24.14.1)
- typecheck: βœ…
- lint (--max-warnings=0): βœ…
- format check: βœ…
- 33 / 33 tests passing
- production build succeeds (`dist/` valid MV3 extension)

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjpaulino jjpaulino self-assigned this May 13, 2026
jjpaulino and others added 3 commits May 12, 2026 22:52
Audit-driven follow-up that completes the v2 rewrite. Every "done" feature
that was actually a stub or had a real bug is now wired end-to-end, and
all dead code identified in the audit is gone.

Stubs wired up
- Environment switcher: `buildUrl` / `buildSchemaUrl` / `buildCurlCommand`
  now accept a host override. Every panel button, fetch, copy-as-cURL,
  and `o p` / `o c` shortcut routes through the selected env's host.
  Options page exposes a host input per environment (local / dev /
  staging / prod) and the env pill shows whether the active env has a
  configured host.
- Highlight intensity slider: outline colors became `rgba()` with a
  `var(--clay-slip-outline-opacity)` driver. `setHighlightOpacity()`
  updates the var, and `App.tsx` subscribes to `prefs.highlightOpacity`
  so the slider is live.
- "Not a Clay page" popup: manifest now declares `default_popup`. On
  Clay pages the content script sends `CLAY_DETECTED` and the service
  worker clears the per-tab popup (so the icon click toggles the
  panel). On non-Clay pages the popup shows automatically.
  `tabs.onUpdated` resets the popup on navigation so the same tab works
  for both kinds of pages.

Bugs fixed
- Outlines now paint on first page load (initial bootstrap calls
  `applyHighlights`).
- Auto-theme listener now reacts to OS theme changes (tracks
  `systemDark` state instead of setting state to the same value).
- Click-to-select no longer hijacks real interactive elements
  (`<a>`, `<button>`, `<input>`, contenteditable, etc.) - selection
  still updates but the original click goes through.
- Tree-row hover now shows the actual hover outline via
  `highlighter.setHovered`, replacing the dead `cs-tree-hover` class.
- Toolbar badge is cleared on every navigation (loading state in
  `tabs.onUpdated`) and bootstrapping a non-Clay page sends `count: 0`.

Dead code removed
- `setHighlightingEnabled` is now wired to a header toggle (eye icon)
  and a new `h` shortcut.
- `DEFAULT_ENVIRONMENTS` replaced by `DEFAULT_ENVIRONMENT_HOSTS` /
  `ENVIRONMENT_ORDER` / `ENVIRONMENT_LABELS`, all consumed by the
  options page and switcher.
- `COPY_TO_CLIPBOARD` runtime message removed (clipboard is handled
  directly in content script).
- Replaced unused `resolveTheme` with inline computation in
  `useThemedRoot`.

Cosmetic / minor
- `Header` drops `forwardRef` for the React 19 ref-as-prop syntax.
- Options page "Saved" toast clears its previous timer on rapid
  saves (no more overlapping flashes).
- New keyboard shortcut: `h` toggles outlines from anywhere on the
  page; shortcut overlay updated.

Tests
- Added coverage for `buildUrl` host override, `splitHostAndPath`,
  `normalizeHost`, and host-aware `buildSchemaUrl` / `buildCurlCommand`.
- Total: 47 tests, all passing on Node 24.14.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two reported bugs:

1. Settings button no-op'd because `chrome.runtime.openOptionsPage()` is
   not available in content scripts. Routed through the service worker
   via a new `OPEN_OPTIONS` runtime message.

2. Collapsed panel was 180px wide but still tried to render the title,
   status pill, and 4 buttons side-by-side, so the expand button was
   crushed off the right edge with no way to get the panel back.

   Reworked the collapsed state:
   - Hides the "Clay Slip" title text, status pill, ?/help, and gear
     buttons (`.cs-collapsed-hide`).
   - Keeps the logo, component count badge, eye toggle, and expand
     button β€” the always-useful at-a-glance bits.
   - Panel shrinks to its content (`width: auto; max-width: 240px`)
     instead of a fixed 180px, so the expand button is always visible.
   - Header padding tightened and bottom border removed when collapsed
     so it reads as a compact pill.

   Also promoted the component count to its own pill-shaped badge in
   the header so it's readable at a glance whether expanded or not.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the next batch of features picked from the roadmap. Each one slots
into the v2 architecture and reuses the env switcher / store / shadow
host plumbing already in place.

Features

- Open in Clay editor β€” Edit button on the page and on every component
  opens the page in `?edit=true` mode, focused on the component instance
  via a hash anchor.
- Sticky-note annotations β€” pinned notes per component URI in
  chrome.storage.local. Editor lives in ComponentDetails; an orange dot
  appears on annotated components on the page when Slip is open; new
  Notes tab lists every note across pages with click-to-jump.
- Page audit export β€” Export β–Ύ button on the Inspect tab downloads the
  full page manifest as JSON / CSV / Markdown with a stable filename.
- Cross-environment diff β€” Diff tab now has a Compare: select with the
  existing Published vs Draft entry plus one entry per configured env
  host (Production vs Staging, etc.); options for unconfigured envs are
  disabled with a helpful empty state.
- Shareable selection links β€” Share button on a selected component
  copies a `?clay-slip-select=<uri>` link; opening it on another machine
  auto-mounts the panel, scrolls to that component, and selects it.
- Find-on-page β€” Tree tab search now dims non-matches on the page,
  shows a match counter, and supports Enter / Shift+Enter to cycle and
  Esc to clear. The current match gets an accent border in the tree.
- SEO tab β€” extracts <title>, description, canonical, robots, every
  og:* / twitter:* meta, and JSON-LD blocks; renders Twitter +
  Facebook/Slack card previews; lints common issues (length, missing
  og:image, duplicate <h1>, etc.). Live-updates via MutationObserver.
- Recently viewed components β€” last N (configurable, default 20)
  selections persisted in chrome.storage.local with a dedicated
  Recently viewed section in the Inspect tab; one click jumps to the
  component (or opens its source page).
- Resizable + dockable panel β€” drag the inner edge to resize live
  (clamped 280–720px, persisted to prefs). Two new dock modes:
  left-side and right-side, both full-height like a sidebar. Resize
  handle flips to the inner edge based on dock side.
- Component screenshot β€” Screenshot button uses
  chrome.tabs.captureVisibleTab + canvas crop scaled by devicePixelRatio
  to get a pixel-perfect PNG of just the selected component, written to
  the system clipboard. Panel auto-hides during capture so it doesn't
  appear in the screenshot.

Bonus

- Copy-as menu in ComponentDetails: URI / cURL / fetch() / Playwright
  locator / CSS selector β€” all env-host aware.
- Auto-theme listener now actually re-themes when the OS theme changes
  (carried over from the previous fix).
- Header stays usable when collapsed; settings opens via service-worker
  message instead of the no-op content-script API call (carried over).

Manifest

- No new permissions needed. captureVisibleTab uses activeTab (already
  granted) and the existing <all_urls> host_permissions; clipboardWrite
  was already declared.

Tests

- 71 tests passing on Node 24.14.1.
- New test files: annotations, recents, exporter, seo.
- Extended clay-uri tests for buildEditorUrl, buildShareLink /
  parseShareTarget, and the copy-as snippet helpers.

Docs

- README updated with the new feature list, the expanded usage table,
  and an annotated lib/ tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjpaulino jjpaulino changed the title πŸ• v2.0: full rewrite β€” MV3, TypeScript + React, Shadow DOM panel, tests, CI πŸ• v2.0: full rewrite + Node 24 + 10 new daily-driver features May 13, 2026
jjpaulino and others added 10 commits May 12, 2026 23:39
Two real bugs working together:

1. Shadow-DOM event retargeting broke the click-outside detection. The
   document-level mousedown listener used `e.target` which gets
   retargeted to the shadow host element at the document boundary. Since
   the shadow host is not a descendant of `wrapperRef.current` (which
   lives inside the shadow tree), the very first mousedown after
   opening the menu β€” including the one on a menu item β€” closed the
   menu before the click event ever fired the export.

   Fixed by using `e.composedPath()` which traces the real path through
   the shadow tree, and switching to `pointerdown` in capture phase for
   consistency.

2. The menu was `position: absolute` inside the panel body, which has
   `overflow-y: auto`. Even when the menu opened, it was clipped by the
   scroll container so users only saw a sliver (or nothing).

   Switched the menu to `position: fixed` with the trigger button's
   `getBoundingClientRect()` driving its top/right inline. Also added a
   flip-up heuristic when there isn't room below, an Escape-to-close
   handler, and an auto-close on resize/scroll so the menu doesn't drift.

The download path (`<a download>` + blob URL appended to document.body)
was always correct β€” it just wasn't being reached.

Co-authored-by: Cursor <cursoragent@cursor.com>
The panel was only resizable horizontally because there was a single
width-only handle. Worse, useDraggable positioned the panel via
left/top β€” so making a bottom-right anchored panel wider grew it off
the right edge of the screen instead of inward.

This change:

- Adds panelHeight to UserPreferences (default 540px) and a height
  slider in Options.
- Rewrites useDraggable to use anchor-edge CSS (right/bottom for
  right/bottom-anchored panels), so resizing always grows the panel
  toward the viewport interior.
- Generalizes ResizeHandle to width / height / corner modes:
    * width  β€” vertical strip on the inner vertical edge
    * height β€” horizontal strip on the inner horizontal edge
    * corner β€” small grabber in the inner corner that resizes both
      with a diagonal cursor
- App.tsx renders width-only for side-dock modes and all three
  handles for the four corner modes.
- Cursors: ew-resize, ns-resize, nwse/nesw-resize as appropriate.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Clay editor only operates on the unpublished version of a page,
but buildEditorUrl was passing the page URI through unchanged β€” which
on a published article includes the `@published` suffix, sending the
editor to a URL it can't load.

Strip `@published` from the page URI before constructing the editor
URL. Component instance hashes are unaffected (the instance regex
already stops at `@`).

Co-authored-by: Cursor <cursoragent@cursor.com>
Different forks of this extension serve different brands and domains,
and the staging/qa hostname conventions vary. Hardcoding any of that
in the codebase isn't sustainable, so this introduces a per-instance
mapping table.

What's new:

- types: SiteEnv ('prod' | 'staging' | 'qa'), SiteHostMapping, and a
  new UserPreferences.siteHosts array (default []).
- lib/site-host.ts: three pure functions β€”
    findMappingForHost(host, mappings)
    rewriteUrlToEnv(url, toEnv, mappings)
    availableEnvsFor(host, mappings)
  Exact case-insensitive hostname matching only β€” no wildcards, no
  prefix stripping. Predictable, idempotent.
- Options page: new "Site host mappings" section with an add/edit/
  remove table (Label Β· Prod Β· Staging Β· QA Β· βœ•). Empty cells mean
  "not deployed in that env". Persists to chrome.storage.sync.
- PageInfo: renders a "View on:" pill row when the current hostname
  matches a configured mapping. One pill per other configured env;
  click opens the rewritten URL in a new tab.
- ComponentDetails: Share button is now a split control. The main
  face copies a share link for the current page (existing behaviour);
  the trailing β–Ύ opens a tiny menu of cross-env share links built
  via rewriteUrlToEnv.
- Tests: 13 new tests covering exact-match / case-insensitivity /
  unmapped / partial-mapping / invalid-URL paths (now 85 total).
- Docs: new "Configuration β†’ Site host mappings" section in README
  with an example table for a multi-brand setup.

dev and local envs are intentionally out of scope for site mappings β€”
those are configured via the existing Environments section for the
Clay API hosts.

Co-authored-by: Cursor <cursoragent@cursor.com>
The download flow was overkill for the common case β€” most uses of
the page manifest are pasting it into a Linear ticket, a PR
description, or a CSV cell. Generating a .json/.csv/.md file then
asking the user to open it just to copy from it adds friction.

Changes:

- ExportMenu items now copy the formatted manifest to the clipboard
  instead of triggering a Blob/anchor download. Labels are now
  "Copy as JSON / CSV / Markdown" and the toast reports the format.
- Removed the unused downloadManifest() helper from src/lib/exporter
  along with its Blob/URL.createObjectURL plumbing β€” buildManifest
  and formatManifest stay (still tested).
- README updated to reflect the new behaviour.

Co-authored-by: Cursor <cursoragent@cursor.com>
The previous "collapsed" state was a horizontal pill stuck in the
panel's anchor corner β€” useful only briefly. The standard browser-
extension chrome pattern (Sentry, Hotjar, Crisp, Intercom, React
DevTools' floating toggle, etc.) is a small circular FAB that
expands into the full UI on click. This commit adopts that pattern.

Behavior:

- The panel now auto-mounts on every Clay page in collapsed/idle
  state, rendered as a 48px circular Clay button anchored to the
  user's preferred corner. A live component-count badge (capped at
  "99+") rides in its top-right.
- Click the FAB β†’ expands into the full panel.
- Click the panel's existing collapse button β†’ returns to the FAB.
- The toolbar icon's mount/unmount toggle is preserved as the
  "fully hide on this tab" escape hatch.

Implementation:

- New Fab.tsx component (corner-anchored circular button + badge).
- store.collapsed defaults to `true` (FAB visible on first paint).
- App.tsx splits its render: collapsed = `<Fab/>` in a themed root,
  expanded = the existing panel chrome. positionStyle is no longer
  applied when collapsed (FAB has its own anchor logic).
- Header drops the `cs-collapsed-hide` plumbing β€” Header is only
  rendered when expanded now, so all elements are always visible.
- styles.css gains .cs-fab + .cs-fab-logo + .cs-fab-badge styles
  with hover lift, active press, focus-visible ring, and elevated
  drop shadow. Retires the dead .cs-panel.cs-collapsed pill rules.
- content/index.ts auto-mounts on bootstrap so the FAB shows on
  first page load (previously only mounted on share-link landings
  or toolbar-icon click).

Side-dock modes (left-side / right-side) collapse to the matching
bottom corner β€” full-height side panels can't shrink to a pill in
a meaningful way.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two follow-ups:

1. **Use the real Clay icon.** The "S" placeholder is gone β€” both the
   FAB and the panel header now use the cream-colored Clay character
   that lives in the master branch (`clay.png`). Imported as a
   bundled asset; @crxjs auto-registered it under
   `web_accessible_resources` so the content script can fetch it.
   FAB grew to 52px with a light elevated background and an accent
   border on hover, so the cream character pops instead of clashing
   with the previous solid-red background.

2. **Fix keyboard shortcuts firing while typing in panel inputs.**
   Pressing `t` in the Notes textarea was switching the active tab to
   Tree (and stealing focus). The handler did check
   `target.tagName === 'TEXTAREA'`, but Shadow DOM event retargeting
   rewrites `e.target` to the shadow host at document level, so the
   actual `<textarea>` was invisible to the check. Switched to
   `composedPath()` and walk every node looking for an `input`,
   `textarea`, `select`, or `contentEditable` element β€” works
   transparently across the shadow boundary. Affects every shortcut
   (`t`, `i`, `h`, `[`, `?`, `y`+, `o`+).

Co-authored-by: Cursor <cursoragent@cursor.com>
The previous import resolved to a relative URL (`/assets/clay-icon-…png`)
that the content script tried to fetch from the **host page's** origin β€”
not the extension's β€” so the FAB and panel-header logo silently 404'd
on every page.

Switch the import to `?inline` so Vite emits the PNG as a base64 data
URL embedded directly in the bundled JS. No fetch, no
`chrome.runtime.getURL()` wiring, and no dependency on the host page's
ability to resolve `/assets/...`. Bundle grows by ~12 KB (the expected
33% base64 inflation of an 8 KB PNG) β€” fine for an asset that has to
be present whenever the panel mounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Remove the (redundant) Edit button from the Component section: editing a
  component always means opening the parent page in editor mode, which is
  already exposed on the Page section. Two Edit buttons in the same panel
  was confusing.
- Refactor `ShareMenu`:
  - Read `location.href` at click time so SPA navigation can never produce
    a stale share URL.
  - When no site-host mappings are configured, render a plain pill button
    (full radius/border) instead of a half-pill split that visually looked
    broken next to nothing.
  - Always surface a toast on copy success/failure, including the env name
    so it's obvious which environment was copied.
  - Add `type="button"` everywhere so an enclosing form can't accidentally
    submit instead of copying.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the Playwright locator option from the Component "Copy as…" menu β€”
not used in our QA flow. Drops `copyAsPlaywrightLocator` and its test, and
trims the README highlight.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjpaulino jjpaulino changed the title πŸ• v2.0: full rewrite + Node 24 + 10 new daily-driver features πŸ• v2.0: full rewrite β€” MV3, TypeScript + React, Shadow DOM, FAB, cross-env tooling May 13, 2026
@jjpaulino jjpaulino merged commit 2beb9df into master May 13, 2026
1 check passed
@jjpaulino jjpaulino deleted the feat/modernize-clay-devtools branch May 13, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant