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.
Files changed (112) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.md +192 -0
  4. package/README.md +242 -2
  5. package/dist/bin.js +1186 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/blur.cjs +41 -0
  8. package/dist/blur.cjs.map +1 -0
  9. package/dist/blur.d.cts +17 -0
  10. package/dist/blur.d.ts +17 -0
  11. package/dist/blur.js +41 -0
  12. package/dist/blur.js.map +1 -0
  13. package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
  14. package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
  15. package/dist/blurService-D5TpMNji.d.ts +83 -0
  16. package/dist/blurService-DacwylJx.d.cts +83 -0
  17. package/dist/chunk-2OVNXHTN.cjs +514 -0
  18. package/dist/chunk-2OVNXHTN.cjs.map +1 -0
  19. package/dist/chunk-3ICG2SLL.cjs +135 -0
  20. package/dist/chunk-3ICG2SLL.cjs.map +1 -0
  21. package/dist/chunk-4ASFG3KB.js +514 -0
  22. package/dist/chunk-4ASFG3KB.js.map +1 -0
  23. package/dist/chunk-AI3JJLSA.js +2370 -0
  24. package/dist/chunk-AI3JJLSA.js.map +1 -0
  25. package/dist/chunk-CV23O4YU.js +145 -0
  26. package/dist/chunk-CV23O4YU.js.map +1 -0
  27. package/dist/chunk-G7LLSKBV.js +158 -0
  28. package/dist/chunk-G7LLSKBV.js.map +1 -0
  29. package/dist/chunk-IB3TEDLR.js +294 -0
  30. package/dist/chunk-IB3TEDLR.js.map +1 -0
  31. package/dist/chunk-KJVYWKIG.cjs +158 -0
  32. package/dist/chunk-KJVYWKIG.cjs.map +1 -0
  33. package/dist/chunk-NEDBIIE3.cjs +226 -0
  34. package/dist/chunk-NEDBIIE3.cjs.map +1 -0
  35. package/dist/chunk-QHU7FUO4.cjs +37 -0
  36. package/dist/chunk-QHU7FUO4.cjs.map +1 -0
  37. package/dist/chunk-QQHZJKNA.js +135 -0
  38. package/dist/chunk-QQHZJKNA.js.map +1 -0
  39. package/dist/chunk-S2IFAWJK.js +37 -0
  40. package/dist/chunk-S2IFAWJK.js.map +1 -0
  41. package/dist/chunk-TT5C7K3Q.js +226 -0
  42. package/dist/chunk-TT5C7K3Q.js.map +1 -0
  43. package/dist/chunk-U2TWTEBY.cjs +2370 -0
  44. package/dist/chunk-U2TWTEBY.cjs.map +1 -0
  45. package/dist/chunk-WISG74VL.cjs +294 -0
  46. package/dist/chunk-WISG74VL.cjs.map +1 -0
  47. package/dist/chunk-YRXXQUDE.cjs +145 -0
  48. package/dist/chunk-YRXXQUDE.cjs.map +1 -0
  49. package/dist/cli.cjs +1241 -0
  50. package/dist/cli.cjs.map +1 -0
  51. package/dist/cli.d.cts +99 -0
  52. package/dist/cli.d.ts +99 -0
  53. package/dist/cli.js +1194 -0
  54. package/dist/cli.js.map +1 -0
  55. package/dist/headless.cjs +27 -0
  56. package/dist/headless.cjs.map +1 -0
  57. package/dist/headless.d.cts +4 -0
  58. package/dist/headless.d.ts +4 -0
  59. package/dist/headless.js +27 -0
  60. package/dist/headless.js.map +1 -0
  61. package/dist/index-BIdnqtW8.d.ts +234 -0
  62. package/dist/index-Brk_uHs6.d.cts +234 -0
  63. package/dist/index-CPMlaQsC.d.ts +635 -0
  64. package/dist/index-DHINe-rA.d.cts +635 -0
  65. package/dist/index.cjs +65 -0
  66. package/dist/index.cjs.map +1 -0
  67. package/dist/index.d.cts +43 -0
  68. package/dist/index.d.ts +43 -0
  69. package/dist/index.js +65 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/memoryAdapter-Ck0ccZbF.d.cts +25 -0
  72. package/dist/memoryAdapter-DxEKaG0b.d.ts +25 -0
  73. package/dist/placeholders-C4ftwaII.d.ts +5 -0
  74. package/dist/placeholders-x_3rt65M.d.cts +5 -0
  75. package/dist/react.cjs +3779 -0
  76. package/dist/react.cjs.map +1 -0
  77. package/dist/react.d.cts +476 -0
  78. package/dist/react.d.ts +476 -0
  79. package/dist/react.js +3779 -0
  80. package/dist/react.js.map +1 -0
  81. package/dist/server-handler.cjs +14 -0
  82. package/dist/server-handler.cjs.map +1 -0
  83. package/dist/server-handler.d.cts +3 -0
  84. package/dist/server-handler.d.ts +3 -0
  85. package/dist/server-handler.js +14 -0
  86. package/dist/server-handler.js.map +1 -0
  87. package/dist/server.cjs +1801 -0
  88. package/dist/server.cjs.map +1 -0
  89. package/dist/server.d.cts +142 -0
  90. package/dist/server.d.ts +142 -0
  91. package/dist/server.js +1801 -0
  92. package/dist/server.js.map +1 -0
  93. package/dist/storage.cjs +447 -0
  94. package/dist/storage.cjs.map +1 -0
  95. package/dist/storage.d.cts +163 -0
  96. package/dist/storage.d.ts +163 -0
  97. package/dist/storage.js +447 -0
  98. package/dist/storage.js.map +1 -0
  99. package/dist/styles/animated-image.css +112 -0
  100. package/dist/styles/dropzone-and-tiles.css +247 -0
  101. package/dist/styles/modals-and-dock.css +248 -0
  102. package/dist/styles/primitives.css +197 -0
  103. package/dist/styles/quality-and-inputs.css +184 -0
  104. package/dist/styles/tokens.css +123 -0
  105. package/dist/styles.css +1122 -0
  106. package/dist/types-BrP-o9nd.d.cts +85 -0
  107. package/dist/types-sZ3KoFQY.d.ts +85 -0
  108. package/dist/types-yShGNQnW.d.cts +61 -0
  109. package/dist/types-yShGNQnW.d.ts +61 -0
  110. package/dist/upload-C5Trd6Dh.d.cts +154 -0
  111. package/dist/upload-C5Trd6Dh.d.ts +154 -0
  112. 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
- # Temporary Holding Version
1
+ # img-upload-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ [![Live Interactive Demo](https://img.shields.io/badge/Live%20Demo-upload--kit.buildwithsherbo.com-0284c7?style=for-the-badge)](https://upload-kit.buildwithsherbo.com)
6
+ [![npm version](https://img.shields.io/npm/v/img-upload-kit?color=38bdf8&style=for-the-badge)](https://www.npmjs.com/package/img-upload-kit)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](./LICENSE)
8
+ [![Studio Repo](https://img.shields.io/badge/Studio%20Code-image--upload--kit--studio-1e293b?style=for-the-badge&logo=github)](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).