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
26 changes: 13 additions & 13 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# openpicker Design
# OpenPicker Design

> An open-source CSS element picker. Ships a browser extension + npm SDK so that
> any third-party web page can invoke an element picker through one open protocol
Expand All @@ -21,7 +21,7 @@ page" lock-in behavior. All code and protocol naming are written from scratch.

### 1.1 Design principle: openness by parameter

openpicker is a platform, not one product's tool. Capabilities are exposed as **parameters and
OpenPicker is a platform, not one product's tool. Capabilities are exposed as **parameters and
extensible enums**, not hardcoded behavior, so integrators decide what fits their use case:

- **Behavior is opt-in via parameters / string enums**, never baked in. Examples already in v1:
Expand All @@ -44,7 +44,7 @@ extensible enums**, not hardcoded behavior, so integrators decide what fits thei
│ Integrator page (any origin) │
│ │
│ Integrator code │
│ └── calls openpicker SDK (npm) │
│ └── calls OpenPicker SDK (npm) │
│ │ window.postMessage (with id) │
│ ▼ │
│ content script ◄── injected by extension │
Expand Down Expand Up @@ -171,7 +171,7 @@ after the user confirms with OK.
Layout (top → bottom):
```
┌────────────────────────────────────────┐
│ ⏏ ⇄ openpicker ? ✕ │ header: dock/eject, swap side, help, close
│ ⏏ ⇄ OpenPicker ? ✕ │ header: dock/eject, swap side, help, close
├────────────────────────────────────────┤
│ [ div[data-hveid] > div:nth-of-type(5) ]⚙│ editable selector input + settings
├────────────────────────────────────────┤
Expand Down Expand Up @@ -236,12 +236,12 @@ map onto `@medv/finder` configuration, surfaced as UI.
```

- The generated selector always targets **exactly one element** (finder's unique selector). A
"list / group" mode was considered but cut — openpicker's purpose is targeting a single element,
"list / group" mode was considered but cut — OpenPicker's purpose is targeting a single element,
and the feature was borrowed from a reference UI without a real use case (see git history).
- **Per-dimension rules** (id / class / attr / tag): each has an **enable** toggle plus an **allow**
and an **ignore** regex, mapping onto finder's `idName` / `className` / `attr` / `tagName`
predicates. For `attr`, allow/ignore match the attribute **name**. An empty **allow** falls back to
openpicker's stable-name default (skips hashed ids/classes; prefers test hooks + finder's curated
OpenPicker's stable-name default (skips hashed ids/classes; prefers test hooks + finder's curated
attribute set). The settings are one resolved `SelectorSettings` object; legacy stored shapes
(boolean toggles + a single ignore + an attr name-list) are migrated on load.
- **Layering**: built-in defaults < the user's **global default** (options page) < the user's
Expand Down Expand Up @@ -431,7 +431,7 @@ dashboard.example.com" — for transparency.
## 5d. Source↔target tab mapping, reuse, and continuity (v2.x, designed)

This is the heart of robust cross-tab picking. The architecture follows a proven, production cross-
tab pattern (studied for ideas; **all code is openpicker's own — no third-party code is copied, and
tab pattern (studied for ideas; **all code is OpenPicker's own — no third-party code is copied, and
no third-party product name appears anywhere in the repo**). Vendor-specific concerns from that
pattern — injecting a vendor SDK, CSP adaptation, debugger UI, hardcoded origin allowlists — are
deliberately dropped; only the generic "open a tab, pick there, route the result back, survive
Expand All @@ -454,14 +454,14 @@ op:targetToSource:<targetTabId> → { sourceTabId, params, key?, pickId? }
### Target tab reuse (the "flow_id" substitution)
When a pick requests a `url` and a target tab is already mapped to this source, decide reuse vs.
open-new the same way the reference pattern does — except the business-identity dimension (its
`flow_id`) is replaced by openpicker-native inputs, since openpicker has no business concepts:
`flow_id`) is replaced by openpicker-native inputs, since OpenPicker has no business concepts:

