Skip to content

Commit 1dce120

Browse files
committed
docs: refactor image cropper demo
1 parent 3eadf31 commit 1dce120

File tree

9 files changed

+120
-66
lines changed

9 files changed

+120
-66
lines changed

packages/machines/image-cropper/src/image-cropper.connect.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ export function connect<T extends PropTypes>(
103103
const offset = context.get("offset")
104104
const viewportRect = context.get("viewportRect")
105105

106-
const canvas = document.createElement("canvas")
106+
const doc = scope.getDoc()
107+
const canvas = doc.createElement("canvas")
107108
canvas.width = crop.width
108109
canvas.height = crop.height
109110

@@ -335,6 +336,8 @@ export function connect<T extends PropTypes>(
335336
const roundedCrop = getRoundedCrop(crop)
336337

337338
const hasViewportRect = viewportRect.width > 0 && viewportRect.height > 0
339+
const hasCrop = crop.width > 0 && crop.height > 0
340+
const isMeasured = hasViewportRect && hasCrop
338341
const maxX = hasViewportRect ? Math.max(0, Math.round(viewportRect.width - crop.width)) : undefined
339342
const ariaValueMax = maxX != null ? maxX : Math.max(roundedCrop.x, 0)
340343
const ariaValueText = translations.selectionValueText({ shape: cropShape, ...roundedCrop })
@@ -355,6 +358,7 @@ export function connect<T extends PropTypes>(
355358
"aria-description": translations.selectionInstructions,
356359
"data-disabled": dataAttr(disabled),
357360
"data-shape": cropShape,
361+
"data-measured": dataAttr(isMeasured),
358362
style: {
359363
"--width": toPx(crop.width),
360364
"--height": toPx(crop.height),
@@ -364,6 +368,7 @@ export function connect<T extends PropTypes>(
364368
left: "var(--x)",
365369
width: "var(--width)",
366370
height: "var(--height)",
371+
visibility: isMeasured ? undefined : "hidden",
367372
},
368373
onPointerDown(event) {
369374
if (disabled) {

packages/machines/image-cropper/src/image-cropper.props.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createProps } from "@zag-js/types"
22
import { createSplitProps } from "@zag-js/utils"
3-
import type { ImageCropperProps } from "./image-cropper.types"
3+
import type { HandlePosition, ImageCropperProps } from "./image-cropper.types"
44

55
export const props = createProps<ImageCropperProps>()([
66
"id",
@@ -36,3 +36,14 @@ export const props = createProps<ImageCropperProps>()([
3636
])
3737

3838
export const splitProps = createSplitProps<Partial<ImageCropperProps>>(props)
39+
40+
export const handles: HandlePosition[] = [
41+
"top-left",
42+
"top",
43+
"top-right",
44+
"right",
45+
"bottom-right",
46+
"bottom",
47+
"bottom-left",
48+
"left",
49+
]

website/data/snippets/react/image-cropper/usage.mdx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,6 @@ import * as imageCropper from "@zag-js/image-cropper"
33
import { normalizeProps, useMachine } from "@zag-js/react"
44
import { useId } from "react"
55

6-
const handles: imageCropper.HandlePosition[] = [
7-
"top-left",
8-
"top",
9-
"top-right",
10-
"right",
11-
"bottom-right",
12-
"bottom",
13-
"bottom-left",
14-
"left",
15-
]
16-
176
export function ImageCropper() {
187
const service = useMachine(imageCropper.machine, {
198
id: useId(),
@@ -31,7 +20,7 @@ export function ImageCropper() {
3120
/>
3221

3322
<div {...api.getSelectionProps()}>
34-
{handles.map((position) => (
23+
{imageCropper.handles.map((position) => (
3524
<div key={position} {...api.getHandleProps({ position })}>
3625
<span />
3726
</div>

website/data/snippets/solid/image-cropper/usage.mdx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,6 @@ import * as imageCropper from "@zag-js/image-cropper"
33
import { normalizeProps, useMachine } from "@zag-js/solid"
44
import { For, createMemo, createUniqueId } from "solid-js"
55

6-
const handles: imageCropper.HandlePosition[] = [
7-
"top-left",
8-
"top",
9-
"top-right",
10-
"right",
11-
"bottom-right",
12-
"bottom",
13-
"bottom-left",
14-
"left",
15-
]
16-
176
export function ImageCropper() {
187
const service = useMachine(imageCropper.machine, {
198
id: createUniqueId(),
@@ -31,7 +20,7 @@ export function ImageCropper() {
3120
/>
3221

3322
<div {...api().getSelectionProps()}>
34-
<For each={handles}>
23+
<For each={imageCropper.handles}>
3524
{(position) => (
3625
<div {...api().getHandleProps({ position })}>
3726
<span />

website/data/snippets/svelte/image-cropper/usage.mdx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,6 @@
33
import * as imageCropper from "@zag-js/image-cropper"
44
import { normalizeProps, useMachine } from "@zag-js/svelte"
55
6-
const handles: imageCropper.HandlePosition[] = [
7-
"top-left",
8-
"top",
9-
"top-right",
10-
"right",
11-
"bottom-right",
12-
"bottom",
13-
"bottom-left",
14-
"left",
15-
]
16-
176
const id = $props.id()
187
const service = useMachine(imageCropper.machine, {
198
id,
@@ -30,7 +19,7 @@
3019
/>
3120
3221
<div {...api.getSelectionProps()}>
33-
{#each handles as position}
22+
{#each imageCropper.handles as position}
3423
<div {...api.getHandleProps({ position })}>
3524
<span />
3625
</div>

website/data/snippets/vue/image-cropper/usage.mdx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,6 @@
44
import { normalizeProps, useMachine } from "@zag-js/vue"
55
import { computed } from "vue"
66
7-
const handles = [
8-
"top-left",
9-
"top",
10-
"top-right",
11-
"right",
12-
"bottom-right",
13-
"bottom",
14-
"bottom-left",
15-
"left",
16-
]
17-
187
const service = useMachine(imageCropper.machine, {
198
id: "image-cropper",
209
})
@@ -33,7 +22,7 @@
3322

3423
<div v-bind="api.getSelectionProps()">
3524
<div
36-
v-for="position in handles"
25+
v-for="position in imageCropper.handles"
3726
:key="position"
3827
v-bind="api.getHandleProps({ position })"
3928
>

website/demos/image-cropper.tsx

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,76 @@
11
import * as imageCropper from "@zag-js/image-cropper"
22
import { normalizeProps, useMachine } from "@zag-js/react"
3-
import { handlePositions } from "@zag-js/shared"
4-
import { useEffect, useId, useState } from "react"
3+
import { useId, useState } from "react"
54

65
interface ImageCropperProps extends Omit<imageCropper.Props, "id"> {}
76

87
export function ImageCropper(props: ImageCropperProps) {
8+
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
9+
910
const service = useMachine(imageCropper.machine, {
1011
id: useId(),
12+
initialCrop: { x: 120, y: 40, width: 120, height: 120 },
1113
...props,
1214
})
1315

1416
const api = imageCropper.connect(service, normalizeProps)
1517

18+
const handleShowPreview = async () => {
19+
const result = await api.getCroppedImage()
20+
let url: string | null = null
21+
if (result instanceof Blob) {
22+
// Revoke previous URL if it exists
23+
if (previewUrl) {
24+
URL.revokeObjectURL(previewUrl)
25+
}
26+
url = URL.createObjectURL(result)
27+
} else if (typeof result === "string") {
28+
url = result
29+
}
30+
setPreviewUrl(url)
31+
}
32+
33+
const revokePreview = () => {
34+
// Revoke URL after image loads for performance
35+
if (previewUrl) {
36+
URL.revokeObjectURL(previewUrl)
37+
}
38+
}
39+
1640
return (
17-
<div {...api.getRootProps()}>
18-
<div {...api.getViewportProps()}>
19-
<img
20-
src="https://placedog.net/500/280?id=2"
21-
crossOrigin="anonymous"
22-
{...api.getImageProps()}
23-
/>
24-
<div {...api.getSelectionProps()}>
25-
{handlePositions.map((position) => (
26-
<div key={position} {...api.getHandleProps({ position })}>
27-
<div />
28-
</div>
29-
))}
41+
<div className="image-cropper-container">
42+
<div {...api.getRootProps()}>
43+
<div {...api.getViewportProps()}>
44+
<img
45+
src="https://placedog.net/500/280?id=2"
46+
crossOrigin="anonymous"
47+
{...api.getImageProps()}
48+
/>
49+
<div {...api.getSelectionProps()}>
50+
{imageCropper.handles.map((position) => (
51+
<div key={position} {...api.getHandleProps({ position })}>
52+
<div />
53+
</div>
54+
))}
55+
</div>
3056
</div>
3157
</div>
58+
59+
<button className="preview-button" onClick={handleShowPreview}>
60+
Show Preview
61+
</button>
62+
63+
{previewUrl && (
64+
<div>
65+
<h3>Cropped Image Preview</h3>
66+
<img
67+
className="preview-image"
68+
src={previewUrl}
69+
alt="Cropped preview"
70+
onLoad={revokePreview}
71+
/>
72+
</div>
73+
)}
3274
</div>
3375
)
3476
}

website/sidebar.config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ const sidebar: Record<"docs", SidebarItem[]> = {
9090
{ type: "doc", label: "File Upload", id: "file-upload" },
9191
{ type: "doc", label: "Floating Panel", id: "floating-panel" },
9292
{ type: "doc", label: "Hover Card", id: "hover-card" },
93-
// { type: "doc", label: "Image Cropper", id: "image-cropper", beta: true },
93+
{
94+
type: "doc",
95+
label: "Image Cropper",
96+
id: "image-cropper",
97+
beta: true,
98+
},
9499
{ type: "doc", label: "Listbox", id: "listbox" },
95100
{ type: "doc", label: "Menu", id: "menu" },
96101
{ type: "doc", label: "Context Menu", id: "context-menu" },

website/styles/machines/image-cropper.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,38 @@
9696
transform: translate(-50%, -50%);
9797
cursor: ew-resize;
9898
}
99+
100+
.image-cropper-container {
101+
display: flex;
102+
flex-direction: column;
103+
align-items: flex-start;
104+
gap: 1rem;
105+
106+
h3 {
107+
margin-bottom: 0.5rem;
108+
font-weight: 500;
109+
font-size: 0.875rem;
110+
}
111+
112+
.preview-button {
113+
margin-top: 1rem;
114+
display: inline-flex;
115+
align-items: center;
116+
justify-content: center;
117+
text-align: center;
118+
cursor: pointer;
119+
font-weight: 500;
120+
padding-inline: 1rem;
121+
padding-block: 0.5rem;
122+
background: var(--colors-bg-primary-subtle);
123+
color: #ffffff;
124+
border: none;
125+
}
126+
127+
.preview-image {
128+
width: 100px;
129+
height: 100px;
130+
object-fit: contain;
131+
border: 1px solid var(--colors-border-subtle);
132+
}
133+
}

0 commit comments

Comments
 (0)