@withwiz/gallery 0.1.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/README.ko.md +484 -0
- package/README.md +484 -0
- package/dist/chunk-B3MQL6ZR.mjs +79 -0
- package/dist/chunk-B3MQL6ZR.mjs.map +1 -0
- package/dist/chunk-BC3TPJQY.mjs +122 -0
- package/dist/chunk-BC3TPJQY.mjs.map +1 -0
- package/dist/chunk-BTLELFKC.mjs +11 -0
- package/dist/chunk-BTLELFKC.mjs.map +1 -0
- package/dist/chunk-GDUGKVDQ.mjs +50 -0
- package/dist/chunk-GDUGKVDQ.mjs.map +1 -0
- package/dist/chunk-GFOWZGJX.js +50 -0
- package/dist/chunk-GFOWZGJX.js.map +1 -0
- package/dist/chunk-ILSZPLME.js +47 -0
- package/dist/chunk-ILSZPLME.js.map +1 -0
- package/dist/chunk-M7CCTDUG.js +122 -0
- package/dist/chunk-M7CCTDUG.js.map +1 -0
- package/dist/chunk-WVRTM6E7.js +19 -0
- package/dist/chunk-WVRTM6E7.js.map +1 -0
- package/dist/chunk-XLPMNJJV.js +11 -0
- package/dist/chunk-XLPMNJJV.js.map +1 -0
- package/dist/chunk-XSPWM4DD.js +79 -0
- package/dist/chunk-XSPWM4DD.js.map +1 -0
- package/dist/chunk-YUMB3GBN.mjs +47 -0
- package/dist/chunk-YUMB3GBN.mjs.map +1 -0
- package/dist/chunk-ZEZCUJJD.mjs +19 -0
- package/dist/chunk-ZEZCUJJD.mjs.map +1 -0
- package/dist/components/gallery.css +1074 -0
- package/dist/components/index.d.mts +156 -0
- package/dist/components/index.d.ts +156 -0
- package/dist/components/index.js +1215 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +1215 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/errors-B9gzUsX-.d.ts +39 -0
- package/dist/errors-DYrlDCzP.d.mts +39 -0
- package/dist/gallery.css +1074 -0
- package/dist/hooks/index.d.mts +65 -0
- package/dist/hooks/index.d.ts +65 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +14 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +37 -0
- package/dist/index.mjs.map +1 -0
- package/dist/presets/ballet.d.mts +64 -0
- package/dist/presets/ballet.d.ts +64 -0
- package/dist/presets/ballet.js +162 -0
- package/dist/presets/ballet.js.map +1 -0
- package/dist/presets/ballet.mjs +162 -0
- package/dist/presets/ballet.mjs.map +1 -0
- package/dist/server/index.d.mts +157 -0
- package/dist/server/index.d.ts +157 -0
- package/dist/server/index.js +619 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +619 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +137 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.mts +22 -0
- package/dist/useGalleryLightbox-BYzPyBYB.d.ts +22 -0
- package/dist/validators/index.d.mts +68 -0
- package/dist/validators/index.d.ts +68 -0
- package/dist/validators/index.js +7 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/index.mjs +7 -0
- package/dist/validators/index.mjs.map +1 -0
- package/package.json +85 -0
- package/prisma/gallery.schema.prisma +62 -0
- package/prisma/migrations/2026-05-24-enum-to-table.sql +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# @withwiz/gallery
|
|
2
|
+
|
|
3
|
+
**English** | [한국어](./README.ko.md)
|
|
4
|
+
|
|
5
|
+
Host-independent gallery module for Next.js 16 + Prisma 7. Self-contained admin UI, headless lightbox, drop-in API route factories.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
**v0.1.0** — Sprints 1–6 complete (`@withwiz/gallery` scaffold + validators + services + server layer + hooks/primitive UI + admin composite UI + Prisma partial + ballet preset + README). Sprint 7 (ballet migration) not yet executed.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Headless** — drop into any Next.js host via `setGalleryConfig` (Prisma client / API wrapper / storage / revalidate / i18n / limits).
|
|
14
|
+
- **Admin UI** — 5 composite components (`GalleryAdminManager`, `GalleryEditForm`, `GalleryHomePreview`, `GalleryManagerLayout`, `CategoryAdminManager`) + 2 primitives (`ImageDropZone`, `ToggleSwitch`). Self-contained 3-pane layout.
|
|
15
|
+
- **API routes** — `createGalleryRoutes(config)` returns Next.js Route Handlers for 6 endpoint groups (collection / item / publish toggle / bulk / category collection / category item).
|
|
16
|
+
- **RSC loaders** — `getGalleryItems`, `getFeaturedGalleries`, `getRecentGalleries`, `getGalleryCount` for server components / dashboards.
|
|
17
|
+
- **Public preset** — `PublicGalleryMosaic` (presets/ballet) — 1–7 tile adaptive mosaic + lightbox.
|
|
18
|
+
- **Headless lightbox hook** — `useGalleryLightbox` with ESC / Arrow key bindings + wrap-around.
|
|
19
|
+
- **Image upload primitive** — `useImageDropZone` + `<ImageDropZone>` (host-side validate + accept/maxSize).
|
|
20
|
+
- **Storage-agnostic** — R2 / S3 / local filesystem — host injects via `config.storage`.
|
|
21
|
+
- **Host-model-name-agnostic** — the Prisma `Gallery` model makes no assumption about the host's User/Admin/Account model name. Only an `authorId: String` column.
|
|
22
|
+
- **Typed error hierarchy** — `GalleryNotFoundError`, `CategoryNotFoundError`, `CategoryInUseError`, `PermissionDeniedError` for the host to catch and handle.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Currently consumed via `file:` reference inside the monorepo (not yet published to npm):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# host root
|
|
30
|
+
npm install file:../node-packages/withwiz-gallery
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
After publishing:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @withwiz/gallery
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Peer dependencies
|
|
40
|
+
|
|
41
|
+
| Package | Range |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `next` | `>=16` |
|
|
44
|
+
| `react` / `react-dom` | `>=19` |
|
|
45
|
+
| `@prisma/client` | `>=7` |
|
|
46
|
+
| `zod` | `>=4` |
|
|
47
|
+
| `clsx` | `>=2` |
|
|
48
|
+
| `tailwind-merge` | `>=3` |
|
|
49
|
+
| `sonner` | `>=2` (optional) |
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
### 1. Merge the Prisma schema
|
|
54
|
+
|
|
55
|
+
Copy or symlink this package's partial schema into the host's `prisma/` directory.
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// host package.json
|
|
59
|
+
{
|
|
60
|
+
"prisma": { "schema": "./prisma" }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# in the host
|
|
66
|
+
cp node_modules/@withwiz/gallery/prisma/gallery.schema.prisma prisma/
|
|
67
|
+
npx prisma generate
|
|
68
|
+
npx prisma migrate dev --create-only
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The partial defines two models (`GalleryCategory`, `Gallery`) plus indexes and `@@map`. Because `authorId` is a plain `String` column (no `@relation`), the host's User-model name is irrelevant and the multi-file schema validates cleanly.
|
|
72
|
+
|
|
73
|
+
Migrating from a ballet-style enum-based schema? See [Migration from ballet enum-based schema](#migration-from-ballet-enum-based-schema) below.
|
|
74
|
+
|
|
75
|
+
### 2. `setGalleryConfig` (host bootstrap)
|
|
76
|
+
|
|
77
|
+
**Call this at module top-level** — client components invoke `getGalleryConfig()` on mount.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// host: lib/gallery-config.ts
|
|
81
|
+
import { setGalleryConfig } from "@withwiz/gallery/server";
|
|
82
|
+
import type { GalleryConfig } from "@withwiz/gallery/server";
|
|
83
|
+
import { prisma } from "./prisma";
|
|
84
|
+
import { withAdminApi } from "@/lib/api-middleware"; // host's auth wrapper
|
|
85
|
+
import { isR2Enabled, collectR2Keys, deleteR2Keys } from "@/lib/r2";
|
|
86
|
+
import { revalidatePath } from "next/cache";
|
|
87
|
+
|
|
88
|
+
export const galleryConfig: GalleryConfig = {
|
|
89
|
+
prisma,
|
|
90
|
+
apiWrapper: withAdminApi,
|
|
91
|
+
authorIdFromContext: (ctx) => ctx.user!.id,
|
|
92
|
+
limits: {
|
|
93
|
+
maxFeatured: 7,
|
|
94
|
+
mosaicCount: 7,
|
|
95
|
+
batchMax: 20,
|
|
96
|
+
captionMaxLength: 200,
|
|
97
|
+
},
|
|
98
|
+
revalidate: revalidatePath,
|
|
99
|
+
revalidatePaths: ["/", "/home/v1"],
|
|
100
|
+
storage: {
|
|
101
|
+
isEnabled: isR2Enabled,
|
|
102
|
+
collectKeys: collectR2Keys,
|
|
103
|
+
deleteKeys: deleteR2Keys,
|
|
104
|
+
},
|
|
105
|
+
i18n: {
|
|
106
|
+
"admin.title": "Gallery",
|
|
107
|
+
"admin.newButton": "New image",
|
|
108
|
+
"form.caption": "Caption",
|
|
109
|
+
"form.save": "Save",
|
|
110
|
+
"form.cancel": "Cancel",
|
|
111
|
+
// see @withwiz/gallery/types → GalleryI18nKey for the full key list
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
setGalleryConfig(galleryConfig);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Mount route handlers
|
|
119
|
+
|
|
120
|
+
Thin re-export per endpoint.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// host: app/api/admin/galleries/route.ts
|
|
124
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
125
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
126
|
+
|
|
127
|
+
const { collection } = createGalleryRoutes(galleryConfig);
|
|
128
|
+
export const { GET, POST, DELETE } = collection;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// host: app/api/admin/galleries/[id]/route.ts
|
|
133
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
134
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
135
|
+
|
|
136
|
+
const { item } = createGalleryRoutes(galleryConfig);
|
|
137
|
+
export const { GET, PUT, DELETE } = item;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// host: app/api/admin/galleries/[id]/publish/route.ts
|
|
142
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
143
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
144
|
+
|
|
145
|
+
const { publishToggle } = createGalleryRoutes(galleryConfig);
|
|
146
|
+
export const { PATCH } = publishToggle;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// host: app/api/admin/galleries/bulk/route.ts
|
|
151
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
152
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
153
|
+
|
|
154
|
+
const { bulk } = createGalleryRoutes(galleryConfig);
|
|
155
|
+
export const { POST, PATCH } = bulk;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// host: app/api/admin/gallery-categories/route.ts
|
|
160
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
161
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
162
|
+
|
|
163
|
+
const { categoryCollection } = createGalleryRoutes(galleryConfig);
|
|
164
|
+
export const { GET, POST } = categoryCollection;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// host: app/api/admin/gallery-categories/[id]/route.ts
|
|
169
|
+
import { createGalleryRoutes } from "@withwiz/gallery/server";
|
|
170
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
171
|
+
|
|
172
|
+
const { categoryItem } = createGalleryRoutes(galleryConfig);
|
|
173
|
+
export const { GET, PUT, DELETE } = categoryItem;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 4. Mount the admin UI
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// host: app/admin/galleries/page.tsx
|
|
180
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
181
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
182
|
+
import "@/lib/gallery-config"; // setGalleryConfig as a module side effect
|
|
183
|
+
|
|
184
|
+
export default function GalleryAdminPage() {
|
|
185
|
+
return <GalleryAdminManager />;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// host: app/admin/galleries/new/page.tsx
|
|
191
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
192
|
+
import "@/lib/gallery-config";
|
|
193
|
+
|
|
194
|
+
export default function GalleryNewPage() {
|
|
195
|
+
return <GalleryAdminManager initialMode="new" />;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// host: app/admin/galleries/[id]/page.tsx
|
|
201
|
+
import { GalleryAdminManager } from "@withwiz/gallery/components";
|
|
202
|
+
import "@/lib/gallery-config";
|
|
203
|
+
|
|
204
|
+
export default async function GalleryEditPage({
|
|
205
|
+
params,
|
|
206
|
+
}: {
|
|
207
|
+
params: Promise<{ id: string }>;
|
|
208
|
+
}) {
|
|
209
|
+
const { id } = await params;
|
|
210
|
+
return <GalleryAdminManager initialSelectedId={id} />;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// host: app/admin/gallery-categories/page.tsx
|
|
216
|
+
import { CategoryAdminManager } from "@withwiz/gallery/components";
|
|
217
|
+
import "@/lib/gallery-config";
|
|
218
|
+
|
|
219
|
+
export default function CategoryAdminPage() {
|
|
220
|
+
return <CategoryAdminManager />;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 5. Public gallery (RSC + client)
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// host: app/page.tsx (RSC)
|
|
228
|
+
import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
|
|
229
|
+
import { getFeaturedGalleries } from "@withwiz/gallery/server";
|
|
230
|
+
import { galleryConfig } from "@/lib/gallery-config";
|
|
231
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
232
|
+
|
|
233
|
+
export default async function HomePage() {
|
|
234
|
+
const featured = await getFeaturedGalleries(galleryConfig, 7);
|
|
235
|
+
const images = featured.map((g) => ({
|
|
236
|
+
src: g.imageUrl,
|
|
237
|
+
alt: g.caption ?? "Gallery",
|
|
238
|
+
}));
|
|
239
|
+
return (
|
|
240
|
+
<PublicGalleryMosaic
|
|
241
|
+
images={images}
|
|
242
|
+
count={7}
|
|
243
|
+
i18n={{ sectionLabel: "Gallery", moments: "Moments from the stage" }}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## API reference
|
|
250
|
+
|
|
251
|
+
### `@withwiz/gallery` (main entry)
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import {
|
|
255
|
+
setGalleryConfig,
|
|
256
|
+
getGalleryConfig,
|
|
257
|
+
cn,
|
|
258
|
+
getVariantUrl,
|
|
259
|
+
GalleryError,
|
|
260
|
+
GalleryNotFoundError,
|
|
261
|
+
CategoryNotFoundError,
|
|
262
|
+
CategoryInUseError,
|
|
263
|
+
PermissionDeniedError,
|
|
264
|
+
} from "@withwiz/gallery";
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `@withwiz/gallery/server`
|
|
268
|
+
|
|
269
|
+
| Export | Signature |
|
|
270
|
+
|---|---|
|
|
271
|
+
| `setGalleryConfig(config)` | DI setter — call once at module top-level |
|
|
272
|
+
| `getGalleryConfig()` | Returns the registered config (throws if unset) |
|
|
273
|
+
| `createGalleryService(config)` | CRUD object (`list` / `get` / `create` / `update` / `remove` / `bulk*`) |
|
|
274
|
+
| `createCategoryService(config)` | Category CRUD (`list` / `getBySlug` / `create` / `update` / `remove` / `reorder`) |
|
|
275
|
+
| `createGalleryRoutes(config)` | 6 Next.js Route Handler groups (`collection` / `item` / `publishToggle` / `bulk` / `categoryCollection` / `categoryItem`) |
|
|
276
|
+
| `getGalleryItems(config, opts)` | RSC pagination loader |
|
|
277
|
+
| `getFeaturedGalleries(config, limit?)` | RSC featured list |
|
|
278
|
+
| `getRecentGalleries(config, limit)` | RSC recent items (dashboard) |
|
|
279
|
+
| `getGalleryCount(config, opts?)` | RSC count (dashboard) |
|
|
280
|
+
| `buildPaginatedResult(items, page, limit, total)` | helper |
|
|
281
|
+
|
|
282
|
+
Typed errors (also re-exported from the server entry):
|
|
283
|
+
|
|
284
|
+
- `GalleryError` (base)
|
|
285
|
+
- `GalleryNotFoundError`
|
|
286
|
+
- `CategoryNotFoundError`
|
|
287
|
+
- `CategoryInUseError`
|
|
288
|
+
- `PermissionDeniedError`
|
|
289
|
+
|
|
290
|
+
### `@withwiz/gallery/components`
|
|
291
|
+
|
|
292
|
+
| Export | Description |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `GalleryAdminManager` | 3-pane admin mount point (`initialMode?` / `initialSelectedId?`) |
|
|
295
|
+
| `GalleryManagerLayout` | 3-pane primitive (left = list / center = form / right = preview) |
|
|
296
|
+
| `GalleryEditForm` | Single/multi form (`value` / `multipleMode` / `onSubmit` / `onSubmitMany`) |
|
|
297
|
+
| `GalleryHomePreview` | 7-tile mosaic + drag reorder + featured toggle |
|
|
298
|
+
| `CategoryAdminManager` | Category CRUD UI |
|
|
299
|
+
| `ImageDropZone` | Image dropzone (`accept` / `maxSize` / `multiple` / `disabled` / `validate`) |
|
|
300
|
+
| `ToggleSwitch` | size sm/md/lg, ARIA switch role |
|
|
301
|
+
|
|
302
|
+
CSS (host imports explicitly):
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import "@withwiz/gallery/components/gallery.css";
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### `@withwiz/gallery/hooks`
|
|
309
|
+
|
|
310
|
+
| Export | Signature |
|
|
311
|
+
|---|---|
|
|
312
|
+
| `useGalleryLightbox(images)` | `{ isOpen, currentIndex, current, open, close, next, prev }` — auto-bound ESC / Arrow, wrap-around |
|
|
313
|
+
| `useImageDropZone(opts)` | Headless dropzone — `inputProps / containerProps / files / rejectedReasons / clear` |
|
|
314
|
+
| `useScrollReveal(opts?)` | IntersectionObserver-based fade-in — `{ ref, isVisible }` |
|
|
315
|
+
|
|
316
|
+
### `@withwiz/gallery/validators`
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { createGallerySchemas } from "@withwiz/gallery/validators";
|
|
320
|
+
|
|
321
|
+
const schemas = createGallerySchemas({
|
|
322
|
+
batchMax: 20,
|
|
323
|
+
captionMaxLength: 200,
|
|
324
|
+
});
|
|
325
|
+
// schemas.CreateGallerySchema / UpdateGallerySchema / BatchCreateGallerySchema
|
|
326
|
+
// / BulkUpdateSchema / CreateCategorySchema / UpdateCategorySchema / ReorderCategorySchema
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### `@withwiz/gallery/types`
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import type {
|
|
333
|
+
GalleryConfig,
|
|
334
|
+
GalleryI18nKey,
|
|
335
|
+
GallerySlotName,
|
|
336
|
+
GalleryListItem,
|
|
337
|
+
GalleryDetail,
|
|
338
|
+
GalleryCategoryItem,
|
|
339
|
+
CreateGalleryInput,
|
|
340
|
+
UpdateGalleryInput,
|
|
341
|
+
CreateCategoryInput,
|
|
342
|
+
UpdateCategoryInput,
|
|
343
|
+
PaginatedResult,
|
|
344
|
+
PaginationMeta,
|
|
345
|
+
SortOrder,
|
|
346
|
+
PrismaLike,
|
|
347
|
+
ApiContext,
|
|
348
|
+
RouteHandler,
|
|
349
|
+
ApiWrapper,
|
|
350
|
+
} from "@withwiz/gallery/types";
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `@withwiz/gallery/presets/ballet`
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import { PublicGalleryMosaic } from "@withwiz/gallery/presets/ballet";
|
|
357
|
+
import type { PublicGalleryMosaicProps } from "@withwiz/gallery/presets/ballet";
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Signature:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
function PublicGalleryMosaic(props: {
|
|
364
|
+
images: { src: string; alt?: string }[];
|
|
365
|
+
count?: number; // default 7
|
|
366
|
+
i18n?: {
|
|
367
|
+
sectionLabel?: string;
|
|
368
|
+
moments?: string;
|
|
369
|
+
expandAria?: string;
|
|
370
|
+
lbClose?: string;
|
|
371
|
+
lbPrev?: string;
|
|
372
|
+
lbNext?: string;
|
|
373
|
+
};
|
|
374
|
+
className?: string;
|
|
375
|
+
scrollReveal?: boolean; // default true
|
|
376
|
+
hideHeader?: boolean; // default false
|
|
377
|
+
}): JSX.Element | null;
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Host-independence guarantees
|
|
381
|
+
|
|
382
|
+
- **Zero `@withwiz/pms` imports** — this package does not depend on any domain package.
|
|
383
|
+
- **Zero direct `process.env` reads** — environment variables are read by the host and bound into `config.storage`, etc.
|
|
384
|
+
- **No assumption about the host's User model name** — the Prisma `Gallery` model has only an `authorId: String` column and no `@relation`.
|
|
385
|
+
- **All infrastructure is injected via `GalleryConfig`** — Prisma client / API wrapper / storage / revalidate / i18n / limits.
|
|
386
|
+
- **Storage / auth / revalidate are the host's responsibility** — this package only deals with keys/paths; actual R2/S3/local calls are delegated to the host via `config.storage.deleteKeys`, etc.
|
|
387
|
+
- **`presets/ballet` does not import `next/image`, `next/navigation`, or `useI18n`** — images use plain `<img>`, labels come from props.
|
|
388
|
+
|
|
389
|
+
## CSS customization
|
|
390
|
+
|
|
391
|
+
Override the CSS variables from the host's root CSS.
|
|
392
|
+
|
|
393
|
+
```css
|
|
394
|
+
/* host: app/globals.css */
|
|
395
|
+
:root {
|
|
396
|
+
--gallery-accent: 212 175 55; /* RGB triplet (NOT hex) */
|
|
397
|
+
--gallery-bg: 10 10 10;
|
|
398
|
+
--gallery-fg: 254 254 254;
|
|
399
|
+
--gallery-border: 30 30 30;
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
These four variables are defined as fallbacks inside `:where(.gallery-toggle, .gallery-dropzone, .gallery-manager, .gallery-edit-form, .gallery-home-preview, .gallery-category-admin, .gallery-public-mosaic, .gallery-lightbox, ...)`, so the package works out of the box (dark tone + gold accent) without any host override.
|
|
404
|
+
|
|
405
|
+
The admin components also support per-slot customization via `config.ui.classNames[slot]` / `config.ui.slots[slot]`.
|
|
406
|
+
|
|
407
|
+
## Migration from ballet enum-based schema
|
|
408
|
+
|
|
409
|
+
ballet's existing schema:
|
|
410
|
+
|
|
411
|
+
```prisma
|
|
412
|
+
enum GalleryCategory { PERFORMANCE | REHEARSAL | ACTIVITY | ARTIST }
|
|
413
|
+
model Gallery {
|
|
414
|
+
category GalleryCategory
|
|
415
|
+
// ...
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
This package's new schema replaces the enum with a `gallery_categories` table + a `Gallery.categoryId` FK. Migration steps:
|
|
420
|
+
|
|
421
|
+
1. Merge this package's `gallery.schema.prisma` into the host's `prisma/` (copy or symlink).
|
|
422
|
+
2. Run `npx prisma migrate dev --create-only` to auto-generate the migration SQL.
|
|
423
|
+
3. Manually remove `DROP COLUMN category` / `DROP TYPE "GalleryCategory"` from the generated SQL (separate them into a follow-up migration).
|
|
424
|
+
4. Insert the contents of `prisma/migrations/2026-05-24-enum-to-table.sql` (shipped with this package) at that location — seeds the 4 categories, backfills `galleries.category_id`, and includes a PL/pgSQL verification block.
|
|
425
|
+
5. `npx prisma migrate dev` to apply.
|
|
426
|
+
6. Once verification passes, run the second migration (separate file) to `DROP` the enum column and type.
|
|
427
|
+
|
|
428
|
+
Reference the shipped `prisma/migrations/2026-05-24-enum-to-table.sql` as-is.
|
|
429
|
+
|
|
430
|
+
The ballet code (`src/components/sections/Gallery.tsx` — `gallery-mosaic` / `lightbox-*` classes) and this package (`gallery-public-mosaic` / `gallery-lightbox__*` classes) use different prefixes and are isolated. During ballet migration, remove those classes from the existing `main.css` and import only this package's `gallery.css`.
|
|
431
|
+
|
|
432
|
+
## Architecture
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
@withwiz/toolkit (lowest)
|
|
436
|
+
↑
|
|
437
|
+
@withwiz/pms (performance-management domain CMS)
|
|
438
|
+
↑ ↑
|
|
439
|
+
│ │ consumed as a host
|
|
440
|
+
host app (ballet, yeroom) │
|
|
441
|
+
↓ │
|
|
442
|
+
└─→ @withwiz/gallery ┘ (this package — depends on no host)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`@withwiz/gallery` depends on no host project (ballet, yeroom, etc.), no domain package (`@withwiz/pms`, etc.), and no host domain model (`User`, `Admin`, `Account`, etc.). Whatever the host uses (Next 16 / Prisma 7 assumed), a single `setGalleryConfig` call integrates it.
|
|
446
|
+
|
|
447
|
+
## Repository layout
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
node-packages/withwiz-gallery/
|
|
451
|
+
├── package.json
|
|
452
|
+
├── tsup.config.ts
|
|
453
|
+
├── README.md
|
|
454
|
+
├── prisma/
|
|
455
|
+
│ ├── gallery.schema.prisma # GalleryCategory + Gallery partial models
|
|
456
|
+
│ └── migrations/
|
|
457
|
+
│ └── 2026-05-24-enum-to-table.sql # ballet enum → table backfill
|
|
458
|
+
├── docs/superpowers/{specs,plans}/ # design docs + sprint contracts/reports
|
|
459
|
+
└── src/
|
|
460
|
+
├── index.ts # main entry — config, types, errors, utils
|
|
461
|
+
├── config.ts
|
|
462
|
+
├── errors.ts
|
|
463
|
+
├── types/ # GalleryConfig / domain / pagination / api-context / i18n
|
|
464
|
+
├── validators/ # createGallerySchemas
|
|
465
|
+
├── services/ # createGalleryService / createCategoryService / helpers
|
|
466
|
+
├── server/ # createGalleryRoutes + loaders + server re-exports
|
|
467
|
+
├── components/ # admin UI + ImageDropZone + ToggleSwitch + gallery.css
|
|
468
|
+
├── hooks/ # useGalleryLightbox / useImageDropZone / useScrollReveal
|
|
469
|
+
├── utils/ # cn / image-variants / api-helpers
|
|
470
|
+
└── presets/
|
|
471
|
+
└── ballet.tsx # PublicGalleryMosaic
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Scripts
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
npm run build # tsup multi-entry CJS/ESM/DTS
|
|
478
|
+
npm test # vitest run (221 tests, 23 files)
|
|
479
|
+
npm run test:watch # vitest watch
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## License
|
|
483
|
+
|
|
484
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/hooks/useScrollReveal.ts
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
function useScrollReveal(opts) {
|
|
4
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
5
|
+
const once = opts?.once ?? true;
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (typeof window === "undefined") return;
|
|
10
|
+
if (typeof globalThis.IntersectionObserver === "undefined") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!el) return;
|
|
15
|
+
const observer = new IntersectionObserver(
|
|
16
|
+
(entries) => {
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.isIntersecting) {
|
|
19
|
+
setIsVisible(true);
|
|
20
|
+
if (once) observer.unobserve(entry.target);
|
|
21
|
+
} else if (!once) {
|
|
22
|
+
setIsVisible(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{ threshold }
|
|
27
|
+
);
|
|
28
|
+
observer.observe(el);
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}, [threshold, once]);
|
|
31
|
+
return { ref, isVisible };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/hooks/useGalleryLightbox.ts
|
|
35
|
+
import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
|
|
36
|
+
function useGalleryLightbox(images) {
|
|
37
|
+
const [currentIndex, setCurrentIndex] = useState2(-1);
|
|
38
|
+
const isOpen = currentIndex >= 0 && currentIndex < images.length;
|
|
39
|
+
const current = isOpen ? images[currentIndex] ?? null : null;
|
|
40
|
+
const open = useCallback(
|
|
41
|
+
(index) => {
|
|
42
|
+
if (images.length === 0) return;
|
|
43
|
+
if (index < 0 || index >= images.length) return;
|
|
44
|
+
setCurrentIndex(index);
|
|
45
|
+
},
|
|
46
|
+
[images.length]
|
|
47
|
+
);
|
|
48
|
+
const close = useCallback(() => {
|
|
49
|
+
setCurrentIndex(-1);
|
|
50
|
+
}, []);
|
|
51
|
+
const next = useCallback(() => {
|
|
52
|
+
if (images.length === 0) return;
|
|
53
|
+
setCurrentIndex((prev2) => prev2 < 0 ? prev2 : (prev2 + 1) % images.length);
|
|
54
|
+
}, [images.length]);
|
|
55
|
+
const prev = useCallback(() => {
|
|
56
|
+
if (images.length === 0) return;
|
|
57
|
+
setCurrentIndex((p) => p < 0 ? p : (p - 1 + images.length) % images.length);
|
|
58
|
+
}, [images.length]);
|
|
59
|
+
useEffect2(() => {
|
|
60
|
+
if (!isOpen) return;
|
|
61
|
+
if (typeof window === "undefined") return;
|
|
62
|
+
const handler = (e) => {
|
|
63
|
+
if (e.key === "Escape") close();
|
|
64
|
+
else if (e.key === "ArrowLeft") prev();
|
|
65
|
+
else if (e.key === "ArrowRight") next();
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener("keydown", handler);
|
|
68
|
+
return () => {
|
|
69
|
+
window.removeEventListener("keydown", handler);
|
|
70
|
+
};
|
|
71
|
+
}, [isOpen, close, next, prev]);
|
|
72
|
+
return { isOpen, currentIndex: isOpen ? currentIndex : -1, current, open, close, next, prev };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
useScrollReveal,
|
|
77
|
+
useGalleryLightbox
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=chunk-B3MQL6ZR.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/useScrollReveal.ts","../src/hooks/useGalleryLightbox.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\n/** IntersectionObserver 기반 스크롤 reveal hook.\n *\n * - SSR safe: `typeof window === \"undefined\"` 이면 effect 가 nothing.\n * - jsdom 등 `IntersectionObserver` 미정의 환경에서도 effect 가 nothing (가드).\n * - `once: true` (default) — 한번 visible 후 unobserve, 이후 다시 false 로 돌아가지 않음.\n * - `once: false` — visible 진입/이탈에 따라 toggle.\n *\n * 반환되는 ref 는 React 19 의 `RefObject<HTMLElement | null>` 시그니처 — host 가 어떤\n * 구체 element 타입에든 마운트 가능하도록 광폭 타입을 둔다.\n */\nexport function useScrollReveal(opts?: { threshold?: number; once?: boolean }): {\n ref: React.RefObject<HTMLElement | null>;\n isVisible: boolean;\n} {\n const threshold = opts?.threshold ?? 0.1;\n const once = opts?.once ?? true;\n const ref = useRef<HTMLElement | null>(null);\n const [isVisible, setIsVisible] = useState(false);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (typeof (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver === \"undefined\") {\n return;\n }\n const el = ref.current;\n if (!el) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n setIsVisible(true);\n if (once) observer.unobserve(entry.target);\n } else if (!once) {\n setIsVisible(false);\n }\n }\n },\n { threshold },\n );\n\n observer.observe(el);\n return () => observer.disconnect();\n }, [threshold, once]);\n\n return { ref, isVisible };\n}\n","import { useCallback, useEffect, useState } from \"react\";\n\nexport interface LightboxImage {\n src: string;\n alt?: string;\n}\n\nexport interface UseGalleryLightboxReturn {\n isOpen: boolean;\n currentIndex: number;\n current: LightboxImage | null;\n open: (index: number) => void;\n close: () => void;\n next: () => void;\n prev: () => void;\n}\n\n/** headless 갤러리 라이트박스. open 상태에서 ESC / ←/→ 키바인딩 자동 등록.\n *\n * - 이미지 0개일 때 `open` 호출은 no-op.\n * - `next` / `prev` 는 wrap-around (마지막 다음 = 처음).\n * - SSR safe: window keydown listener 는 isOpen && typeof window !== \"undefined\" 일 때만 등록.\n */\nexport function useGalleryLightbox(images: LightboxImage[]): UseGalleryLightboxReturn {\n const [currentIndex, setCurrentIndex] = useState(-1);\n const isOpen = currentIndex >= 0 && currentIndex < images.length;\n const current = isOpen ? images[currentIndex] ?? null : null;\n\n const open = useCallback(\n (index: number) => {\n if (images.length === 0) return;\n if (index < 0 || index >= images.length) return;\n setCurrentIndex(index);\n },\n [images.length],\n );\n\n const close = useCallback(() => {\n setCurrentIndex(-1);\n }, []);\n\n const next = useCallback(() => {\n if (images.length === 0) return;\n setCurrentIndex((prev) => (prev < 0 ? prev : (prev + 1) % images.length));\n }, [images.length]);\n\n const prev = useCallback(() => {\n if (images.length === 0) return;\n setCurrentIndex((p) => (p < 0 ? p : (p - 1 + images.length) % images.length));\n }, [images.length]);\n\n useEffect(() => {\n if (!isOpen) return;\n if (typeof window === \"undefined\") return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") close();\n else if (e.key === \"ArrowLeft\") prev();\n else if (e.key === \"ArrowRight\") next();\n };\n window.addEventListener(\"keydown\", handler);\n return () => {\n window.removeEventListener(\"keydown\", handler);\n };\n }, [isOpen, close, next, prev]);\n\n return { isOpen, currentIndex: isOpen ? currentIndex : -1, current, open, close, next, prev };\n}\n"],"mappings":";AAAA,SAAS,WAAW,QAAQ,gBAAgB;AAYrC,SAAS,gBAAgB,MAG9B;AACA,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,OAAO,MAAM,QAAQ;AAC3B,QAAM,MAAM,OAA2B,IAAI;AAC3C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,YAAU,MAAM;AACd,QAAI,OAAO,WAAW,YAAa;AACnC,QAAI,OAAQ,WAAkD,yBAAyB,aAAa;AAClG;AAAA,IACF;AACA,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AAET,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,cAAI,MAAM,gBAAgB;AACxB,yBAAa,IAAI;AACjB,gBAAI,KAAM,UAAS,UAAU,MAAM,MAAM;AAAA,UAC3C,WAAW,CAAC,MAAM;AAChB,yBAAa,KAAK;AAAA,UACpB;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,UAAU;AAAA,IACd;AAEA,aAAS,QAAQ,EAAE;AACnB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,WAAW,IAAI,CAAC;AAEpB,SAAO,EAAE,KAAK,UAAU;AAC1B;;;AChDA,SAAS,aAAa,aAAAA,YAAW,YAAAC,iBAAgB;AAuB1C,SAAS,mBAAmB,QAAmD;AACpF,QAAM,CAAC,cAAc,eAAe,IAAIA,UAAS,EAAE;AACnD,QAAM,SAAS,gBAAgB,KAAK,eAAe,OAAO;AAC1D,QAAM,UAAU,SAAS,OAAO,YAAY,KAAK,OAAO;AAExD,QAAM,OAAO;AAAA,IACX,CAAC,UAAkB;AACjB,UAAI,OAAO,WAAW,EAAG;AACzB,UAAI,QAAQ,KAAK,SAAS,OAAO,OAAQ;AACzC,sBAAgB,KAAK;AAAA,IACvB;AAAA,IACA,CAAC,OAAO,MAAM;AAAA,EAChB;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,oBAAgB,EAAE;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO,YAAY,MAAM;AAC7B,QAAI,OAAO,WAAW,EAAG;AACzB,oBAAgB,CAACC,UAAUA,QAAO,IAAIA,SAAQA,QAAO,KAAK,OAAO,MAAO;AAAA,EAC1E,GAAG,CAAC,OAAO,MAAM,CAAC;AAElB,QAAM,OAAO,YAAY,MAAM;AAC7B,QAAI,OAAO,WAAW,EAAG;AACzB,oBAAgB,CAAC,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,UAAU,OAAO,MAAO;AAAA,EAC9E,GAAG,CAAC,OAAO,MAAM,CAAC;AAElB,EAAAF,WAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,QAAI,OAAO,WAAW,YAAa;AACnC,UAAM,UAAU,CAAC,MAAqB;AACpC,UAAI,EAAE,QAAQ,SAAU,OAAM;AAAA,eACrB,EAAE,QAAQ,YAAa,MAAK;AAAA,eAC5B,EAAE,QAAQ,aAAc,MAAK;AAAA,IACxC;AACA,WAAO,iBAAiB,WAAW,OAAO;AAC1C,WAAO,MAAM;AACX,aAAO,oBAAoB,WAAW,OAAO;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,QAAQ,OAAO,MAAM,IAAI,CAAC;AAE9B,SAAO,EAAE,QAAQ,cAAc,SAAS,eAAe,IAAI,SAAS,MAAM,OAAO,MAAM,KAAK;AAC9F;","names":["useEffect","useState","prev"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/hooks/useImageDropZone.ts
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
var DEFAULT_ACCEPT = ["image/jpeg", "image/png", "image/webp"];
|
|
4
|
+
var DEFAULT_MAX_SIZE = 10 * 1024 * 1024;
|
|
5
|
+
function useImageDropZone(opts) {
|
|
6
|
+
const {
|
|
7
|
+
onFiles,
|
|
8
|
+
accept = DEFAULT_ACCEPT,
|
|
9
|
+
maxSize = DEFAULT_MAX_SIZE,
|
|
10
|
+
multiple = true,
|
|
11
|
+
validate,
|
|
12
|
+
disabled = false
|
|
13
|
+
} = opts;
|
|
14
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
15
|
+
const [rejectedReasons, setRejectedReasons] = useState([]);
|
|
16
|
+
const dragCounter = useRef(0);
|
|
17
|
+
const processFiles = useCallback(
|
|
18
|
+
(incoming) => {
|
|
19
|
+
if (disabled) return;
|
|
20
|
+
const accepted = [];
|
|
21
|
+
const rejected = [];
|
|
22
|
+
const slice = multiple ? incoming : incoming.slice(0, 1);
|
|
23
|
+
for (const file of slice) {
|
|
24
|
+
if (accept.length > 0 && !accept.includes(file.type)) {
|
|
25
|
+
rejected.push(`${file.name}: \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD (${file.type || "unknown"})`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (file.size > maxSize) {
|
|
29
|
+
rejected.push(
|
|
30
|
+
`${file.name}: \uD30C\uC77C \uD06C\uAE30 \uCD08\uACFC (${(file.size / 1024 / 1024).toFixed(1)}MB > ${(maxSize / 1024 / 1024).toFixed(1)}MB)`
|
|
31
|
+
);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (validate) {
|
|
35
|
+
const result = validate(file);
|
|
36
|
+
if (!result.ok) {
|
|
37
|
+
rejected.push(`${file.name}: ${result.reason}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
accepted.push(file);
|
|
42
|
+
}
|
|
43
|
+
setRejectedReasons(rejected);
|
|
44
|
+
if (accepted.length > 0) {
|
|
45
|
+
void onFiles(accepted);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[accept, maxSize, multiple, validate, disabled, onFiles]
|
|
49
|
+
);
|
|
50
|
+
const onDragEnter = useCallback(
|
|
51
|
+
(e) => {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
if (disabled) return;
|
|
55
|
+
dragCounter.current += 1;
|
|
56
|
+
const types = e.dataTransfer?.types;
|
|
57
|
+
if (!types || (typeof types.includes === "function" ? types.includes("Files") : Array.from(types).includes("Files"))) {
|
|
58
|
+
setIsDragging(true);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[disabled]
|
|
62
|
+
);
|
|
63
|
+
const onDragOver = useCallback(
|
|
64
|
+
(e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
},
|
|
68
|
+
[]
|
|
69
|
+
);
|
|
70
|
+
const onDragLeave = useCallback(
|
|
71
|
+
(e) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
if (disabled) return;
|
|
75
|
+
dragCounter.current = Math.max(0, dragCounter.current - 1);
|
|
76
|
+
if (dragCounter.current === 0) {
|
|
77
|
+
setIsDragging(false);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
[disabled]
|
|
81
|
+
);
|
|
82
|
+
const onDrop = useCallback(
|
|
83
|
+
(e) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
dragCounter.current = 0;
|
|
87
|
+
setIsDragging(false);
|
|
88
|
+
if (disabled) return;
|
|
89
|
+
const incoming = Array.from(e.dataTransfer?.files ?? []);
|
|
90
|
+
processFiles(incoming);
|
|
91
|
+
},
|
|
92
|
+
[disabled, processFiles]
|
|
93
|
+
);
|
|
94
|
+
const onChange = useCallback(
|
|
95
|
+
(e) => {
|
|
96
|
+
if (disabled) return;
|
|
97
|
+
const incoming = Array.from(e.target.files ?? []);
|
|
98
|
+
processFiles(incoming);
|
|
99
|
+
try {
|
|
100
|
+
e.target.value = "";
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[disabled, processFiles]
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
isDragging,
|
|
108
|
+
dropProps: { onDragEnter, onDragOver, onDragLeave, onDrop },
|
|
109
|
+
inputProps: {
|
|
110
|
+
type: "file",
|
|
111
|
+
accept: accept.join(","),
|
|
112
|
+
multiple,
|
|
113
|
+
onChange
|
|
114
|
+
},
|
|
115
|
+
rejectedReasons
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
useImageDropZone
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=chunk-BC3TPJQY.mjs.map
|