```
reference: different host → new tab; same host but different flow_id → new tab; same host, different pathname → new tab
openpicker: different host → new tab; same host but different `key` → new tab; (optional) different pathname → new tab
OpenPicker: different host → new tab; same host but different `key` → new tab; (optional) different pathname → new tab
```
- `key` is an **optional, caller-supplied** opaque string (e.g. the integrator's own step id).
openpicker never interprets it — it only compares equality, exactly as the reference compares
OpenPicker never interprets it — it only compares equality, exactly as the reference compares
flow_id. This keeps the reuse logic identical while staying business-agnostic and open (§1.1).
- No `key` → fall back to host/URL comparison alone.
- **On reuse, the target tab is only focused — never re-navigated.** Reuse is host-gated, so the
Expand All @@ -470,7 +470,7 @@ openpicker: different host → new tab; same host but different `key` → new ta
where they went. So a reused pick runs on the tab's *current* page, not the requested `url`.
(The `url` only opens the tab the first time and decides reuse-vs-new; it matches the reference.)
- Why caller-supplied, not internal: "is this the same task?" is a business judgment only the
integrator knows. The reference could read flow_id because it *is* the business; openpicker is a
integrator knows. The reference could read flow_id because it *is* the business; OpenPicker is a
tool, so the business identity must come in from the caller.

**One target per source (decided: keep it single).** A source holds exactly one target mapping
Expand All @@ -496,7 +496,7 @@ pick re-arms **in navigate mode**, not select mode — the user may need to hop
and re-arming select mode would re-lock the page each time. The user returns to select mode
explicitly via "Resume picking"; the flag is cleared then and when the pick ends.

openpicker's twist over the reference: after re-arming on a new page/tab, the result still has to
OpenPicker's twist over the reference: after re-arming on a new page/tab, the result still has to
reach the original **source** tab. Delivery is **stateless** — there is no long-lived port and no
in-memory registry of pending picks in the background. Instead:
- The source sends a one-shot `crossTab:open` (with a `pickId`) and then waits for a `crossTab:deliver`
Expand All @@ -509,7 +509,7 @@ in-memory registry of pending picks in the background. Instead:
Because every step reads the map fresh, a pick **survives the MV3 service worker being recycled
mid-pick** (an earlier port + in-memory-resolver design lost the pick when the worker died, and
needed the worker kept alive). This matches how the reference stays resilient. (The reference's
injected SDK carried its own connection; openpicker has no resident SDK, so it routes through the map.)
injected SDK carried its own connection; OpenPicker has no resident SDK, so it routes through the map.)

### Phasing
1. **Phase 1:** background bidirectional map + reuse decision (host/URL + optional `key`) + refocus-
Expand Down
10 changes: 5 additions & 5 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# openpicker — Privacy Policy
# OpenPicker — Privacy Policy

_Last updated: 2026-06-03_

openpicker is an open-source browser extension that lets you point at an element on a web page
OpenPicker is an open-source browser extension that lets you point at an element on a web page
and get a stable CSS selector for it. This policy explains what the extension does and does not do
with your data.

## The short version

**openpicker does not collect, transmit, or sell your data. There are no openpicker servers.**
**OpenPicker does not collect, transmit, or sell your data. There are no OpenPicker servers.**
Everything happens locally in your browser. We have no analytics, no tracking, and no ads.

## What is stored, and where
Expand All @@ -27,9 +27,9 @@ You can review or clear these at any time from the extension's options page.

- The **selector and basic element details** produced by a pick are delivered to the web page that
started the pick (or shown to you when you pick from the toolbar). This stays within your browser;
openpicker does not send it to us or any third party.
OpenPicker does not send it to us or any third party.
- A **screenshot** is captured only when a pick explicitly requests one. It is returned to the
requesting page / shown in the picker, again only within your browser. openpicker does not upload
requesting page / shown in the picker, again only within your browser. OpenPicker does not upload
it anywhere.

## Permissions
Expand Down
6 changes: 3 additions & 3 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# openpicker Protocol v1
# OpenPicker Protocol v1

> The contract between the **page SDK** (npm) and the **browser extension**. It defines how
> they discover each other, exchange requests/responses, correlate them, handle consent, and
Expand All @@ -15,7 +15,7 @@ There are two communication hops; only the first is part of this public protocol
```
[ integrator code ]
│ function calls
[ openpicker SDK ] ── window.postMessage ──► [ extension content script ] ◄── THIS PROTOCOL
[ OpenPicker SDK ] ── window.postMessage ──► [ extension content script ] ◄── THIS PROTOCOL
│ chrome.runtime.* (extension-internal)
[ background service worker ]
```
Expand Down Expand Up @@ -80,7 +80,7 @@ Unknown fields MUST be ignored (forward compatibility). Receivers must not assum

