# MarkUS

**A drop-in visual review and annotation layer for any website.** MarkUS lets
reviewers highlight text, draw shapes, place pins, sketch freehand, and leave
threaded comments directly on top of a live page.

MarkUS has two operating modes:

- **Local-only mode:** add the script with no backend config. Comments stay in
  the reviewer's browser `localStorage` and can be downloaded/imported as JSON.
- **Live shared mode:** add a review id, MarkUS service URL, and public review
  key. Comments, replies, resolved state, and solution markers sync through a
  self-hosted PocketBase service.

```html
<script src="https://unpkg.com/@vulture916/markus/markus.js" defer></script>
```

That single line is enough for local-only reviews.

---

## Why MarkUS?

- **One script tag.** No build step, framework adapter, account, or vendor
  service is required.
- **Works anywhere.** Plain HTML, React, Vue, Svelte, WordPress, Webflow,
  Shopify, static sites, and any browser-rendered app can load it.
- **Local-first fallback.** Without live config, every comment stays private to
  the reviewer's browser until they explicitly export or share it.
- **Self-hosted live reviews.** Docker Compose starts the MarkUS PocketBase
  service for shared threads, replies, solution markers, and future admin
  exports.
- **Reviewer-focused UI.** The toolbar, comments panel, filters, deep links,
  download/import flow, and light/dark themes are injected without touching the
  host app.
- **Tiny and dependency-free client.** The browser layer is vanilla JavaScript.

---

## Features

| Tool | What it does |
|------|--------------|
| Highlight | Select text to highlight and comment on it. |
| Rectangle | Draw a box around any region. |
| Circle | Circle anything that needs attention. |
| Pin | Drop a point marker anywhere. |
| Freehand | Sketch directly on the page. |
| Section note | Hover configured blocks for a margin comment button. |

The comments panel supports threaded replies, resolve/reopen, Open / Solutions /
Resolved / All filters, search, deep links to `#an=<id>`, solution markers on
comments and replies, and JSON download/import.

---

## Quick Start

### Local-only mode

Add this before `</body>`:

```html
<script src="https://unpkg.com/@vulture916/markus/markus.js" defer></script>
```

To pin a production version:

```html
<script src="https://unpkg.com/@vulture916/markus@1.1.2/markus.js" defer></script>
```

Self-host the client by copying [`markus.js`](./markus.js) or
[`annotate.js`](./annotate.js) next to your HTML:

```html
<script src="/markus.js" defer></script>
```

### Live shared mode

Live mode is enabled only when all three live attributes match an enabled
PocketBase review session:

```html
<script
  src="https://unpkg.com/@vulture916/markus/markus.js"
  data-project="marketing-site"
  data-page="/pricing"
  data-review-id="launch-homepage-v3"
  data-api-base-url="https://reviews.example.com"
  data-public-key="rvw_pub_..."
  defer
></script>
```

Before embedding that snippet, create the review scope in the MarkUS service:

1. Generate a browser-visible review key:

   ```bash
   MARKUS_PUBLIC_KEY="rvw_pub_$(openssl rand -hex 24)"
   printf '%s\n' "$MARKUS_PUBLIC_KEY"
   ```

2. In PocketBase, create a `review_sessions` record:
   - `slug`: the value you will use as `data-review-id`, for example
     `launch-homepage-v3`.
   - `publicKey`: the generated `rvw_pub_...` value.
   - `enabled`: `true`.
3. Create a `review_origins` record for each site origin that may use that
   session:
   - `session`: the review session from step 2.
   - `origin`: exact browser origin only, for example
     `https://www.example.com` or `http://localhost:4200`.
   - `enabled`: `true`.
4. Put the same slug and public key into the script tag as `data-review-id` and
   `data-public-key`.

The review key is generated by the site/service operator, not by the browser
client. It is intentionally embedded in the page, so do not reuse admin tokens,
API tokens, or passwords. Treat it as an unguessable scoped review capability:
rotate it by replacing `review_sessions.publicKey` and updating the embed.

In live mode, MarkUS fetches shared comments for the configured review and page,
opens PocketBase realtime through `/api/realtime`, posts new comments/replies
to the service, and keeps failed writes as explicit offline drafts in
`localStorage`. It does not silently pretend an offline draft has been shared.

---

## Configuration

Configure with `data-` attributes on the script tag:

```html
<script
  src="https://unpkg.com/@vulture916/markus/markus.js"
  data-project="marketing-site"
  data-page="/pricing"
  data-accent="#6d28d9"
  data-theme="auto"
  data-position="bottom-right"
  data-start-open="true"
  data-note="Focus on the hero copy and pricing."
  data-share-email="reviews@example.com"
  data-review-id="launch-homepage-v3"
  data-api-base-url="https://reviews.example.com"
  data-public-key="rvw_pub_..."
  data-realtime="true"
  defer
></script>
```

