@synerise/ds-image 0.1.1 → 0.1.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 +4 -0
- package/CLAUDE.md +145 -0
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.1.2](https://github.com/Synerise/synerise-design/compare/@synerise/ds-image@0.1.1...@synerise/ds-image@0.1.2) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @synerise/ds-image
|
|
9
|
+
|
|
6
10
|
## [0.1.1](https://github.com/Synerise/synerise-design/compare/@synerise/ds-image@0.1.0...@synerise/ds-image@0.1.1) (2026-07-16)
|
|
7
11
|
|
|
8
12
|
**Note:** Version bump only for package @synerise/ds-image
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Image (`@synerise/ds-image`)
|
|
2
|
+
|
|
3
|
+
> Image components: inline **Thumbnail** tiles, a **Gallery** of thumbnails with
|
|
4
|
+
> uniform sizing, and a controlled full-screen **ImagePreview** (lightbox) with
|
|
5
|
+
> zoom/pan and a bottom-center navigation + zoom toolbar. Broken-image fallbacks
|
|
6
|
+
> are handled in one place (`useImageStatus` / `ImageContent`).
|
|
7
|
+
|
|
8
|
+
## Package structure
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
src/
|
|
12
|
+
index.ts — public named exports
|
|
13
|
+
shared/
|
|
14
|
+
Image.shared.types.ts — ImageSource, AspectRatio, ThumbnailSize, ThumbnailBackground, ObjectFit, InitialZoom, ImageTexts
|
|
15
|
+
useImageStatus.ts — single source of truth for load/error detection (handles cached `complete`)
|
|
16
|
+
useImageTexts.ts — react-intl defaults (DS.IMAGE.*) merged with the `texts` override
|
|
17
|
+
ImageContent.tsx — <img> + swap to fallback on error; styled per consumer; used by Thumbnail
|
|
18
|
+
ImageContent.styles.ts — default broken-image placeholder
|
|
19
|
+
Thumbnail/
|
|
20
|
+
Thumbnail.tsx — sized tile; resolves gallery context → own props → defaults
|
|
21
|
+
Thumbnail.types.ts — ThumbnailProps
|
|
22
|
+
Thumbnail.styles.ts — Tile (sizing+ring), Clip (clips image+overlay), ThumbnailImage, EmptyPlaceholder, HoverOverlay, DeleteButton
|
|
23
|
+
Thumbnail.const.ts — SIZE_MAP, ASPECT_RATIO_MAP, defaults
|
|
24
|
+
__specs__/Thumbnail.spec.tsx
|
|
25
|
+
Gallery/
|
|
26
|
+
Gallery.tsx — context provider + thumbnails + owns one ImagePreview
|
|
27
|
+
Gallery.types.ts — GalleryProps
|
|
28
|
+
Gallery.styles.ts — flex-wrap Container
|
|
29
|
+
__specs__/Gallery.spec.tsx
|
|
30
|
+
Preview/
|
|
31
|
+
ImagePreview.tsx — controlled lightbox (portal, zoom/pan, nav, download, focus trap)
|
|
32
|
+
ImagePreview.types.ts — ImagePreviewProps
|
|
33
|
+
ImagePreview.styles.ts — Overlay, ImageWrapper, Image, Fallback/FallbackBox, CloseWrapper, ToolbarWrapper
|
|
34
|
+
ImagePreview.const.ts — FIT_SCALE, DEFAULT_ZOOM_STEP (1.4), DEFAULT_MAX_ZOOM (3)
|
|
35
|
+
PreviewToolbar.tsx — bottom-center nav pill + zoom/download pill
|
|
36
|
+
__specs__/ImagePreview.spec.tsx
|
|
37
|
+
context/
|
|
38
|
+
ImageGalleryContext.ts — gallery-level settings; optional read (ListContext style)
|
|
39
|
+
hooks/
|
|
40
|
+
useImagePreview.ts — open/close/next/prev + previewProps to spread
|
|
41
|
+
__specs__/useImagePreview.spec.ts
|
|
42
|
+
Image.figma.tsx — Figma Code Connect (node-id 20563-10777; excluded from build)
|
|
43
|
+
modules.d.ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Public exports
|
|
47
|
+
|
|
48
|
+
`Thumbnail`, `Gallery`, `ImagePreview` (all default-exported from their modules,
|
|
49
|
+
re-exported as named) + `useImagePreview`. Types: `ThumbnailProps`, `GalleryProps`,
|
|
50
|
+
`ImagePreviewProps`, `UseImagePreviewReturn`, `ImageSource`, `AspectRatio`,
|
|
51
|
+
`ThumbnailSize`, `ThumbnailBackground`, `ObjectFit`, `InitialZoom`. There is **no
|
|
52
|
+
default export** — the family has no single "main" component.
|
|
53
|
+
|
|
54
|
+
See `packages/storybook/stories/components/Image/Image.mdx` for the full props
|
|
55
|
+
tables.
|
|
56
|
+
|
|
57
|
+
## Implementation notes
|
|
58
|
+
|
|
59
|
+
- **One image-loading core** — `useImageStatus(src)` returns `{ ref, status,
|
|
60
|
+
handleLoad, handleError }` and is the only place that decides "is this image
|
|
61
|
+
broken?": it resets to `loading` on `src` change and re-reads the element's
|
|
62
|
+
cached `complete`/`naturalWidth` (so a cached src-swap that doesn't fire
|
|
63
|
+
`onLoad` still resolves). `ImageContent` wraps it for `Thumbnail`; `ImagePreview`
|
|
64
|
+
uses the hook directly because it also measures natural size for zoom.
|
|
65
|
+
- **Gallery → Thumbnail via context** — `Gallery` broadcasts `aspectRatio`,
|
|
66
|
+
`size`, `height`, `background`, `objectFit`, `loading`, `fallback` through
|
|
67
|
+
`ImageGalleryContext`. Each `Thumbnail` resolves `ownProp ?? context ?? default`,
|
|
68
|
+
so a standalone `<Thumbnail>` still works. Mirrors `ListContext`.
|
|
69
|
+
- **Thumbnail box** — `size` maps to a fixed height (`SIZE_MAP`); a fixed
|
|
70
|
+
`aspectRatio` sets CSS `aspect-ratio` (width derived); `source` keeps the
|
|
71
|
+
image's intrinsic ratio (`width: auto`). `objectFit` is `contain` (default) or
|
|
72
|
+
`cover`. Interactive tiles (`openZoom`/`onClick`) are `role="button"`,
|
|
73
|
+
`tabIndex=0`, Enter/Space-activated (a `<div>`, not a `<button>`, so the delete
|
|
74
|
+
`<button>` can nest without invalid HTML).
|
|
75
|
+
- **Tile vs Clip** — `Tile` owns sizing (`height`/`aspect-ratio`), the focus ring,
|
|
76
|
+
and positioning but does **not** clip; the inner `Clip` (`overflow:hidden` +
|
|
77
|
+
radius + background) clips the image and hover overlay. This lets the delete
|
|
78
|
+
affordance overhang the corner without being cut off.
|
|
79
|
+
- **Thumbnail states** — *Hover* (interactive tiles): a full-cover `HoverOverlay`
|
|
80
|
+
(`grey-900` at ~30%, alpha `4D`) with a centered white `ShowS` icon, faded in on
|
|
81
|
+
`:hover`. *Focused* (`:focus-visible`): a 2px `blue-600` ring via outset
|
|
82
|
+
`box-shadow` (no layout shift; the ring is on the non-clipping `Tile`).
|
|
83
|
+
*Hover-deletable* (`deletable`): a bare `DeleteButton` (`<button>`, `red-600`)
|
|
84
|
+
rendering a `Close3FullBackgroundM` icon (white X on a red disc — the disc is the
|
|
85
|
+
icon's `currentColor` fill, driven by the button's `color`), whose **center sits
|
|
86
|
+
on the tile's top-right corner** (24×24, icon-sized — no extra clickable padding —
|
|
87
|
+
`translate(50%,-50%)`), revealed on hover/focus-within. Its click stops propagation (so it never opens the preview)
|
|
88
|
+
and blurs the button so the control doesn't linger via `:focus-within` after a
|
|
89
|
+
click. *Empty* (no `src`): the grey placeholder.
|
|
90
|
+
- **Preview is controlled** — `open` / `images` / `index` / `onIndexChange` /
|
|
91
|
+
`onClose`. Navigation (`‹ N of M ›`) shows only when `images.length > 1`; arrow
|
|
92
|
+
keys page (with wrap-around). `useImagePreview` owns this state for non-gallery
|
|
93
|
+
triggers; `Gallery` owns it internally for thumbnail clicks.
|
|
94
|
+
- **Zoom model** — RZPP scale 1 = the fitted view. `measureZoom` computes the
|
|
95
|
+
factor from the fitted display to natural size; `maxScale = factor * maxZoom`
|
|
96
|
+
(300% by default). The toolbar shows the percentage relative to natural
|
|
97
|
+
(`scale / factor * 100`). `initialZoom: 'real-size'` jumps to natural via
|
|
98
|
+
`centerView` on load; `'fit'` stays fitted. `zoomStep` is a multiplier (1.4).
|
|
99
|
+
Button zoom (`stepZoom`) steps on a stable multiplicative grid anchored at
|
|
100
|
+
natural size — levels are `naturalScaleFactor * zoomStep^k` clamped to
|
|
101
|
+
`[FIT_SCALE, maxScale]` — and applies the exact target scale via `centerView`,
|
|
102
|
+
so 100% is always a stop and zoom-in/out round-trip exactly. The wheel keeps
|
|
103
|
+
RZPP's continuous relative step (`zoomStep - 1`).
|
|
104
|
+
- **Preview spacing** — the image keeps a constant 32px gap from the viewport
|
|
105
|
+
edge (`PREVIEW_PADDING`); the close button and bottom toolbar sit 16px from the
|
|
106
|
+
edge (`CONTROLS_INSET`). The padding is **not** toggled off on zoom: it lives on
|
|
107
|
+
the wrapper RZPP observes, so changing it mid-zoom would resize the working
|
|
108
|
+
area, make RZPP re-fit, and corrupt the first zoom step. `measureZoom` therefore
|
|
109
|
+
computes the fit factor against the **content box** (`clientWidth - 2 *
|
|
110
|
+
PREVIEW_PADDING`), so the natural-size anchor (100%) is accurate.
|
|
111
|
+
- **Pan when zoomed** — panning is disabled at the fitted view and enabled once
|
|
112
|
+
zoomed (`panning.disabled = !isZoomed`), so the image can be dragged with the
|
|
113
|
+
mouse. `onPanningStart`/`onPanningStop` drive an `isPanning` flag that toggles
|
|
114
|
+
the pannable area's cursor between `grab` and `grabbing` for discoverability.
|
|
115
|
+
- **Bottom-center toolbar** — `PreviewToolbar` renders a nav `ToolbarGroup` and a
|
|
116
|
+
zoom/download `ToolbarGroup` (`@synerise/ds-toolbar`). The close button stays
|
|
117
|
+
top-right (`CloseWrapper`). The download control is a `ToolbarButton` rendered
|
|
118
|
+
as an anchor (`href`/`download`/`target="_blank"` forwarded to `<a>` by
|
|
119
|
+
`ds-button`) so it downloads same-origin images and opens cross-origin ones in
|
|
120
|
+
a new tab; it shows whenever a valid image is loaded (independent of zoom).
|
|
121
|
+
- **Error/Empty fallback** — broken `src` → `fallback` (per-image `ImageSource.fallback`
|
|
122
|
+
→ component `fallback` → default `ImageM` placeholder). No `src` on a Thumbnail →
|
|
123
|
+
the Empty placeholder.
|
|
124
|
+
- **Tooltips & i18n** — every icon control has a tooltip + `aria-label` resolved by
|
|
125
|
+
`useImageTexts` (`react-intl` `useIntl().formatMessage`, ids `DS.IMAGE.*`,
|
|
126
|
+
English defaults) merged with the `texts` override. `texts` flows `Gallery` →
|
|
127
|
+
context → `Thumbnail` (delete) and `Gallery`/`ImagePreview` → toolbar (zoom/nav/
|
|
128
|
+
download/close). Toolbar buttons use `ToolbarButton tooltipProps`; the custom
|
|
129
|
+
delete `<button>` is wrapped in `@synerise/ds-tooltip`.
|
|
130
|
+
- **Open/close fade** — the `Overlay` hides via `opacity`/`visibility`/`pointer-events`
|
|
131
|
+
(not `display:none`, which can't transition) with a ~160ms transition.
|
|
132
|
+
`destroyOnClose` still unmounts (returns `null`).
|
|
133
|
+
- **Origin** — built from the updated Figma "Image" spec (`node-id=20563-10777`).
|
|
134
|
+
|
|
135
|
+
## Key dependencies
|
|
136
|
+
|
|
137
|
+
- `react-zoom-pan-pinch` — preview zoom & pan engine.
|
|
138
|
+
- `@synerise/ds-icon` — `AngleLeftM`/`AngleRightM` (nav), `FormulaMinusM`/`FormulaPlusM`
|
|
139
|
+
(zoom), `FileDownloadM` (download), `CloseM` (close), `ShowS` (hover overlay),
|
|
140
|
+
`Close3FullBackgroundM` (delete), `ImageM` (fallback/empty).
|
|
141
|
+
- `@synerise/ds-toolbar` — `ToolbarGroup` / `ToolbarButton` (+ `tooltipProps`) / `ToolbarLabel`.
|
|
142
|
+
- `@synerise/ds-tooltip` — wraps the thumbnail delete button.
|
|
143
|
+
- `@synerise/ds-utils` — `useFocusTrap`.
|
|
144
|
+
- `react-intl` (peer) — control labels/tooltips via `useImageTexts`.
|
|
145
|
+
- `@synerise/ds-core` (peer) — theme palette + `box-shadow-*` variables.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synerise/ds-image",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Image UI Component for the Synerise Design System",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"repository": "Synerise/synerise-design",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"/dist",
|
|
19
19
|
"CHANGELOG.md",
|
|
20
|
+
"CLAUDE.md",
|
|
20
21
|
"README.md",
|
|
21
22
|
"package.json",
|
|
22
23
|
"LICENSE.md"
|
|
@@ -41,10 +42,10 @@
|
|
|
41
42
|
],
|
|
42
43
|
"types": "dist/index.d.ts",
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@synerise/ds-icon": "^1.18.
|
|
45
|
-
"@synerise/ds-toolbar": "^1.1.
|
|
46
|
-
"@synerise/ds-tooltip": "^1.5.
|
|
47
|
-
"@synerise/ds-utils": "^1.10.
|
|
45
|
+
"@synerise/ds-icon": "^1.18.5",
|
|
46
|
+
"@synerise/ds-toolbar": "^1.1.64",
|
|
47
|
+
"@synerise/ds-tooltip": "^1.5.4",
|
|
48
|
+
"@synerise/ds-utils": "^1.10.2",
|
|
48
49
|
"react-zoom-pan-pinch": "^3.6.1"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"styled-components": "^5.3.3",
|
|
55
56
|
"vitest": "4"
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "d0a43cc43d8528a36f105aceea52ab470edb71d9"
|
|
58
59
|
}
|