- `id` format: `op:<instanceId>:<seq>`
- `instanceId` — a short random id (e.g. nanoid) generated once per SDK instance. Disambiguates
multiple SDK instances on the same page (host app + an embedded widget both using openpicker).
multiple SDK instances on the same page (host app + an embedded widget both using OpenPicker).
- `seq` — a monotonically increasing counter within that instance.
- A response MUST echo the request's `id` exactly.
- The SDK keeps a pending map `id → { resolve, reject, timer }`. On a matching `res` it settles
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="./.github/openpicker.svg" alt="openpicker" width="88" height="88" />
</p>

<h1 align="center">openpicker</h1>
<h1 align="center">OpenPicker</h1>

<p align="center">An open-source CSS element picker for the web.</p>

Expand Down Expand Up @@ -42,7 +42,7 @@ straight from the toolbar, with no code at all.
A web page can already inspect its *own* DOM. What it can't do is ask the user to point at an
element on **another tab or origin** and hand the selector back — browsers isolate origins for
good reason. That cross-tab/cross-origin hop is exactly what an extension can bridge, and it's the
reason openpicker exists. It's a natural fit for product onboarding/tours, event tagging, no-code
reason OpenPicker exists. It's a natural fit for product onboarding/tours, event tagging, no-code
automation, scraping config, and anything that needs "let me point at that element."

