Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00d613d
Remove old CSS
flipbit Feb 27, 2026
8ddf021
feat: add autoResize option to AnnotateImageOptions
flipbit Feb 27, 2026
f6c8d0a
feat: compute scale factors from rendered vs natural image dimensions
flipbit Feb 27, 2026
db8770a
feat: scale annotation view positions by scaleX/scaleY
flipbit Feb 27, 2026
ab51927
feat: scale edit area positioning by scaleX/scaleY
flipbit Feb 27, 2026
a61e36b
feat: add ResizeObserver for dynamic annotation rescaling
flipbit Feb 27, 2026
68597c1
feat: pass autoResize option through React and Vue wrappers
flipbit Feb 27, 2026
5bbfb47
feat: add scaling demo page with CSS-constrained, explicit, and respo…
flipbit Feb 27, 2026
cdfcf04
test: add e2e tests for auto-scaling demo page
flipbit Feb 27, 2026
835ecce
docs: document auto-scaling feature and autoResize option
flipbit Feb 27, 2026
390618b
fix: correct scaling E2E tests for viewport resize and flaky hover
flipbit Feb 27, 2026
f63e160
refactor: extract shared createScaledTestImage helper, remove redunda…
flipbit Feb 27, 2026
2a73717
docs: design for code review fixes (C1 canvas wrap, C2 conversion, H1…
flipbit Feb 27, 2026
d9f2685
fix: restore accidentally removed docs/migration.md
flipbit Feb 27, 2026
2ee5857
docs: implementation plan for code review fixes
flipbit Feb 27, 2026
a8512d3
feat: add toRendered/toNatural coordinate conversion methods with isF…
flipbit Feb 27, 2026
83a23fa
refactor: use toRendered/toNatural for all scale conversions (C2 fix)
flipbit Feb 27, 2026
e6b4d89
refactor: wrap image inside canvas instead of using background-image …
flipbit Feb 27, 2026
61eb711
fix: defer rescale during active edits to prevent data loss (H1 fix)
flipbit Feb 27, 2026
0c284cf
test: add edge case tests for ResizeObserver guards (M2/M3)
flipbit Feb 27, 2026
efed1bf
fix: use idiomatic framework defaults for autoResize prop (H3/H4/H5)
flipbit Feb 27, 2026
10f85ba
chore: add autoResize to defaults (D3), fix var in demo (L1)
flipbit Feb 27, 2026
bf22568
docs: update for canvas-wrapping architecture
flipbit Feb 27, 2026
41fe3ff
fix: update E2E tests and CSS for canvas-wrapping DOM structure
flipbit Feb 27, 2026
de4e30b
test: add deferred rescale flush-after-save test
flipbit Feb 27, 2026
65d9d17
Cleanup Claude TODO lists
flipbit Mar 6, 2026
eb158fd
Update CI build trigger branch to "main"
flipbit Mar 6, 2026
bf89ff5
Fix unit tests
flipbit Mar 6, 2026
886e2ee
ci: use trusted publishing for npm, bump to 2.0.0-beta.2
flipbit Mar 6, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
pull_request:
branches: [master]
branches: [main]

jobs:
ci:
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
publish:
needs: verify
runs-on: ubuntu-latest
environment: npm-publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4

Expand All @@ -57,12 +61,10 @@ jobs:
- name: Publish to npm
run: |
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
npm publish --tag beta
npm publish --provenance --access public --tag beta
else
npm publish
npm publish --provenance --access public
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

github-release:
needs: publish
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/
test-results/
playwright-report/
.DS_Store
docs
126 changes: 126 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

jQuery Image Annotation Plugin - creates Flickr-like comment annotations on images embedded in web pages. Users can draw rectangular regions on images, add text notes, and save/load annotations via AJAX or static data. Licensed under GNU GPL v2.

**Runtime dependencies:** jQuery 3.x/4.x.

## Build Commands

```sh
# Install all dependencies
npm install

# Build (type-check, bundle core + jQuery adapters, minify CSS)
npm run build

# Type-check only (no emit)
npm run build:check

# Clean dist directory only
npm run clean

# Run tests
npm test

# Run tests against jQuery 4
npm run test:jquery4
```

## Architecture

The plugin is written in TypeScript with vanilla DOM internals. jQuery is only used in the thin adapter layer (`src/jquery.annotate.ts`) that registers `$.fn.annotateImage`. Drag/resize uses vanilla pointer events (`src/interactions.ts`).

### Repository Structure

```
src/
index.ts - Core entry point (annotate() factory, type exports)
types.ts - Shared interfaces (AnnotationNote, AnnotateImageOptions, InteractionHandlers)
annotate-image.ts - AnnotateImage class (canvas, load/save, hover, add note, destroy)
annotate-edit.ts - AnnotateEdit class (edit mode, drag/resize area, form, save/delete/cancel)
annotate-view.ts - AnnotateView class (annotation display, hover, click-to-edit)
interactions.ts - Vanilla drag/resize via pointer events (InteractionHandlers)
jquery.annotate.ts - jQuery adapter ($.fn.annotateImage registration + destroy dispatch)
annotation.css - Plugin styles (icons are inline SVG data URIs)
demo/
static.html - Demo with hardcoded annotations
ajax.html - Demo with AJAX endpoints
fixtures/ - Mock AJAX endpoint files (get.json, save.json, delete.json)
images/ - Demo-only images
test/ - Vitest test suite
dist/ - Built output (gitignored)
docs/ - Migration plans and design documents
```

### Class Structure

- **`AnnotateImage`** (`src/annotate-image.ts`) — Orchestrates the plugin. Wraps the target image in a canvas div with view/edit overlays (image provides intrinsic sizing, overlays use CSS `inset: 0`). Loads annotations (static or via `fetch`), manages mode switching, creates icon-only "Add Note" button inside the canvas (hover-to-show, always visible on touch). Coordinates stored in natural image pixels; `toRendered()`/`toNatural()` convert between natural and scaled coordinates. Rescale is deferred during active edits. Instance stored via `$(img).data('annotateImage')`.
- **`AnnotateEdit`** (`src/annotate-edit.ts`) — Edit mode. Manages the draggable/resizable area (via injected `InteractionHandlers`), inline form with textarea, save/delete/cancel buttons. Uses `api.save`/`api.delete` callbacks for persistence.
- **`AnnotateView`** (`src/annotate-view.ts`) — View mode. Renders annotation area + tooltip, hover show/hide, click-to-edit for editable annotations. Helper functions `readInlinePosition`/`readInlineSize` read from inline styles (jsdom-compatible).

### Core API

`src/index.ts` is the vanilla entry point. It exports:
- `annotate(img, options?)` — factory function, returns `AnnotateImage` instance
- `AnnotateImage` class (for typing/instanceof)
- `AnnotationNote`, `AnnotateImageOptions` types

Unified defaults (both core and jQuery): `editable: true`, `notes: []`, `autoResize: true`.

### jQuery Adapter

`src/jquery.annotate.ts` is the jQuery entry point. It:
- Registers `$.fn.annotateImage(options)` which creates an `AnnotateImage` instance
- Supports method dispatch: `$(img).annotateImage('destroy')`
- Stores the instance via `$(img).data('annotateImage')`

### Plugin Options

```typescript
{
editable: true, // Enable editing
notes: [], // Static annotation data
api?: { // Server persistence (omit for static-only mode)
load: string | (() => Promise<AnnotationNote[]>),
save: string | ((note: NoteData) => Promise<SaveResult>),
delete: string | ((note: NoteData) => Promise<void>),
},
labels?: { // Configurable button labels (omit for English defaults)
addNote?: string, // default: "Add Note"
save?: string, // default: "OK"
delete?: string, // default: "Delete"
cancel?: string, // default: "Cancel"
placeholder?: string // default: "" (no placeholder)
},
autoResize?: boolean, // Re-scale on container resize (default: true)
onError?: (context: AnnotateErrorContext) => void,
}
```

Each `api` field accepts either a URL string (shorthand for default fetch) or a function (full control). Omitting `api` entirely means static mode — no network calls.

### Annotation Data Shape

```typescript
{ top: number, left: number, width: number, height: number, text: string, id: string, editable: boolean }
```

### Build Output

`dist/` contains the built artifacts (gitignored):
- `dist/core.js` — Core library (ESM, no dependencies)
- `dist/core.min.js` — Core library (IIFE, minified, `AnnotateImage` global)
- `dist/jquery.js` — jQuery adapter (ESM, jQuery external)
- `dist/jquery.min.js` — jQuery adapter (IIFE, minified, jQuery external)
- `dist/css/annotate.min.css` — Minified styles (icons embedded as SVG data URIs)
- `dist/types/` — TypeScript declaration files (`.d.ts`)

Package exports: `"."` (core), `"./jquery"` (jQuery adapter), `"./css"` (styles).

### CSS Classes

All classes are prefixed `image-annotate-` (e.g., `.image-annotate-canvas`, `.image-annotate-view`, `.image-annotate-edit`, `.image-annotate-area`, `.image-annotate-note`).
1 change: 1 addition & 0 deletions demo/ajax.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>AJAX</h1>
Expand Down
1 change: 1 addition & 0 deletions demo/custom-labels.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html" class="active">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>Custom Labels</h1>
Expand Down
5 changes: 5 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>jQuery Image Annotate</h1>
Expand Down Expand Up @@ -55,6 +56,10 @@ <h1>jQuery Image Annotate</h1>
<a href="custom-labels.html">Custom Labels</a>
<span class="link-desc">Override button labels for internationalization or icon-only mode.</span>
</li>
<li>
<a href="scaling.html">Scaling</a>
<span class="link-desc">Annotations scale automatically with CSS-constrained, explicit-size, and responsive images.</span>
</li>
</ul>
</div>
</body>
Expand Down
1 change: 1 addition & 0 deletions demo/jquery-basics.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>jQuery Basics</h1>
Expand Down
1 change: 1 addition & 0 deletions demo/multiple-instances.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<a href="multiple-instances.html" class="active">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>Multiple Instances</h1>
Expand Down
1 change: 1 addition & 0 deletions demo/programmatic-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html" class="active">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>Programmatic API</h1>
Expand Down
1 change: 1 addition & 0 deletions demo/react.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>React</h1>
Expand Down
95 changes: 95 additions & 0 deletions demo/scaling.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scaling — Image Annotate</title>
<link rel="stylesheet" href="../dist/css/annotate.min.css">
<link rel="stylesheet" href="demo.css">
<style>
.constrained-500 {
max-width: 500px;
}
.responsive-container {
width: 50%;
min-width: 200px;
}
.resize-hint {
color: #888;
font-size: 13px;
font-style: italic;
margin-top: 8px;
}
</style>
</head>
<body>
<nav class="demo-nav">
<span class="nav-title">Image Annotate</span>
<a href="index.html">Home</a>
<a href="jquery-basics.html">jQuery Basics</a>
<a href="vanilla-basics.html">Vanilla JS</a>
<a href="react.html">React</a>
<a href="vue.html">Vue</a>
<a href="ajax.html">AJAX</a>
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html" class="active">Scaling</a>
</nav>
<div class="demo-content">
<h1>Scaling</h1>
<p>Annotations scale automatically to match the rendered image size. Coordinates are always stored in natural (original) image pixels.</p>

<div class="demo-section">
<h2>CSS-Constrained (max-width: 500px)</h2>
<p>The image is 960&times;760 but constrained to 500px wide by CSS. Annotations scale proportionally.</p>
<div class="constrained-500">
<img id="css-constrained" src="images/starry-night.jpg" alt="Starry Night" width="960" height="760">
</div>
</div>

<div class="demo-section">
<h2>Explicit Smaller Attributes (width=400)</h2>
<p>The image is displayed at 400&times;317 via HTML attributes on a 960&times;760 original.</p>
<img id="explicit-size" src="images/starry-night.jpg" alt="Starry Night" width="400" height="317">
</div>

<div class="demo-section">
<h2>Responsive Container (50% width)</h2>
<p>The image fills 50% of the content area. Resize the browser window to see annotations reposition.</p>
<div class="responsive-container">
<img id="responsive" src="images/starry-night.jpg" alt="Starry Night" width="960" height="760" style="max-width: 100%; height: auto;">
</div>
<p class="resize-hint">Try resizing the browser window.</p>
</div>
</div>

<script type="module">
import { annotate } from '../dist/core.js';

const notes = [
{ top: 80, left: 30, width: 100, height: 350, text: "Cypress tree", id: "note-1", editable: true },
{ top: 20, left: 200, width: 500, height: 200, text: "Swirling sky", id: "note-2", editable: true },
{ top: 500, left: 150, width: 550, height: 200, text: "Village below", id: "note-3", editable: true },
{ top: 30, left: 750, width: 120, height: 120, text: "Crescent moon", id: "note-4", editable: true }
];

window.addEventListener('load', function() {
annotate(document.getElementById('css-constrained'), {
editable: true,
notes: JSON.parse(JSON.stringify(notes))
});

annotate(document.getElementById('explicit-size'), {
editable: true,
notes: JSON.parse(JSON.stringify(notes))
});

annotate(document.getElementById('responsive'), {
editable: true,
notes: JSON.parse(JSON.stringify(notes))
});
});
</script>
</body>
</html>
1 change: 1 addition & 0 deletions demo/vanilla-basics.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>Vanilla JS</h1>
Expand Down
1 change: 1 addition & 0 deletions demo/vue.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<a href="multiple-instances.html">Multiple Instances</a>
<a href="programmatic-api.html">Programmatic API</a>
<a href="custom-labels.html">Custom Labels</a>
<a href="scaling.html">Scaling</a>
</nav>
<div class="demo-content">
<h1>Vue</h1>
Expand Down
Loading