Пакети / Зображення
@tykonpm/image-react
v0.5.1<TykoImage>, вікно бібліотеки зображень, перегляд на весь екран із zoom і жестами.
Read this before using the packages. Every package README (shipped in node_modules/@tykonpm/*/README.md) repeats its own part.
What it is
Small, framework-light toolkit for **responsive image presets**: decide which preview widths to make, generate them in the browser, render <img srcset>, and pick images from a folder library. **Storage is not included** — you plug your own via adapters.
| Package | Purpose | Runs in |
|---|---|---|
@tykonpm/image-core | Pure rules: widths, variant choice, srcset, paths, adapter types, folder tree, in-memory library | anywhere (browser, SSR, Workers, Node) |
@tykonpm/image-react | <TykoImage>, <ImageLibraryDialog> | React ≥18, client |
@tykonpm/image-browser | Resize files into variants via canvas | browser only |
Principles (hard guarantees)
- **Never crops.**
TykoImageand library thumbnails default toobject-fit: contain.fit="cover"is opt-in only. - **Never upscales.** Widths larger than the original are skipped; small originals are served as-is.
- **No backend dependency.** No Supabase/S3 SDK; no network calls of its own.
- **No bundled images.**
- Default preset widths:
160, 320, 800, 1600(override per call). Originals under320pxare flagged "small" (isSmallOriginal).
Data model
ImageEntry { src: string; originalWidth?: number; variants: { width: number; url: string }[] }
ImageManifest Record<src, ImageEntry>
MediaFolder { id; name; parentId: string | null }
MediaFile { id; name; url; folderId: string | null; width?; height? }Adapters you implement
interface ImageStorageAdapter { // variants for TykoImage
upload(path, data: Blob, contentType): Promise<string>; // returns public URL
saveVariants(src, entry: ImageEntry): Promise<void>;
loadVariants(srcs: string[]): Promise<ImageManifest>;
remove?(urls: string[]): Promise<void>; // optional, for replaceVariants
}
interface MediaLibraryAdapter { // ImageLibraryDialog
listFolders(): Promise<MediaFolder[]>;
listFiles(folderId | null): Promise<MediaFile[]>; // null = ALL files
upload(file: Blob, name, folderId | null): Promise<MediaFile>;
addUrl?(url, name, folderId | null): Promise<MediaFile>; // optional
}
Typical managed-backend implementation: a public storage bucket + tables media_folders(id, name, parent_id) and media_files(id, name, url, folder_id, width, height). Variant path convention: variantPath(src, w) → /thumbs/{w}/{name}.webp.
Typical flow
1. User picks/uploads in <ImageLibraryDialog prepare={...}>.
2. resizeToVariants(file) → WebP blobs at planned widths.
3. Upload each via your adapter, save ImageEntry.
4. Render with <TykoImage entry={entry} width={displayPx} alt="…" />.
API cheat sheet
- core:
planWidths(orig, widths?),pickVariant(entry, displayW, dpr?),buildSrcSet(entry),variantPath(src, w, base?, ext?),isSmallOriginal(w),folderTree(folders),createMemoryLibrary(folders?, files?),publishVariants(storage, src, { blob, width }, variants, opts?),createMemoryStorage(),DEFAULT_WIDTHS,SMALL_ORIGINAL_WIDTH; typesVariant,ImageEntry,ImageManifest,VariantBlob,PublishOptions. Also:normalizePreset({ widths?, quality?, type?, auto? })→{ config, issues }(invalid values → safe defaults,DEFAULT_PRESET);missingWidths(entry, widths?);mergeVariants(entry, add);runBatch(items, job, { concurrency?, signal?, onProgress?, id? })→{ ok, failed, skipped, cancelled };describeImage(entry, { displayWidth, dpr }?, widths?). - react:
<TykoImage entry? src? width priority? placeholder? fit? sizes?>— no entry yet →srcoriginal or placeholder; broken variant → original;priority= eager + fetchpriority high;useImageManifest(loader, srcs, { bypassCache? })→{ manifest, loading, error };<TykoImage entry width fit? sizes? …img props>(lazy + async decoding by default);<ImageLibraryDialog open onClose onSelect adapter prepare? labels? className? narrowBelow?=640>(title?is deprecated →labels.title).<ImageInfo entry displayWidth? dpr? widths? decoded? labels?>(optional info panel,IMAGE_INFO_LABELS_UK/EN);<Lightbox items={[{ entry?, src?, alt, caption? }]} index open onClose onIndexChange labels? maxZoom?=5>— fullscreen viewer: wheel/trackpad/pinch zoom anchored at the pointer, double-tap 2.5×, drag pan, swipe prev/next, swipe down closes, keys ←/→/Esc/+/−/0, Fullscreen API button, focus trap/restore,LIGHTBOX_LABELS_UK/EN; best variant at 1×, original when zoomed; never crops. - browser:
resizeToVariants(file, { widths?, type?='image/webp', quality?=0.82 })→{width,height,blob}[];fitWidth(size, w);readImageSize(file);resizeOne(file, width, opts?)(one size);probeImage(url)→ real decoded size ornull(= unknown).
Full preset cycle
resizeToVariants(file, { widths: [160,320,800,1600] }) (image-browser) → publishVariants(storage, src, { blob: file, width }, variants) (image-core) uploads original + all variants, then calls storage.saveVariants; nothing is saved if any upload fails. Render with <TykoImage entry width> — it picks the best variant for the display width × DPR. For demos/tests swap your storage for createMemoryStorage() (object URLs, data lost on reload); the rest of the code is unchanged.
ImageLibraryDialog behaviour
Loading / error states with retry; stale list responses ignored; repeated upload blocked while busy; below narrowBelow (640px dialog width) it becomes full-screen with one column (folders → files, back button); focus is trapped, Escape closes, focus returns to the opener. All texts via labels (LIBRARY_LABELS_UK default, LIBRARY_LABELS_EN exported).
Shared manifest cache (opt-in)
const loader = createManifestLoader(storage, { maxEntries: 500, missingTtlMs: 60_000 });
await loader.load(["/a.jpg", "/b.jpg"]); // one loadVariants call for all uncached srcs
await loader.load(["/a.jpg"], { bypassCache: true }); // force fresh read (check presets)
loader.set(src, entry); loader.invalidate(src);
// React: const { manifest, loading, error } = useImageManifest(loader, visibleSrcs);
LRU eviction; concurrent loads of the same src share one request; "not found" remembered only for missingTtlMs; failed requests are not cached. Create ONE loader per app (module scope) and pass only visible/needed srcs.
Safe regeneration (opt-in)
replaceVariants(storage, src, { url, width }, variants, { previous, version? }) → { entry, leftovers }. Uploads new variants under versioned names (/thumbs/{w}/{name}.{version}.webp), saves the entry, and ONLY THEN removes previous variants. If an upload or save fails, newly uploaded files are removed and the old entry stays. The original is never uploaded or changed. Needs optional storage.remove(urls); without it old URLs are returned in leftovers. publishVariants is unchanged.
Point / bulk generation (opt-in)
const need = missingWidths(entry); // "create missing copies"
const report = await runBatch(selected, async (photo, signal) => {
const v = await resizeOne(photo.file, 320); // or resizeToVariants(file, { widths: need })
const url = await storage.upload(variantPath(photo.src, v.width), v.blob, "image/webp");
await storage.saveVariants(photo.src, mergeVariants(photo.entry, [{ width: v.width, url }]));
}, { signal: abort.signal, onProgress: setProgress, id: (p) => p.src });
// report.failed → show errors; report.skipped → cancelled ones
One failed photo never stops the batch. Unreachable files are "unknown" (probeImage → null), not "missing".
Limitations / gotchas
image-browserneedscreateImageBitmap+OffscreenCanvas(modern Chrome/Edge/Firefox, Safari ≥16.4). **Do not call during SSR** — only in event handlers/effects.- Resizing does not read EXIF orientation explicitly; modern browsers apply it in
createImageBitmap. HEIC is not supported by browsers. - EXIF/metadata is stripped by re-encoding (good for privacy, loses camera data).
- No server-side resizing; no
image-cliyet (planned). ImageLibraryDialog: one level of folder filtering (selecting a parent does NOT include child folders' files); no search, rename, delete, or pagination — load lists in your adapter; no multi-select (picks one file). Labels default to Ukrainian — passlabelsfor other languages.- Dialog styling is inline; theme with CSS vars
--tyko-bg,--tyko-fg,--tyko-muted,--tyko-border. It does not use Tailwind/shadcn. createMemoryLibraryis for demos/tests only — data disappears on reload.TykoImageneedswidth(CSS px) to choose fallbacksrcand defaultsizes; give accuratesizesfor fluid layouts.- Versions < 1.0: minor releases may change APIs; check CHANGELOG.md in each package.
Install / update in an AI platform
Chat: "Install @tykonpm/image-core, @tykonpm/image-react and @tykonpm/image-browser from npm and follow docs/AI-GUIDE.md from github.com/tykonpm/tyko-packages." Update: "Update @tykonpm/* to latest."