A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add image cropper

+791 -14
+1
apps/docs/package.json
··· 12 12 "dependencies": { 13 13 "@mdx-js/react": "^3.1.1", 14 14 "@mdx-js/rollup": "^3.1.1", 15 + "@origin-space/image-cropper": "^0.1.9", 15 16 "@react-aria/overlays": "^3.30.0", 16 17 "@react-aria/utils": "^3.31.0", 17 18 "@react-stately/utils": "^3.10.8",
+11 -5
apps/docs/src/components/dialog/index.tsx
··· 17 17 import { Size, StyleXComponentProps } from "../theme/types"; 18 18 import { fontSize, typeramp } from "../theme/typography.stylex"; 19 19 import { useDialogStyles } from "../theme/useDialogStyles"; 20 + 20 21 const styles = stylex.create({ 21 22 dialog: { 23 + overflow: "auto", 22 24 paddingBottom: spacing["2"], 23 - paddingTop: spacing["2"], 24 25 }, 25 26 header: { 26 27 gap: spacing["2"], 27 28 alignItems: "center", 29 + backgroundColor: uiColor.bg, 28 30 display: "flex", 29 31 fontSize: fontSize["lg"], 30 32 justifyContent: "space-between", 33 + position: "sticky", 34 + zIndex: 1, 31 35 height: spacing["8"], 32 36 paddingBottom: spacing["2"], 33 37 paddingLeft: spacing["4"], 34 38 paddingRight: spacing["4"], 39 + paddingTop: spacing["2"], 40 + top: 0, 35 41 36 42 borderBottomColor: uiColor.border1, 37 43 borderBottomStyle: "solid", ··· 39 45 }, 40 46 description: { 41 47 color: uiColor.text1, 42 - paddingBottom: spacing["4"], 48 + marginBottom: spacing["4"], 49 + marginTop: spacing["4"], 43 50 paddingLeft: spacing["4"], 44 51 paddingRight: spacing["4"], 45 - paddingTop: spacing["4"], 46 52 }, 47 53 body: { 48 - paddingBottom: spacing["4"], 54 + marginBottom: spacing["4"], 55 + marginTop: spacing["4"], 49 56 paddingLeft: spacing["4"], 50 57 paddingRight: spacing["4"], 51 - paddingTop: spacing["4"], 52 58 }, 53 59 footer: { 54 60 gap: spacing["2"],
+280
apps/docs/src/components/image-cropper/index.tsx
··· 1 + "use client"; 2 + 3 + import type { ComponentProps } from "react"; 4 + 5 + import { Cropper as OriginCropper } from "@origin-space/image-cropper"; 6 + import * as stylex from "@stylexjs/stylex"; 7 + 8 + import { uiColor } from "../theme/color.stylex"; 9 + import { radius } from "../theme/radius.stylex"; 10 + import { StyleXComponentProps } from "../theme/types"; 11 + 12 + const styles = stylex.create({ 13 + root: { 14 + borderColor: uiColor.border1, 15 + borderRadius: radius.md, 16 + borderStyle: "solid", 17 + borderWidth: 1, 18 + outline: { 19 + default: "none", 20 + ":focus-visible": `2px solid ${uiColor.solid1}`, 21 + }, 22 + overflow: "hidden", 23 + alignItems: "center", 24 + cursor: "move", 25 + display: "flex", 26 + justifyContent: "center", 27 + outlineOffset: { 28 + default: "0px", 29 + ":focus-visible": "2px", 30 + }, 31 + position: "relative", 32 + touchAction: "none", 33 + }, 34 + image: { 35 + objectFit: "cover", 36 + pointerEvents: "none", 37 + userSelect: "none", 38 + height: "100%", 39 + width: "100%", 40 + }, 41 + cropArea: { 42 + borderColor: uiColor.bg, 43 + borderStyle: "dashed", 44 + borderWidth: 2, 45 + boxShadow: `0 0 0 9999px rgba(0, 0, 0, 0.6)`, 46 + pointerEvents: "none", 47 + position: "absolute", 48 + }, 49 + description: { 50 + margin: -1, 51 + padding: 0, 52 + borderWidth: 0, 53 + overflow: "hidden", 54 + clip: "rect(0, 0, 0, 0)", 55 + position: "absolute", 56 + whiteSpace: "nowrap", 57 + height: 1, 58 + width: 1, 59 + }, 60 + }); 61 + 62 + /** 63 + * Area object representing the cropped region in pixels relative to the original image. 64 + */ 65 + export type CropArea = { 66 + x: number; 67 + y: number; 68 + width: number; 69 + height: number; 70 + }; 71 + 72 + /** 73 + * Props for the ImageCropper root component. 74 + */ 75 + export interface ImageCropperRootProps extends StyleXComponentProps< 76 + Omit< 77 + ComponentProps<typeof OriginCropper.Root>, 78 + "className" | "style" | "children" 79 + > 80 + > { 81 + /** 82 + * URL of the image to crop. 83 + */ 84 + image: string; 85 + /** 86 + * The desired width/height aspect ratio (e.g., 1, 1.5, 4/3, 16/9). 87 + * @default 1 88 + */ 89 + aspectRatio?: number; 90 + /** 91 + * Minimum padding (in pixels) between the crop area edges and the container edges. 92 + * @default 25 93 + */ 94 + cropPadding?: number; 95 + /** 96 + * Minimum zoom level (1 = 100% original size relative to crop area). 97 + * @default 1 98 + */ 99 + minZoom?: number; 100 + /** 101 + * Maximum zoom level. 102 + * @default 3 103 + */ 104 + maxZoom?: number; 105 + /** 106 + * Multiplier for mouse wheel delta to control zoom speed. 107 + * @default 0.005 108 + */ 109 + zoomSensitivity?: number; 110 + /** 111 + * Number of pixels to pan the image when using arrow keys. 112 + * @default 10 113 + */ 114 + keyboardStep?: number; 115 + /** 116 + * Controlled zoom level. If provided, component zoom state is controlled externally. 117 + */ 118 + zoom?: number; 119 + /** 120 + * Callback function triggered whenever the crop area changes. 121 + * Receives pixel data or null if invalid. 122 + */ 123 + onCropChange?: (pixels: CropArea | null) => void; 124 + /** 125 + * Callback function triggered when the zoom level changes interactively. 126 + * Essential for controlled zoom prop. 127 + */ 128 + onZoomChange?: (zoom: number) => void; 129 + /** 130 + * Child components. Should include ImageCropper.Image, ImageCropper.CropArea, and ImageCropper.Description. 131 + */ 132 + children: React.ReactNode; 133 + } 134 + 135 + /** 136 + * Root component for the image cropper. Handles logic, state, and interactions. 137 + * 138 + * @example 139 + * ```tsx 140 + * <ImageCropper.Root 141 + * image="https://example.com/image.jpg" 142 + * aspectRatio={1} 143 + * onCropChange={(area) => console.log(area)} 144 + * > 145 + * <ImageCropper.Description /> 146 + * <ImageCropper.Image /> 147 + * <ImageCropper.CropArea /> 148 + * </ImageCropper.Root> 149 + * ``` 150 + */ 151 + export function ImageCropperRoot({ 152 + style, 153 + image, 154 + aspectRatio = 1, 155 + cropPadding = 25, 156 + minZoom = 1, 157 + maxZoom = 3, 158 + zoomSensitivity = 0.005, 159 + keyboardStep = 10, 160 + zoom, 161 + onCropChange, 162 + onZoomChange, 163 + children, 164 + ...props 165 + }: ImageCropperRootProps) { 166 + return ( 167 + <OriginCropper.Root 168 + {...props} 169 + image={image} 170 + aspectRatio={aspectRatio} 171 + cropPadding={cropPadding} 172 + minZoom={minZoom} 173 + maxZoom={maxZoom} 174 + zoomSensitivity={zoomSensitivity} 175 + keyboardStep={keyboardStep} 176 + zoom={zoom} 177 + onCropChange={onCropChange} 178 + onZoomChange={onZoomChange} 179 + {...stylex.props(styles.root, style)} 180 + > 181 + {children} 182 + </OriginCropper.Root> 183 + ); 184 + } 185 + 186 + /** 187 + * Props for the ImageCropper image component. 188 + */ 189 + export interface ImageCropperImageProps extends StyleXComponentProps< 190 + Omit<ComponentProps<typeof OriginCropper.Image>, "className" | "style"> 191 + > {} 192 + 193 + /** 194 + * Renders the actual image element. It's positioned and scaled by ImageCropper.Root. 195 + * 196 + * @example 197 + * ```tsx 198 + * <ImageCropper.Image /> 199 + * ``` 200 + */ 201 + export function ImageCropperImage({ style, ...props }: ImageCropperImageProps) { 202 + return ( 203 + <OriginCropper.Image {...props} {...stylex.props(styles.image, style)} /> 204 + ); 205 + } 206 + 207 + /** 208 + * Props for the ImageCropper crop area component. 209 + */ 210 + export interface ImageCropperCropAreaProps extends StyleXComponentProps< 211 + Omit<ComponentProps<typeof OriginCropper.CropArea>, "className" | "style"> 212 + > {} 213 + 214 + /** 215 + * A visual indicator representing the crop area bounds. Style this component to show the crop overlay. 216 + * 217 + * @example 218 + * ```tsx 219 + * <ImageCropper.CropArea /> 220 + * ``` 221 + */ 222 + export function ImageCropperCropArea({ 223 + style, 224 + ...props 225 + }: ImageCropperCropAreaProps) { 226 + return ( 227 + <OriginCropper.CropArea 228 + {...props} 229 + {...stylex.props(styles.cropArea, style)} 230 + /> 231 + ); 232 + } 233 + 234 + /** 235 + * Props for the ImageCropper description component. 236 + */ 237 + export interface ImageCropperDescriptionProps extends StyleXComponentProps< 238 + Omit<ComponentProps<typeof OriginCropper.Description>, "className" | "style"> 239 + > { 240 + /** 241 + * Accessibility instructions for screen reader users. 242 + * This component is required for accessibility. 243 + */ 244 + children?: React.ReactNode; 245 + } 246 + 247 + /** 248 + * Renders accessibility instructions for screen reader users. 249 + * Its id is automatically linked via aria-describedby on the Root element. 250 + * This component is required for accessibility. 251 + * 252 + * @example 253 + * ```tsx 254 + * <ImageCropper.Description> 255 + * Use mouse wheel or pinch to zoom, drag to pan, and arrow keys to move the image. 256 + * </ImageCropper.Description> 257 + * ``` 258 + */ 259 + export function ImageCropperDescription({ 260 + style, 261 + children = "Use mouse wheel or pinch to zoom, drag to pan, and arrow keys to move the image.", 262 + ...props 263 + }: ImageCropperDescriptionProps) { 264 + return ( 265 + <OriginCropper.Description 266 + {...props} 267 + {...stylex.props(styles.description, style)} 268 + > 269 + {children} 270 + </OriginCropper.Description> 271 + ); 272 + } 273 + 274 + // eslint-disable-next-line react-refresh/only-export-components 275 + export const ImageCropper = { 276 + Root: ImageCropperRoot, 277 + Image: ImageCropperImage, 278 + CropArea: ImageCropperCropArea, 279 + Description: ImageCropperDescription, 280 + };
+1 -2
apps/docs/src/components/theme/useDialogStyles.ts
··· 43 43 [mediaQueries.supportsSquircle]: radius["4xl"], 44 44 }, 45 45 outline: "none", 46 + overflow: "hidden", 46 47 boxShadow: shadow["lg"], 47 48 display: "flex", 48 49 flexDirection: "column", ··· 58 59 }, 59 60 dialog: { 60 61 outline: "none", 61 - display: "flex", 62 - flexDirection: "column", 63 62 flexGrow: 1, 64 63 minHeight: 0, 65 64 },
+68
apps/docs/src/docs/components/content/image-cropper.mdx
··· 1 + --- 2 + title: ImageCropper 3 + description: A composable, headless React component for interactive image cropping with zoom, pan, and aspect ratio control. 4 + --- 5 + 6 + import { PropDocs } from '../../../lib/PropDocs' 7 + import { Example } from '../../../lib/Example' 8 + import { Basic } from '../../../examples/image-cropper/basic' 9 + import { DialogExample } from '../../../examples/image-cropper/dialog' 10 + 11 + <Example src={Basic} /> 12 + 13 + ## Installation 14 + 15 + Run the following command to add the image-cropper component to your project. 16 + 17 + ```bash 18 + pnpm hip install image-cropper 19 + ``` 20 + 21 + ## Props 22 + 23 + <PropDocs components={["ImageCropperRoot", "ImageCropperImage", "ImageCropperCropArea", "ImageCropperDescription"]} /> 24 + 25 + ## Example 26 + 27 + ### In a Dialog 28 + 29 + <Example src={DialogExample} /> 30 + 31 + ## Features 32 + 33 + ### Interactive Cropping 34 + 35 + The ImageCropper component provides a fully interactive cropping experience: 36 + 37 + - **Zoom**: Use mouse wheel or pinch gestures to zoom in/out 38 + - **Pan**: Drag with mouse or touch to move the image 39 + - **Keyboard Navigation**: Use arrow keys to pan the image 40 + - **Aspect Ratio**: Enforce a specific aspect ratio for the crop area 41 + - **Controlled/Uncontrolled**: Manage zoom state internally or control it via props 42 + 43 + ### Crop Data 44 + 45 + The `onCropChange` callback provides precise pixel coordinates of the cropped area relative to the original image: 46 + 47 + ```typescript 48 + type CropArea = { 49 + x: number; // X coordinate of top-left corner 50 + y: number; // Y coordinate of top-left corner 51 + width: number; // Width of cropped area 52 + height: number; // Height of cropped area 53 + } 54 + ``` 55 + 56 + ### Accessibility 57 + 58 + The ImageCropper component is designed with accessibility in mind: 59 + 60 + - **Required Description**: The `ImageCropper.Description` component is required for screen reader users 61 + - **ARIA Attributes**: Automatically includes proper ARIA attributes 62 + - **Keyboard Support**: Full keyboard navigation support 63 + 64 + ## Related Components 65 + 66 + - [AspectRatio](/docs/components/content/aspect-ratio) - For maintaining aspect ratios in layouts 67 + - [Card](/docs/components/content/card) - For grouping content in containers 68 +
+23
apps/docs/src/examples/image-cropper/basic.tsx
··· 1 + import { ImageCropper } from "@/components/image-cropper"; 2 + import * as stylex from "@stylexjs/stylex"; 3 + 4 + const styles = stylex.create({ 5 + example: { 6 + height: "320px", 7 + width: "max(80%, 320px)", 8 + }, 9 + }); 10 + 11 + export function Basic() { 12 + return ( 13 + <ImageCropper.Root 14 + image="https://images.unsplash.com/photo-1494790108377-be9c29b29330" 15 + aspectRatio={1} 16 + style={styles.example} 17 + > 18 + <ImageCropper.Description /> 19 + <ImageCropper.Image /> 20 + <ImageCropper.CropArea /> 21 + </ImageCropper.Root> 22 + ); 23 + }
+79
apps/docs/src/examples/image-cropper/dialog.tsx
··· 1 + import { Button } from "@/components/button"; 2 + import { 3 + Dialog, 4 + DialogBody, 5 + DialogDescription, 6 + DialogFooter, 7 + DialogHeader, 8 + } from "@/components/dialog"; 9 + import { Flex } from "@/components/flex"; 10 + import { ImageCropper } from "@/components/image-cropper"; 11 + import { Slider } from "@/components/slider"; 12 + import { spacing } from "../../components/theme/spacing.stylex"; 13 + import * as stylex from "@stylexjs/stylex"; 14 + import { ZoomInIcon, ZoomOutIcon } from "lucide-react"; 15 + import { useState } from "react"; 16 + 17 + const styles = stylex.create({ 18 + example: { 19 + height: "320px", 20 + }, 21 + sliderWrapper: { 22 + width: "100%", 23 + boxSizing: "border-box", 24 + paddingLeft: spacing["4"], 25 + paddingRight: spacing["4"], 26 + }, 27 + slider: { 28 + flexGrow: 1, 29 + }, 30 + }); 31 + 32 + export function DialogExample() { 33 + const [zoom, setZoom] = useState(1); 34 + 35 + return ( 36 + <Dialog trigger={<Button>Open Dialog</Button>}> 37 + <DialogHeader>Crop Image</DialogHeader> 38 + <DialogDescription> 39 + Choose the area of the image you want to crop. 40 + </DialogDescription> 41 + <DialogBody> 42 + <Flex direction="column" gap="4"> 43 + <ImageCropper.Root 44 + image="https://images.unsplash.com/photo-1494790108377-be9c29b29330" 45 + aspectRatio={1} 46 + style={styles.example} 47 + zoom={zoom} 48 + onZoomChange={setZoom} 49 + minZoom={0.2} 50 + maxZoom={5} 51 + > 52 + <ImageCropper.Description /> 53 + <ImageCropper.Image /> 54 + <ImageCropper.CropArea /> 55 + </ImageCropper.Root> 56 + <Flex align="center" gap="4" style={styles.sliderWrapper}> 57 + <ZoomOutIcon /> 58 + <Slider 59 + minValue={0.2} 60 + maxValue={5} 61 + step={0.01} 62 + value={zoom} 63 + onChange={setZoom} 64 + showValueLabel={false} 65 + style={styles.slider} 66 + /> 67 + <ZoomInIcon /> 68 + </Flex> 69 + </Flex> 70 + </DialogBody> 71 + <DialogFooter> 72 + <Button slot="close" variant="secondary"> 73 + Cancel 74 + </Button> 75 + <Button slot="close">Crop</Button> 76 + </DialogFooter> 77 + </Dialog> 78 + ); 79 + }
+1
packages/hip-ui/package.json
··· 27 27 }, 28 28 "dependencies": { 29 29 "@inkjs/ui": "^2.0.0", 30 + "@origin-space/image-cropper": "^0.1.9", 30 31 "@radix-ui/colors": "^3.0.0", 31 32 "@react-aria/overlays": "^3.30.0", 32 33 "@react-aria/utils": "^3.31.0",
+2
packages/hip-ui/src/cli/install.tsx
··· 47 47 import { headerLayoutConfig } from "../components/header-layout/header-layout-config.js"; 48 48 import { hoverCardConfig } from "../components/hover-card/hover-card-config.js"; 49 49 import { iconButtonConfig } from "../components/icon-button/icon-button-config.js"; 50 + import { imageCropperConfig } from "../components/image-cropper/image-cropper-config.js"; 50 51 import { kbdConfig } from "../components/kbd/kbd-config.js"; 51 52 import { labelConfig } from "../components/label/label-config.js"; 52 53 import { linkConfig } from "../components/link/link-config.js"; ··· 166 167 emptyStateConfig, 167 168 toastConfig, 168 169 windowSplitterConfig, 170 + imageCropperConfig, 169 171 ]; 170 172 171 173 function StringSetting({
+11 -5
packages/hip-ui/src/components/dialog/index.tsx
··· 17 17 import { Size, StyleXComponentProps } from "../theme/types"; 18 18 import { fontSize, typeramp } from "../theme/typography.stylex"; 19 19 import { useDialogStyles } from "../theme/useDialogStyles"; 20 + 20 21 const styles = stylex.create({ 21 22 dialog: { 23 + overflow: "auto", 22 24 paddingBottom: spacing["2"], 23 - paddingTop: spacing["2"], 24 25 }, 25 26 header: { 26 27 gap: spacing["2"], 27 28 alignItems: "center", 29 + backgroundColor: uiColor.bg, 28 30 display: "flex", 29 31 fontSize: fontSize["lg"], 30 32 justifyContent: "space-between", 33 + position: "sticky", 34 + zIndex: 1, 31 35 height: spacing["8"], 32 36 paddingBottom: spacing["2"], 33 37 paddingLeft: spacing["4"], 34 38 paddingRight: spacing["4"], 39 + paddingTop: spacing["2"], 40 + top: 0, 35 41 36 42 borderBottomColor: uiColor.border1, 37 43 borderBottomStyle: "solid", ··· 39 45 }, 40 46 description: { 41 47 color: uiColor.text1, 42 - paddingBottom: spacing["4"], 48 + marginBottom: spacing["4"], 49 + marginTop: spacing["4"], 43 50 paddingLeft: spacing["4"], 44 51 paddingRight: spacing["4"], 45 - paddingTop: spacing["4"], 46 52 }, 47 53 body: { 48 - paddingBottom: spacing["4"], 54 + marginBottom: spacing["4"], 55 + marginTop: spacing["4"], 49 56 paddingLeft: spacing["4"], 50 57 paddingRight: spacing["4"], 51 - paddingTop: spacing["4"], 52 58 }, 53 59 footer: { 54 60 gap: spacing["2"],
+16
packages/hip-ui/src/components/image-cropper/image-cropper-config.ts
··· 1 + import { ComponentConfig } from "../../types"; 2 + 3 + export const imageCropperConfig: ComponentConfig = { 4 + name: "image-cropper", 5 + filepath: "./index.tsx", 6 + hipDependencies: [ 7 + "../theme/spacing.stylex.tsx", 8 + "../theme/radius.stylex.tsx", 9 + "../theme/shadow.stylex.tsx", 10 + "../theme/color.stylex.tsx", 11 + "../theme/types.ts", 12 + ], 13 + dependencies: { 14 + "@origin-space/image-cropper": "^0.1.9", 15 + }, 16 + };
+280
packages/hip-ui/src/components/image-cropper/index.tsx
··· 1 + "use client"; 2 + 3 + import type { ComponentProps } from "react"; 4 + 5 + import { Cropper as OriginCropper } from "@origin-space/image-cropper"; 6 + import * as stylex from "@stylexjs/stylex"; 7 + 8 + import { uiColor } from "../theme/color.stylex"; 9 + import { radius } from "../theme/radius.stylex"; 10 + import { StyleXComponentProps } from "../theme/types"; 11 + 12 + const styles = stylex.create({ 13 + root: { 14 + borderColor: uiColor.border1, 15 + borderRadius: radius.md, 16 + borderStyle: "solid", 17 + borderWidth: 1, 18 + outline: { 19 + default: "none", 20 + ":focus-visible": `2px solid ${uiColor.solid1}`, 21 + }, 22 + overflow: "hidden", 23 + alignItems: "center", 24 + cursor: "move", 25 + display: "flex", 26 + justifyContent: "center", 27 + outlineOffset: { 28 + default: "0px", 29 + ":focus-visible": "2px", 30 + }, 31 + position: "relative", 32 + touchAction: "none", 33 + }, 34 + image: { 35 + objectFit: "cover", 36 + pointerEvents: "none", 37 + userSelect: "none", 38 + height: "100%", 39 + width: "100%", 40 + }, 41 + cropArea: { 42 + borderColor: uiColor.bg, 43 + borderStyle: "dashed", 44 + borderWidth: 2, 45 + boxShadow: `0 0 0 9999px rgba(0, 0, 0, 0.6)`, 46 + pointerEvents: "none", 47 + position: "absolute", 48 + }, 49 + description: { 50 + margin: -1, 51 + padding: 0, 52 + borderWidth: 0, 53 + overflow: "hidden", 54 + clip: "rect(0, 0, 0, 0)", 55 + position: "absolute", 56 + whiteSpace: "nowrap", 57 + height: 1, 58 + width: 1, 59 + }, 60 + }); 61 + 62 + /** 63 + * Area object representing the cropped region in pixels relative to the original image. 64 + */ 65 + export type CropArea = { 66 + x: number; 67 + y: number; 68 + width: number; 69 + height: number; 70 + }; 71 + 72 + /** 73 + * Props for the ImageCropper root component. 74 + */ 75 + export interface ImageCropperRootProps extends StyleXComponentProps< 76 + Omit< 77 + ComponentProps<typeof OriginCropper.Root>, 78 + "className" | "style" | "children" 79 + > 80 + > { 81 + /** 82 + * URL of the image to crop. 83 + */ 84 + image: string; 85 + /** 86 + * The desired width/height aspect ratio (e.g., 1, 1.5, 4/3, 16/9). 87 + * @default 1 88 + */ 89 + aspectRatio?: number; 90 + /** 91 + * Minimum padding (in pixels) between the crop area edges and the container edges. 92 + * @default 25 93 + */ 94 + cropPadding?: number; 95 + /** 96 + * Minimum zoom level (1 = 100% original size relative to crop area). 97 + * @default 1 98 + */ 99 + minZoom?: number; 100 + /** 101 + * Maximum zoom level. 102 + * @default 3 103 + */ 104 + maxZoom?: number; 105 + /** 106 + * Multiplier for mouse wheel delta to control zoom speed. 107 + * @default 0.005 108 + */ 109 + zoomSensitivity?: number; 110 + /** 111 + * Number of pixels to pan the image when using arrow keys. 112 + * @default 10 113 + */ 114 + keyboardStep?: number; 115 + /** 116 + * Controlled zoom level. If provided, component zoom state is controlled externally. 117 + */ 118 + zoom?: number; 119 + /** 120 + * Callback function triggered whenever the crop area changes. 121 + * Receives pixel data or null if invalid. 122 + */ 123 + onCropChange?: (pixels: CropArea | null) => void; 124 + /** 125 + * Callback function triggered when the zoom level changes interactively. 126 + * Essential for controlled zoom prop. 127 + */ 128 + onZoomChange?: (zoom: number) => void; 129 + /** 130 + * Child components. Should include ImageCropper.Image, ImageCropper.CropArea, and ImageCropper.Description. 131 + */ 132 + children: React.ReactNode; 133 + } 134 + 135 + /** 136 + * Root component for the image cropper. Handles logic, state, and interactions. 137 + * 138 + * @example 139 + * ```tsx 140 + * <ImageCropper.Root 141 + * image="https://example.com/image.jpg" 142 + * aspectRatio={1} 143 + * onCropChange={(area) => console.log(area)} 144 + * > 145 + * <ImageCropper.Description /> 146 + * <ImageCropper.Image /> 147 + * <ImageCropper.CropArea /> 148 + * </ImageCropper.Root> 149 + * ``` 150 + */ 151 + export function ImageCropperRoot({ 152 + style, 153 + image, 154 + aspectRatio = 1, 155 + cropPadding = 25, 156 + minZoom = 1, 157 + maxZoom = 3, 158 + zoomSensitivity = 0.005, 159 + keyboardStep = 10, 160 + zoom, 161 + onCropChange, 162 + onZoomChange, 163 + children, 164 + ...props 165 + }: ImageCropperRootProps) { 166 + return ( 167 + <OriginCropper.Root 168 + {...props} 169 + image={image} 170 + aspectRatio={aspectRatio} 171 + cropPadding={cropPadding} 172 + minZoom={minZoom} 173 + maxZoom={maxZoom} 174 + zoomSensitivity={zoomSensitivity} 175 + keyboardStep={keyboardStep} 176 + zoom={zoom} 177 + onCropChange={onCropChange} 178 + onZoomChange={onZoomChange} 179 + {...stylex.props(styles.root, style)} 180 + > 181 + {children} 182 + </OriginCropper.Root> 183 + ); 184 + } 185 + 186 + /** 187 + * Props for the ImageCropper image component. 188 + */ 189 + export interface ImageCropperImageProps extends StyleXComponentProps< 190 + Omit<ComponentProps<typeof OriginCropper.Image>, "className" | "style"> 191 + > {} 192 + 193 + /** 194 + * Renders the actual image element. It's positioned and scaled by ImageCropper.Root. 195 + * 196 + * @example 197 + * ```tsx 198 + * <ImageCropper.Image /> 199 + * ``` 200 + */ 201 + export function ImageCropperImage({ style, ...props }: ImageCropperImageProps) { 202 + return ( 203 + <OriginCropper.Image {...props} {...stylex.props(styles.image, style)} /> 204 + ); 205 + } 206 + 207 + /** 208 + * Props for the ImageCropper crop area component. 209 + */ 210 + export interface ImageCropperCropAreaProps extends StyleXComponentProps< 211 + Omit<ComponentProps<typeof OriginCropper.CropArea>, "className" | "style"> 212 + > {} 213 + 214 + /** 215 + * A visual indicator representing the crop area bounds. Style this component to show the crop overlay. 216 + * 217 + * @example 218 + * ```tsx 219 + * <ImageCropper.CropArea /> 220 + * ``` 221 + */ 222 + export function ImageCropperCropArea({ 223 + style, 224 + ...props 225 + }: ImageCropperCropAreaProps) { 226 + return ( 227 + <OriginCropper.CropArea 228 + {...props} 229 + {...stylex.props(styles.cropArea, style)} 230 + /> 231 + ); 232 + } 233 + 234 + /** 235 + * Props for the ImageCropper description component. 236 + */ 237 + export interface ImageCropperDescriptionProps extends StyleXComponentProps< 238 + Omit<ComponentProps<typeof OriginCropper.Description>, "className" | "style"> 239 + > { 240 + /** 241 + * Accessibility instructions for screen reader users. 242 + * This component is required for accessibility. 243 + */ 244 + children?: React.ReactNode; 245 + } 246 + 247 + /** 248 + * Renders accessibility instructions for screen reader users. 249 + * Its id is automatically linked via aria-describedby on the Root element. 250 + * This component is required for accessibility. 251 + * 252 + * @example 253 + * ```tsx 254 + * <ImageCropper.Description> 255 + * Use mouse wheel or pinch to zoom, drag to pan, and arrow keys to move the image. 256 + * </ImageCropper.Description> 257 + * ``` 258 + */ 259 + export function ImageCropperDescription({ 260 + style, 261 + children = "Use mouse wheel or pinch to zoom, drag to pan, and arrow keys to move the image.", 262 + ...props 263 + }: ImageCropperDescriptionProps) { 264 + return ( 265 + <OriginCropper.Description 266 + {...props} 267 + {...stylex.props(styles.description, style)} 268 + > 269 + {children} 270 + </OriginCropper.Description> 271 + ); 272 + } 273 + 274 + // eslint-disable-next-line react-refresh/only-export-components 275 + export const ImageCropper = { 276 + Root: ImageCropperRoot, 277 + Image: ImageCropperImage, 278 + CropArea: ImageCropperCropArea, 279 + Description: ImageCropperDescription, 280 + };
+1 -2
packages/hip-ui/src/components/theme/useDialogStyles.ts
··· 43 43 [mediaQueries.supportsSquircle]: radius["4xl"], 44 44 }, 45 45 outline: "none", 46 + overflow: "hidden", 46 47 boxShadow: shadow["lg"], 47 48 display: "flex", 48 49 flexDirection: "column", ··· 58 59 }, 59 60 dialog: { 60 61 outline: "none", 61 - display: "flex", 62 - flexDirection: "column", 63 62 flexGrow: 1, 64 63 minHeight: 0, 65 64 },
+17
pnpm-lock.yaml
··· 83 83 '@mdx-js/rollup': 84 84 specifier: ^3.1.1 85 85 version: 3.1.1(rollup@4.52.5) 86 + '@origin-space/image-cropper': 87 + specifier: ^0.1.9 88 + version: 0.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 86 89 '@react-aria/overlays': 87 90 specifier: ^3.30.0 88 91 version: 3.30.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ··· 392 395 '@inkjs/ui': 393 396 specifier: ^2.0.0 394 397 version: 2.0.0(ink@6.3.1(@types/react@19.2.0)(react@19.2.0)) 398 + '@origin-space/image-cropper': 399 + specifier: ^0.1.9 400 + version: 0.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 395 401 '@radix-ui/colors': 396 402 specifier: ^3.0.0 397 403 version: 3.0.0 ··· 1152 1158 '@oozcitak/util@8.3.8': 1153 1159 resolution: {integrity: sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==} 1154 1160 engines: {node: '>=8.0'} 1161 + 1162 + '@origin-space/image-cropper@0.1.9': 1163 + resolution: {integrity: sha512-Ebl57I6beh7uKjSv8+r3VObEoOXIXhx4SicDUdxa9U7NVY18SzEGfqPDuhNyXuFIB5G/S8nYUToNlIRp9wwB9Q==} 1164 + peerDependencies: 1165 + react: ^19.1.0 1166 + react-dom: ^19.1.0 1155 1167 1156 1168 '@parcel/watcher-android-arm64@2.5.1': 1157 1169 resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} ··· 8665 8677 '@oozcitak/util': 8.3.8 8666 8678 8667 8679 '@oozcitak/util@8.3.8': {} 8680 + 8681 + '@origin-space/image-cropper@0.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': 8682 + dependencies: 8683 + react: 19.2.0 8684 + react-dom: 19.2.0(react@19.2.0) 8668 8685 8669 8686 '@parcel/watcher-android-arm64@2.5.1': 8670 8687 optional: true