@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/CLAUDE.md +145 -0
  3. 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.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.4",
45
- "@synerise/ds-toolbar": "^1.1.63",
46
- "@synerise/ds-tooltip": "^1.5.3",
47
- "@synerise/ds-utils": "^1.10.1",
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": "a81ab6519d49a3dea9c0cfebcdc9104cbb4f4226"
58
+ "gitHead": "d0a43cc43d8528a36f105aceea52ab470edb71d9"
58
59
  }