From b2aae70bba3515767f89a2b771bf995d622e0fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D8=B9=D9=84=DB=8C=D8=B1=D8=B6=D8=A7?= Date: Sun, 11 Oct 2026 12:29:28 +0330 Subject: [PATCH] feat: let the editor fit a narrow, phone-sized container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A flex item defaults to `min-width: auto`, which is a floor at its content's min-content width: it refuses to shrink below what it holds. 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, measured at ~13,600px of min-content — so in a 375px viewport the container did not constrain the editor. The editor stretched the container, the container stretched its parent, and the page grew a horizontal scrollbar: the tool rail was pushed off-screen and the whole UI read as zoomed in. Only ~1 of the 8 tool icons stayed reachable. Both the wrapper and the container now 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` still override it, matching the existing "flex first, a default the consumer can override" contract, so this is not a behaviour change for a container wide enough to hold the editor anyway. Also add `className` and `wrapperClassName`. The editor's own chrome belongs to the embed bundle, so the only way to rearrange it for a phone is a breakpoint of the consumer's — and a media query cannot be expressed in the `style` / `wrapperStyle` objects this component already accepts. The class names are the missing hook; `editorId` remains the id-based one. The README's new "Sizing and mobile" section states the three rules: the wrapper never widens its parent, the container needs a definite height (`minHeight` defaults to 500, so a full-height layout passes 0 or '100%'), and the embed's chrome is adapted through a breakpoint. --- README.md | 60 ++++++++++++++++++++++++++++++++++----------- src/ImageEditor.tsx | 17 +++++++++++-- src/types.ts | 8 ++++++ test/index.test.tsx | 36 +++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 16 deletions(-) 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();