Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9bc565e
feat: add image cropper for react
nelsonlaidev Oct 23, 2025
5303574
feat: add image cropper for solid
nelsonlaidev Oct 23, 2025
c78c667
feat: add image cropper for svelte
nelsonlaidev Oct 24, 2025
8fb55c6
feat: add image cropper for vue
nelsonlaidev Oct 24, 2025
af2c4ba
Merge branch 'main' into feat/image-cropper
nelsonlaidev Oct 25, 2025
e78a8ef
chore: revert some changes in package.json
nelsonlaidev Oct 25, 2025
9b6fdc4
Merge branch 'main' into feat/image-cropper
nelsonlaidev Oct 29, 2025
205e3bf
Merge branch 'main' into feat/image-cropper
nelsonlaidev Oct 30, 2025
bf3f53c
refactor: don't separate the split props to a new file
nelsonlaidev Oct 30, 2025
682ce71
Merge branch 'main' into feat/image-cropper
nelsonlaidev Oct 31, 2025
c0bf9ec
chore: update image cropper stories for vue
nelsonlaidev Oct 31, 2025
1f5dc6a
refactor: add the missing exports
nelsonlaidev Oct 31, 2025
a3e5cc3
fix: add the missing root provider for vue
nelsonlaidev Oct 31, 2025
f4ef4c3
refactor: use IntlTranslations type from image cropper directly
nelsonlaidev Oct 31, 2025
659d42b
refactor: import package from preferred path
nelsonlaidev Nov 2, 2025
eb14f5f
Merge branch 'main' into feat/image-cropper
nelsonlaidev Nov 2, 2025
cab1d4c
Merge branch 'main' into feat/image-cropper
nelsonlaidev Nov 3, 2025
dd2e4fc
refactor: use the exported handles from image cropper
nelsonlaidev Nov 3, 2025
614a2cf
Merge branch 'main' into feat/image-cropper
nelsonlaidev Nov 5, 2025
cadf27a
feat: add grid part
nelsonlaidev Nov 5, 2025
350adf5
chore: add more examples
nelsonlaidev Nov 5, 2025
f23cdc5
fix: export root provider in vue
nelsonlaidev Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@import url('./styles/file-upload.css');
@import url('./styles/floating-panel.css');
@import url('./styles/hover-card.css');
@import url('./styles/image-cropper.css');
@import url('./styles/json-tree-view.css');
@import url('./styles/listbox.css');
@import url('./styles/menu.css');
Expand Down
56 changes: 56 additions & 0 deletions .storybook/styles/image-cropper.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[data-scope='image-cropper'][data-part='root'] {
max-width: fit-content;
}

[data-scope='image-cropper'][data-part='selection'] {
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: move;
&[data-shape='circle'] {
border-radius: 50%;
}
}

[data-scope='image-cropper'][data-part='image'] {
display: block;
max-width: 100%;
}

[data-scope='image-cropper'][data-part='handle'] {
--handle-size: 12px;
width: var(--handle-size);
height: var(--handle-size);

&[data-position='n'],
&[data-position='s'] {
height: 6px;
}

&[data-position='e'],
&[data-position='w'] {
width: 6px;
}

& > div {
width: calc(var(--handle-size) / 2);
height: calc(var(--handle-size) / 2);
background: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

[data-scope='image-cropper'][data-part='grid'] {
--grid-color: rgba(255, 255, 255, 0.7);
--grid-line-width: 1px;

&[data-axis='vertical'] {
border-inline: var(--grid-line-width) solid var(--grid-color);
}

&[data-axis='horizontal'] {
border-block: var(--grid-line-width) solid var(--grid-color);
}
}
244 changes: 95 additions & 149 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"@zag-js/focus-trap": "1.27.1",
"@zag-js/highlight-word": "1.27.1",
"@zag-js/hover-card": "1.27.1",
"@zag-js/image-cropper": "1.27.1",
"@zag-js/i18n-utils": "1.27.1",
"@zag-js/json-tree-utils": "1.27.1",
"@zag-js/listbox": "1.27.1",
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/components/anatomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { fieldsetAnatomy } from './fieldset/fieldset.anatomy'
export { fileUploadAnatomy } from './file-upload/file-upload.anatomy'
export { floatingPanelAnatomy } from './floating-panel/floating-panel.anatomy'
export { hoverCardAnatomy } from './hover-card/hover-card.anatomy'
export { imageCropperAnatomy } from './image-cropper/image-cropper.anatomy'
export { listboxAnatomy } from './listbox/listbox.anatomy'
export { marqueeAnatomy } from './marquee/marquee.anatomy'
export { menuAnatomy } from './menu/menu.anatomy'
Expand Down
20 changes: 20 additions & 0 deletions packages/react/src/components/image-cropper/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ImageCropper } from '@ark-ui/react/image-cropper'

export const Basic = () => {
return (
<ImageCropper.Root>
<ImageCropper.Viewport>
<ImageCropper.Image src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800" alt="Sample" />
<ImageCropper.Selection>
{ImageCropper.handles.map((position) => (
<ImageCropper.Handle key={position} position={position}>
<div />
</ImageCropper.Handle>
))}
<ImageCropper.Grid axis="horizontal" />
<ImageCropper.Grid axis="vertical" />
</ImageCropper.Selection>
</ImageCropper.Viewport>
</ImageCropper.Root>
)
}
18 changes: 18 additions & 0 deletions packages/react/src/components/image-cropper/examples/circle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ImageCropper } from '@ark-ui/react/image-cropper'

