img-upload-kit 0.0.0-stage → 2.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/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +192 -0
- package/README.md +242 -2
- package/dist/bin.js +1186 -0
- package/dist/bin.js.map +1 -0
- package/dist/blur.cjs +41 -0
- package/dist/blur.cjs.map +1 -0
- package/dist/blur.d.cts +17 -0
- package/dist/blur.d.ts +17 -0
- package/dist/blur.js +41 -0
- package/dist/blur.js.map +1 -0
- package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
- package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
- package/dist/blurService-D5TpMNji.d.ts +83 -0
- package/dist/blurService-DacwylJx.d.cts +83 -0
- package/dist/chunk-2OVNXHTN.cjs +514 -0
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/chunk-3ICG2SLL.cjs +135 -0
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/chunk-4ASFG3KB.js +514 -0
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/chunk-AI3JJLSA.js +2370 -0
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/chunk-CV23O4YU.js +145 -0
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/chunk-G7LLSKBV.js +158 -0
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/chunk-IB3TEDLR.js +294 -0
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/chunk-KJVYWKIG.cjs +158 -0
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/chunk-NEDBIIE3.cjs +226 -0
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs +37 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -0
- package/dist/chunk-QQHZJKNA.js +135 -0
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/chunk-S2IFAWJK.js +37 -0
- package/dist/chunk-S2IFAWJK.js.map +1 -0
- package/dist/chunk-TT5C7K3Q.js +226 -0
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/chunk-U2TWTEBY.cjs +2370 -0
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/chunk-WISG74VL.cjs +294 -0
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/chunk-YRXXQUDE.cjs +145 -0
- package/dist/chunk-YRXXQUDE.cjs.map +1 -0
- package/dist/cli.cjs +1241 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +99 -0
- package/dist/cli.d.ts +99 -0
- package/dist/cli.js +1194 -0
- package/dist/cli.js.map +1 -0
- package/dist/headless.cjs +27 -0
- package/dist/headless.cjs.map +1 -0
- package/dist/headless.d.cts +4 -0
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +27 -0
- package/dist/headless.js.map +1 -0
- package/dist/index-BIdnqtW8.d.ts +234 -0
- package/dist/index-Brk_uHs6.d.cts +234 -0
- package/dist/index-CPMlaQsC.d.ts +635 -0
- package/dist/index-DHINe-rA.d.cts +635 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/memoryAdapter-Ck0ccZbF.d.cts +25 -0
- package/dist/memoryAdapter-DxEKaG0b.d.ts +25 -0
- package/dist/placeholders-C4ftwaII.d.ts +5 -0
- package/dist/placeholders-x_3rt65M.d.cts +5 -0
- package/dist/react.cjs +3779 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +476 -0
- package/dist/react.d.ts +476 -0
- package/dist/react.js +3779 -0
- package/dist/react.js.map +1 -0
- package/dist/server-handler.cjs +14 -0
- package/dist/server-handler.cjs.map +1 -0
- package/dist/server-handler.d.cts +3 -0
- package/dist/server-handler.d.ts +3 -0
- package/dist/server-handler.js +14 -0
- package/dist/server-handler.js.map +1 -0
- package/dist/server.cjs +1801 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +142 -0
- package/dist/server.d.ts +142 -0
- package/dist/server.js +1801 -0
- package/dist/server.js.map +1 -0
- package/dist/storage.cjs +447 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +163 -0
- package/dist/storage.d.ts +163 -0
- package/dist/storage.js +447 -0
- package/dist/storage.js.map +1 -0
- package/dist/styles/animated-image.css +112 -0
- package/dist/styles/dropzone-and-tiles.css +247 -0
- package/dist/styles/modals-and-dock.css +248 -0
- package/dist/styles/primitives.css +197 -0
- package/dist/styles/quality-and-inputs.css +184 -0
- package/dist/styles/tokens.css +123 -0
- package/dist/styles.css +1122 -0
- package/dist/types-BrP-o9nd.d.cts +85 -0
- package/dist/types-sZ3KoFQY.d.ts +85 -0
- package/dist/types-yShGNQnW.d.cts +61 -0
- package/dist/types-yShGNQnW.d.ts +61 -0
- package/dist/upload-C5Trd6Dh.d.cts +154 -0
- package/dist/upload-C5Trd6Dh.d.ts +154 -0
- package/package.json +198 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `img-upload-kit` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0.0] - 2026-11-06
|
|
9
|
+
|
|
10
|
+
See [`MIGRATION.md`](./MIGRATION.md) for step-by-step v1.x to v2.0.0 upgrade instructions and before/after examples.
|
|
11
|
+
|
|
12
|
+
### Breaking Changes
|
|
13
|
+
|
|
14
|
+
- **Dependencies**: Moved `sharp` and `lqip-modern` to optional `peerDependencies` (loaded lazily in `img-upload-kit/server` and `img-upload-kit/cli` with actionable install errors). Removed `lucide-react` in favor of zero-dependency inlined SVG icons (`src/react/icons.tsx`).
|
|
15
|
+
- **SVG Disabled by Default**: Removed `image/svg+xml` from `DEFAULT_ALLOWED_FORMATS` and all `STANDARD_PRESETS`. Enable via `allowSvg: true` on `createImageUploader`, a preset, or per-upload options.
|
|
16
|
+
- **Secure-by-Default HTTP Handler**: `createUploadHttpHandler` now ignores client `FormData` overrides (`customFilename`, `folder`, `maxWidth`, `maxHeight`, `outputFormat`, `quality`) unless explicitly opted in via `allowClientOverrides`.
|
|
17
|
+
- **Context-Aware `authorize(ctx)`**: `UploadHttpHandlerOptions.authorize` now receives `AuthorizeContext` (`{ action, request, path?, bucket?, preset? }`).
|
|
18
|
+
- **`<AnimatedImage>` Reveal Parity & Error State**: Default `animationDuration` changed from `700` ms to `600` ms with `scale(1.02 -> 1)` settle reveal (`revealScale`, default `1.02`), `inset: 0; scale(1.1); blur(20px)` placeholder geometry, immediate `5/6` placeholder fade, and `colorPlaceholder="flat"` default for color-only placeholders. On load error, the placeholder remains visible while the broken image is hidden (`data-state="error"`).
|
|
19
|
+
- **Storage, Blur & CLI Contracts**: Removed phantom `-blur.avif` / `-thumb.avif` deletes in Supabase `deleteByUrl`; made `?t=` preview cache-busting opt-in (`appendCacheBuster`); excluded 40px LQIP data URLs from `createSrcSetWithBlur`; preserved `/<bucket>/<path>` in `normalizeImagePath` for Supabase URLs; made `mirrorBufferLocally` async (`Promise<string>`); moved CLI auto-execution to `dist/bin.js`.
|
|
20
|
+
- **Package `exports`**: Updated `package.json` `exports` to nested `import` / `require` conditions with `.d.ts` and `.d.cts` types.
|
|
21
|
+
|
|
22
|
+
### Security
|
|
23
|
+
|
|
24
|
+
- **Binary Magic-Byte Sniffing (`sniffImageFormat`)**: Verified binary signatures (JPEG, PNG, GIF, WebP, AVIF/HEIF `ftyp`, TIFF, SVG) against `allowedFormats` before Sharp decoding, blocking spoofed MIME types.
|
|
25
|
+
- **Built-in SVG Sanitizer (`sanitizeSvg`)**: Dependency-free allowlist sanitizer when `allowSvg: true` that strips `<script>`, `<foreignObject>`, `<iframe|embed|object>`, `on*` handlers, unsafe `href`/`xlink:href` targets, and `<!ENTITY`/`DOCTYPE` declarations.
|
|
26
|
+
- **HTTP Handler Guards**: Added streaming `maxBodyBytes` enforcement (HTTP `413`), `allowedOrigins` CSRF validation (HTTP `403`), `DELETE` bucket allowlist enforcement (HTTP `403`), client filename extension enforcement + non-upsert collision suffixing, generic HTTP `500` error bodies with `onError(error, ctx)`, and `limitInputPixels` (default 50 MP) decompression-bomb protection.
|
|
27
|
+
- **Local Filesystem Containment**: Hardened `createLocalFsStorageAdapter` with `path.relative` traversal checks, Windows drive/colon rejection, and atomic `flag: 'wx'` writes when `upsert: false`.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Responsive Image Variants (`variantWidths`)**: Generate multi-width responsive variants (`<base>-<w>w.<ext>`) in parallel, returning `variants: ImageVariant[]` and `srcSet` on `UploadedImageData` and cleaning up all variants in `deleteImage`. `<AnimatedImage image={data} />` automatically wires `srcSet` and default `sizes="100vw"` when `fill` is set.
|
|
32
|
+
- **Clipboard Paste & Mobile Camera Capture**: Added `enablePaste` (default `true`), `pasteTarget: 'dropzone' | 'document'`, and `capture?: 'user' | 'environment'` across `<SingleImageUpload>`, `<MultiImageUpload>`, and headless controllers (`useClipboardPaste`).
|
|
33
|
+
- **Per-Tile Cancel & Retry**: Added per-upload `AbortController` cancellation (`cancel(id)` / tile Cancel button) and per-tile or batch retry (`retry(id)` / `retryAll()`), plus `onUploadStart`, `onUploadComplete`, and `onUploadCancel` callbacks.
|
|
34
|
+
- **Form Integration (`name`, `formValue`, `ref`, `required`, `minImages`)**: Synchronized `<input type="hidden" name={name} />` (`formValue: 'url' | 'json'`), `forwardRef<ImageUploadHandle>` (`{ focus, open, clear }`), `onBlur`, and native constraint validation for `required` and `minImages`.
|
|
35
|
+
- **`<AnimatedImage>` Props**: Added first-class `priority` and `quality` props (preventing Next.js `loading="lazy"` conflicts), `revealScale`, `colorPlaceholder: 'flat' | 'gradient'`, `neutralColors`, `fallbackSrc` (single automatic retry), `renderError`, and `var(--token)` support in `resolveBackgroundColor`.
|
|
36
|
+
- **Edge-Safe Handler & Modular CSS Exports**: Added `img-upload-kit/server/handler` (zero Sharp/`node:fs` imports), `img-upload-kit/styles/*.css` layer exports, and `scripts/check-dist.mjs` build verification.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **RSC `'use client'` & Shared Singletons**: Preserved `'use client'` directives at the top of all client chunks and deduplicated `ImageUploadContext` and `blurService` across `img-upload-kit/react`, `img-upload-kit/headless`, and `img-upload-kit/blur`.
|
|
41
|
+
- **Server & Storage Correctness**: Fixed passthrough EXIF `.rotate()` and dimension swapping (EXIF 5–8), preserved GIF/TIFF/HEIF format labels, held `AsyncSemaphore` slots until timed-out Sharp jobs settle, rolled back uploaded storage objects when `onUploadRecord` throws, added S3 `IfNoneMatch: '*'` for `upsert: false`, and paginated S3/Supabase `list()`.
|
|
42
|
+
- **React State, Memory & Accessibility**: Fixed parallel upload progress races (`uploadTracker`), stale alt text on replacement, `URL.createObjectURL` preview cleanup, modal focus trapping/restoration (`useDialog`), WAI-ARIA radio keyboard navigation in `<QualitySelector>`, `role="progressbar"` attributes, and `aria-live="polite"` upload announcements.
|
|
43
|
+
|
|
44
|
+
### Performance
|
|
45
|
+
|
|
46
|
+
- **Single-Decode Blur Pipeline**: `generateBlurPlaceholder` now decodes the source image once into a 128 px buffer and derives the WebP LQIP data URL, 4-edge dominant color histogram, and BlurHash from the shared buffer (reducing Sharp decodes from 3–4 to 1).
|
|
47
|
+
- **Web Crypto & Object URLs**: Replaced `FileReader.readAsDataURL` client previews with zero-copy `URL.createObjectURL` and migrated server hashing to `globalThis.crypto`.
|
|
48
|
+
|
|
49
|
+
## [1.1.0] - 2026-10-31
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **AnimatedImage Component (`img-upload-kit/react`)**:
|
|
54
|
+
- High-performance progressive blur-up display component with GPU-accelerated crossfade.
|
|
55
|
+
- Native `<img>` attribute pass-through and framework-agnostic `as` prop (e.g. `as={Image}` for Next.js with `imageComponentProps`).
|
|
56
|
+
- Hierarchical placeholder fallback cascade: `blurDataUrl` (LQIP) > `blurHash` (client canvas decode) > `blurColor` (gradient SVG).
|
|
57
|
+
- Shimmer highlight animation (`showShimmer`), staggered transition timing, and full `prefers-reduced-motion` compliance.
|
|
58
|
+
- Semantic slot targeting (`data-slot`, `data-state`, `aria-busy`) and unstyled mode support.
|
|
59
|
+
- **Headless Reveal State Machine (`useImageReveal`)**:
|
|
60
|
+
- Headless hook managing progressive reveal states (`isLoaded`, `showPlaceholder`, `hasError`, `imgRef`, `handleLoad`, `handleError`).
|
|
61
|
+
- Guarantees pre-decode verification via `HTMLImageElement.decode()` before revealing sharp asset.
|
|
62
|
+
- Synchronous reset on `srcKey` changes and automatic detection of pre-cached images.
|
|
63
|
+
- **Placeholder Utilities**:
|
|
64
|
+
- `createFallbackBlurSvg`: Generates lightweight inline SVG gradients derived from dominant color.
|
|
65
|
+
- `resolveMeaningfulColor`: Filters out generic neutral/black/white database defaults and validates color tokens.
|
|
66
|
+
- `resolvePlaceholderUrl`: Deterministically selects the best available placeholder asset.
|
|
67
|
+
|
|
68
|
+
### Changed & Hardened
|
|
69
|
+
|
|
70
|
+
- **Server Validation Error Mapping**: Updated `createUploadHttpHandler.POST` to map client validation errors directly to HTTP 400 Bad Request.
|
|
71
|
+
- **Collision Retry Discrimination**: Fixed collision detection in `uploadRetry.ts` to strictly match `already exists` and `EEXIST` error codes.
|
|
72
|
+
- **Sharp Mobile EXIF Auto-Rotation**: Added `.rotate()` during Sharp pipeline initialization in `imageOptimizer.ts`.
|
|
73
|
+
- **Transparent PNG Color Extraction**: Hardened `colorHistogram.ts` to ignore pixels with `alpha < 8`.
|
|
74
|
+
- **BlurHash & SVG Detection**: Enhanced SVG validation in `blurGenerator.ts` and clamped BlurHash components to valid ranges (1–9).
|
|
75
|
+
- **Storage Parity & Upsert Semantics**: Implemented `deleteByUrl` in `createLocalFsStorageAdapter` and aligned default `upsert` semantics to `true`.
|
|
76
|
+
- **React Pure State Updaters & Controlled Value Sync**: Refactored `useMultiImageState.ts` and `useSingleImageState.ts` for strict React 18/19 concurrent rendering purity.
|
|
77
|
+
- **Batch Preemption Clearing in Multi-Upload**: Fixed retention window in `MultiImageUpload.tsx` when preempted by a new upload batch.
|
|
78
|
+
|
|
79
|
+
## [1.0.0] - 2026-10-31
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **Core Types & Utilities (`img-upload-kit`)**: Universal visual role presets (`thumbnail`, `avatar`, `card`, `cover`, `hero`, `logo`, `original`), `defineUploadPresets`, `UploadKitRegister`, bilingual (`en` / `ar`) validation, byte formatters, and RGB/HEX color utilities.
|
|
84
|
+
- **Server Optimization & Upload Pipeline (`img-upload-kit/server`)**: `createImageUploader`, `createUploadHttpHandler`, `optimizeImage` with Sharp AVIF size-guarding, and `generateBlurPlaceholder` (WebP LQIP, BlurHash, 4-edge color histogram).
|
|
85
|
+
- **Pluggable Storage Adapters (`img-upload-kit/storage`)**: `createSupabaseStorageAdapter`, `createS3StorageAdapter`, `createLocalFsStorageAdapter`, `createMemoryStorageAdapter`, and `onUploadRecord`.
|
|
86
|
+
- **Blur Manifest Service (`img-upload-kit/blur`)**: `BlurService` and `createBlurService` with bounded `LRUCache`.
|
|
87
|
+
- **Pure React UI & Styles (`img-upload-kit/react`, `img-upload-kit/styles.css`)**: `<ImageUploadProvider>`, `<ImageUpload>`, `<SingleImageUpload>`, `<MultiImageUpload>`, `<BlurImage>`, and `<QualitySelector>` with RTL & `--iuk-*` tokens.
|
|
88
|
+
- **Headless Hooks & Primitives (`img-upload-kit/headless`)**: `HeadlessSingleUpload`, `HeadlessMultiUpload`, `useSingleUpload`, `useMultiUpload`, `useImageUpload`, `useDropzone`, `useFilePreview`.
|
|
89
|
+
- **Blur Manifest CLI (`img-upload-kit/cli`)**: `img-upload-kit` / `blur-cli` binaries (`generate`, `validate`, `sync`, `cleanup`, `health`, `inspect`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mo Elsherbiny (https://www.buildwithsherbo.com/)
|
|
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/MIGRATION.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Migrating from v1.x to v2.0.0
|
|
2
|
+
|
|
3
|
+
`img-upload-kit` v2.0.0 hardens security by default, aligns `<AnimatedImage>` with production motion standards, fixes RSC `'use client'` bundling and shared React contexts, and introduces responsive variants, clipboard paste, camera capture, per-tile cancel/retry, and native form integration.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Dependencies & Peer Dependencies (`sharp`, `lqip-modern`, `lucide-react`)
|
|
8
|
+
|
|
9
|
+
- **`sharp` & `lqip-modern` are now optional peer dependencies.** Client-only or Edge consumers no longer download native Sharp binaries when installing `img-upload-kit`. Server (`img-upload-kit/server`) and CLI (`img-upload-kit/cli`) consumers must install `sharp` (and optionally `lqip-modern`).
|
|
10
|
+
- **`lucide-react` has been removed.** All UI icons are now inlined inside `img-upload-kit/react` (`src/react/icons.tsx`) with zero external icon dependencies.
|
|
11
|
+
- **Node.js `>= 20.0.0`** is recommended for development and server runtimes.
|
|
12
|
+
|
|
13
|
+
### Before (v1.x)
|
|
14
|
+
```bash
|
|
15
|
+
npm install img-upload-kit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### After (v2.0.0)
|
|
19
|
+
```bash
|
|
20
|
+
# Client-only / React UI:
|
|
21
|
+
npm install img-upload-kit react react-dom
|
|
22
|
+
|
|
23
|
+
# Server / Full-Stack (Next.js, Remix, Hono, Node.js) or CLI:
|
|
24
|
+
npm install img-upload-kit sharp lqip-modern
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 2. SVG Uploads Disabled by Default (`allowSvg`) & Magic-Byte Sniffing
|
|
30
|
+
|
|
31
|
+
- **SVG (`image/svg+xml`) is disabled by default** across `DEFAULT_ALLOWED_FORMATS` and all `STANDARD_PRESETS` (including `logo`) to prevent stored XSS and XXE out of the box.
|
|
32
|
+
- **Opt-in with `allowSvg: true`** on `createImageUploader`, a preset, or a per-upload call. When enabled, SVGs pass through a built-in dependency-free allowlist sanitizer (`sanitizeSvg`) that strips `<script>`, `<foreignObject>`, `on*` handlers, unsafe `href`/`xlink:href` URIs, and `<!ENTITY` declarations.
|
|
33
|
+
- **Mandatory magic-byte sniffing (`sniffImageFormat`)**: Every upload inspects binary magic bytes rather than trusting `file.type`. Spoofed files (e.g. SVG or HTML disguised as `image/png`) are rejected with `UploadValidationError` (HTTP `400`).
|
|
34
|
+
|
|
35
|
+
### Before (v1.x)
|
|
36
|
+
```ts
|
|
37
|
+
const uploader = createImageUploader({ storage });
|
|
38
|
+
// SVG was accepted on card, cover, hero, logo, original without sanitization
|
|
39
|
+
await uploader.uploadImage(svgFile, { preset: 'logo' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### After (v2.0.0)
|
|
43
|
+
```ts
|
|
44
|
+
const uploader = createImageUploader({
|
|
45
|
+
storage,
|
|
46
|
+
allowSvg: false, // Default — opt in globally or per preset:
|
|
47
|
+
presets: defineUploadPresets({
|
|
48
|
+
logo: {
|
|
49
|
+
folder: 'logos',
|
|
50
|
+
maxDimensions: { width: 800, height: 800 },
|
|
51
|
+
allowSvg: true, // Sanitizes SVG before storage
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. Secure-by-Default HTTP Handler (`allowClientOverrides`, `maxBodyBytes`, `allowedOrigins`, `onError`)
|
|
60
|
+
|
|
61
|
+
`createUploadHttpHandler` now ignores client-supplied `FormData` overrides (`customFilename`, `folder`, `maxWidth`, `maxHeight`, `outputFormat`, `quality`) unless explicitly enabled via `allowClientOverrides`:
|
|
62
|
+
|
|
63
|
+
- **Client overrides**: Clamped to preset `maxDimensions`. Client `customFilename` never forces `upsert: true` (preventing arbitrary object overwrites) and always uses the optimized output extension. Invalid `quality` values return HTTP `400`.
|
|
64
|
+
- **Body size & CSRF protection**: Enforces `maxBodyBytes` (streaming byte limit, HTTP `413`) and `allowedOrigins` (HTTP `403`).
|
|
65
|
+
- **`DELETE` bucket allowlist**: Rejects attempts to delete URLs/buckets outside the uploader's configured buckets (HTTP `403`).
|
|
66
|
+
- **Generic `500` responses**: Internal error messages and paths are never leaked to clients (`{ "error": "Internal server error" }`); inspect raw errors with `onError(error, ctx)`.
|
|
67
|
+
- **Edge-safe handler subpath**: Import `createUploadHttpHandler` from `img-upload-kit/server/handler` when running in Edge runtimes without Sharp or `node:fs`.
|
|
68
|
+
|
|
69
|
+
### Before (v1.x)
|
|
70
|
+
```ts
|
|
71
|
+
// Client FormData could freely override folder, customFilename, maxWidth, quality
|
|
72
|
+
export const { POST, DELETE } = createUploadHttpHandler({ uploader });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### After (v2.0.0)
|
|
76
|
+
```ts
|
|
77
|
+
export const { POST, DELETE } = createUploadHttpHandler({
|
|
78
|
+
uploader,
|
|
79
|
+
maxBodyBytes: 15 * 1024 * 1024,
|
|
80
|
+
allowedOrigins: ['https://app.example.com'],
|
|
81
|
+
allowClientOverrides: {
|
|
82
|
+
filename: true,
|
|
83
|
+
folder: (folder) => (folder.startsWith('users/') ? folder : null),
|
|
84
|
+
dimensions: true, // Clamped to preset maxDimensions
|
|
85
|
+
format: true,
|
|
86
|
+
quality: true,
|
|
87
|
+
},
|
|
88
|
+
onError: (error, ctx) => {
|
|
89
|
+
console.error(`[upload:${ctx.action}]`, error);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. Context-Aware `authorize(ctx)` Signature
|
|
97
|
+
|
|
98
|
+
`UploadHttpHandlerOptions.authorize` now receives an `AuthorizeContext` object (`{ action, request, path?, bucket?, preset? }`) instead of a bare `Request`. Throwing inside `authorize` now returns HTTP `401 Unauthorized` instead of `500`.
|
|
99
|
+
|
|
100
|
+
### Before (v1.x)
|
|
101
|
+
```ts
|
|
102
|
+
const handler = createUploadHttpHandler({
|
|
103
|
+
uploader,
|
|
104
|
+
authorize: async (request: Request) => {
|
|
105
|
+
return request.headers.get('authorization') === 'Bearer secret';
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### After (v2.0.0)
|
|
111
|
+
```ts
|
|
112
|
+
const handler = createUploadHttpHandler({
|
|
113
|
+
uploader,
|
|
114
|
+
authorize: async ({ action, request, preset, bucket, path }) => {
|
|
115
|
+
if (action === 'delete' && bucket !== 'media') return false;
|
|
116
|
+
return request.headers.get('authorization') === 'Bearer secret';
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 5. `<AnimatedImage>` Visual Defaults, `priority`, & Error Handling
|
|
124
|
+
|
|
125
|
+
1. **Reveal animation & timing**: Default `animationDuration` changed from `700` ms to `600` ms. The image now transitions from `opacity: 0; transform: scale(1.02)` to `opacity: 1; transform: scale(1)` (`revealScale` prop, default `1.02`). The placeholder uses `inset: 0; scale(1.1); blur(20px)`, fades immediately (`0ms` delay) over `duration * 5 / 6` (`500ms`), and unmounts at `duration + 50ms`.
|
|
126
|
+
2. **Color-only placeholder (`colorPlaceholder`)**: When only `blurColor` is provided (no `blurDataUrl` or `blurHash`), `<AnimatedImage>` defaults to `colorPlaceholder="flat"` (paints wrapper `backgroundColor` without mounting an extra SVG `<img>`). Pass `colorPlaceholder="gradient"` for the v1 SVG layer. Wrapper backgrounds also accept `var(--css-token)`.
|
|
127
|
+
3. **First-class `priority` & `quality`**: Passing `priority` sets `loading="eager"` + `fetchPriority="high"` on native `<img>` and forwards `priority` to custom `as={Image}` components without conflicting with `loading="lazy"`.
|
|
128
|
+
4. **Placeholder-preserving error state**: On load error, the placeholder remains visible, the broken `<img>` is hidden (`visibility: hidden`, `data-state="error"`), `fallbackSrc` is retried once if provided, and `renderError={({ src, retry }) => ...}` renders custom recovery UI.
|
|
129
|
+
|
|
130
|
+
### Before (v1.x)
|
|
131
|
+
```tsx
|
|
132
|
+
// Next.js threw when passing priority via imageComponentProps due to forced loading="lazy"
|
|
133
|
+
<AnimatedImage
|
|
134
|
+
image={img}
|
|
135
|
+
as={Image}
|
|
136
|
+
imageComponentProps={{ priority: true, quality: 85 }}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### After (v2.0.0)
|
|
141
|
+
```tsx
|
|
142
|
+
// First-class priority & quality props, fallbackSrc, and optional v1 fade restore:
|
|
143
|
+
<AnimatedImage
|
|
144
|
+
image={img}
|
|
145
|
+
as={Image}
|
|
146
|
+
priority
|
|
147
|
+
quality={85}
|
|
148
|
+
fallbackSrc="/images/fallback.webp"
|
|
149
|
+
renderError={({ retry }) => <button onClick={retry}>Retry</button>}
|
|
150
|
+
// Optional: restore v1 pure opacity fade & SVG color gradient
|
|
151
|
+
animationDuration={700}
|
|
152
|
+
revealScale={1}
|
|
153
|
+
colorPlaceholder="gradient"
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 6. Storage, Preview Cache-Busting, Blur & CLI Updates
|
|
160
|
+
|
|
161
|
+
- **Supabase `deleteByUrl`**: No longer issues speculative deletes for non-existent `-blur.avif` / `-thumb.avif` sibling paths.
|
|
162
|
+
- **Opt-in `?t=` preview cache-buster**: `<SingleImageUpload>` and `<PreviewTile>` no longer append `?t=...` by default (which previously invalidated pre-signed S3/R2/GCS URLs). Opt in with `<PreviewTile appendCacheBuster />` or `appendCacheBuster(url, { enabled: true })` (signed URLs are always skipped).
|
|
163
|
+
- **`createSrcSetWithBlur` & `preloadImageWithBlur`**: `createSrcSetWithBlur` no longer injects the 40px LQIP data URL into `srcset`, and `preloadImageWithBlur` deduplicates `<link rel="preload">` tags without overriding `imagesrcset`.
|
|
164
|
+
- **`normalizeImagePath`**: Preserves `/<bucket>/<path>` for Supabase public URLs consistently with S3 path-style URLs.
|
|
165
|
+
- **`mirrorBufferLocally` & CLI entry**: `mirrorBufferLocally` is now async (`Promise<string>`). Importing `img-upload-kit/cli` has zero side effects (the CLI binary entry moved to `dist/bin.js`).
|
|
166
|
+
|
|
167
|
+
### Before (v1.x)
|
|
168
|
+
```tsx
|
|
169
|
+
// Mutated preview URLs with ?t=..., breaking X-Amz-Signature on presigned URLs
|
|
170
|
+
<PreviewTile preview={item} onRemove={onRemove} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### After (v2.0.0)
|
|
174
|
+
```tsx
|
|
175
|
+
// Preserves URLs verbatim by default; opt in when overwriting static paths:
|
|
176
|
+
<PreviewTile preview={item} onRemove={onRemove} appendCacheBuster />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 7. Package `exports` & Per-Layer CSS Subpaths
|
|
182
|
+
|
|
183
|
+
`package.json` `exports` now use nested `import` / `require` conditions with dedicated `.d.ts` and `.d.cts` declarations, and expose individual CSS layers under `img-upload-kit/styles/*.css`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Full stylesheet (unchanged):
|
|
187
|
+
import 'img-upload-kit/styles.css';
|
|
188
|
+
|
|
189
|
+
// Or import individual CSS layers in v2.0:
|
|
190
|
+
import 'img-upload-kit/styles/tokens.css';
|
|
191
|
+
import 'img-upload-kit/styles/animated-image.css';
|
|
192
|
+
```
|
package/README.md
CHANGED
|
@@ -1,3 +1,243 @@
|
|
|
1
|
-
#
|
|
1
|
+
# img-upload-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Production-grade, zero-lock-in image upload and processing framework with **Sharp AVIF/WebP optimization**, **smart size-guarding**, **responsive `srcSet` variants**, **WebP LQIP & BlurHash placeholders**, **4-edge dominant color extraction**, **pluggable storage adapters**, **4 styling tiers**, and the **`<AnimatedImage />` blur-up display component** for React & Next.js.
|
|
4
|
+
|
|
5
|
+
[](https://upload-kit.buildwithsherbo.com)
|
|
6
|
+
[](https://www.npmjs.com/package/img-upload-kit)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://github.com/MoElsherbiny/image-upload-kit-studio)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Interactive Studio & Live Demo
|
|
13
|
+
|
|
14
|
+
Experience the full pipeline live in your browser: 👉 **[upload-kit.buildwithsherbo.com](https://upload-kit.buildwithsherbo.com)**
|
|
15
|
+
|
|
16
|
+
- **Watch Code Stream Live**: Synchronized Shiki IDE writing `<AnimatedImage />`, upload route, and UI code while the live preview mutates at 60 FPS.
|
|
17
|
+
- **4-Layer Placeholder Lab**: Side-by-side comparison of **4-Edge Ambient Color** (0 B runtime), **Client BlurHash** (~28 chars), **WebP LQIP Data URL** (<1 KB), and the final **Sharp AVIF/WebP** asset.
|
|
18
|
+
- **Sharp Size-Guard Sandbox**: Drop or paste any image to inspect real-time byte savings, responsive `srcSet` variants, dimensions, and color extraction.
|
|
19
|
+
- **Upgrading from v1.x?** Read the [**v2.0 Migration Guide (`MIGRATION.md`)**](./MIGRATION.md).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Client / React UI only (zero native dependencies):
|
|
27
|
+
npm install img-upload-kit react react-dom
|
|
28
|
+
|
|
29
|
+
# Server / Full-Stack (Next.js, Remix, Hono, SvelteKit, Node) or CLI:
|
|
30
|
+
npm install img-upload-kit sharp lqip-modern
|
|
31
|
+
|
|
32
|
+
# pnpm / yarn / bun:
|
|
33
|
+
pnpm add img-upload-kit sharp lqip-modern
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> **Requirements**: Node.js `>= 20.0.0` on the server for `sharp` and Web Fetch `Request`/`FormData` APIs. React `>= 18.0.0` for UI components (`lucide-react` is no longer required — all icons are inlined).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Subpath Exports Reference
|
|
41
|
+
|
|
42
|
+
| Subpath | Environment | Key Exports |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `img-upload-kit` | Universal | `STANDARD_PRESETS`, `defineUploadPresets`, types, color utils, validators, `createMemoryStorageAdapter`, `decodeBlurHashToDataURL`, `getBlurData` |
|
|
45
|
+
| `img-upload-kit/server` | Node.js | `createImageUploader`, `createUploadHttpHandler`, `optimizeImage`, `generateBlurPlaceholder`, `sniffImageFormat`, `sanitizeSvg` |
|
|
46
|
+
| `img-upload-kit/server/handler` | Node / Edge | Edge-safe `createUploadHttpHandler` (zero Sharp or `node:fs` imports at module load) |
|
|
47
|
+
| `img-upload-kit/storage` | Node / Edge | `createStorageAdapter`, `createS3StorageAdapter`, `createSupabaseStorageAdapter`, `createLocalFsStorageAdapter` |
|
|
48
|
+
| `img-upload-kit/blur` | Universal | `BlurService`, `createBlurService`, `decodeBlurHashToDataURL`, `LRUCache` |
|
|
49
|
+
| `img-upload-kit/react` | React Client | `<ImageUploadProvider>`, `<SingleImageUpload>`, `<MultiImageUpload>`, `<AnimatedImage>`, `<BlurImage>` |
|
|
50
|
+
| `img-upload-kit/headless` | React Client | `<HeadlessSingleUpload>`, `<HeadlessMultiUpload>`, `useSingleUpload`, `useMultiUpload`, `useDropzone` |
|
|
51
|
+
| `img-upload-kit/cli` | Node CLI | CLI binaries (`img-upload-kit` / `blur-cli`) for build-time manifest generation |
|
|
52
|
+
| `img-upload-kit/styles.css` | CSS | Complete stylesheet (`--iuk-*` tokens, dark mode, RTL) + per-layer `img-upload-kit/styles/*.css` |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Universal Presets & Responsive Variants (`variantWidths`)
|
|
57
|
+
|
|
58
|
+
Built-in visual role presets balance dimension constraints, compression quality, and blur generation. By default, only raster formats (`JPEG, PNG, WebP, AVIF`) are accepted and verified via binary magic-byte sniffing; opt into sanitized SVG support per preset or uploader with `allowSvg: true`.
|
|
59
|
+
|
|
60
|
+
| Preset | Max Dimensions | Max Size | Quality | Default Formats | Notes |
|
|
61
|
+
| --- | --- | --- | --- | --- | --- |
|
|
62
|
+
| `thumbnail` | `600x600` | 5 MB | Q78 | JPEG, PNG, WebP, AVIF | Compact grid thumbnails |
|
|
63
|
+
| `avatar` | `512x512` | 5 MB | Q80 | JPEG, PNG, WebP, AVIF | Square user profiles |
|
|
64
|
+
| `card` | `1200x900` | 8 MB | Q85 | JPEG, PNG, WebP, AVIF | Content & product cards |
|
|
65
|
+
| `cover` | `1920x1080` | 10 MB | Q88 | JPEG, PNG, WebP, AVIF | Wide section banners |
|
|
66
|
+
| `hero` | `2560x1440` | 15 MB | Q92 | JPEG, PNG, WebP, AVIF | Full-bleed hero headers |
|
|
67
|
+
| `logo` | `800x800` | 5 MB | Q90 | JPEG, PNG, WebP, AVIF | Set `allowSvg: true` for sanitized vector SVGs |
|
|
68
|
+
| `original` | `4096x4096` | 25 MB | Q95 | JPEG, PNG, WebP, AVIF | Master archival fidelity |
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { defineUploadPresets } from 'img-upload-kit';
|
|
72
|
+
|
|
73
|
+
export const presets = defineUploadPresets({
|
|
74
|
+
hero: {
|
|
75
|
+
folder: 'banners/hero',
|
|
76
|
+
maxDimensions: { width: 2560, height: 1440 },
|
|
77
|
+
variantWidths: [640, 1024, 1920], // Encodes <base>-640w.avif, etc. + populates data.srcSet
|
|
78
|
+
maxSizeMB: 15,
|
|
79
|
+
quality: 90,
|
|
80
|
+
includeBlurHash: true,
|
|
81
|
+
},
|
|
82
|
+
logo: {
|
|
83
|
+
folder: 'brand/logos',
|
|
84
|
+
maxDimensions: { width: 800, height: 800 },
|
|
85
|
+
allowSvg: true, // Runs built-in allowlist SVG sanitizer before storage
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Server Uploader & Secure-by-Default HTTP Handler
|
|
93
|
+
|
|
94
|
+
`createUploadHttpHandler` enforces binary magic-byte sniffing, streaming `maxBodyBytes` limits (HTTP `413`), `allowedOrigins` CSRF checks (HTTP `403`), bucket allowlists on `DELETE`, and generic `500` responses with `onError`. Client `FormData` overrides are ignored unless explicitly enabled in `allowClientOverrides`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// app/api/upload/route.ts (Next.js App Router, Remix, Hono, SvelteKit)
|
|
98
|
+
import { createImageUploader, createUploadHttpHandler } from 'img-upload-kit/server';
|
|
99
|
+
import { createS3StorageAdapter } from 'img-upload-kit/storage';
|
|
100
|
+
import { presets } from '@/lib/upload-presets';
|
|
101
|
+
|
|
102
|
+
const storage = createS3StorageAdapter({
|
|
103
|
+
publicBaseUrl: 'https://cdn.example.com',
|
|
104
|
+
defaultBucket: 'media',
|
|
105
|
+
putObject: (input) => s3.send(new PutObjectCommand(input)),
|
|
106
|
+
deleteObject: (input) => s3.send(new DeleteObjectCommand(input)),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const uploader = createImageUploader({ storage, defaultBucket: 'media', presets });
|
|
110
|
+
|
|
111
|
+
export const { POST, DELETE } = createUploadHttpHandler({
|
|
112
|
+
uploader,
|
|
113
|
+
maxBodyBytes: 16 * 1024 * 1024,
|
|
114
|
+
allowedOrigins: ['https://app.example.com'],
|
|
115
|
+
authorize: async ({ action, request, preset, bucket, path }) => {
|
|
116
|
+
const user = await verifySession(request);
|
|
117
|
+
if (!user) return false;
|
|
118
|
+
if (action === 'delete') return user.role === 'admin' && bucket === 'media';
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
allowClientOverrides: {
|
|
122
|
+
filename: true, // Sanitized, forced to output extension, never overwrites existing files
|
|
123
|
+
folder: (folder) => (folder.startsWith('users/') ? folder : null),
|
|
124
|
+
dimensions: true, // Clamped to preset maxDimensions
|
|
125
|
+
format: true,
|
|
126
|
+
quality: true,
|
|
127
|
+
},
|
|
128
|
+
onError: (error, ctx) => console.error(`[upload:${ctx.action}]`, error),
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## React Upload UI: Paste, Camera Capture, Cancel/Retry & Forms
|
|
135
|
+
|
|
136
|
+
`<SingleImageUpload>` and `<MultiImageUpload>` support **clipboard image paste** (`Ctrl+V` / `Cmd+V`), **mobile camera capture** (`capture="environment"`), **per-tile cancel & retry** (backed by per-upload `AbortController`), and **native HTML form / React Hook Form integration** (`name`, `formValue`, `ref`, `required`, `minImages`, `onBlur`):
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useRef, useState } from 'react';
|
|
140
|
+
import {
|
|
141
|
+
ImageUploadProvider,
|
|
142
|
+
SingleImageUpload,
|
|
143
|
+
MultiImageUpload,
|
|
144
|
+
type ImageUploadHandle,
|
|
145
|
+
} from 'img-upload-kit/react';
|
|
146
|
+
import type { UploadedImageData } from 'img-upload-kit';
|
|
147
|
+
import 'img-upload-kit/styles.css';
|
|
148
|
+
|
|
149
|
+
export function ProductMediaForm() {
|
|
150
|
+
const uploadRef = useRef<ImageUploadHandle>(null);
|
|
151
|
+
const [cover, setCover] = useState<UploadedImageData | null>(null);
|
|
152
|
+
const [gallery, setGallery] = useState<UploadedImageData[]>([]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<ImageUploadProvider endpoint="/api/upload" locale="en">
|
|
156
|
+
<form onSubmit={(e) => { e.preventDefault(); /* FormData includes coverUrl & galleryJson */ }}>
|
|
157
|
+
<SingleImageUpload
|
|
158
|
+
ref={uploadRef}
|
|
159
|
+
preset="cover"
|
|
160
|
+
label="Cover Photo"
|
|
161
|
+
name="coverUrl"
|
|
162
|
+
formValue="url"
|
|
163
|
+
required
|
|
164
|
+
enablePaste
|
|
165
|
+
pasteTarget="document"
|
|
166
|
+
capture="environment"
|
|
167
|
+
value={cover}
|
|
168
|
+
onChange={setCover}
|
|
169
|
+
/>
|
|
170
|
+
<MultiImageUpload
|
|
171
|
+
mode="multiple"
|
|
172
|
+
preset="card"
|
|
173
|
+
label="Product Gallery"
|
|
174
|
+
name="galleryJson"
|
|
175
|
+
formValue="json"
|
|
176
|
+
required
|
|
177
|
+
minImages={2}
|
|
178
|
+
maxImages={10}
|
|
179
|
+
value={gallery}
|
|
180
|
+
onChange={setGallery}
|
|
181
|
+
onUploadCancel={(id) => console.log('Cancelled tile:', id)}
|
|
182
|
+
/>
|
|
183
|
+
<button type="submit">Save Product</button>
|
|
184
|
+
</form>
|
|
185
|
+
</ImageUploadProvider>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## AnimatedImage (Progressive Blur-Up & Settle Reveal)
|
|
193
|
+
|
|
194
|
+
`<AnimatedImage />` delivers a 60 FPS settle reveal (`opacity: 0 -> 1` + `scale(1.02 -> 1)` over `600ms ease-in-out`), pre-decodes via `img.decode()` before crossfading, auto-wires responsive `srcSet` from `image.variants`, and preserves the blurred backdrop on load error with automatic `fallbackSrc` retry:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { AnimatedImage } from 'img-upload-kit/react';
|
|
198
|
+
import Image from 'next/image';
|
|
199
|
+
import 'img-upload-kit/styles.css';
|
|
200
|
+
|
|
201
|
+
// 1. Direct shorthand with UploadedImageData (auto-wires variants srcSet, LQIP, blurColor):
|
|
202
|
+
<AnimatedImage image={img} showShimmer />
|
|
203
|
+
|
|
204
|
+
// 2. Next.js App Router (as={Image} + first-class priority & quality props):
|
|
205
|
+
<div style={{ position: 'relative', aspectRatio: '16/9' }}>
|
|
206
|
+
<AnimatedImage
|
|
207
|
+
image={img}
|
|
208
|
+
as={Image}
|
|
209
|
+
fill
|
|
210
|
+
priority
|
|
211
|
+
quality={85}
|
|
212
|
+
sizes="(max-width: 768px) 100vw, 1200px"
|
|
213
|
+
fallbackSrc="/images/fallback-hero.webp"
|
|
214
|
+
renderError={({ retry }) => <button type="button" onClick={retry}>Reload</button>}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## The 4 Styling Tiers
|
|
222
|
+
|
|
223
|
+
1. **Tier 1 — CSS Variables (`--iuk-*`)**: Automatic dark mode and RTL (`dir="rtl"`) with customizable `--iuk-primary`, `--iuk-surface`, and `--iuk-radius-*` tokens.
|
|
224
|
+
2. **Tier 2 — Unstyled + Slots (`unstyled` + `classNames`)**: Strip built-in styles and target slots (`dropzone`, `tile`, `browseButton`) with Tailwind `data-[state=dragging|uploading|error]:*` selectors.
|
|
225
|
+
3. **Tier 3 — Partial Slot Renderers**: Override specific regions via `renderDropzone`, `renderPreview`, `renderTile`, `renderHeader`, or `renderEmptyDropzone`.
|
|
226
|
+
4. **Tier 4 — Headless Primitives (`img-upload-kit/headless`)**: Full control via `<HeadlessSingleUpload>`, `<HeadlessMultiUpload>`, `useSingleUpload`, and `useMultiUpload` (including `cancel(id)`, `retry(id)`, `retryAll()`, and clipboard paste).
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Documentation & Links
|
|
231
|
+
|
|
232
|
+
- [v2.0 Migration Guide (`MIGRATION.md`)](./MIGRATION.md) & [Post-v2.0 Roadmap (`ROADMAP.md`)](./ROADMAP.md)
|
|
233
|
+
- [AI Agent Contributor Guide (`AGENTS.md`)](./AGENTS.md)
|
|
234
|
+
- [Custom Styling & Headless Guide (`docs/CUSTOM_STYLING.md`)](./docs/CUSTOM_STYLING.md)
|
|
235
|
+
- [AnimatedImage & Blur-Up Guide (`docs/ANIMATED_IMAGE.md`)](./docs/ANIMATED_IMAGE.md)
|
|
236
|
+
- [Architecture & Internals (`docs/ARCHITECTURE.md`)](./docs/ARCHITECTURE.md)
|
|
237
|
+
- [Framework & Storage Recipes (`docs/FRAMEWORK_RECIPES.md`)](./docs/FRAMEWORK_RECIPES.md)
|
|
238
|
+
- [Contributing Guide (`CONTRIBUTING.md`)](./CONTRIBUTING.md) & [Changelog (`CHANGELOG.md`)](./CHANGELOG.md)
|
|
239
|
+
|
|
240
|
+
## License & Author
|
|
241
|
+
|
|
242
|
+
**Author & Identity**: [buildwithsherbo](https://www.buildwithsherbo.com/) — Mohamed Elsherbiny ([@MoElsherbiny](https://github.com/MoElsherbiny) · [mo@buildwithsherbo.com](mailto:mo@buildwithsherbo.com))
|
|
243
|
+
Distributed under the [MIT License](./LICENSE).
|