diff --git a/README.md b/README.md index 2ea5a0d..3f3538d 100644 --- a/README.md +++ b/README.md @@ -48,20 +48,22 @@ The component works out of the box in React Server Components environments (e.g. ## Props -| Prop | Type | Description | -| -------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `image` | `string` (required) | Image URL or base64 data URL to edit. | -| `options` | `ImageEditorOptions` | Editor configuration: `projectId`, `user`, `features`, `theme`, `locale`, `translations`, `env`, `offline`, `licenseUrl`, `defaultPrompt`, `autoSubmitPrompt`, `aiAssistantOpenState`. | -| `editorId` | `string` | id for the container div. Cosmetic — the editor mounts by element reference. | -| `minHeight` | `number \| string` | Minimum height of the editor container. Defaults to `500`. | -| `style` | `CSSProperties` | Styles applied to the container div. Overrides the default `flex: 1`. | -| `wrapperStyle` | `CSSProperties` | Styles applied to the outer wrapper div, which owns `minHeight` and the flex layout. Set this to drop the editor into a non-flex layout. | -| `ariaLabel` | `string` | Accessible name for the editor region. Defaults to `'Image editor'`. | -| `onLoad` | `(editor) => void` | Called with the editor instance once it is mounted. | -| `onSave` | `({ dataUrl, blob }) => void` | Called when the user saves the edited image. | -| `onCancel` | `() => void` | Called when the user cancels editing. | -| `onLoadError` | `() => void` | Called when the image fails to load into the canvas (CORS, 404, decode error). | -| `onError` | `(error: Error) => void` | Wrapper-level failures: embed script load, editor creation, or image reset. Falls back to `console.error` when absent. | +| Prop | Type | Description | +| ------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `image` | `string` (required) | Image URL or base64 data URL to edit. | +| `options` | `ImageEditorOptions` | Editor configuration: `projectId`, `user`, `features`, `theme`, `locale`, `translations`, `env`, `offline`, `licenseUrl`, `defaultPrompt`, `autoSubmitPrompt`, `aiAssistantOpenState`. | +| `editorId` | `string` | id for the container div. Cosmetic — the editor mounts by element reference. | +| `minHeight` | `number \| string` | Minimum height of the editor container. Defaults to `500`. Pass `0` (or `'100%'`) in a full-height layout so the editor fits a phone-sized viewport instead of overflowing it. | +| `style` | `CSSProperties` | Styles applied to the container div. Overrides the default `flex: 1`. | +| `className` | `string` | Class name for the container div — the hook for a breakpoint or a stylesheet. | +| `wrapperStyle` | `CSSProperties` | Styles applied to the outer wrapper div, which owns `minHeight` and the flex layout. Set this to drop the editor into a non-flex layout. | +| `wrapperClassName` | `string` | Class name for the outer wrapper div. | +| `ariaLabel` | `string` | Accessible name for the editor region. Defaults to `'Image editor'`. | +| `onLoad` | `(editor) => void` | Called with the editor instance once it is mounted. | +| `onSave` | `({ dataUrl, blob }) => void` | Called when the user saves the edited image. | +| `onCancel` | `() => void` | Called when the user cancels editing. | +| `onLoadError` | `() => void` | Called when the image fails to load into the canvas (CORS, 404, decode error). | +| `onError` | `(error: Error) => void` | Wrapper-level failures: embed script load, editor creation, or image reset. Falls back to `console.error` when absent. | ## Editor instance (ref) @@ -88,6 +90,36 @@ const dataUrl = editorRef.current?.editor?.getImage(); | Any other `options` key | Full remount — the editor is destroyed and recreated with the new configuration. | | `onSave` / `onCancel` / `onLoadError` / `onLoad` / `onError` | Always call the latest handler; changing them never remounts. | +## Sizing and mobile + +The editor fills the container it is given. It has no desktop/phone layout switch of its own — the +tool rail, panels and toolbar come from the embed bundle — so a small screen is about the three +rules below, plus a breakpoint of your own if you want to rearrange that chrome. + +**The wrapper never widens its parent.** A flex item defaults to `min-width: auto`, refusing to +shrink below its content, and this editor's content is very wide: the sticker and shape panels lay +hundreds of tiles out in rows that never wrap. Both the wrapper and the container therefore default +to `min-width: 0`, so a narrow container really does constrain the editor and those wide rows scroll +inside their own panel. (`style` / `wrapperStyle` can put the old behaviour back.) + +**Give the container a definite height.** The wrapper owns `minHeight`, which defaults to `500`. In +a full-height layout pass `minHeight={0}` or `minHeight="100%"`; otherwise a phone viewport overflows +and the whole page scrolls. + +**Adapt the embed's chrome with a breakpoint.** `className` and `wrapperClassName` reach the container +and the wrapper, so a stylesheet can target what the embed renders inside them: + +```css +@media (max-width: 640px) { + .my-editor .image-editor-root .toolbar { + /* ... */ + } +} +``` + +The embed's own class names are not part of this component's API — pin `scriptUrl` to a specific +editor version before depending on them. + ## Error handling Two distinct channels: diff --git a/src/ImageEditor.tsx b/src/ImageEditor.tsx index c6c994a..c947bae 100644 --- a/src/ImageEditor.tsx +++ b/src/ImageEditor.tsx @@ -20,7 +20,9 @@ function ImageEditorInner( scriptUrl, minHeight = 500, style = {}, + className, wrapperStyle = {}, + wrapperClassName, ariaLabel = 'Image editor', } = props; @@ -213,11 +215,21 @@ function ImageEditorInner( // eslint-disable-next-line react-hooks/exhaustive-deps }, [editor, updatableKey]); + // The editor's content is enormously wide: the sticker and shape panels + // lay hundreds of tiles out in rows that never wrap, well over 10,000px of + // min-content width. A flex item refuses to shrink below its content + // unless its min-width is 0, so without the two minWidth defaults below the + // editor stretches whatever contains it — on a phone the page grows a + // horizontal scrollbar and the tool rail is pushed out of view — instead of + // scrolling inside the panel. With them, any container constrains the + // editor and responsive tweaks are a breakpoint away. return (
); diff --git a/src/types.ts b/src/types.ts index fb1b831..3670b72 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,11 +103,19 @@ export interface ImageEditorProps { minHeight?: number | string; /** Styles applied to the container div. Overrides the default `flex: 1`. */ style?: CSSProperties; + /** + * Class name for the container div (the element the editor mounts into). + * Combine with `editorId` (e.g. `#my-editor`) to target the editor from a + * stylesheet and rearrange its chrome below a breakpoint. + */ + className?: string; /** * Styles applied to the outer wrapper div, which owns `minHeight` and the * flex layout. Set this to drop the editor into a non-flex layout. */ wrapperStyle?: CSSProperties; + /** Class name for the outer wrapper div. */ + wrapperClassName?: string; /** * Accessible name for the editor region. The embed renders no landmark * and no name of its own. Defaults to 'Image editor'. diff --git a/test/index.test.tsx b/test/index.test.tsx index 706b7ed..2f3b43f 100644 --- a/test/index.test.tsx +++ b/test/index.test.tsx @@ -461,6 +461,42 @@ it('lets style override the container default and wrapperStyle reach the wrapper expect(wrapper.style.minHeight).toBe('0px'); }); +it('does not let the editor widen its container', async () => { + render(); + await flush(); + + const container = document.querySelector('#narrow') as HTMLElement; + const wrapper = container.parentElement as HTMLElement; + + // Both are flex items, and a flex item defaults to min-width:auto — it + // refuses to shrink below its content. The editor's content is far wider + // than any phone (the sticker and shape panels lay hundreds of tiles out + // in rows that never wrap), so without an explicit 0 the editor stretches + // its container instead of fitting it. + expect(container.style.minWidth).toBe('0px'); + expect(wrapper.style.minWidth).toBe('0px'); +}); + +it('reaches both elements with a class name, and lets style override minWidth', async () => { + render( + + ); + await flush(); + + const container = document.querySelector('#hooked') as HTMLElement; + const wrapper = container.parentElement as HTMLElement; + expect(container.className).toBe('my-editor'); + expect(wrapper.className).toBe('my-editor-shell'); + // The min-width:0 above is a default, not a hardcoded override. + expect(container.style.minWidth).toBe('320px'); +}); + it('gives the editor region an accessible name, overridable via ariaLabel', async () => { const { rerender } = render(); await flush();