export const Circle = () => {
return (
<ImageCropper.Root cropShape="circle">
<ImageCropper.Viewport>
<ImageCropper.Image src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800" alt="Sample" />
<ImageCropper.Selection>
{ImageCropper.handles.map((position) => (
<ImageCropper.Handle key={position} position={position}>
<div />
</ImageCropper.Handle>
))}
</ImageCropper.Selection>
</ImageCropper.Viewport>
</ImageCropper.Root>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ImageCropper } from '@ark-ui/react/image-cropper'
import { useState } from 'react'

export const Controlled = () => {
const [zoom, setZoom] = useState(1)

return (
<>
<button onClick={() => setZoom(zoom + 0.1)}>Zoom In</button>
<button onClick={() => setZoom(zoom - 0.1)}>Zoom Out</button>

<ImageCropper.Root zoom={zoom} onZoomChange={(e) => setZoom(e.zoom)}>
<ImageCropper.Viewport>
<ImageCropper.Image src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800" alt="Sample" />
<ImageCropper.Selection>
{ImageCropper.handles.map((position) => (
<ImageCropper.Handle key={position} position={position}>
<div />
</ImageCropper.Handle>
))}
<ImageCropper.Grid axis="horizontal" />
<ImageCropper.Grid axis="vertical" />
</ImageCropper.Selection>
</ImageCropper.Viewport>
</ImageCropper.Root>
</>
)
}
15 changes: 15 additions & 0 deletions packages/react/src/components/image-cropper/examples/fixed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ImageCropper } from '@ark-ui/react/image-cropper'

