@ascentsparksoftware/react-image-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ascentspark Software Private Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ <div align="center">
2
+
3
+ <a href="https://github.com/ascentspark/react-image-editor"><img src="image-editor-logo.png" alt="React Image Editor" height="120"></a>
4
+
5
+ # @ascentsparksoftware/react-image-editor
6
+
7
+ A standalone, themeable **React 19** image editor โ€” crop, filter, draw, redact, layers and
8
+ **in-browser AI background removal** โ€” built from scratch on **Fabric.js v7**.
9
+
10
+ by&nbsp;<a href="https://ascentspark.com" target="_blank" rel="noopener"><img src="https://cdn.ascentspark.com/assets/images/asc-logo-full.svg" alt="Ascentspark" height="22" valign="middle"></a>
11
+
12
+ [![npm version](https://img.shields.io/npm/v/@ascentsparksoftware/react-image-editor.svg?color=087ea4)](https://www.npmjs.com/package/@ascentsparksoftware/react-image-editor)
13
+ [![downloads](https://img.shields.io/npm/dm/@ascentsparksoftware/react-image-editor.svg)](https://www.npmjs.com/package/@ascentsparksoftware/react-image-editor)
14
+ [![React 19](https://img.shields.io/badge/React-19-087ea4.svg)](https://react.dev)
15
+ [![license MIT](https://img.shields.io/github/license/ascentspark/react-image-editor?color=3b82f6)](https://github.com/ascentspark/react-image-editor/blob/main/LICENSE)
16
+
17
+ **[โœจ Features](#features)** &nbsp;ยท&nbsp;
18
+ **[๐ŸŽจ Theming](#theming)** &nbsp;ยท&nbsp;
19
+ **[๐Ÿงฉ API](#api)** &nbsp;ยท&nbsp;
20
+ **[โ–ฒ Next.js](#nextjs--ssr)** &nbsp;ยท&nbsp;
21
+ **[๐Ÿ”’ Security](#security)**
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ A full image editor you drop into a React app as a single component: crop, rotate, filters, draw,
28
+ text, shapes, freehand redaction, layers, and export to PNG / JPEG / WEBP / SVG / PDF / JSON. Put
29
+ `<ImageEditor>` on the page, pass your image and options as props, and read results back as a
30
+ `Blob`. It targets **React 19**, is StrictMode-safe, **React Compiler**-safe, and works under a
31
+ nonce CSP without `unsafe-eval`.
32
+
33
+ It is the React sibling of
34
+ [@ascentsparksoftware/angular-image-editor](https://github.com/ascentspark/angular-image-editor)
35
+ with **exact feature parity** โ€” both wrap the same framework-agnostic Fabric.js v7 engine.
36
+
37
+ It is built from scratch on **[Fabric.js v7](https://fabricjs.com/) (MIT)** โ€” a free, open-source
38
+ alternative to commercial editors such as Syncfusion, Pintura and Filerobot. No license keys, no
39
+ telemetry, no per-seat pricing. Fabric is the only heavyweight runtime dependency and it is
40
+ lazy-loaded, so it never weighs down your initial bundle.
41
+
42
+ > ## ๐Ÿ†“ In-browser AI background removal, free
43
+ >
44
+ > One-click background removal is a paid feature in most commercial editors. Here it runs **on-device
45
+ > for free** (MIT): the **Remove background** and **Cut out subject** tools run an ONNX model
46
+ > entirely in the browser โ€” no image ever leaves the page, no API key, no per-call cost.
47
+ > The heavy model packages live **in your app, not in this library** โ€” you opt in by installing them
48
+ > and passing a loader prop (`backgroundRemovalLoader={() => import('@imgly/background-removal')}`).
49
+ > If you don't, the rest of the editor is unaffected and the AI tools simply don't appear. There's
50
+ > also a dependency-free **Magic wand** (flood-fill erase) for clearing flat color regions by click.
51
+ > See [Optional heavy features](#optional-heavy-features-ai--heic).
52
+
53
+ ## Features
54
+
55
+ - **Four modes** โ€” `viewer` ยท `basic` ยท `advanced` ยท `full`, each with sensible default chrome and
56
+ tools.
57
+ - **Editing** โ€” crop (preset + custom CMS aspect ratios), rotate / straighten, flip, zoom / **pan**,
58
+ fine-tune adjustments (brightness / contrast / saturation / vibrance / hue / blur / โ€ฆ), filter
59
+ looks (B&W / sepia / invert / sharpen / tint), draw / pen, highlighter, **eraser**, shapes, arrows,
60
+ **text with web fonts**, freehand **redaction** (solid / blur / pixelate), frames, **background
61
+ color / gradient**, and undo / redo history.
62
+ - **Smart & AI tools** โ€” **Magic wand** flood-fill erase (no dependencies), plus in-browser AI
63
+ **Remove background** and **Cut out subject**.
64
+ - **Layers panel** (persistent companion) โ€” drag-to-reorder z-order, per-layer **lock**, show / hide,
65
+ **opacity**, inline **rename**, plus the object-ops on the selection: **group / ungroup**,
66
+ **align**, **duplicate**, delete, and multi-select (shift / โŒ˜ / ctrl). Available regardless of the
67
+ active tool.
68
+ - **Multiple images** โ€” replace the canvas image or **add an image as its own layer** to composite.
69
+ - **Precision** โ€” optional **rulers** with draggable, snapping **guides**, **snapping + alignment
70
+ guides** (magnet toggle), and an **artboard / output size** (presets + custom Wร—H) for exact-pixel
71
+ raster and PDF export.
72
+ - **Export** โ€” PNG / JPEG / WEBP / SVG / **PDF** (lazy `jspdf`) / JSON, plus engine
73
+ `exportScene` / `loadScene` for save-and-reload templates.
74
+ - **Usage-based UI** โ€” a grouped, Photoshop-style **flyout toolbar** (tools double as modes), a tool
75
+ **Options** panel that adapts to the selection, and top-bar **actions** (undo / redo, zoom, fit,
76
+ history, image, export).
77
+ - **Color anywhere** โ€” preset swatches **plus a custom color picker** for fill, draw, text, shapes,
78
+ frame and background.
79
+ - **Web fonts** โ€” a curated Google-font list plus an "add any Google font" search.
80
+ - **Correct compositing** โ€” the highlighter is genuinely translucent; **redact** bakes the
81
+ *composited* pixels under the region, concealing everything beneath, not just the base image.
82
+ - **Responsive** โ€” the editor adapts to **its own width** via CSS container queries (single column on
83
+ narrow hosts), so it works in a sidebar, a modal or full-page without horizontal overflow.
84
+ - **3-prop runtime theming** โ€” `baseColor` + `accentColor` + `themeMode` derive the whole palette as
85
+ scoped `--asp-*` CSS variables, with guaranteed WCAG **AA** contrast โ€” re-skin live, no remount.
86
+ - **Robust & accessible** โ€” oversized imports auto-downscale; recoverable failures surface via an
87
+ `onError` callback instead of throwing; keyboard-operable, `:focus-visible` rings,
88
+ `prefers-reduced-motion`. No `any` anywhere.
89
+
90
+ ## Install
91
+
92
+ ```bash
93
+ npm install @ascentsparksoftware/react-image-editor fabric
94
+ ```
95
+
96
+ - **Peer dependencies:** `react` and `react-dom` (`^19`).
97
+ - **Runtime dependency:** `fabric` `^7.4.0` (declared by the package; install it alongside). Fabric
98
+ is lazy-loaded on demand, so it never weighs down your initial bundle.
99
+ - **No WASM/ML in the default install.** The core editor's import graph contains only plain-JS
100
+ dynamic imports (`fabric`, `jspdf`), so it loads cleanly in any bundler/dev server out of the box โ€”
101
+ no `optimizeDeps` config, no cache dance. The AI / HEIC features are **opt-in** and live in your
102
+ app (see [Optional heavy features](#optional-heavy-features-ai--heic)). Without them, **Remove
103
+ background**, **Cut out subject**, and HEIC import are simply unavailable; everything else works.
104
+
105
+ ## Quick start
106
+
107
+ ```tsx
108
+ import { ImageEditor } from '@ascentsparksoftware/react-image-editor';
109
+ import '@ascentsparksoftware/react-image-editor/styles.css';
110
+
111
+ export function PhotoScreen() {
112
+ return (
113
+ <div style={{ height: 640 }}>
114
+ <ImageEditor
115
+ src="https://example.com/photo.jpg"
116
+ mode="advanced"
117
+ baseColor="#f4f6f9"
118
+ accentColor="#02375e"
119
+ themeMode="light"
120
+ onSaved={(blob) => {
121
+ /* upload or preview the edited image */
122
+ }}
123
+ />
124
+ </div>
125
+ );
126
+ }
127
+ ```
128
+
129
+ Import the stylesheet once (root layout / entry). Give the host a size โ€” the editor fills its
130
+ container. Either set the `width` / `height` props, or size the wrapper in CSS:
131
+
132
+ ```tsx
133
+ {/* props accept px numbers, %, vh, or any CSS length / calc() */}
134
+ <ImageEditor src={url} width="100%" height="70vh" />
135
+ ```
136
+
137
+ ## Next.js / SSR
138
+
139
+ The editor is client-only but **import-safe on the server** (nothing touches `window` at module
140
+ scope, and every component ships `"use client"`). In Next.js, load it lazily so the ~1.3 MB Fabric
141
+ chunk stays out of your initial bundle:
142
+
143
+ ```tsx
144
+ 'use client';
145
+ import dynamic from 'next/dynamic';
146
+
147
+ const ImageEditor = dynamic(
148
+ () => import('@ascentsparksoftware/react-image-editor').then((m) => m.ImageEditor),
149
+ { ssr: false },
150
+ );
151
+ ```
152
+
153
+ Works under React StrictMode (double-mount safe), with the React Compiler enabled, and under a
154
+ nonce-based CSP without `unsafe-eval`.
155
+
156
+ ## Modes
157
+
158
+ `mode` sets the chrome and a sensible default tool set:
159
+
160
+ | mode | chrome | default tools |
161
+ | --- | --- | --- |
162
+ | `viewer` | minimal: zoom + export | none (read-only) |
163
+ | `basic` | a compact card (also openable as a modal) | crop, rotate, flip, zoom |
164
+ | `advanced` *(default)* | full 3-column workspace | curated everyday set |
165
+ | `full` | full workspace | every tool + every filter |
166
+
167
+ ```tsx
168
+ <ImageEditor mode="viewer" src={url} />
169
+ <ImageEditor mode="full" src={url} />
170
+ ```
171
+
172
+ ## Tool configuration
173
+
174
+ Resolution order, from easiest to most precise:
175
+
176
+ 1. **`mode`** sets the default tool set.
177
+ 2. **`tools`** (if provided) is an explicit allowlist that *replaces* the mode default, in the order
178
+ you give.
179
+ 3. **`disabledTools`** is then subtracted from the result.
180
+
181
+ ```tsx
182
+ {/* advanced, but without filters or stickers */}
183
+ <ImageEditor mode="advanced" disabledTools={['filters', 'sticker']} />
184
+
185
+ {/* a bespoke rail (order preserved) */}
186
+ <ImageEditor tools={['crop', 'rotate', 'text']} />
187
+ ```
188
+
189
+ Filters follow the same idea via `filters`: an explicit `AspFilter[]`, the literal `'all'` (every
190
+ Fabric filter), or `null` for the mode default.
191
+
192
+ ```tsx
193
+ <ImageEditor mode="advanced" filters={['brightness', 'contrast', 'grayscale']} />
194
+ <ImageEditor mode="advanced" filters="all" />
195
+ ```
196
+
197
+ ## Smart & AI tools
198
+
199
+ | Tool | What it does | Dependency |
200
+ | --- | --- | --- |
201
+ | **Magic wand** (`magicwand`) | Click a flat color region to erase it to transparency; tolerance slider. | none |
202
+ | **Remove background** (`removebg`) | In-browser AI that replaces the base image's background with transparency. | optional |
203
+ | **Cut out subject** (`selectsubject`) | In-browser AI that extracts the subject onto its own layer. | optional |
204
+
205
+ The magic wand has no dependencies and works everywhere. The two **AI** tools run
206
+ `@imgly/background-removal` (an ONNX model, on-device โ€” no image data leaves the browser); HEIC/HEIF
207
+ import uses `heic2any`. Both are opt-in โ€” see below.
208
+
209
+ ### Optional heavy features (AI + HEIC)
210
+
211
+ `@imgly/background-removal` (with `onnxruntime-web`) and `heic2any` ship **WASM, web workers and deep
212
+ CJS graphs** that dev-server bundlers cannot pre-bundle reliably. So the core editor **does not
213
+ import them**. Instead you opt in by installing the package you want and passing a loader prop. The
214
+ heavy `import()` then lives in **your** bundle, where you own the config:
215
+
216
+ ```bash
217
+ # only if you want these features
218
+ npm install @imgly/background-removal onnxruntime-web # AI background removal / cut-out
219
+ npm install heic2any # HEIC/HEIF import
220
+ ```
221
+
222
+ ```tsx
223
+ <ImageEditor
224
+ src={url}
225
+ backgroundRemovalLoader={() => import('@imgly/background-removal')}
226
+ heicDecoderLoader={() => import('heic2any')}
227
+ />
228
+ ```
229
+
230
+ - Pass **only what you need** โ€” pass just `heicDecoderLoader` and the AI tools stay hidden, or
231
+ neither and you get the lean default editor.
232
+ - When a loader is absent, the matching tool is **removed from the rail** (and HEIC import throws a
233
+ clear, catchable error) โ€” nothing breaks.
234
+ - A progress bar and busy cursor show while the model loads on first AI use.
235
+
236
+ ## Theming
237
+
238
+ Three props derive the **entire** UI palette at runtime โ€” set them and the editor blends into your
239
+ brand in light or dark, with WCAG **AA** text contrast guaranteed (AAA for primary text), no extra
240
+ config:
241
+
242
+ - **`baseColor`** โ€” neutral anchor; surfaces, ink and borders are tinted toward its hue.
243
+ - **`accentColor`** โ€” interactive accent; buttons, active tool, focus ring, selection.
244
+ - **`themeMode`** โ€” `'light'` or `'dark'`.
245
+
246
+ The derived values are applied as scoped CSS custom properties on the editor root, so they never
247
+ clash with your page โ€” and changing the props re-skins the editor live, without a remount. For fine
248
+ control you can override any individual variable:
249
+
250
+ ```css
251
+ .asp-image-editor {
252
+ --asp-radius-md: 12px;
253
+ --asp-accent: #ff5a5f;
254
+ }
255
+ ```
256
+
257
+ <details><summary>All <code>--asp-*</code> tokens</summary>
258
+
259
+ | token | role |
260
+ | --- | --- |
261
+ | `--asp-bg` | app/backdrop base |
262
+ | `--asp-surface` | card / panel surface |
263
+ | `--asp-surface-2` | inset surface (inputs) |
264
+ | `--asp-surface-sunk` | sunk wells / hover, checkerboard cell |
265
+ | `--asp-ink` | primary text |
266
+ | `--asp-ink-700` | secondary text / icons |
267
+ | `--asp-ink-muted` | muted labels |
268
+ | `--asp-ink-faint` | disabled / faint |
269
+ | `--asp-line` | hairline borders |
270
+ | `--asp-line-strong` | control borders |
271
+ | `--asp-accent` | accent fill |
272
+ | `--asp-accent-ink` | on-accent text (AA) |
273
+ | `--asp-accent-hover` | accent hover |
274
+ | `--asp-accent-soft` | active-tool background |
275
+ | `--asp-accent-soft-ink` | text on soft accent (AA) |
276
+ | `--asp-ring` | focus ring |
277
+ | `--asp-scrim` | modal scrim |
278
+ | `--asp-success` / `--asp-warning` / `--asp-error` | status colors |
279
+ | `--asp-radius-sm` / `-md` / `-lg` / `-pill` | radii |
280
+ | `--asp-ctl-h` / `--asp-ctl-h-sm` | control heights |
281
+ | `--asp-font-mono` | numeric readouts |
282
+
283
+ You can also call the pure helper directly: `deriveTheme(baseColor, accentColor, mode)` returns the
284
+ full token map, and `applyTheme(element, tokens)` sets them.
285
+ </details>
286
+
287
+ ## Modal dialog
288
+
289
+ For avatar / quick-edit flows, open the `basic` editor as a modal and await the result:
290
+
291
+ ```tsx
292
+ import { openImageEditorDialog } from '@ascentsparksoftware/react-image-editor';
293
+
294
+ async function editAvatar(src: string): Promise<void> {
295
+ const blob = await openImageEditorDialog({
296
+ src,
297
+ heading: 'Update profile photo',
298
+ aspectPresets: ['1:1', '4:3', 'free'],
299
+ initialAspect: '1:1', // open already square & centered
300
+ });
301
+ if (blob) {
302
+ /* user saved โ€” upload `blob` */
303
+ } // else: user cancelled (close, scrim click, or Escape)
304
+ }
305
+ ```
306
+
307
+ **Starting crop.** Pass `initialAspect` (e.g. `'1:1'`) โ€” on `openImageEditorDialog()` or as the
308
+ `initialAspect` prop on `<ImageEditor>` โ€” to open already constrained to that ratio. If you omit it
309
+ and supply a single non-`free` preset (e.g. `aspectPresets: ['1:1']`), that ratio is auto-selected;
310
+ otherwise the crop opens unconstrained (`free`). In `basic` mode the crop frame is live immediately;
311
+ in `advanced`/`full` it becomes the crop tool's starting aspect.
312
+
313
+ ## API
314
+
315
+ | Prop | Type | Default | What it does |
316
+ | --- | --- | --- | --- |
317
+ | `src` | `string \| Blob \| null` | `null` (sample image) | Image to edit โ€” URL, `Blob`/`File`, or data URL |
318
+ | `mode` | `'viewer' \| 'basic' \| 'advanced' \| 'full'` | `'advanced'` | Layout + default tool set |
319
+ | `width` / `height` | `number \| string \| null` | fill container | px number, `%`, `vh`, any CSS length / `calc()` |
320
+ | `tools` | `AspTool[] \| null` | mode default | Explicit tool allowlist (order preserved) |
321
+ | `disabledTools` | `AspTool[]` | `[]` | Subtracted from the resolved set |
322
+ | `filters` | `AspFilter[] \| 'all' \| null` | mode default | Adjustments + looks offered |
323
+ | `aspectPresets` | `AspAspectPreset[]` | `['free','1:1','4:3','16:9']` | Crop preset chips |
324
+ | `initialAspect` | `AspAspectPreset \| null` | `null` | Open already cropped to this ratio |
325
+ | `aspectRatios` | `AspAspectOption[]` | `[]` | Custom CMS targets, e.g. `aspectOption(1200, 630)` |
326
+ | `exportFormats` | `AspExportFormat[]` | `['png','jpeg','webp']` | Offered formats (+ `'svg'`, `'pdf'`, `'json'`) |
327
+ | `exportQuality` | `number` | `90` | 10โ€“100 |
328
+ | `baseColor` | `string` | `'#f4f6f9'` | Theme neutral anchor |
329
+ | `accentColor` | `string` | `'#1f6feb'` | Theme accent |
330
+ | `themeMode` | `'light' \| 'dark'` | `'light'` | Theme mode |
331
+ | `heading` | `string` | `'Edit image'` | Basic-mode title |
332
+ | `showHistory` | `boolean` | `true` | Show the history panel |
333
+ | `keyboardEnabled` | `boolean` | `true` | Editor keyboard shortcuts |
334
+ | `fonts` | `FontOption[]` | `DEFAULT_FONTS` | Text font choices |
335
+ | `backgroundRemovalLoader` | `AspBackgroundRemovalLoader \| null` | `null` | Opt-in AI background removal |
336
+ | `heicDecoderLoader` | `AspHeicDecoderLoader \| null` | `null` | Opt-in HEIC/HEIF import |
337
+ | `onSaved` | `(blob: Blob) => void` | โ€” | Export / Save produced a Blob |
338
+ | `onCanceled` | `() => void` | โ€” | Basic-mode Cancel |
339
+ | `onImageLoaded` | `() => void` | โ€” | An image finished loading |
340
+ | `onExported` | `(blob: Blob) => void` | โ€” | Export download produced a Blob |
341
+ | `onError` | `(error: AspEditorError) => void` | โ€” | Recoverable load/export/init error |
342
+
343
+ ### Keyboard shortcuts
344
+
345
+ While the pointer is over the editor (and not typing in a field): **Ctrl/Cmd+Z** undo,
346
+ **Ctrl/Cmd+Shift+Z / Ctrl+Y** redo, **Delete/Backspace** remove selection, **Ctrl/Cmd+C / V / D**
347
+ copy / paste / duplicate, **Ctrl/Cmd+A** select all, **Esc** deselect (or cancel the basic modal),
348
+ **Space** (hold) to pan. Disable with `keyboardEnabled={false}`.
349
+
350
+ ### Headless engine
351
+
352
+ For headless or advanced use, `EditorEngine` (the Fabric wrapper) and the pure helpers
353
+ (`resolveTools`, `resolveFilters`, `deriveTheme`, `applyTheme`, `EditHistory`, `DeltaHistory`) are
354
+ all exported. `EditorEngine` exposes scene save/load (`exportScene` / `loadScene`), layers, guides,
355
+ artboard sizing and the AI/magic operations directly.
356
+
357
+ ## Accessibility
358
+
359
+ WCAG AA color contrast (derivation-guaranteed), keyboard-operable controls, `:focus-visible` rings,
360
+ `prefers-reduced-motion` honored. Run `axe` against the demo to verify in your own setup.
361
+
362
+ ## Security
363
+
364
+ This library renders consumer-supplied images and SVGs onto an HTML canvas; SVGs are rasterized
365
+ through a sandboxed `<img>` element, so scripts inside an SVG do not execute. Still treat
366
+ user-uploaded files as untrusted: validate type and size at your upload boundary, and sanitize any
367
+ text you overlay from untrusted sources. Exported blobs and PDFs reflect exactly what was drawn. The
368
+ optional AI tools download a model at runtime but never send your image data anywhere. It contains
369
+ no `eval` / `new Function` paths and injects no inline styles, so it runs under a nonce CSP without
370
+ `unsafe-eval`. Full policy and private reporting:
371
+ [SECURITY.md](https://github.com/ascentspark/react-image-editor/blob/main/SECURITY.md).
372
+
373
+ ## Versions
374
+
375
+ The package version is decoupled from framework majors: `1.x` targets `react@^19`. Within a line we
376
+ follow semver โ€” patches are backward-compatible fixes, minors add backward-compatible features. A
377
+ future React major that needs a breaking peer bump gets a new package major.
378
+
379
+ ## Documentation
380
+
381
+ A live playground and full docs for every tool, theming, and the complete API live in the demo app
382
+ (`apps/demo` โ€” `npm run dev -w demo`).
383
+
384
+ ## Local development
385
+
386
+ npm-workspaces monorepo โ€” library in `packages/react-image-editor`, demo in `apps/demo`,
387
+ Playwright e2e in `e2e/`:
388
+
389
+ ```bash
390
+ npm install
391
+ npm run dev -w demo # serve the demo (http://localhost:5173)
392
+ npm run build # build the library + demo
393
+ npm test # unit tests (Vitest)
394
+ npm run lint
395
+ npm run e2e # Playwright end-to-end + visual smoke
396
+ ```
397
+
398
+ Contributions are welcome โ€” see [CONTRIBUTING.md](CONTRIBUTING.md).
399
+
400
+ ## Help keep it healthy
401
+
402
+ We genuinely try to keep this library current, bug-free and secure, and the best way to get there is
403
+ together. If something breaks, please
404
+ [open an issue](https://github.com/ascentspark/react-image-editor/issues) with a minimal
405
+ reproduction; if you can fix a bug or add something useful, pull requests are very welcome, big or
406
+ small. An open-source library stays dependable only when people use it, tell us what's broken, and
407
+ pitch in now and then โ€” so thank you in advance for anything you send our way. ๐Ÿ’›
408
+
409
+ ## License
410
+
411
+ [MIT](./LICENSE), by [Ascentspark](https://ascentspark.com).