## Features
Expand All @@ -69,7 +69,7 @@ Two communication hops; only the first is the public protocol:
```
[ your code ]
│ function calls
[ openpicker SDK ] ── window.postMessage ──► [ extension content script ] ◄── PUBLIC PROTOCOL
[ OpenPicker SDK ] ── window.postMessage ──► [ extension content script ] ◄── PUBLIC PROTOCOL
│ chrome.runtime.* (extension-internal)
[ background service worker ]
```
Expand Down Expand Up @@ -206,7 +206,7 @@ display-only; only the verified `event.origin` is authoritative. See [PROTOCOL.m
openpicker/
├── packages/
│ ├── protocol/ # @openpicker/protocol — shared types & constants (source-only, private)
│ ├── sdk/ # openpicker — the npm SDK integrators install (tsup build)
│ ├── sdk/ # OpenPicker — the npm SDK integrators install (tsup build)
│ └── extension/ # @openpicker/extension — the browser extension (WXT + React + Tailwind, MV3)
├── apps/
│ ├── test/ # interactive Ping/Pick test page
Expand Down
12 changes: 6 additions & 6 deletions apps/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/openpicker.svg" />
<link rel="stylesheet" href="./style.css" />
<title>openpicker — live demo</title>
<title>OpenPicker — live demo</title>
<meta
name="description"
content="Live demo of openpicker: pick an element on any page and get a stable CSS selector back, via @openpicker/sdk."
content="Live demo of OpenPicker: pick an element on any page and get a stable CSS selector back, via @openpicker/sdk."
/>
</head>
<body class="min-h-screen bg-gradient-to-b from-white to-slate-50 font-sans text-slate-800 antialiased">
Expand All @@ -18,7 +18,7 @@
href="https://openpicker.dev"
>
<img src="/openpicker.svg" width="28" height="28" alt="" />
<span>openpicker</span>
<span>OpenPicker</span>
</a>
<nav class="flex gap-4 text-sm text-slate-500">
<a class="hover:text-slate-900" href="https://www.npmjs.com/package/@openpicker/sdk">npm</a>
Expand All @@ -32,7 +32,7 @@ <h1 class="text-3xl font-semibold tracking-tight text-slate-900">
Pick an element, get its selector
</h1>
<p class="mt-3 text-slate-500">
openpicker opens a page, lets you point at any element, and hands a stable CSS selector
OpenPicker opens a page, lets you point at any element, and hands a stable CSS selector
back to the calling app — this page is a live caller of
<code class="rounded bg-slate-100 px-1.5 py-0.5 text-slate-700">@openpicker/sdk</code>.
</p>
Expand All @@ -48,7 +48,7 @@ <h1 class="text-3xl font-semibold tracking-tight text-slate-900">
<section id="install" hidden class="mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 class="font-semibold text-lg">Install the extension first</h2>
<p class="mt-1 text-slate-600 text-sm">
The demo needs the openpicker browser extension. Add it from the Chrome Web Store, then
The demo needs the OpenPicker browser extension. Add it from the Chrome Web Store, then
re-check below.
</p>
<div class="mt-4 flex items-center gap-2.5">
Expand All @@ -75,7 +75,7 @@ <h2 class="font-semibold text-lg">Install the extension first</h2>
<section id="demo" hidden class="mt-5 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 class="font-semibold text-lg">Try it</h2>
<p class="mt-1 text-slate-500 text-sm">
Enter a URL. openpicker opens it in a new tab, you click an element there, and the selector
Enter a URL. OpenPicker opens it in a new tab, you click an element there, and the selector
comes back here.
</p>

Expand Down
2 changes: 1 addition & 1 deletion apps/demo/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@openpicker/sdk"
import { getSelectorConfig, mountRulesIsland } from "./rules-island"

const op = createOpenpicker({ appName: "openpicker demo", pingTimeout: 700 })
const op = createOpenpicker({ appName: "OpenPicker demo", pingTimeout: 700 })

const byId = <T extends HTMLElement>(id: string) => document.getElementById(id) as T
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const GITHUB = "https://github.com/usertour/openpicker"
export default defineConfig({
title: "openpicker",
description:
"Documentation for openpicker — an open-source CSS element picker for the web (browser extension + SDK).",
"Documentation for OpenPicker — an open-source CSS element picker for the web (browser extension + SDK).",
cleanUrls: true,
lastUpdated: true,
srcExclude: ["**/README.md"],
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @openpicker/docs

The documentation site for openpicker (**docs.openpicker.dev**), built with
The documentation site for OpenPicker (**docs.openpicker.dev**), built with
[VitePress](https://vitepress.dev). User guide + developer/SDK reference. Deployed to Cloudflare
(Workers static assets), like the rest of the apps.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/developers/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ try {
// this origin isn't allowed (see Authorization)
break
case "extension_not_installed":
// prompt the user to install openpicker
// prompt the user to install OpenPicker
break
default:
// surface err.message
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/developers/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createOpenpicker, OpenpickerError } from "@openpicker/sdk"
const op = createOpenpicker({ appName: "My App" })

if (!(await op.isAvailable())) {
// prompt the user to install the openpicker extension
// prompt the user to install the OpenPicker extension
}

try {
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/guide/appearance.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Appearance

openpicker's own UI (the popup, the options page, and the picker sidebar) supports three themes:
OpenPicker's own UI (the popup, the options page, and the picker sidebar) supports three themes:

| Theme | Behavior |
| --- | --- |
Expand All @@ -19,5 +19,5 @@ Both write the same setting, so changing it in one place updates the other immed
sidebar simply follows your choice.

::: tip
This theme controls **openpicker's interface only** — it never changes the websites you pick on.
This theme controls **OpenPicker's interface only** — it never changes the websites you pick on.
:::
6 changes: 3 additions & 3 deletions apps/docs/guide/configuring-selectors.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Configuring selectors

Different sites have different conventions — one relies on `data-testid`, another on stable ids or
class names. openpicker lets you control **how selectors are built**, with a global default and
class names. OpenPicker lets you control **how selectors are built**, with a global default and
per-site overrides.

You can set the rules in two places:
Expand All @@ -20,7 +20,7 @@ has two regex filters:

| Field | What it does |
| --- | --- |
| **Allow** | Only names matching this regex may be used. Empty = openpicker's stable-name default. |
| **Allow** | Only names matching this regex may be used. Empty = OpenPicker's stable-name default. |
| **Ignore** | Names matching this regex are never used (applied on top of Allow). |

Examples:
Expand All @@ -34,7 +34,7 @@ For attributes, Allow / Ignore match the attribute **name**.

## Built-in stability filters

When an **Allow** box is empty, openpicker uses a sensible default and automatically avoids anchors
When an **Allow** box is empty, OpenPicker uses a sensible default and automatically avoids anchors
that won't survive a redeploy: auto-generated ids (Ember, Radix, React `useId`, long hex hashes),
hashed class names (Emotion, styled-components, CSS modules), and it prefers test hooks
(`data-testid`, `data-cy`, …) plus a curated set of stable attributes (`name`, `aria-label`, `role`,
Expand Down
Loading
Loading