Skip to content

Commit 20e481f

Browse files
committed
feat(minimap): preview and wrapper customization; expand Storybook examples
MiniMap: add previewStyle, wrapperClassName, and previewClassName; apply wrapper transforms via explicit style properties; stop spreading outer props onto the inner wrapper div. Stories: add axis-lock, image-annotations, initial-transform, medical-viewer, stadium, velocity, and keep-scale map examples; refresh existing examples; shared viewer styles and controls utilities; replace miro image asset with inline example; update minimap tests and Storybook preview. Made-with: Cursor
1 parent f2a7d20 commit 20e481f

43 files changed

Lines changed: 5147 additions & 445 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.storybook/preview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const preview: Preview = {
1515
order: [
1616
"Docs",
1717
"Basic",
18+
["Image", "*"],
1819
"Advanced",
1920
"Components",
2021
"Examples",

__tests__/features/minimap/minimap.rendering.spec.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ describe("MiniMap [Rendering]", () => {
6262
});
6363

6464
describe("preview styles", () => {
65-
it("should have red border by default", () => {
65+
it("should use default red border on preview", () => {
6666
renderMiniMap();
6767
const preview = document.querySelector(
6868
".rzpp-minimap-preview",
6969
) as HTMLElement;
70+
expect(preview.style.border).toBe("3px solid red");
7071
expect(preview.style.borderColor).toBe("red");
7172
});
7273

@@ -78,6 +79,17 @@ describe("MiniMap [Rendering]", () => {
7879
expect(preview.style.borderColor).toBe("blue");
7980
});
8081

82+
it("should merge previewStyle after defaults", () => {
83+
renderMiniMap({
84+
previewStyle: { borderRadius: "8px", border: "1px solid lime" },
85+
});
86+
const preview = document.querySelector(
87+
".rzpp-minimap-preview",
88+
) as HTMLElement;
89+
expect(preview.style.borderRadius).toBe("8px");
90+
expect(preview.style.border).toBe("1px solid lime");
91+
});
92+
8193
it("should have box-shadow for viewport dimming", () => {
8294
renderMiniMap();
8395
const preview = document.querySelector(

src/components/mini-map/mini-map.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export type MiniMapProps = {
2121
width?: number;
2222
height?: number;
2323
borderColor?: string;
24+
/** Merged onto the viewport indicator (`.rzpp-minimap-preview`). */
25+
previewStyle?: React.CSSProperties;
26+
/** Extra class name(s) appended to `.rzpp-minimap-wrapper`. */
27+
wrapperClassName?: string;
28+
/** Extra class name(s) appended to `.rzpp-minimap-preview`. */
29+
previewClassName?: string;
2430
panning?: boolean;
2531
} & React.DetailedHTMLProps<
2632
React.HTMLAttributes<HTMLDivElement>,
@@ -43,6 +49,9 @@ export const MiniMap: React.FC<MiniMapProps> = ({
4349
width = 200,
4450
height = 200,
4551
borderColor = "red",
52+
previewStyle,
53+
wrapperClassName,
54+
previewClassName,
4655
children,
4756
panning = true,
4857
...rest
@@ -116,11 +125,14 @@ export const MiniMap: React.FC<MiniMapProps> = ({
116125
// overflow: "hidden",
117126
} as const;
118127

119-
Object.keys(style).forEach((key) => {
120-
if (wrapperRef.current) {
121-
wrapperRef.current.style[key] = style[key];
122-
}
123-
});
128+
if (wrapperRef.current) {
129+
const el = wrapperRef.current.style;
130+
el.transform = style.transform;
131+
el.transformOrigin = style.transformOrigin;
132+
el.position = style.position;
133+
el.boxSizing = style.boxSizing;
134+
el.zIndex = String(style.zIndex);
135+
}
124136
};
125137

126138
const transformMiniMap = () => {
@@ -261,20 +273,19 @@ export const MiniMap: React.FC<MiniMapProps> = ({
261273
{...rest}
262274
ref={mainRef}
263275
style={wrapperStyle}
264-
className={`rzpp-mini-map ${rest.className || ""}`}
276+
className={`rzpp-mini-map ${rest.className || ""}`.trim()}
265277
>
266278
<div
267-
{...rest}
268279
style={{ pointerEvents: "none" }}
269280
ref={wrapperRef}
270-
className="rzpp-minimap-wrapper"
281+
className={`rzpp-minimap-wrapper ${wrapperClassName || ""}`.trim()}
271282
>
272283
{children}
273284
</div>
274285
<div
275-
className="rzpp-minimap-preview"
286+
className={`rzpp-minimap-preview ${previewClassName || ""}`.trim()}
276287
ref={previewRef}
277-
style={{ ...previewStyles, borderColor }}
288+
style={{ ...previewStyles, borderColor, ...previewStyle }}
278289
/>
279290
</div>
280291
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Meta, Story, ArgsTable, Canvas } from "@storybook/blocks";
2+
3+
import { TransformWrapper } from "../../../components";
4+
import { argsTypes } from "../../types/args.types.ts";
5+
import { Example } from "./example";
6+
7+
export const Template = (args) => <Example {...args} />;
8+
9+
<Meta
10+
title="Basic/Axis lock"
11+
component={TransformWrapper}
12+
argTypes={argsTypes}
13+
/>
14+
15+
# Axis lock
16+
17+
Use **`panning.lockAxisX`** and **`panning.lockAxisY`** when the viewport should
18+
only move along one axis — for example a **timeline** (horizontal only) or a
19+
**tall document strip** (vertical only).
20+
21+
- **`lockAxisX: true`** — horizontal panning is disabled; drag and trackpad pan
22+
move the content **vertically** only.
23+
- **`lockAxisY: true`** — vertical panning is disabled; movement is **horizontal**
24+
only.
25+
26+
The buttons above the canvas switch the demo between **free** panning and each
27+
locked mode. Use the control bar to reset or re-center if you get lost.
28+
29+
<Canvas>
30+
<Story name="Axis lock">{(args) => <Template {...args} />}</Story>
31+
</Canvas>
32+
33+
## Component API
34+
35+
<ArgsTable story="Axis lock" />
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useMemo, useState } from "react";
2+
3+
import { TransformWrapper, TransformComponent } from "../../../components";
4+
import {
5+
Controls,
6+
normalizeArgs,
7+
viewerChrome,
8+
FreeMovementIcon,
9+
LockVerticalIcon,
10+
LockHorizontalIcon,
11+
} from "../../utils";
12+
13+
type AxisMode = "free" | "vertical" | "horizontal";
14+
15+
const viewer: React.CSSProperties = {
16+
...viewerChrome,
17+
width: "560px",
18+
height: "440px",
19+
maxWidth: "90vw",
20+
maxHeight: "78vh",
21+
};
22+
23+
const font = "system-ui, -apple-system, sans-serif";
24+
25+
function GridCanvas() {
26+
const cells = useMemo(() => {
27+
const out: { key: string; row: number; col: number }[] = [];
28+
for (let row = 0; row < 6; row += 1) {
29+
for (let col = 0; col < 8; col += 1) {
30+
out.push({ key: `${row}-${col}`, row, col });
31+
}
32+
}
33+
return out;
34+
}, []);
35+
36+
return (
37+
<div
38+
style={{
39+
width: 1400,
40+
height: 900,
41+
display: "grid",
42+
gridTemplateColumns: "repeat(8, 1fr)",
43+
gridTemplateRows: "repeat(6, 1fr)",
44+
gap: 3,
45+
padding: 24,
46+
boxSizing: "border-box",
47+
background:
48+
"linear-gradient(145deg, #12122a 0%, #0d1528 40%, #0a1020 100%)",
49+
}}
50+
>
51+
{cells.map(({ key, row, col }) => (
52+
<div
53+
key={key}
54+
style={{
55+
borderRadius: 10,
56+
background: `rgba(${40 + col * 18}, ${60 + row * 12}, ${120 + (row + col) * 8}, 0.35)`,
57+
border: "1px solid rgba(255,255,255,0.06)",
58+
display: "flex",
59+
alignItems: "center",
60+
justifyContent: "center",
61+
fontFamily: font,
62+
fontSize: 13,
63+
fontWeight: 600,
64+
color: "rgba(255,255,255,0.45)",
65+
}}
66+
>
67+
{col + 1},{row + 1}
68+
</div>
69+
))}
70+
</div>
71+
);
72+
}
73+
74+
function axisButtons(
75+
mode: AxisMode,
76+
setMode: (m: AxisMode) => void,
77+
) {
78+
const btn = (
79+
id: AxisMode,
80+
label: string,
81+
Icon: React.FC,
82+
) => ({
83+
label,
84+
icon: <Icon />,
85+
onClick: () => setMode(mode === id ? "free" : id),
86+
active: mode === id,
87+
"data-tooltip": label,
88+
});
89+
90+
return [
91+
btn("free", "Free pan", FreeMovementIcon),
92+
btn("vertical", "Vertical only", LockVerticalIcon),
93+
btn("horizontal", "Horizontal only", LockHorizontalIcon),
94+
];
95+
}
96+
97+
export const Example: React.FC<Record<string, unknown>> = (args) => {
98+
const normalized = normalizeArgs(args);
99+
const [mode, setMode] = useState<AxisMode>("free");
100+
101+
const panning = {
102+
...normalized.panning,
103+
lockAxisX: mode === "vertical",
104+
lockAxisY: mode === "horizontal",
105+
};
106+
107+
return (
108+
<div style={{ fontFamily: font }}>
109+
<TransformWrapper {...normalized} centerOnInit panning={panning}>
110+
{(utils) => (
111+
<div style={{ position: "relative", display: "inline-block" }}>
112+
<Controls
113+
{...utils}
114+
extraButtons={axisButtons(mode, setMode)}
115+
/>
116+
<TransformComponent
117+
wrapperStyle={viewer}
118+
contentStyle={{ width: 1400, height: 900 }}
119+
>
120+
<GridCanvas />
121+
</TransformComponent>
122+
</div>
123+
)}
124+
</TransformWrapper>
125+
</div>
126+
);
127+
};

src/stories/examples/bounds/bounds.stories.mdx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Meta, Story, ArgsTable, Canvas } from "@storybook/blocks";
22

33
import { TransformWrapper, TransformComponent } from "../../../components";
44
import { argsTypes } from "../../types/args.types.ts";
5-
import { normalizeArgs, BoundsOverlay } from "../../utils";
5+
import { normalizeArgs, BoundsOverlay, viewerChrome } from "../../utils";
66
import { BoundsContent, contentSize } from "./bounds.data";
77

88
export const minX = -(contentSize - 500);
@@ -22,14 +22,11 @@ export const Template = (args) => (
2222
<div style={{ position: "relative", display: "inline-block" }}>
2323
<TransformComponent
2424
wrapperStyle={{
25+
...viewerChrome,
2526
width: "500px",
2627
height: "500px",
2728
maxWidth: "80vw",
2829
maxHeight: "80vh",
29-
borderRadius: "12px",
30-
border: "2px solid #333",
31-
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
32-
background: "#0f0f1a",
3330
}}
3431
>
3532
<BoundsContent />

src/stories/examples/cinema/example.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useRef } from "react";
22

33
import { TransformWrapper, TransformComponent } from "../../../components";
4-
import { normalizeArgs, Controls } from "../../utils";
4+
import { normalizeArgs, Controls, CloseIcon, viewerChrome } from "../../utils";
55
import {
66
CinemaLayout,
77
CATEGORY_STYLES,
@@ -48,16 +48,15 @@ export const Example: React.FC<any> = (args: any) => {
4848
apiRef.current = api;
4949
return (
5050
<>
51-
<Controls {...api} extraButtons={selectedSeat ? [{ label: "Deselect", onClick: handleReset }] : []} />
51+
<Controls {...api} extraButtons={selectedSeat ? [{ label: "Deselect seat", icon: <CloseIcon />, onClick: handleReset }] : []} />
5252
<SeatBadge seat={selectedSeat} />
5353
<TransformComponent
5454
wrapperStyle={{
55+
...viewerChrome,
5556
width: "800px",
5657
maxWidth: "100%",
5758
height: "600px",
5859
maxHeight: "70vh",
59-
borderRadius: "12px",
60-
background: "#060612",
6160
}}
6261
contentStyle={{
6362
width: CANVAS_WIDTH,

src/stories/examples/content-rerendering/content-updating.stories.mdx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ export const Template = (args) => <Example {...args} />;
1515
# Rerendering
1616

1717
Proves that **frequent React re-renders** inside `TransformComponent` do not
18-
disrupt the current zoom or pan state. Three content blocks update on
19-
independent timers (1 s, 3 s, 12 s) — zoom in, pan around, and watch the
20-
blocks toggle without resetting your viewport.
21-
22-
This is important for dashboards, live data feeds, or any UI where child
23-
components update independently of the user's zoom/pan interaction.
18+
disrupt the current zoom or pan state. Three regions update on independent
19+
timers (1 s, 3 s, 12 s). Each toggle is not a text swap — it **restructures the
20+
layout**: compact card vs three-column grid, sidebar split vs hero + stats +
21+
two-column dashboard, minimal strip vs tall stack with gallery, list rows, and
22+
footer. Total content height changes a lot between states.
23+
24+
Zoom in, pan around, and watch the layout reflow without resetting your
25+
viewport. This matters for dashboards, live data, and any UI where structure and
26+
size change under the user’s feet.
2427

2528
<Canvas>
2629
<Story name="Rerendering">{(args) => <Template {...args} />}</Story>

0 commit comments

Comments
 (0)