| Attribute | Default | Description |
|-----------|---------|-------------|
| `data-project` | `""` | Namespace/display label. Also separates local stored comments. |
| `data-page` | `location.pathname` | Page key comments are grouped under. |
| `data-accent` | MarkUS default | Brand color for primary buttons and the active tool. |
| `data-theme` | `auto` | `light`, `dark`, or `auto`. |
| `data-position` | `bottom-right` | `bottom-right` or `bottom-left`. |
| `data-blocks` | sensible default | CSS selector for section-note targets. |
| `data-start-open` | `false` | Set to `true` to show the toolbar immediately. |
| `data-note` | `""` | Author note shown when reviewers start and atop the panel. |
| `data-share-email` | `""` | Email address or chat URL used by the Share flow. |
| `data-review-id` | `""` | Live review session slug. Required for live mode. |
| `data-api-base-url` | `""` | MarkUS service origin, for example `https://reviews.example.com`. |
| `data-public-key` | `""` | Public scoped key for this review session. Required for live mode. |
| `data-realtime` | `true` | Set to `false` to disable EventSource live updates. |

Prefer JavaScript config? Set `window.MarkUSConfig` before the script loads:

```html
<script>
  window.MarkUSConfig = {
    project: "marketing-site",
    page: "/pricing",
    accent: "#6d28d9",
    reviewId: "launch-homepage-v3",
    apiBaseUrl: "https://reviews.example.com",
    publicKey: "rvw_pub_...",
  };
</script>
<script src="https://unpkg.com/@vulture916/markus/markus.js" defer></script>
```

`window.AnnotateConfig` remains supported as a compatibility alias.

---

## Self-Hosted MarkUS Service

The live backend scaffold lives in [`markus-service/`](./markus-service). It
uses PocketBase for SQLite-backed persistence, file storage, an admin dashboard,
and realtime-ready collections.

Run it locally:

```bash
PB_ADMIN_EMAIL=admin@example.com \
PB_ADMIN_PASSWORD=change-me-now \
PB_ENCRYPTION="$(openssl rand -hex 16)" \
docker compose up markus-pocketbase
```

PocketBase listens on container port `8090`; Coolify can route that service
port directly. For a local Compose run, inspect the published host port with
`docker compose port markus-pocketbase 8090`.

The Compose command intentionally passes only PocketBase flags. The pinned
image entrypoint creates or updates the superuser from `PB_ADMIN_EMAIL` and
`PB_ADMIN_PASSWORD` before serving, but only when the command is empty or starts
with a flag.

The service image is built from `markus-service/Dockerfile` so the committed
PocketBase migrations and hooks are copied into the container image. This avoids
depending on host bind mounts in Coolify.

Create a review session in the PocketBase admin UI:

1. Generate a public review key:

   ```bash
   MARKUS_PUBLIC_KEY="rvw_pub_$(openssl rand -hex 24)"
   printf '%s\n' "$MARKUS_PUBLIC_KEY"
   ```

2. Add a `review_sessions` record with `slug`, the generated `publicKey`, and
   `enabled=true`.
3. Add a `review_origins` record for the exact browser origin that will embed
   MarkUS, such as `https://www.example.com` or `http://localhost:4200`.
4. Use the session slug as `data-review-id` and the public key as
   `data-public-key`.

The public service validates request `Origin`, `X-Markus-Public-Key`, enabled
session state, and enabled origins before accepting public review actions.
Public keys are scoped to review actions only and are expected to be visible in
the browser embed. Admin actions require PocketBase admin authentication and
admin credentials must never be exposed to the browser client.

The service hook routes are:

- `GET /api/markus/v1/health`
- `GET /api/markus/v1/reviews/{reviewId}/comments?pageKey=/path`
- `POST /api/markus/v1/reviews/{reviewId}/comments`
- `PATCH /api/markus/v1/reviews/{reviewId}/comments/{commentId}`
- `POST /api/markus/v1/reviews/{reviewId}/comments/{commentId}/replies`
- `POST /api/markus/v1/reviews/{reviewId}/solutions`

Realtime updates use PocketBase's native `GET /api/realtime` SSE connection and
`POST /api/realtime` subscription handshake. MarkUS subscribes to
`review_comments/*` with the configured `pageKey` and public review key, then
refreshes the scoped thread list when comment events arrive.

The current browser client live data layer enables from the same embed
attributes and expects the configured service origin to provide the review API
under the client route prefix. When exposing the PocketBase scaffold beyond
local development, put it behind your platform's TLS proxy and keep the API
origin aligned with the deployed client version.

### Reverse proxy

