@synerise/ds-image 0.1.5 → 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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
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
+ # [2.0.0](https://github.com/Synerise/synerise-design/compare/@synerise/ds-image@0.2.0...@synerise/ds-image@2.0.0) (2026-08-26)
7
+
8
+ **Note:** Version bump only for package @synerise/ds-image
9
+
10
+ # [0.2.0](https://github.com/Synerise/synerise-design/compare/@synerise/ds-image@0.1.5...@synerise/ds-image@0.2.0) (2026-08-25)
11
+
12
+ ### Features
13
+
14
+ - **image:** stack the preview through the DS overlay z-index ([573932c](https://github.com/Synerise/synerise-design/commit/573932c6197c4e9613265cb17f117326f5fb6a71))
15
+
6
16
  ## [0.1.5](https://github.com/Synerise/synerise-design/compare/@synerise/ds-image@0.1.4...@synerise/ds-image@0.1.5) (2026-08-18)
7
17
 
8
18
  **Note:** Version bump only for package @synerise/ds-image
package/CLAUDE.md CHANGED
@@ -87,6 +87,13 @@ tables.
87
87
  `translate(50%,-50%)`), revealed on hover/focus-within. Its click stops propagation (so it never opens the preview)
88
88
  and blurs the button so the control doesn't linger via `:focus-within` after a
89
89
  click. *Empty* (no `src`): the grey placeholder.
90
+ - **Overlay z-index** — resolved through `useResolvedOverlayZIndex` from `ds-core`,
91
+ the same path `ds-modal` / `ds-drawer` take: an explicit `zIndex` prop wins,
92
+ otherwise one step above the enclosing overlay (read from React context, since
93
+ every overlay portals to `body`), otherwise the `zindex-modal` token. The prop
94
+ is the escape hatch for a trigger that sits outside the DS scale — an app-level
95
+ surface pinned above `zindex-tooltip` needs it, because no DS token reaches
96
+ that high. `Gallery` forwards it to the preview it owns.
90
97
  - **Preview is controlled** — `open` / `images` / `index` / `onIndexChange` /
91
98
  `onClose`. Navigation (`‹ N of M ›`) shows only when `images.length > 1`; arrow
92
99
  keys page (with wrap-around). `useImagePreview` owns this state for non-gallery
@@ -142,4 +149,5 @@ tables.
142
149
  - `@synerise/ds-tooltip` — wraps the thumbnail delete button.
143
150
  - `@synerise/ds-utils` — `useFocusTrap`.
144
151
  - `react-intl` (peer) — control labels/tooltips via `useImageTexts`.
145
- - `@synerise/ds-core` (peer) — theme palette + `box-shadow-*` variables.
152
+ - `@synerise/ds-core` (peer) — theme palette, `box-shadow-*` variables and
153
+ `useResolvedOverlayZIndex` (overlay stacking).
package/README.md CHANGED
@@ -51,5 +51,67 @@ const Example = () => {
51
51
  };
52
52
  ```
53
53
 
54
- See the Storybook overview page for full props tables, behavior notes, and
54
+ ## API
55
+
56
+ ### Thumbnail
57
+
58
+ | Property | Description | Type | Default |
59
+ | ----------- | --------------------------------------------------------------------------------- | -------------------------------- | --------- |
60
+ | src | Image source URL. Omit it to render the Empty state. | string | --- |
61
+ | alt | Alternative text. Required for accessibility. | string | --- |
62
+ | aspectRatio | Aspect ratio of the tile. `source` keeps the image's intrinsic ratio. | '1:1' / '4:3' / '16:9' / 'source' | `'1:1'` |
63
+ | size | Height token. `custom` defers to `height`. | 'custom' / 'xxs' … 'xxl' | `'m'` |
64
+ | height | Explicit height in px, used when `size` is `custom`. | number | --- |
65
+ | background | Tile backdrop. | 'none' / 'subtle-grey' | `'none'` |
66
+ | objectFit | How the image fills the tile. | 'contain' / 'cover' | `'contain'` |
67
+ | loading | Native loading hint. | 'lazy' / 'eager' | `'lazy'` |
68
+ | deletable | Show a delete affordance on hover / focus. | boolean | `false` |
69
+ | onDelete | Called when the delete affordance is activated. | () => void | --- |
70
+ | openZoom | Render the tile as an interactive trigger (e.g. opens a preview). | boolean | `false` |
71
+ | onClick | Click handler, fired for mouse and keyboard (Enter / Space) activation. | (event: MouseEvent) => void | --- |
72
+ | fallback | Rendered when the image fails to load. | React.ReactNode | broken-image box |
73
+ | texts | Override the delete tooltip / accessible label. | Partial<ImageTexts> | DS translations |
74
+ | className | Class applied to the tile. | string | --- |
75
+
76
+ Every value except `src`, `alt`, `onDelete` and `onClick` falls back to the
77
+ enclosing `Gallery` before the default listed above.
78
+
79
+ ### Gallery
80
+
81
+ | Property | Description | Type | Default |
82
+ | ------------------------------------------------- | ------------------------------------------------------------- | --------------------------- | ------- |
83
+ | images | Images rendered as thumbnails and paged through in the preview. | ImageSource[] | --- |
84
+ | aspectRatio / size / height / background | Applied to every thumbnail. | see Thumbnail | see Thumbnail |
85
+ | objectFit / loading / fallback | Applied to every thumbnail. | see Thumbnail | see Thumbnail |
86
+ | deletable | Show a delete affordance on each thumbnail. | boolean | `false` |
87
+ | onDelete | Called with the index of the thumbnail whose delete fired. | (index: number) => void | --- |
88
+ | openZoom | Whether clicking a thumbnail opens the preview. | boolean | `true` |
89
+ | zoomable / zoomStep / maxZoom / initialZoom | Forwarded to the preview it owns. | see ImagePreview | see ImagePreview |
90
+ | getContainer / zIndex | Forwarded to the preview it owns. | see ImagePreview | see ImagePreview |
91
+ | texts | Override tooltips / accessible labels for thumbnails and the preview. | Partial<ImageTexts> | DS translations |
92
+ | className | Class applied to the gallery container. | string | --- |
93
+
94
+ ### ImagePreview
95
+
96
+ | Property | Description | Type | Default |
97
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------- | ---------------- |
98
+ | open | Whether the viewer is visible. | boolean | --- |
99
+ | images | Images to page through. Navigation shows only when there is more than one. | ImageSource[] | --- |
100
+ | index | Controlled index of the currently shown image. | number | --- |
101
+ | onIndexChange | Called when navigation (arrows / keyboard) requests a different image. | (index: number) => void | --- |
102
+ | onClose | Called on Escape, backdrop click and the close button. | () => void | --- |
103
+ | zoomable | Whether zoom and pan are enabled. | boolean | `true` |
104
+ | zoomStep | Per-click zoom multiplier. | number | `1.4` |
105
+ | maxZoom | Maximum zoom relative to the image's natural size. | number | `3` |
106
+ | initialZoom | Where zoom starts. | 'fit' / 'real-size' | `'fit'` |
107
+ | closable | Whether to render the close button and allow Escape to close. | boolean | `true` |
108
+ | maskClosable | Whether clicking the dimmed backdrop closes the viewer. | boolean | `true` |
109
+ | zIndex | z-index of the overlay. Omitted, it stacks one step above the enclosing modal / drawer. Set it to opt out of the DS stack. | number | `zindex-modal` |
110
+ | fallback | Rendered when an image fails to load (per-image override via `ImageSource.fallback`). | React.ReactNode | broken-image box |
111
+ | getContainer | Portal target. | () => HTMLElement | `document.body` |
112
+ | destroyOnClose | Unmount the viewer (and reset zoom state) when closed. | boolean | `false` |
113
+ | onZoom | Called with the current scale whenever the zoom transform changes. | (scale: number) => void | --- |
114
+ | texts | Override the control tooltips / accessible labels. | Partial<ImageTexts> | DS translations |
115
+
116
+ See the Storybook overview page for behavior notes, live examples and
55
117
  accessibility guidance.
@@ -1,3 +1,3 @@
1
1
  import { GalleryProps } from './Gallery.types';
2
- declare const Gallery: ({ images, aspectRatio, size, height, background, objectFit, loading, fallback, deletable, onDelete, openZoom, zoomable, zoomStep, maxZoom, initialZoom, getContainer, texts, className, }: GalleryProps) => JSX.Element;
2
+ declare const Gallery: ({ images, aspectRatio, size, height, background, objectFit, loading, fallback, deletable, onDelete, openZoom, zoomable, zoomStep, maxZoom, initialZoom, getContainer, zIndex, texts, className, }: GalleryProps) => JSX.Element;
3
3
  export default Gallery;
@@ -21,6 +21,7 @@ const Gallery = ({
21
21
  maxZoom,
22
22
  initialZoom,
23
23
  getContainer,
24
+ zIndex,
24
25
  texts,
25
26
  className
26
27
  }) => {
@@ -37,7 +38,7 @@ const Gallery = ({
37
38
  }), [aspectRatio, size, height, background, objectFit, loading, fallback, texts]);
38
39
  return /* @__PURE__ */ jsxs(ImageGalleryContext.Provider, { value: contextValue, children: [
39
40
  /* @__PURE__ */ jsx(Container, { className, "data-testid": "image-gallery", children: images.map((image, imageIndex) => /* @__PURE__ */ jsx(Thumbnail, { src: image.src, alt: image.alt, fallback: image.fallback, deletable, onDelete: onDelete ? () => onDelete(imageIndex) : void 0, openZoom, onClick: openZoom ? () => setPreviewIndex(imageIndex) : void 0 }, `${image.src}-${imageIndex}`)) }),
40
- openZoom && /* @__PURE__ */ jsx(ImagePreview, { open: previewIndex !== null, images, index: previewIndex ?? 0, onIndexChange: setPreviewIndex, onClose: () => setPreviewIndex(null), zoomable, zoomStep, maxZoom, initialZoom, fallback, getContainer, texts })
41
+ openZoom && /* @__PURE__ */ jsx(ImagePreview, { open: previewIndex !== null, images, index: previewIndex ?? 0, onIndexChange: setPreviewIndex, onClose: () => setPreviewIndex(null), zoomable, zoomStep, maxZoom, initialZoom, fallback, getContainer, zIndex, texts })
41
42
  ] });
42
43
  };
43
44
  export {
@@ -33,6 +33,8 @@ export type GalleryProps = {
33
33
  initialZoom?: InitialZoom;
34
34
  /** Preview pass-through — portal target. */
35
35
  getContainer?: () => HTMLElement;
36
+ /** Preview pass-through — z-index of the preview overlay. */
37
+ zIndex?: number;
36
38
  /** Override tooltip / accessible labels for thumbnails and the preview. */
37
39
  texts?: Partial<ImageTexts>;
38
40
  className?: string;
@@ -1,4 +1,4 @@
1
1
  import { default as React } from 'react';
2
2
  import { ImagePreviewProps } from './ImagePreview.types';
3
- declare const ImagePreview: ({ open, images, index, onIndexChange, onClose, zoomable, zoomStep, maxZoom, initialZoom, closable, maskClosable, fallback, getContainer, destroyOnClose, onZoom, texts: textsProp, }: ImagePreviewProps) => React.ReactPortal | null;
3
+ declare const ImagePreview: ({ open, images, index, onIndexChange, onClose, zoomable, zoomStep, maxZoom, initialZoom, closable, maskClosable, fallback, getContainer, destroyOnClose, onZoom, texts: textsProp, zIndex, }: ImagePreviewProps) => React.ReactPortal | null;
4
4
  export default ImagePreview;
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect, useCallback } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
5
+ import { useResolvedOverlayZIndex } from "@synerise/ds-core";
5
6
  import Icon, { ImageM, CloseM } from "@synerise/ds-icon";
6
7
  import { ToolbarGroup, ToolbarButton } from "@synerise/ds-toolbar";
7
8
  import { useFocusTrap } from "@synerise/ds-utils";
@@ -39,9 +40,11 @@ const ImagePreview = ({
39
40
  getContainer,
40
41
  destroyOnClose = false,
41
42
  onZoom,
42
- texts: textsProp
43
+ texts: textsProp,
44
+ zIndex
43
45
  }) => {
44
46
  const texts = useImageTexts(textsProp);
47
+ const resolvedZIndex = useResolvedOverlayZIndex(zIndex);
45
48
  const overlayRef = useRef(null);
46
49
  const workingAreaRef = useRef(null);
47
50
  const transformRef = useRef(null);
@@ -178,7 +181,7 @@ const ImagePreview = ({
178
181
  const showDownload = !hasError && Boolean(currentSrc);
179
182
  const image = /* @__PURE__ */ jsx(Image, { ref: imgRef, src: currentSrc, alt: currentImage?.alt ?? "", onLoad: handleImageLoad, onError: handleError, onClick: stopPropagation, draggable: false, "data-testid": "image-preview-image" }, index);
180
183
  const content = hasError ? /* @__PURE__ */ jsx(Fallback, { onClick: stopPropagation, "data-testid": "image-preview-fallback", children: currentImage?.fallback ?? fallback ?? /* @__PURE__ */ jsx(FallbackBox, { children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(ImageM, {}) }) }) }) : image;
181
- return createPortal(/* @__PURE__ */ jsxs(Overlay, { ref: overlayRef, role: "dialog", "aria-modal": true, "aria-label": currentImage?.alt, tabIndex: -1, $hidden: !open, onMouseDown: handleOverlayPointerDown, onClick: handleMaskClick, "data-testid": "image-preview", children: [
184
+ return createPortal(/* @__PURE__ */ jsxs(Overlay, { ref: overlayRef, role: "dialog", "aria-modal": true, "aria-label": currentImage?.alt, tabIndex: -1, $hidden: !open, $zIndex: resolvedZIndex, onMouseDown: handleOverlayPointerDown, onClick: handleMaskClick, "data-testid": "image-preview", children: [
182
185
  /* @__PURE__ */ jsx(ImageWrapper, { ref: workingAreaRef, children: zoomable && !hasError ? /* @__PURE__ */ jsx(TransformWrapper, { ref: transformRef, minScale: FIT_SCALE, maxScale, initialScale: FIT_SCALE, centerOnInit: true, doubleClick: {
183
186
  mode: "reset"
184
187
  }, wheel: {
@@ -1,6 +1,7 @@
1
1
  import { ThemeProps } from '@synerise/ds-core';
2
2
  export declare const Overlay: import('styled-components').StyledComponent<"div", any, ThemeProps & {
3
3
  $hidden?: boolean;
4
+ $zIndex: number;
4
5
  }, never>;
5
6
  export declare const ImageWrapper: import('styled-components').StyledComponent<"div", any, {}, never>;
6
7
  export declare const Image: import('styled-components').StyledComponent<"img", any, ThemeProps, never>;
@@ -1,6 +1,5 @@
1
1
  import styled, { keyframes } from "styled-components";
2
2
  import { PREVIEW_PADDING } from "./ImagePreview.const.js";
3
- const OVERLAY_Z_INDEX = 1050;
4
3
  const imageFadeIn = /* @__PURE__ */ keyframes(["from{opacity:0;}to{opacity:1;}"]);
5
4
  const BACKDROP_ALPHA = "CC";
6
5
  const CONTROLS_INSET = 16;
@@ -8,7 +7,7 @@ const IMAGE_BORDER_RADIUS = 8;
8
7
  const Overlay = /* @__PURE__ */ styled.div.withConfig({
9
8
  displayName: "ImagePreviewstyles__Overlay",
10
9
  componentId: "sc-17aatff-0"
11
- })(["position:fixed;inset:0;z-index:", ";display:flex;align-items:center;justify-content:center;outline:none;background-color:", ";opacity:", ";visibility:", ";pointer-events:", ";transition:opacity 160ms ease,visibility 160ms ease;"], OVERLAY_Z_INDEX, (props) => `${props.theme.palette["grey-900"]}${BACKDROP_ALPHA}`, (props) => props.$hidden ? 0 : 1, (props) => props.$hidden ? "hidden" : "visible", (props) => props.$hidden ? "none" : "auto");
10
+ })(["position:fixed;inset:0;z-index:", ";display:flex;align-items:center;justify-content:center;outline:none;background-color:", ";opacity:", ";visibility:", ";pointer-events:", ";transition:opacity 160ms ease,visibility 160ms ease;"], (props) => props.$zIndex, (props) => `${props.theme.palette["grey-900"]}${BACKDROP_ALPHA}`, (props) => props.$hidden ? 0 : 1, (props) => props.$hidden ? "hidden" : "visible", (props) => props.$hidden ? "none" : "auto");
12
11
  const ImageWrapper = /* @__PURE__ */ styled.div.withConfig({
13
12
  displayName: "ImagePreviewstyles__ImageWrapper",
14
13
  componentId: "sc-17aatff-1"
@@ -30,6 +30,12 @@ export type ImagePreviewProps = {
30
30
  fallback?: ReactNode;
31
31
  /** Portal target. Defaults to `document.body`. */
32
32
  getContainer?: () => HTMLElement;
33
+ /**
34
+ * z-index of the overlay. Omit it and the preview stacks one step above the
35
+ * nearest enclosing modal/drawer, falling back to the `zindex-modal` token
36
+ * when nothing encloses it. Set it to opt out of that stack.
37
+ */
38
+ zIndex?: number;
33
39
  /** Unmount the viewer (and reset zoom state) when closed. Defaults to false. */
34
40
  destroyOnClose?: boolean;
35
41
  /** Called with the current scale whenever the zoom transform changes. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synerise/ds-image",
3
- "version": "0.1.5",
3
+ "version": "2.0.0",
4
4
  "description": "Image UI Component for the Synerise Design System",
5
5
  "license": "ISC",
6
6
  "repository": "Synerise/synerise-design",
@@ -42,10 +42,10 @@
42
42
  ],
43
43
  "types": "dist/index.d.ts",
44
44
  "dependencies": {
45
- "@synerise/ds-icon": "^1.18.5",
46
- "@synerise/ds-toolbar": "^1.1.67",
47
- "@synerise/ds-tooltip": "^1.6.0",
48
- "@synerise/ds-utils": "^1.10.2",
45
+ "@synerise/ds-icon": "^2.0.0",
46
+ "@synerise/ds-toolbar": "^2.0.0",
47
+ "@synerise/ds-tooltip": "^2.0.0",
48
+ "@synerise/ds-utils": "^2.0.0",
49
49
  "react-zoom-pan-pinch": "^3.6.1"
50
50
  },
51
51
  "peerDependencies": {
@@ -55,5 +55,5 @@
55
55
  "styled-components": "^5.3.3",
56
56
  "vitest": "4"
57
57
  },
58
- "gitHead": "e1fece64a41fd18a78583ecb6c21ee00fe8c0759"
58
+ "gitHead": "033854a18f038079e9a16b0ff250f8401a2287c2"
59
59
  }