export const Fixed = () => {
return (
<ImageCropper.Root fixedCropArea>
<ImageCropper.Viewport>
<ImageCropper.Image src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800" alt="Sample" />
<ImageCropper.Selection>
<ImageCropper.Grid axis="horizontal" />
<ImageCropper.Grid axis="vertical" />
</ImageCropper.Selection>
</ImageCropper.Viewport>
</ImageCropper.Root>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ImageCropper, useImageCropper } from '@ark-ui/react/image-cropper'

export const RootProvider = () => {
const imageCropper = useImageCropper()

return (
<>
<button onClick={() => imageCropper.setZoom(imageCropper.zoom + 0.1)}>Zoom In</button>
<button onClick={() => imageCropper.setZoom(imageCropper.zoom - 0.1)}>Zoom Out</button>

<ImageCropper.RootProvider value={imageCropper}>
<ImageCropper.Viewport>
<ImageCropper.Image src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800" alt="Sample" />
<ImageCropper.Selection>
{ImageCropper.handles.map((position) => (
<ImageCropper.Handle key={position} position={position}>
<div />
</ImageCropper.Handle>
))}
<ImageCropper.Grid axis="horizontal" />
<ImageCropper.Grid axis="vertical" />
</ImageCropper.Selection>
</ImageCropper.Viewport>
</ImageCropper.RootProvider>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ReactNode } from 'react'
import { type UseImageCropperContext, useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperContextProps {
children: (context: UseImageCropperContext) => ReactNode
}

export const ImageCropperContext = (props: ImageCropperContextProps) => props.children(useImageCropperContext())
18 changes: 18 additions & 0 deletions packages/react/src/components/image-cropper/image-cropper-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { GridProps } from '@zag-js/image-cropper'
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperGridBaseProps extends PolymorphicProps, GridProps {}
export interface ImageCropperGridProps extends HTMLProps<'div'>, ImageCropperGridBaseProps {}

export const ImageCropperGrid = forwardRef<HTMLDivElement, ImageCropperGridProps>((props, ref) => {
const { axis, ...localProps } = props
const imageCropper = useImageCropperContext()
const mergedProps = mergeProps(imageCropper.getGridProps({ axis }), localProps)

return <ark.div {...mergedProps} ref={ref} />
})

ImageCropperGrid.displayName = 'ImageCropperGrid'
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { HandleProps } from '@zag-js/image-cropper'
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperHandleBaseProps extends PolymorphicProps, HandleProps {}
export interface ImageCropperHandleProps extends HTMLProps<'div'>, ImageCropperHandleBaseProps {}

export const ImageCropperHandle = forwardRef<HTMLDivElement, ImageCropperHandleProps>((props, ref) => {
const { position, ...localProps } = props
const imageCropper = useImageCropperContext()
const mergedProps = mergeProps(imageCropper.getHandleProps({ position }), localProps)

return <ark.div {...mergedProps} ref={ref} />
})

ImageCropperHandle.displayName = 'ImageCropperHandle'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperImageBaseProps extends PolymorphicProps {}
export interface ImageCropperImageProps extends HTMLProps<'img'>, ImageCropperImageBaseProps {}

export const ImageCropperImage = forwardRef<HTMLImageElement, ImageCropperImageProps>((props, ref) => {
const imageCropper = useImageCropperContext()
const mergedProps = mergeProps(imageCropper.getImageProps(), props)

return <ark.img {...mergedProps} ref={ref} />
})

ImageCropperImage.displayName = 'ImageCropperImage'
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { createSplitProps } from '../../utils/create-split-props'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import type { UseImageCropperReturn } from './use-image-cropper'
import { ImageCropperProvider } from './use-image-cropper-context'

interface RootProviderProps {
value: UseImageCropperReturn
}

export interface ImageCropperRootProviderBaseProps extends RootProviderProps, PolymorphicProps {}
export interface ImageCropperRootProviderProps extends HTMLProps<'div'>, ImageCropperRootProviderBaseProps {}

export const ImageCropperRootProvider = forwardRef<HTMLDivElement, ImageCropperRootProviderProps>((props, ref) => {
const [{ value: imageCropper }, localProps] = createSplitProps<RootProviderProps>()(props, ['value'])
const mergedProps = mergeProps(imageCropper.getRootProps(), localProps)

return (
<ImageCropperProvider value={imageCropper}>
<ark.div {...mergedProps} ref={ref} />
</ImageCropperProvider>
)
})

ImageCropperRootProvider.displayName = 'ImageCropperRootProvider'
52 changes: 52 additions & 0 deletions packages/react/src/components/image-cropper/image-cropper-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { createSplitProps } from '../../utils/create-split-props'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { type UseImageCropperProps, useImageCropper } from './use-image-cropper'
import { ImageCropperProvider } from './use-image-cropper-context'

export interface ImageCropperRootBaseProps extends UseImageCropperProps, PolymorphicProps {}
export interface ImageCropperRootProps extends HTMLProps<'div'>, ImageCropperRootBaseProps {}

export const ImageCropperRoot = forwardRef<HTMLDivElement, ImageCropperRootProps>((props, ref) => {
const [useImageCropperProps, localProps] = createSplitProps<UseImageCropperProps>()(props, [
'aspectRatio',
'cropShape',
'defaultFlip',
'defaultRotation',
'defaultZoom',
'fixedCropArea',
'flip',
'id',
'ids',
'initialCrop',
'maxHeight',
'maxWidth',
'maxZoom',
'minHeight',
'minWidth',
'minZoom',
'nudgeStep',
'nudgeStepCtrl',
'nudgeStepShift',
'onCropChange',
'onFlipChange',
'onRotationChange',
'onZoomChange',
'rotation',
'translations',
'zoom',
'zoomSensitivity',
'zoomStep',
])
const imageCropper = useImageCropper(useImageCropperProps)
const mergedProps = mergeProps(imageCropper.getRootProps(), localProps)

return (
<ImageCropperProvider value={imageCropper}>
<ark.div {...mergedProps} ref={ref} />
</ImageCropperProvider>
)
})

ImageCropperRoot.displayName = 'ImageCropperRoot'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperSelectionBaseProps extends PolymorphicProps {}
export interface ImageCropperSelectionProps extends HTMLProps<'div'>, ImageCropperSelectionBaseProps {}

export const ImageCropperSelection = forwardRef<HTMLDivElement, ImageCropperSelectionProps>((props, ref) => {
const imageCropper = useImageCropperContext()
const mergedProps = mergeProps(imageCropper.getSelectionProps(), props)

return <ark.div {...mergedProps} ref={ref} />
})

ImageCropperSelection.displayName = 'ImageCropperSelection'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { mergeProps } from '@zag-js/react'
import { forwardRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { useImageCropperContext } from './use-image-cropper-context'

export interface ImageCropperViewportBaseProps extends PolymorphicProps {}
export interface ImageCropperViewportProps extends HTMLProps<'div'>, ImageCropperViewportBaseProps {}

export const ImageCropperViewport = forwardRef<HTMLDivElement, ImageCropperViewportProps>((props, ref) => {
const imageCropper = useImageCropperContext()
const mergedProps = mergeProps(imageCropper.getViewportProps(), props)

return <ark.div {...mergedProps} ref={ref} />
})

ImageCropperViewport.displayName = 'ImageCropperViewport'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { anatomy as imageCropperAnatomy } from '@zag-js/image-cropper'
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta } from '@storybook/react-vite'

const meta: Meta = {
title: 'Components / Image Cropper',
}

export default meta

export { Basic } from './examples/basic'
export { Circle } from './examples/circle'
export { Controlled } from './examples/controlled'
export { Fixed } from './examples/fixed'
export { RootProvider } from './examples/root-provider'
Loading