The Compose file only runs PocketBase. For production, terminate TLS in your
own proxy or hosting platform and keep PocketBase admin access restricted to
trusted operators. Do not log public keys, admin credentials, comment bodies,
screenshots, cookies, or full URLs with sensitive query strings.

### Backup and restore

Back up the PocketBase data volume before upgrades:

```bash
docker run --rm \
  -v hartford_markus_pb_data:/pb_data:ro \
  -v "$PWD":/backup \
  alpine tar czf /backup/markus-pb-data.tgz -C /pb_data .
```

Restore into an empty data volume:

```bash
docker run --rm \
  -v hartford_markus_pb_data:/pb_data \
  -v "$PWD":/backup \
  alpine sh -c 'tar xzf /backup/markus-pb-data.tgz -C /pb_data'
```

---

## Agent Exports

Solution markers identify the comments or replies that reviewers want
implemented. Agent-facing exports should prioritize solution-marked items while
preserving the full surrounding thread, author, timestamps, resolved state,
anchor/geometry data, and page context. Resolved and solution states are
separate: resolved means the thread is closed; solution means the item is the
intended implementation guidance.

Until admin export endpoints are added, reviewers can still use the panel
Download action for a portable JSON bundle.

---

## Framework Integration

MarkUS is a plain browser script. Load it once after the page has rendered.

### React and Next.js

```jsx
import { useEffect } from "react";

export default function MarkUSReview() {
  useEffect(() => {
    if (document.getElementById("markus-js")) return;
    window.MarkUSConfig = {
      project: "my-react-app",
      accent: "#6d28d9",
    };
    const script = document.createElement("script");
    script.id = "markus-js";
    script.src = "https://unpkg.com/@vulture916/markus/markus.js";
    script.defer = true;
    document.body.appendChild(script);
  }, []);

  return null;
}
```

Next.js App Router:

```jsx
import Script from "next/script";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Script
          src="https://unpkg.com/@vulture916/markus/markus.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
```

### Plain HTML, CMS, and site builders

Paste the script before `</body>` or into the platform's custom footer field:

```html
<script
  src="https://unpkg.com/@vulture916/markus/markus.js"
  data-project="website-review"
  defer
></script>
```

---

## Sharing Local Comments

In local-only mode, sharing is explicit:

1. Open the Comments panel.
2. Click Download to save a `annotate-<page>-<date>.json` file.
3. Send the file to the person collecting feedback.
4. They open the same page, click Import, and select the file.

Live shared mode removes that manual round trip for configured review sessions,
but JSON download/import remains available as a fallback.

---

## JavaScript API

`window.MarkUS` is the canonical API. `window.Annotate` is the compatibility
alias and points to the same object.

```js
MarkUS.open();              // show the review layer and open the comments panel
MarkUS.close();
MarkUS.toggle();
MarkUS.enable();            // show the review layer
MarkUS.disable();           // collapse to the launcher
MarkUS.setTool("highlight");// cursor | highlight | rect | circle | pen | pin
MarkUS.comments();          // array of comment objects for this page
MarkUS.focus(id);           // scroll to and highlight a comment
MarkUS.export();            // trigger JSON download
MarkUS.import();            // open the file picker
MarkUS.clear();             // delete all local comments on this page
MarkUS.toast("Saved!");     // show a toast
MarkUS.version;             // package version
MarkUS.config;              // resolved configuration, including live.enabled
```

### Comment shape

```json
{
  "id": "c...",
  "page": "marketing-site:/pricing",
  "url": "https://example.com/pricing",
  "type": "highlight",
  "author": "Jane Doe",
  "text": "This price looks out of date.",
  "color": "#f59e0b",
  "anchor": { "exact": "...", "prefix": "...", "suffix": "..." },
  "geom": null,
  "resolved": false,
  "solution": false,
  "replies": [],
  "createdAt": "2026-06-16T10:00:00.000Z",
  "updatedAt": "2026-06-16T10:00:00.000Z"
}
```

---

## Keyboard Shortcuts

| Key | Action | Key | Action |
|-----|--------|-----|--------|
| `V` | Browse | `P` | Pin |
| `H` | Highlight | `A` | Comments panel |
| `R` | Rectangle | `O` | Show or hide tools |
| `C` | Circle | `Esc` | Cancel |
| `D` | Freehand | `?` | Shortcuts card |

---

## Try It Locally

```bash
git clone git@github.com:reviewjs/annotate.git
cd annotate
bun install
bun run start
```

The demo serves at `http://localhost:4200`. Framework examples live in
[`examples/`](./examples).

---

## Browser Support

Modern evergreen browsers: Chrome, Edge, Firefox, and Safari. MarkUS uses
standard DOM APIs and gracefully no-ops where `localStorage` is unavailable.

---

## License

[MIT](./LICENSE)
