img-upload-kit 2.0.1 → 2.0.2
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 -89
- package/MIGRATION.md +192 -192
- package/README.md +243 -243
- package/dist/bin.js +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/blur.cjs +3 -3
- package/dist/blur.cjs.map +1 -1
- package/dist/blur.js +2 -2
- package/dist/{chunk-XKQMCY5Z.cjs → chunk-2OVNXHTN.cjs} +1 -1
- package/dist/chunk-2OVNXHTN.cjs.map +1 -0
- package/dist/{chunk-FP5YIWG5.cjs → chunk-3ICG2SLL.cjs} +1 -1
- package/dist/chunk-3ICG2SLL.cjs.map +1 -0
- package/dist/{chunk-J6A44Y3W.js → chunk-4ASFG3KB.js} +1 -1
- package/dist/chunk-4ASFG3KB.js.map +1 -0
- package/dist/{chunk-IDLLCVGB.js → chunk-AI3JJLSA.js} +3 -3
- package/dist/chunk-AI3JJLSA.js.map +1 -0
- package/dist/{chunk-MHGQEBPW.js → chunk-CV23O4YU.js} +1 -1
- package/dist/chunk-CV23O4YU.js.map +1 -0
- package/dist/{chunk-25GYKQ6J.js → chunk-G7LLSKBV.js} +1 -1
- package/dist/chunk-G7LLSKBV.js.map +1 -0
- package/dist/{chunk-EWVIL57J.js → chunk-IB3TEDLR.js} +1 -1
- package/dist/chunk-IB3TEDLR.js.map +1 -0
- package/dist/{chunk-DFCSN3X7.cjs → chunk-KJVYWKIG.cjs} +1 -1
- package/dist/chunk-KJVYWKIG.cjs.map +1 -0
- package/dist/{chunk-34L4TEE3.cjs → chunk-NEDBIIE3.cjs} +1 -1
- package/dist/chunk-NEDBIIE3.cjs.map +1 -0
- package/dist/chunk-QHU7FUO4.cjs.map +1 -1
- package/dist/{chunk-VYVZVUNI.js → chunk-QQHZJKNA.js} +1 -1
- package/dist/chunk-QQHZJKNA.js.map +1 -0
- package/dist/{chunk-HILELQGS.js → chunk-TT5C7K3Q.js} +1 -1
- package/dist/chunk-TT5C7K3Q.js.map +1 -0
- package/dist/{chunk-TA5Y64JM.cjs → chunk-U2TWTEBY.cjs} +24 -24
- package/dist/chunk-U2TWTEBY.cjs.map +1 -0
- package/dist/{chunk-MBQJ6NVJ.cjs → chunk-WISG74VL.cjs} +1 -1
- package/dist/chunk-WISG74VL.cjs.map +1 -0
- package/dist/{chunk-LOSVFO56.cjs → chunk-YRXXQUDE.cjs} +1 -1
- package/dist/chunk-YRXXQUDE.cjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/headless.cjs +4 -4
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.js +3 -3
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/react.cjs +138 -138
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +5 -5
- package/dist/react.js.map +1 -1
- package/dist/server-handler.cjs +2 -2
- package/dist/server-handler.cjs.map +1 -1
- package/dist/server-handler.js +1 -1
- package/dist/server.cjs +30 -30
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/storage.cjs +9 -9
- package/dist/storage.cjs.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/styles/animated-image.css +112 -112
- package/dist/styles/tokens.css +123 -123
- package/dist/styles.css +235 -235
- package/package.json +1 -1
- package/dist/chunk-25GYKQ6J.js.map +0 -1
- package/dist/chunk-34L4TEE3.cjs.map +0 -1
- package/dist/chunk-DFCSN3X7.cjs.map +0 -1
- package/dist/chunk-EWVIL57J.js.map +0 -1
- package/dist/chunk-FP5YIWG5.cjs.map +0 -1
- package/dist/chunk-HILELQGS.js.map +0 -1
- package/dist/chunk-IDLLCVGB.js.map +0 -1
- package/dist/chunk-J6A44Y3W.js.map +0 -1
- package/dist/chunk-LOSVFO56.cjs.map +0 -1
- package/dist/chunk-MBQJ6NVJ.cjs.map +0 -1
- package/dist/chunk-MHGQEBPW.js.map +0 -1
- package/dist/chunk-TA5Y64JM.cjs.map +0 -1
- package/dist/chunk-VYVZVUNI.js.map +0 -1
- package/dist/chunk-XKQMCY5Z.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,243 +1,243 @@
|
|
|
1
|
-
# img-upload-kit
|
|
2
|
-
|
|
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).
|
|
1
|
+
# img-upload-kit
|
|
2
|
+
|
|
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).
|
package/dist/bin.js
CHANGED
|
@@ -1039,7 +1039,7 @@ async function generateHealthReport(imagesDir, manifestFile) {
|
|
|
1039
1039
|
}
|
|
1040
1040
|
|
|
1041
1041
|
// src/cli/commands.ts
|
|
1042
|
-
var CLI_VERSION = "2.0.
|
|
1042
|
+
var CLI_VERSION = "2.0.2".length > 0 ? "2.0.2" : "2.0.0";
|
|
1043
1043
|
var DEFAULT_IMAGES_DIR = "public/images";
|
|
1044
1044
|
var DEFAULT_MANIFEST_FILE = "src/blur-hashes.json";
|
|
1045
1045
|
function findManifestEntry(hashes, imageKey) {
|