@namroodinc/liquid-editorial 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -4
- package/UPGRADING.md +67 -1
- package/dist/components/ImageCropper/index.cjs +1 -1
- package/dist/components/ImageCropper/index.cjs.map +1 -1
- package/dist/components/ImageCropper/index.d.cts +3 -17
- package/dist/components/ImageCropper/index.d.cts.map +1 -1
- package/dist/components/ImageCropper/index.d.ts +3 -17
- package/dist/components/ImageCropper/index.d.ts.map +1 -1
- package/dist/components/ImageCropper/index.js +143 -180
- package/dist/components/ImageCropper/index.js.map +1 -1
- package/dist/components/ImageCropper/style.css +2 -94
- package/dist/components/Pagination/index.cjs +2 -0
- package/dist/components/Pagination/index.cjs.map +1 -0
- package/dist/components/Pagination/index.d.cts +24 -0
- package/dist/components/Pagination/index.d.cts.map +1 -0
- package/dist/components/Pagination/index.d.ts +24 -0
- package/dist/components/Pagination/index.d.ts.map +1 -0
- package/dist/components/Pagination/index.js +96 -0
- package/dist/components/Pagination/index.js.map +1 -0
- package/dist/components/Pagination/style.css +52 -0
- package/dist/components/Popover/index.cjs +1 -1
- package/dist/components/Popover/index.cjs.map +1 -1
- package/dist/components/Popover/index.d.cts +5 -1
- package/dist/components/Popover/index.d.cts.map +1 -1
- package/dist/components/Popover/index.d.ts +5 -1
- package/dist/components/Popover/index.d.ts.map +1 -1
- package/dist/components/Popover/index.js +25 -24
- package/dist/components/Popover/index.js.map +1 -1
- package/dist/components/Select/index.cjs +1 -1
- package/dist/components/Select/index.cjs.map +1 -1
- package/dist/components/Select/index.d.cts +3 -0
- package/dist/components/Select/index.d.cts.map +1 -1
- package/dist/components/Select/index.d.ts +3 -0
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +50 -48
- package/dist/components/Select/index.js.map +1 -1
- package/dist/components/Slider/style.css +14 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/styles.css +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Liquid Editorial
|
|
2
2
|
|
|
3
|
-
Accessible React components for editorial products. The package ships ESM, CommonJS, TypeScript declarations, and explicit CSS entry points.
|
|
3
|
+
Accessible React components for editorial products. The package ships ESM, CommonJS, TypeScript declarations, and explicit CSS entry points. Styles use those CSS entry points; the library does not inject styles through JavaScript.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -43,7 +43,12 @@ If an application only needs the design tokens, import `@namroodinc/liquid-edito
|
|
|
43
43
|
|
|
44
44
|
```tsx
|
|
45
45
|
import { useState } from "react";
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
EntryEditor,
|
|
48
|
+
validateEntryValues,
|
|
49
|
+
type ContentModel,
|
|
50
|
+
type EntryValues,
|
|
51
|
+
} from "@namroodinc/liquid-editorial";
|
|
47
52
|
|
|
48
53
|
const model: ContentModel = {
|
|
49
54
|
fields: [
|
|
@@ -54,8 +59,14 @@ const model: ContentModel = {
|
|
|
54
59
|
|
|
55
60
|
export function ContentEditor() {
|
|
56
61
|
const [value, setValue] = useState<EntryValues>({});
|
|
57
|
-
return
|
|
58
|
-
|
|
62
|
+
return (
|
|
63
|
+
<EntryEditor
|
|
64
|
+
model={model}
|
|
65
|
+
value={value}
|
|
66
|
+
onValueChange={setValue}
|
|
67
|
+
pickerItems={{ gallery: [{ id: "image-1", title: "Cover.jpg" }] }}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
// Before saving: validateEntryValues(model, value) returns errors keyed by field ID.
|
|
@@ -73,6 +84,98 @@ Links open a URL modal from the toolbar or by clicking an existing body link. Sa
|
|
|
73
84
|
|
|
74
85
|
Empty `TableBody` children automatically show “No data found.” across the table; override the text with `Table.emptyMessage`. Use `isEmpty` when custom row components determine whether there are results, or set it to `false` to suppress the message while the UI loads data. Loading indicators and skeleton rows are composed by the consuming UI. Standalone `TableEmpty` uses the same default message and supports a custom `title`. See the [upgrade guide example](UPGRADING.md#013).
|
|
75
86
|
|
|
87
|
+
## Image cropping
|
|
88
|
+
|
|
89
|
+
`ImageCropper` uses the library's custom engine with a fixed crop frame, diagonal background, shared `Slider` with a `Field` label/hint, and optional quarter-turn controls. Shared `Stack` components put the rotate/Reset buttons to the right of the zoom slider and wrap them below on narrow layouts. Supply one positive, finite `aspect` (width / height): `1` for an avatar, `16 / 9` for a cover, or `4 / 5` for a portrait.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useState } from "react";
|
|
93
|
+
import {
|
|
94
|
+
Button,
|
|
95
|
+
ImageCropper,
|
|
96
|
+
cropToBlob,
|
|
97
|
+
type CropRect,
|
|
98
|
+
type CropState,
|
|
99
|
+
} from "@namroodinc/liquid-editorial";
|
|
100
|
+
|
|
101
|
+
function CoverCropper({
|
|
102
|
+
src,
|
|
103
|
+
onSave,
|
|
104
|
+
}: {
|
|
105
|
+
src: string;
|
|
106
|
+
onSave: (blob: Blob) => void;
|
|
107
|
+
}) {
|
|
108
|
+
const [selection, setSelection] = useState<{
|
|
109
|
+
crop: CropRect;
|
|
110
|
+
state: CropState;
|
|
111
|
+
} | null>(null);
|
|
112
|
+
|
|
113
|
+
async function save() {
|
|
114
|
+
if (!selection) return;
|
|
115
|
+
const blob = await cropToBlob(src, selection.crop, {
|
|
116
|
+
outputWidth: 1600,
|
|
117
|
+
rotation: selection.state.rotation,
|
|
118
|
+
type: "image/jpeg",
|
|
119
|
+
});
|
|
120
|
+
onSave(blob);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<ImageCropper
|
|
126
|
+
src={src}
|
|
127
|
+
alt="Article cover"
|
|
128
|
+
aspect={16 / 9}
|
|
129
|
+
onChange={(crop, state) => setSelection({ crop, state })}
|
|
130
|
+
/>
|
|
131
|
+
<Button isDisabled={!selection} onClick={save}>
|
|
132
|
+
Save crop
|
|
133
|
+
</Button>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Drag the image or focus the stage and use arrow keys (Shift for a larger step). The image always covers the fixed crop window; panning, zooming out, resizing, and rotation keep the crop inside the image. Zoom defaults to a maximum of `3`; set `maxZoom` to change it. Rotation is enabled by default; set `rotatable={false}` to hide its buttons. Reset clears pan, zoom, and rotation while retaining the supplied aspect.
|
|
140
|
+
|
|
141
|
+
Changing `aspect` resets pan and zoom while preserving rotation. There is no ratio picker or implicit square default. `onChange(crop, state)` reports image-pixel coordinates plus pan, zoom, and quarter-turn rotation. `cropToBlob` supports same-origin or CORS-enabled sources and remains usable after unmounting with the saved crop/state. The forwarded ref and native HTML attributes apply to the outer container.
|
|
142
|
+
|
|
143
|
+
Cropper.js has been removed; no third-party cropper initialization or styles are needed. Keep importing the library's `styles.css`. See [UPGRADING.md](UPGRADING.md#unreleased) for migration from the temporary native Cropper.js API or the earlier shape-picker API.
|
|
144
|
+
|
|
145
|
+
## Pagination
|
|
146
|
+
|
|
147
|
+
`Pagination` is controlled: keep the page and page size together in application state, and apply the single change callback. Import it from the package root or `@namroodinc/liquid-editorial/components/Pagination`.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { useState } from "react";
|
|
151
|
+
import { Pagination, type PaginationState } from "@namroodinc/liquid-editorial";
|
|
152
|
+
|
|
153
|
+
export function ArticlePagination({ totalResults }: { totalResults: number }) {
|
|
154
|
+
const [pagination, setPagination] = useState<PaginationState>({
|
|
155
|
+
page: 1,
|
|
156
|
+
pageSize: 25,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Pagination
|
|
161
|
+
totalResults={totalResults}
|
|
162
|
+
{...pagination}
|
|
163
|
+
onChange={setPagination}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Pages are one-based. Changing the page size keeps the previously first visible result on the new page; for example, page 3 at 25 per page becomes page 2 at 50 per page. The callback emits `{ page, pageSize }` once per actual user change. Rendering new props emits no callback, and controls wait for the application to apply the new state.
|
|
170
|
+
|
|
171
|
+
The default is a small bar with Previous/Next labels, a per-page selector with `[10, 25, 50, 100]`, and a range count. Navigation uses the standard neutral `Button` component and its shared sizing and interaction styles. Set `size="md"`, `showLabels={false}`, or `menuPlacement="top"` as needed. Controls wrap in reading order on narrow screens. All themes use the shared tokens, and the existing `styles.css` import includes pagination styles.
|
|
172
|
+
|
|
173
|
+
Supply nonnegative integer totals, a positive integer page and page size, and a nonempty list of positive integer `pageSizeOptions` containing the current size. The displayed page and navigation targets are clamped to the available pages. Applications own data fetching, slicing, loading UI, and updating their canonical page after filtering or changing the total. Compose pagination beneath `Table` or another result view; it does not paginate children automatically.
|
|
174
|
+
|
|
175
|
+
Zero results display “No results” and disable every control. `isDisabled` also disables everything. Boundary navigation buttons are disabled, and the size selector is disabled when the total is at most the smallest size option. Override the navigation's `aria-label` when rendering multiple pagination bars.
|
|
176
|
+
|
|
177
|
+
`Select.menuPlacement` and `Popover.placement` also accept `"top"` or `"bottom"`; both default to `"bottom"` and flip when space requires it. A Select with `aria-labelledby` now shares that label with its listbox.
|
|
178
|
+
|
|
76
179
|
## Themes and fonts
|
|
77
180
|
|
|
78
181
|
Set `data-theme` on the document root to `light`, `dark`, `cherry-glaze`, or `black-hawaii`. The default font tokens use system stacks and can be overridden by the consumer without downloading bundled font files:
|
package/UPGRADING.md
CHANGED
|
@@ -2,25 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.6
|
|
6
|
+
|
|
5
7
|
## 0.1.5
|
|
6
8
|
|
|
7
9
|
### Changes
|
|
8
10
|
|
|
11
|
+
- ImageCropper places rotate/Reset buttons to the right of the zoom slider, vertically centred on its track. The shared controls wrap onto another row when space is limited.
|
|
12
|
+
|
|
9
13
|
- Pushes to `main`, including merged PRs, now automatically bump the patch version and publish the package to npm after validation.
|
|
10
14
|
- The release workflow moves these notes under the new version and commits the version files and guide back to `main`.
|
|
15
|
+
- Added controlled `Pagination` with standard neutral Previous/Next `Button`s, a per-page dropdown, a live result range, and wrapping layouts for narrow screens. Pagination supplies layout styles and uses the shared controls' appearance.
|
|
16
|
+
- Added optional `Select.menuPlacement` and `Popover.placement` (`"top"` or `"bottom"`). Select listboxes now share a trigger's `aria-labelledby` when supplied.
|
|
17
|
+
- Simplified `ImageCropper` to one required `aspect` prop controlled by the application. Removed the ratio picker, `ratios`, `onAspectChange`, and the `cropRatios` / `CropRatio` exports.
|
|
18
|
+
- ImageCropper composes shared `Stack`, `Field`, `Slider`, `ButtonGroup`, and `Button` components. Removed duplicate layout, label, hint, value, and slider styling; its CSS now covers only the crop surface, image, frame, and guides.
|
|
19
|
+
- Restored ImageCropper's custom engine, fixed crop frame, diagonal background, zoom slider, rotation buttons, and crop/state export API. Image boundaries remain enforced when panning, zooming out, rotating, or resizing. Removed Cropper.js and the temporary native `onReady` API.
|
|
11
20
|
|
|
12
21
|
### Adoption
|
|
13
22
|
|
|
14
23
|
Library contributors add upgrade guidance here and merge their changes to `main`. GitHub Actions handles versioning, validation, and publishing; there are no local npm release commands or manual GitHub Releases to create. Pull the generated version commit before continuing local work. Consuming applications can update the dependency as usual.
|
|
15
24
|
|
|
25
|
+
To adopt pagination, import `Pagination` from the package root or `@namroodinc/liquid-editorial/components/Pagination` and keep both values in application state:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const [pagination, setPagination] = useState({ page: 1, pageSize: 25 });
|
|
29
|
+
|
|
30
|
+
<Pagination totalResults={totalResults} {...pagination} onChange={setPagination} />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use the emitted page and page size together for requests or local slicing. Changing size preserves the first visible result (page 3 at size 25 becomes page 2 at size 50). Keep canonical state in bounds when filtering changes the total; prop changes do not emit corrections. Supply valid counts and positive integer sizes, with a nonempty `pageSizeOptions` containing the current size. The defaults are `[10, 25, 50, 100]`. The component handles no data loading or slicing; compose it beneath a table or another result view. The existing aggregate CSS import includes its styles. Give multiple pagination bars distinct `aria-label` values.
|
|
34
|
+
|
|
35
|
+
Use `menuPlacement="top"` on Pagination or Select, or `placement="top"` on Popover, to prefer an upward menu. Space constraints can still flip the panel to the other side.
|
|
36
|
+
|
|
37
|
+
Remove any `showFirstLast` or `joinedButtons` props from local integrations of the unreleased component. Pagination provides Previous/Next navigation only; First/Last controls and button joining are no longer part of its API.
|
|
38
|
+
|
|
39
|
+
Update every `ImageCropper` call to supply a positive, finite `aspect` (width / height):
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useState } from "react";
|
|
43
|
+
import { cropToBlob, type CropRect, type CropState } from "@namroodinc/liquid-editorial";
|
|
44
|
+
|
|
45
|
+
const [selection, setSelection] = useState<{ crop: CropRect; state: CropState } | null>(null);
|
|
46
|
+
<ImageCropper src={src} aspect={16 / 9}
|
|
47
|
+
onChange={(crop, state) => setSelection({ crop, state })} />
|
|
48
|
+
|
|
49
|
+
// In your save handler:
|
|
50
|
+
if (selection) {
|
|
51
|
+
const blob = await cropToBlob(src, selection.crop, {
|
|
52
|
+
outputWidth: 1600, rotation: selection.state.rotation, type: "image/jpeg",
|
|
53
|
+
});
|
|
54
|
+
uploadCrop(blob);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use `aspect={1}` to retain the former implicit square shape. Remove `ratios`, `onAspectChange`, `cropRatios`, and `CropRatio` usage. The application owns the destination shape; changing `aspect` resets pan/zoom and preserves rotation. Reset clears pan, zoom, and rotation while keeping that aspect.
|
|
59
|
+
|
|
60
|
+
If adopting local versions of the temporary Cropper.js integration, remove `onReady`, native Cropper type imports, and `$toCanvas()` usage. Use the restored `onChange(crop, state)` callback, `CropRect` / `CropState` types, and `cropToBlob(src, crop, { rotation: state.rotation })`. `CropToBlobOptions`, `maxZoom` (default `3`), and `rotatable` (default `true`) are restored. The standalone helper supports saved crops after unmounting and requires same-origin or CORS-enabled sources. Remove any direct Cropper.js dependency or styles added solely for this component.
|
|
61
|
+
|
|
62
|
+
Keep the library CSS import for the diagonal background, fixed frame, guides, shared Slider/Field, and standard Button controls. Zoom is labelled by the shared Field and displays Slider's standard value label (for example, `1×` or `1.25×`). No prop changes are needed. The instruction text now uses Field's hint and is associated with the Zoom slider through `aria-describedby`. The zoom slider and rotate/Reset buttons share a horizontal Stack, with actions to the right and the Field hint beneath the row. The actions wrap below the slider in narrow containers. Remove selectors for `.ds-cropper__controls`, `.ds-cropper__zoom`, `.ds-cropper__hint`, `.ds-cropper__slider`, `.ds-cropper__zoom-label`, and `.ds-cropper__value`; use accessible names when testing controls. The forwarded ref and native container attributes remain supported. Tests should exercise the stage's direct image and the library's controls; there are no Cropper.js elements or native movable/resizable selections.
|
|
63
|
+
|
|
16
64
|
### Compatibility
|
|
17
65
|
|
|
18
|
-
The
|
|
66
|
+
The ImageCropper control-row adjustment needs no adoption changes. It uses the existing Field, Slider, Stack, ButtonGroup, and Button components; control appearance, accessible associations, zoom values, and crop APIs are unchanged.
|
|
67
|
+
|
|
68
|
+
The workflow publishes stable patch releases under `latest`. Creating a GitHub Release no longer triggers publishing.
|
|
69
|
+
|
|
70
|
+
Pagination is an additive API and requires `totalResults`, `page`, `pageSize`, and `onChange({ page, pageSize })`; it has no uncontrolled mode or implicit result total. The new placement props are optional and retain the previous bottom-first behavior. Select listboxes with `aria-labelledby` now use the visible external label instead of the placeholder; update accessibility assertions that expected the placeholder in that case. The manifest and lockfile versions are unchanged; the release workflow performs the version bump.
|
|
71
|
+
|
|
72
|
+
The required single-aspect API is the remaining ImageCropper breaking change: `aspect` is required and the ratio picker, `ratios`, `onAspectChange`, `cropRatios`, and `CropRatio` are removed. Existing `CropRect`, `CropState`, `CropToBlobOptions`, `onChange`, `maxZoom`, `rotatable`, and `cropToBlob` contracts remain supported. Image-pixel coordinates are rounded and clamped inside the rotated source bounds.
|
|
73
|
+
|
|
74
|
+
The temporary Cropper.js integration is reverted. Its dependency, `onReady` callback, and native export workflow are removed. The fixed frame, rotation controls, theme-driven diagonal background, and custom engine are restored. Root/component imports, ESM/CommonJS, SSR, and React 18/19 remain supported. Styles are supplied through the library CSS entry points. The shared Slider/Field composition changes only the zoom control presentation and DOM; zoom bounds, step, crop callbacks, and the public API remain unchanged.
|
|
19
75
|
|
|
20
76
|
### Verification
|
|
21
77
|
|
|
22
78
|
Check the `Publish to npm` workflow in GitHub Actions and confirm that the new version appears on the npm package page. The workflow runs the existing full validation before committing and publishing.
|
|
23
79
|
|
|
80
|
+
- In consuming applications, verify first/last pages, partial last pages, zero results, custom size options, disabled/loading UI, and state correction after filtering.
|
|
81
|
+
- Change the size while on a later page and confirm that the first visible result remains on the new page and one callback updates both values.
|
|
82
|
+
- Exercise keyboard selection, Escape/Tab dismissal, focus, and accessible labels, including when two pagination bars are present.
|
|
83
|
+
- Check narrow layouts, themes, upward menu placement, and viewport flipping. Confirm Previous/Next match standard neutral Buttons of the same size and that First/Last controls are absent. Run the consuming project's type checks, tests, and build with its existing CSS import.
|
|
84
|
+
- Verify ImageCropper's rotate/Reset buttons sit to the right of the slider on wide layouts and wrap below it without overflow in narrow containers, including with rotation disabled.
|
|
85
|
+
- Confirm ImageCropper's Zoom slider has its visible Field label, associated Field hint, and standard Slider appearance/value label. Adjust it by pointer and keyboard, then check Reset and rotation update its controlled value. Check narrow layouts and all themes, including multiple instances with distinct label associations.
|
|
86
|
+
- For ImageCropper, check square, landscape, and portrait aspects. Confirm the fixed frame, diagonal background, zoom slider, rotation buttons, and Reset are present, and the ratio picker is absent.
|
|
87
|
+
- Pan to every boundary, zoom back out, lower `maxZoom`, rotate through quarter turns, and resize the stage. Confirm the frame stays covered and emitted crop coordinates remain inside the rotated source dimensions. Change `aspect` after pan/zoom/rotation and confirm only rotation is retained.
|
|
88
|
+
- Exercise mouse/touch dragging, keyboard nudges, multiple instances, optional rotation, and all themes. Export crops using `cropToBlob`, including a saved crop after unmounting, and check dimensions and image pixels. Run the consuming project's type checks, tests, build, and SSR/import checks.
|
|
89
|
+
|
|
24
90
|
## 0.1.3
|
|
25
91
|
|
|
26
92
|
### Changes
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../Button/index.cjs"),t=require("../ButtonGroup/index.cjs");let
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../Button/index.cjs"),t=require("../ButtonGroup/index.cjs"),n=require("../Field/index.cjs"),r=require("../Grid/index.cjs"),i=require("../Slider/index.cjs");let a=require("react"),o=require("react/jsx-runtime");var s=1,c={offsetX:0,offsetY:0,zoom:1,rotation:0},l=(0,a.forwardRef)(function({src:l,alt:u=``,aspect:d,maxZoom:f=3,rotatable:p=!0,onChange:m,className:h,...g},_){let v=(0,a.useRef)(null),[y,b]=(0,a.useState)({width:0,height:0}),[x,S]=(0,a.useState)({width:0,height:0}),[C,w]=(0,a.useState)(c),T=(0,a.useRef)(null),E=(0,a.useRef)(null);(0,a.useEffect)(()=>{w(e=>({...c,rotation:e.rotation}))},[d]);let D=e=>w(t=>({...c,rotation:(t.rotation+e+360)%360})),O=(0,a.useCallback)(e=>{e.naturalWidth&&(S({width:e.naturalWidth,height:e.naturalHeight}),w(c))},[]);(0,a.useEffect)(()=>{let e=E.current;e?.complete&&O(e)},[O,l]);let k=C.rotation%180!=0,A={width:k?x.height:x.width,height:k?x.width:x.height},j=A.width&&y.width?Math.max(y.width/A.width,y.height/A.height):0;(0,a.useLayoutEffect)(()=>{let e=v.current;if(!e)return;let t=()=>b({width:e.clientWidth,height:e.clientHeight});t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[d]);let M=(0,a.useCallback)(e=>{let t=Math.min(Math.max(s,f),Math.max(s,e.zoom)),n=j*t,r=n?Math.max(0,(A.width*n-y.width)/2):0,i=n?Math.max(0,(A.height*n-y.height)/2):0,a=Math.min(r,Math.max(-r,e.offsetX)),o=Math.min(i,Math.max(-i,e.offsetY));return t===e.zoom&&a===e.offsetX&&o===e.offsetY?e:{...e,zoom:t,offsetX:a,offsetY:o}},[j,f,A.height,A.width,y.height,y.width]),N=(0,a.useMemo)(()=>M(C),[M,C]);(0,a.useEffect)(()=>{w(N)},[N]);let P=j*N.zoom,F=P?Math.max(0,(A.width*P-y.width)/2):0,I=P?Math.max(0,(A.height*P-y.height)/2):0,L=`Drag the picture to reposition it. Nothing is written until you save — ${F===0&&I===0?`at this zoom the whole picture is in frame.`:`the veiled edges are what gets trimmed.`}`;(0,a.useEffect)(()=>{if(!P||!y.width)return;let e=Math.min(A.width,Math.round(y.width/P)),t=Math.min(A.height,Math.round(y.height/P));m?.({x:Math.max(0,Math.min(A.width-e,Math.round(A.width/2-N.offsetX/P-y.width/P/2))),y:Math.max(0,Math.min(A.height-t,Math.round(A.height/2-N.offsetY/P-y.height/P/2))),width:e,height:t},N)},[P,y.width,y.height,A.width,A.height,N]);let R=e=>{e.button===0&&(T.current={pointerId:e.pointerId,x:e.clientX,y:e.clientY},e.currentTarget.setPointerCapture(e.pointerId))},z=e=>{let t=T.current;if(!t||t.pointerId!==e.pointerId)return;let n=e.clientX-t.x,r=e.clientY-t.y;T.current={...t,x:e.clientX,y:e.clientY},w(e=>M({...e,offsetX:e.offsetX+n,offsetY:e.offsetY+r}))},B=e=>{T.current?.pointerId===e.pointerId&&(T.current=null,e.currentTarget.releasePointerCapture?.(e.pointerId))},V=(e,t)=>w(n=>M({...n,offsetX:n.offsetX+e,offsetY:n.offsetY+t}));return(0,o.jsxs)(r.Stack,{ref:_,className:[`ds-cropper`,h].filter(Boolean).join(` `),...g,children:[(0,o.jsxs)(`div`,{role:`application`,"aria-label":`Crop area — drag to reposition, arrow keys to nudge`,tabIndex:0,className:`ds-cropper__stage`,style:{"--cropper-aspect":String(d)},onPointerDown:R,onPointerMove:z,onPointerUp:B,onPointerCancel:B,onKeyDown:e=>{let t=e.shiftKey?24:8,n={ArrowLeft:[-t,0],ArrowRight:[t,0],ArrowUp:[0,-t],ArrowDown:[0,t]}[e.key];n&&(e.preventDefault(),V(n[0],n[1]))},children:[(0,o.jsx)(`img`,{ref:E,src:l,alt:u,draggable:!1,className:`ds-cropper__image`,onLoad:e=>O(e.currentTarget),style:{inlineSize:x.width*P||void 0,blockSize:x.height*P||void 0,transform:`translate(calc(-50% + ${N.offsetX}px), calc(-50% + ${N.offsetY}px)) rotate(${N.rotation}deg)`}}),(0,o.jsx)(`div`,{className:`ds-cropper__frame`,ref:v,"aria-hidden":`true`,children:(0,o.jsx)(`div`,{className:`ds-cropper__guides`})})]}),(0,o.jsx)(n.Field,{label:`Zoom`,hint:L,children:n=>(0,o.jsxs)(r.Stack,{direction:`horizontal`,gap:`md`,align:`center`,wrap:!0,children:[(0,o.jsx)(r.Stack,{style:{flex:`1 1 50%`,minInlineSize:0},children:(0,o.jsx)(i.Slider,{...n,min:s,max:Math.max(s,f),step:.01,value:N.zoom,suffix:`×`,onValueChange:e=>w(t=>M({...t,zoom:e}))})}),(0,o.jsxs)(t.ButtonGroup,{"aria-label":`Crop actions`,children:[p&&(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(e.Button,{size:`sm`,iconStart:`rotate-ccw`,"aria-label":`Rotate left`,onClick:()=>D(-90)}),(0,o.jsx)(e.Button,{size:`sm`,iconStart:`rotate-cw`,"aria-label":`Rotate right`,onClick:()=>D(90)})]}),(0,o.jsx)(e.Button,{size:`sm`,onClick:()=>w(c),children:`Reset`})]})]})})]})});async function u(e,t,{outputWidth:n,rotation:r=0,type:i=`image/png`,quality:a=.92}={}){let o=new Image;o.crossOrigin=`anonymous`,o.src=e,await o.decode();let s=r%180!=0,c=document.createElement(`canvas`);c.width=s?o.naturalHeight:o.naturalWidth,c.height=s?o.naturalWidth:o.naturalHeight;let l=c.getContext(`2d`);if(!l)throw Error(`Canvas 2D context unavailable`);l.translate(c.width/2,c.height/2),l.rotate(r*Math.PI/180),l.drawImage(o,-o.naturalWidth/2,-o.naturalHeight/2);let u=n??t.width,d=Math.round(t.height/t.width*u),f=document.createElement(`canvas`);f.width=u,f.height=d;let p=f.getContext(`2d`);if(!p)throw Error(`Canvas 2D context unavailable`);return p.drawImage(c,t.x,t.y,t.width,t.height,0,0,u,d),new Promise((e,t)=>{f.toBlob(n=>n?e(n):t(Error(`Could not encode the crop`)),i,a)})}exports.ImageCropper=l,exports.cropToBlob=u;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["forwardRef","useRef","useState","useCallback","ButtonGroup","Button","_Fragment","require_components_Button_index","require_components_ButtonGroup_index","react","react_jsx_runtime","cropRatios","MIN_ZOOM","REST","ImageCropper","rest","frameRef","frame","setFrame","natural","setNatural","state","setState","ratio","setRatio","aspect","dragRef","imageRef","prev","chooseRatio","next","onAspectChange","turn","by","adopt","img","src","quarter","view","baseScale","scale","maxOffsetX","maxOffsetY","node","measure","observer","clamp","nextScale","limitX","limitY","maxZoom","width","height","onChange","onPointerDown","event","onPointerMove","drag","dx","dy","endDrag","nudge","className","ratios","option","step","move","rotatable","cropToBlob","image","rotation","turned","turnedContext","outputWidth","crop","canvas","context","blob","resolve","reject","type","quality"],"sources":["../../../src/components/ImageCropper/index.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport { Button } from \"../Button\";\nimport { ButtonGroup } from \"../ButtonGroup\";\nimport \"./style.css\";\n\nexport interface CropRect {\n /** Left edge of the crop, in the (rotated) image's own pixels. */\n x: number;\n /** Top edge of the crop, in the (rotated) image's own pixels. */\n y: number;\n width: number;\n height: number;\n}\n\nexport interface CropState {\n /** Pan, in stage pixels, from the centre. */\n offsetX: number;\n offsetY: number;\n /** 1 is \"cover the crop window exactly\". */\n zoom: number;\n /** Quarter turns applied to the picture, clockwise: 0, 90, 180 or 270. */\n rotation: number;\n}\n\n/** One offered output shape. `value` is width / height. */\nexport interface CropRatio {\n label: string;\n value: number;\n}\n\n/** The house set: square, portrait, landscape, wide. */\nexport const cropRatios: CropRatio[] = [\n { label: \"1:1\", value: 1 },\n { label: \"4:5\", value: 4 / 5 },\n { label: \"4:3\", value: 4 / 3 },\n { label: \"16:9\", value: 16 / 9 },\n];\n\nexport interface ImageCropperProps\n extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n src: string;\n /** Describes the picture for people who cannot see the stage. */\n alt?: string;\n /** Output shape, width / height. 1 for an avatar, 16 / 9 for a hero. */\n aspect?: number;\n /**\n * Offer the reader a choice of output shapes. Omit it — or pass one ratio —\n * when the shape is fixed by the slot the picture is going into.\n */\n ratios?: CropRatio[];\n /** Fires when the reader picks a different shape. */\n onAspectChange?: (aspect: number) => void;\n /** Highest allowed zoom. Past 3 the result is mush on most uploads. */\n maxZoom?: number;\n /** Offer the quarter-turn controls. */\n rotatable?: boolean;\n /** Fires on every pan, zoom, turn and reset, with the crop in image pixels. */\n onChange?: (crop: CropRect, state: CropState) => void;\n}\n\nconst MIN_ZOOM = 1;\nconst REST: CropState = { offsetX: 0, offsetY: 0, zoom: 1, rotation: 0 };\n\n/**\n * Pan-and-zoom cropping over a fixed frame. The frame never moves — the\n * picture does — and the parts that fall outside it stay visible under a\n * veil, so the reader can see what they are trimming away.\n * Drag with a pointer, or focus the stage and use the arrow keys.\n */\nexport const ImageCropper = forwardRef<HTMLDivElement, ImageCropperProps>(\n function ImageCropper(\n {\n src,\n alt = \"\",\n aspect = 1,\n ratios,\n onAspectChange,\n maxZoom = 3,\n rotatable = true,\n onChange,\n className,\n ...rest\n },\n ref,\n ) {\n const frameRef = useRef<HTMLDivElement | null>(null);\n const [frame, setFrame] = useState({ width: 0, height: 0 });\n const [natural, setNatural] = useState({ width: 0, height: 0 });\n const [state, setState] = useState<CropState>(REST);\n const [ratio, setRatio] = useState(aspect);\n const dragRef = useRef<{ pointerId: number; x: number; y: number } | null>(null);\n const imageRef = useRef<HTMLImageElement | null>(null);\n\n // The prop stays the source of truth when the caller drives the shape.\n useEffect(() => {\n setRatio(aspect);\n setState((prev) => ({ ...REST, rotation: prev.rotation }));\n }, [aspect]);\n\n const chooseRatio = (next: number) => {\n setRatio(next);\n setState((prev) => ({ ...REST, rotation: prev.rotation }));\n onAspectChange?.(next);\n };\n\n const turn = (by: number) =>\n setState((prev) => ({ ...REST, rotation: (prev.rotation + by + 360) % 360 }));\n\n /** Take the picture's own pixels and start from a centred, unzoomed view. */\n const adopt = useCallback((img: HTMLImageElement) => {\n if (!img.naturalWidth) return;\n setNatural({ width: img.naturalWidth, height: img.naturalHeight });\n setState(REST);\n }, []);\n\n // A cached or server-rendered image can already be decoded before React\n // attaches onLoad, and then that event never fires.\n useEffect(() => {\n const img = imageRef.current;\n if (img?.complete) adopt(img);\n }, [adopt, src]);\n\n // After a quarter turn the picture presents its other side to the frame.\n const quarter = state.rotation % 180 !== 0;\n const view = {\n width: quarter ? natural.height : natural.width,\n height: quarter ? natural.width : natural.height,\n };\n\n // The scale at which the image exactly covers the frame; zoom multiplies it.\n const baseScale =\n view.width && frame.width\n ? Math.max(frame.width / view.width, frame.height / view.height)\n : 0;\n const scale = baseScale * state.zoom;\n const maxOffsetX = scale ? Math.max(0, (view.width * scale - frame.width) / 2) : 0;\n const maxOffsetY = scale ? Math.max(0, (view.height * scale - frame.height) / 2) : 0;\n\n useLayoutEffect(() => {\n const node = frameRef.current;\n if (!node) return;\n const measure = () =>\n setFrame({ width: node.clientWidth, height: node.clientHeight });\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const clamp = useCallback(\n (next: CropState): CropState => {\n const nextScale = baseScale * next.zoom;\n const limitX = nextScale\n ? Math.max(0, (view.width * nextScale - frame.width) / 2)\n : 0;\n const limitY = nextScale\n ? Math.max(0, (view.height * nextScale - frame.height) / 2)\n : 0;\n return {\n rotation: next.rotation,\n zoom: Math.min(maxZoom, Math.max(MIN_ZOOM, next.zoom)),\n offsetX: Math.min(limitX, Math.max(-limitX, next.offsetX)),\n offsetY: Math.min(limitY, Math.max(-limitY, next.offsetY)),\n };\n },\n [baseScale, maxZoom, view.height, view.width, frame.height, frame.width],\n );\n\n useEffect(() => {\n if (!scale || !frame.width) return;\n const width = frame.width / scale;\n const height = frame.height / scale;\n onChange?.(\n {\n x: Math.round(view.width / 2 - state.offsetX / scale - width / 2),\n y: Math.round(view.height / 2 - state.offsetY / scale - height / 2),\n width: Math.round(width),\n height: Math.round(height),\n },\n state,\n );\n // onChange is intentionally not a dependency: callers pass inline handlers.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scale, frame.width, frame.height, view.width, view.height, state]);\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.button !== 0) return;\n dragRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== event.pointerId) return;\n const dx = event.clientX - drag.x;\n const dy = event.clientY - drag.y;\n dragRef.current = { ...drag, x: event.clientX, y: event.clientY };\n setState((prev) => clamp({ ...prev, offsetX: prev.offsetX + dx, offsetY: prev.offsetY + dy }));\n };\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (dragRef.current?.pointerId !== event.pointerId) return;\n dragRef.current = null;\n event.currentTarget.releasePointerCapture?.(event.pointerId);\n };\n\n const nudge = (dx: number, dy: number) =>\n setState((prev) => clamp({ ...prev, offsetX: prev.offsetX + dx, offsetY: prev.offsetY + dy }));\n\n return (\n <div ref={ref} className={[\"ds-cropper\", className].filter(Boolean).join(\" \")} {...rest}>\n {ratios && ratios.length > 1 && (\n <ButtonGroup aria-label=\"Crop ratio\" className=\"ds-cropper__ratios\">\n {ratios.map((option) => (\n <Button\n key={option.label}\n size=\"sm\"\n variant={option.value === ratio ? \"active\" : \"neutral\"}\n aria-pressed={option.value === ratio}\n onClick={() => chooseRatio(option.value)}\n >\n {option.label}\n </Button>\n ))}\n </ButtonGroup>\n )}\n <div\n role=\"application\"\n aria-label=\"Crop area — drag to reposition, arrow keys to nudge\"\n tabIndex={0}\n className=\"ds-cropper__stage\"\n style={{ \"--cropper-aspect\": String(ratio) } as CSSProperties}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={(event) => {\n const step = event.shiftKey ? 24 : 8;\n const moves: Record<string, [number, number]> = {\n ArrowLeft: [-step, 0],\n ArrowRight: [step, 0],\n ArrowUp: [0, -step],\n ArrowDown: [0, step],\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n nudge(move[0], move[1]);\n }}\n >\n <img\n ref={imageRef}\n src={src}\n alt={alt}\n draggable={false}\n className=\"ds-cropper__image\"\n onLoad={(event) => adopt(event.currentTarget)}\n style={{\n inlineSize: natural.width * scale || undefined,\n blockSize: natural.height * scale || undefined,\n transform: `translate(calc(-50% + ${state.offsetX}px), calc(-50% + ${state.offsetY}px)) rotate(${state.rotation}deg)`,\n }}\n />\n {/* The window: everything outside it is veiled, not hidden. */}\n <div className=\"ds-cropper__frame\" ref={frameRef} aria-hidden=\"true\">\n <div className=\"ds-cropper__guides\" />\n </div>\n </div>\n\n <div className=\"ds-cropper__controls\">\n <label className=\"ds-cropper__zoom\">\n <span className=\"ds-cropper__zoom-label\">Zoom</span>\n <input\n type=\"range\"\n min={MIN_ZOOM}\n max={maxZoom}\n step={0.01}\n value={state.zoom}\n className=\"ds-cropper__slider\"\n onChange={(event) =>\n setState((prev) => clamp({ ...prev, zoom: Number(event.target.value) }))\n }\n />\n <span className=\"ds-cropper__value\">{state.zoom.toFixed(2)}×</span>\n </label>\n <ButtonGroup aria-label=\"Crop actions\">\n {rotatable && (\n <>\n <Button\n size=\"sm\"\n iconStart=\"rotate-ccw\"\n aria-label=\"Rotate left\"\n onClick={() => turn(-90)}\n />\n <Button\n size=\"sm\"\n iconStart=\"rotate-cw\"\n aria-label=\"Rotate right\"\n onClick={() => turn(90)}\n />\n </>\n )}\n <Button size=\"sm\" onClick={() => setState(REST)}>\n Reset\n </Button>\n </ButtonGroup>\n </div>\n\n <p className=\"ds-cropper__hint\">\n Drag the picture to reposition it. Nothing is written until you save —{\" \"}\n {maxOffsetX === 0 && maxOffsetY === 0\n ? \"at this zoom the whole picture is in frame.\"\n : \"the veiled edges are what gets trimmed.\"}\n </p>\n </div>\n );\n },\n);\n\nexport interface CropToBlobOptions {\n /** Width of the output in pixels; the height follows the crop's aspect. */\n outputWidth?: number;\n /** Quarter turns applied before cropping — pass `state.rotation`. */\n rotation?: number;\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n quality?: number;\n}\n\n/**\n * Renders a `CropRect` to an image blob in the browser. Same-origin or\n * CORS-enabled sources only — a tainted canvas cannot be exported.\n */\nexport async function cropToBlob(\n src: string,\n crop: CropRect,\n { outputWidth, rotation = 0, type = \"image/png\", quality = 0.92 }: CropToBlobOptions = {},\n): Promise<Blob> {\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.src = src;\n await image.decode();\n\n // The crop is expressed against the turned picture, so turn it first.\n const quarter = rotation % 180 !== 0;\n const turned = document.createElement(\"canvas\");\n turned.width = quarter ? image.naturalHeight : image.naturalWidth;\n turned.height = quarter ? image.naturalWidth : image.naturalHeight;\n const turnedContext = turned.getContext(\"2d\");\n if (!turnedContext) throw new Error(\"Canvas 2D context unavailable\");\n turnedContext.translate(turned.width / 2, turned.height / 2);\n turnedContext.rotate((rotation * Math.PI) / 180);\n turnedContext.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2);\n\n const width = outputWidth ?? crop.width;\n const height = Math.round((crop.height / crop.width) * width);\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const context = canvas.getContext(\"2d\");\n if (!context) throw new Error(\"Canvas 2D context unavailable\");\n context.drawImage(turned, crop.x, crop.y, crop.width, crop.height, 0, 0, width, height);\n\n return new Promise((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Could not encode the crop\"))),\n type,\n quality,\n );\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["forwardRef","useRef","useState","useCallback","useMemo","Stack","Field","Slider","ButtonGroup","_Fragment","Button","require_components_Button_index","require_components_ButtonGroup_index","require_components_Field_index","require_components_Grid_index","require_components_Slider_index","react","react_jsx_runtime","MIN_ZOOM","REST","ImageCropper","rest","frameRef","frame","setFrame","natural","setNatural","state","setState","dragRef","imageRef","prev","aspect","turn","by","adopt","img","src","quarter","view","baseScale","node","measure","observer","clamp","zoom","maxZoom","next","nextScale","limitX","limitY","offsetX","offsetY","bounded","scale","maxOffsetX","maxOffsetY","hint","width","height","onChange","onPointerDown","event","onPointerMove","drag","dx","dy","endDrag","nudge","className","step","move","props","rotatable","cropToBlob","image","rotation","turned","turnedContext","outputWidth","crop","canvas","context","blob","resolve","reject","type","quality"],"sources":["../../../src/components/ImageCropper/index.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type HTMLAttributes,\n type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport { Button } from \"../Button\";\nimport { ButtonGroup } from \"../ButtonGroup\";\nimport { Field } from \"../Field\";\nimport { Stack } from \"../Grid\";\nimport { Slider } from \"../Slider\";\nimport \"./style.css\";\n\nexport interface CropRect {\n /** Left edge of the crop, in the (rotated) image's own pixels. */\n x: number;\n /** Top edge of the crop, in the (rotated) image's own pixels. */\n y: number;\n width: number;\n height: number;\n}\n\nexport interface CropState {\n /** Pan, in stage pixels, from the centre. */\n offsetX: number;\n offsetY: number;\n /** 1 is \"cover the crop window exactly\". */\n zoom: number;\n /** Quarter turns applied to the picture, clockwise: 0, 90, 180 or 270. */\n rotation: number;\n}\n\nexport interface ImageCropperProps extends Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onChange\"\n> {\n src: string;\n /** Describes the picture for people who cannot see the stage. */\n alt?: string;\n /** Required output shape, width / height. Must be positive and finite. */\n aspect: number;\n /** Highest allowed zoom. Past 3 the result is mush on most uploads. */\n maxZoom?: number;\n /** Offer the quarter-turn controls. */\n rotatable?: boolean;\n /** Fires on every pan, zoom, turn and reset, with the crop in image pixels. */\n onChange?: (crop: CropRect, state: CropState) => void;\n}\n\nconst MIN_ZOOM = 1;\nconst REST: CropState = { offsetX: 0, offsetY: 0, zoom: 1, rotation: 0 };\n\n/**\n * Pan-and-zoom cropping over a fixed frame. The frame never moves — the\n * picture does — and the parts that fall outside it stay visible under a\n * veil, so the reader can see what they are trimming away.\n * Drag with a pointer, or focus the stage and use the arrow keys.\n */\nexport const ImageCropper = forwardRef<HTMLDivElement, ImageCropperProps>(\n function ImageCropper(\n {\n src,\n alt = \"\",\n aspect,\n maxZoom = 3,\n rotatable = true,\n onChange,\n className,\n ...rest\n },\n ref,\n ) {\n const frameRef = useRef<HTMLDivElement | null>(null);\n const [frame, setFrame] = useState({ width: 0, height: 0 });\n const [natural, setNatural] = useState({ width: 0, height: 0 });\n const [state, setState] = useState<CropState>(REST);\n const dragRef = useRef<{ pointerId: number; x: number; y: number } | null>(\n null,\n );\n const imageRef = useRef<HTMLImageElement | null>(null);\n\n // The prop stays the source of truth when the caller drives the shape.\n useEffect(() => {\n setState((prev) => ({ ...REST, rotation: prev.rotation }));\n }, [aspect]);\n\n const turn = (by: number) =>\n setState((prev) => ({\n ...REST,\n rotation: (prev.rotation + by + 360) % 360,\n }));\n\n /** Take the picture's own pixels and start from a centred, unzoomed view. */\n const adopt = useCallback((img: HTMLImageElement) => {\n if (!img.naturalWidth) return;\n setNatural({ width: img.naturalWidth, height: img.naturalHeight });\n setState(REST);\n }, []);\n\n // A cached or server-rendered image can already be decoded before React\n // attaches onLoad, and then that event never fires.\n useEffect(() => {\n const img = imageRef.current;\n if (img?.complete) adopt(img);\n }, [adopt, src]);\n\n // After a quarter turn the picture presents its other side to the frame.\n const quarter = state.rotation % 180 !== 0;\n const view = {\n width: quarter ? natural.height : natural.width,\n height: quarter ? natural.width : natural.height,\n };\n\n // The scale at which the image exactly covers the frame; zoom multiplies it.\n const baseScale =\n view.width && frame.width\n ? Math.max(frame.width / view.width, frame.height / view.height)\n : 0;\n\n useLayoutEffect(() => {\n const node = frameRef.current;\n if (!node) return;\n const measure = () =>\n setFrame({ width: node.clientWidth, height: node.clientHeight });\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(node);\n return () => observer.disconnect();\n }, [aspect]);\n\n const clamp = useCallback(\n (next: CropState): CropState => {\n const zoom = Math.min(\n Math.max(MIN_ZOOM, maxZoom),\n Math.max(MIN_ZOOM, next.zoom),\n );\n const nextScale = baseScale * zoom;\n const limitX = nextScale\n ? Math.max(0, (view.width * nextScale - frame.width) / 2)\n : 0;\n const limitY = nextScale\n ? Math.max(0, (view.height * nextScale - frame.height) / 2)\n : 0;\n const offsetX = Math.min(limitX, Math.max(-limitX, next.offsetX));\n const offsetY = Math.min(limitY, Math.max(-limitY, next.offsetY));\n if (\n zoom === next.zoom &&\n offsetX === next.offsetX &&\n offsetY === next.offsetY\n )\n return next;\n return { ...next, zoom, offsetX, offsetY };\n },\n [baseScale, maxZoom, view.height, view.width, frame.height, frame.width],\n );\n\n // Keep the rendered transform bounded when the frame or zoom limit changes too.\n const bounded = useMemo(() => clamp(state), [clamp, state]);\n useEffect(() => {\n setState(bounded);\n }, [bounded]);\n const scale = baseScale * bounded.zoom;\n const maxOffsetX = scale\n ? Math.max(0, (view.width * scale - frame.width) / 2)\n : 0;\n const maxOffsetY = scale\n ? Math.max(0, (view.height * scale - frame.height) / 2)\n : 0;\n const hint = `Drag the picture to reposition it. Nothing is written until you save — ${\n maxOffsetX === 0 && maxOffsetY === 0\n ? \"at this zoom the whole picture is in frame.\"\n : \"the veiled edges are what gets trimmed.\"\n }`;\n\n useEffect(() => {\n if (!scale || !frame.width) return;\n const width = Math.min(view.width, Math.round(frame.width / scale));\n const height = Math.min(view.height, Math.round(frame.height / scale));\n onChange?.(\n {\n x: Math.max(\n 0,\n Math.min(\n view.width - width,\n Math.round(\n view.width / 2 -\n bounded.offsetX / scale -\n frame.width / scale / 2,\n ),\n ),\n ),\n y: Math.max(\n 0,\n Math.min(\n view.height - height,\n Math.round(\n view.height / 2 -\n bounded.offsetY / scale -\n frame.height / scale / 2,\n ),\n ),\n ),\n width,\n height,\n },\n bounded,\n );\n // onChange is intentionally not a dependency: callers pass inline handlers.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [scale, frame.width, frame.height, view.width, view.height, bounded]);\n\n const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (event.button !== 0) return;\n dragRef.current = {\n pointerId: event.pointerId,\n x: event.clientX,\n y: event.clientY,\n };\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== event.pointerId) return;\n const dx = event.clientX - drag.x;\n const dy = event.clientY - drag.y;\n dragRef.current = { ...drag, x: event.clientX, y: event.clientY };\n setState((prev) =>\n clamp({\n ...prev,\n offsetX: prev.offsetX + dx,\n offsetY: prev.offsetY + dy,\n }),\n );\n };\n\n const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n if (dragRef.current?.pointerId !== event.pointerId) return;\n dragRef.current = null;\n event.currentTarget.releasePointerCapture?.(event.pointerId);\n };\n\n const nudge = (dx: number, dy: number) =>\n setState((prev) =>\n clamp({\n ...prev,\n offsetX: prev.offsetX + dx,\n offsetY: prev.offsetY + dy,\n }),\n );\n\n return (\n <Stack\n ref={ref}\n className={[\"ds-cropper\", className].filter(Boolean).join(\" \")}\n {...rest}\n >\n <div\n role=\"application\"\n aria-label=\"Crop area — drag to reposition, arrow keys to nudge\"\n tabIndex={0}\n className=\"ds-cropper__stage\"\n style={{ \"--cropper-aspect\": String(aspect) } as CSSProperties}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n onPointerCancel={endDrag}\n onKeyDown={(event) => {\n const step = event.shiftKey ? 24 : 8;\n const moves: Record<string, [number, number]> = {\n ArrowLeft: [-step, 0],\n ArrowRight: [step, 0],\n ArrowUp: [0, -step],\n ArrowDown: [0, step],\n };\n const move = moves[event.key];\n if (!move) return;\n event.preventDefault();\n nudge(move[0], move[1]);\n }}\n >\n <img\n ref={imageRef}\n src={src}\n alt={alt}\n draggable={false}\n className=\"ds-cropper__image\"\n onLoad={(event) => adopt(event.currentTarget)}\n style={{\n inlineSize: natural.width * scale || undefined,\n blockSize: natural.height * scale || undefined,\n transform: `translate(calc(-50% + ${bounded.offsetX}px), calc(-50% + ${bounded.offsetY}px)) rotate(${bounded.rotation}deg)`,\n }}\n />\n {/* The window: everything outside it is veiled, not hidden. */}\n <div className=\"ds-cropper__frame\" ref={frameRef} aria-hidden=\"true\">\n <div className=\"ds-cropper__guides\" />\n </div>\n </div>\n\n <Field label=\"Zoom\" hint={hint}>\n {(props) => (\n <Stack direction=\"horizontal\" gap=\"md\" align=\"center\" wrap>\n <Stack style={{ flex: \"1 1 50%\", minInlineSize: 0 }}>\n <Slider\n {...props}\n min={MIN_ZOOM}\n max={Math.max(MIN_ZOOM, maxZoom)}\n step={0.01}\n value={bounded.zoom}\n suffix=\"×\"\n onValueChange={(zoom) =>\n setState((prev) => clamp({ ...prev, zoom }))\n }\n />\n </Stack>\n <ButtonGroup aria-label=\"Crop actions\">\n {rotatable && (\n <>\n <Button\n size=\"sm\"\n iconStart=\"rotate-ccw\"\n aria-label=\"Rotate left\"\n onClick={() => turn(-90)}\n />\n <Button\n size=\"sm\"\n iconStart=\"rotate-cw\"\n aria-label=\"Rotate right\"\n onClick={() => turn(90)}\n />\n </>\n )}\n <Button size=\"sm\" onClick={() => setState(REST)}>\n Reset\n </Button>\n </ButtonGroup>\n </Stack>\n )}\n </Field>\n </Stack>\n );\n },\n);\n\nexport interface CropToBlobOptions {\n /** Width of the output in pixels; the height follows the crop's aspect. */\n outputWidth?: number;\n /** Quarter turns applied before cropping — pass `state.rotation`. */\n rotation?: number;\n type?: \"image/png\" | \"image/jpeg\" | \"image/webp\";\n quality?: number;\n}\n\n/**\n * Renders a `CropRect` to an image blob in the browser. Same-origin or\n * CORS-enabled sources only — a tainted canvas cannot be exported.\n */\nexport async function cropToBlob(\n src: string,\n crop: CropRect,\n {\n outputWidth,\n rotation = 0,\n type = \"image/png\",\n quality = 0.92,\n }: CropToBlobOptions = {},\n): Promise<Blob> {\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n image.src = src;\n await image.decode();\n\n // The crop is expressed against the turned picture, so turn it first.\n const quarter = rotation % 180 !== 0;\n const turned = document.createElement(\"canvas\");\n turned.width = quarter ? image.naturalHeight : image.naturalWidth;\n turned.height = quarter ? image.naturalWidth : image.naturalHeight;\n const turnedContext = turned.getContext(\"2d\");\n if (!turnedContext) throw new Error(\"Canvas 2D context unavailable\");\n turnedContext.translate(turned.width / 2, turned.height / 2);\n turnedContext.rotate((rotation * Math.PI) / 180);\n turnedContext.drawImage(\n image,\n -image.naturalWidth / 2,\n -image.naturalHeight / 2,\n );\n\n const width = outputWidth ?? crop.width;\n const height = Math.round((crop.height / crop.width) * width);\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const context = canvas.getContext(\"2d\");\n if (!context) throw new Error(\"Canvas 2D context unavailable\");\n context.drawImage(\n turned,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n 0,\n 0,\n width,\n height,\n );\n\n return new Promise((resolve, reject) => {\n canvas.toBlob(\n (blob) =>\n blob ? resolve(blob) : reject(new Error(\"Could not encode the crop\")),\n type,\n quality,\n );\n });\n}\n"],"mappings":"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"}
|
|
@@ -16,26 +16,12 @@ export interface CropState {
|
|
|
16
16
|
/** Quarter turns applied to the picture, clockwise: 0, 90, 180 or 270. */
|
|
17
17
|
rotation: number;
|
|
18
18
|
}
|
|
19
|
-
/** One offered output shape. `value` is width / height. */
|
|
20
|
-
export interface CropRatio {
|
|
21
|
-
label: string;
|
|
22
|
-
value: number;
|
|
23
|
-
}
|
|
24
|
-
/** The house set: square, portrait, landscape, wide. */
|
|
25
|
-
export declare const cropRatios: CropRatio[];
|
|
26
19
|
export interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
27
20
|
src: string;
|
|
28
21
|
/** Describes the picture for people who cannot see the stage. */
|
|
29
22
|
alt?: string;
|
|
30
|
-
/**
|
|
31
|
-
aspect
|
|
32
|
-
/**
|
|
33
|
-
* Offer the reader a choice of output shapes. Omit it — or pass one ratio —
|
|
34
|
-
* when the shape is fixed by the slot the picture is going into.
|
|
35
|
-
*/
|
|
36
|
-
ratios?: CropRatio[];
|
|
37
|
-
/** Fires when the reader picks a different shape. */
|
|
38
|
-
onAspectChange?: (aspect: number) => void;
|
|
23
|
+
/** Required output shape, width / height. Must be positive and finite. */
|
|
24
|
+
aspect: number;
|
|
39
25
|
/** Highest allowed zoom. Past 3 the result is mush on most uploads. */
|
|
40
26
|
maxZoom?: number;
|
|
41
27
|
/** Offer the quarter-turn controls. */
|
|
@@ -62,5 +48,5 @@ export interface CropToBlobOptions {
|
|
|
62
48
|
* Renders a `CropRect` to an image blob in the browser. Same-origin or
|
|
63
49
|
* CORS-enabled sources only — a tainted canvas cannot be exported.
|
|
64
50
|
*/
|
|
65
|
-
export declare function cropToBlob(src: string, crop: CropRect, { outputWidth, rotation, type, quality }?: CropToBlobOptions): Promise<Blob>;
|
|
51
|
+
export declare function cropToBlob(src: string, crop: CropRect, { outputWidth, rotation, type, quality, }?: CropToBlobOptions): Promise<Blob>;
|
|
66
52
|
//# sourceMappingURL=index.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../../../src/components/ImageCropper/index.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.cts","sourceRoot":"","sources":["../../../src/components/ImageCropper/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,OAAO,CAAC;AAMf,OAAO,aAAa,CAAC;AAErB,MAAM,WAAW,QAAQ;IACvB,kEAAkE;IAClE,CAAC,EAAE,MAAM,CAAC;IACV,iEAAiE;IACjE,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACvD;AAKD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,8GA6RxB,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,QAAQ,EACd,EACE,WAAW,EACX,QAAY,EACZ,IAAkB,EAClB,OAAc,GACf,GAAE,iBAAsB,GACxB,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
|
@@ -16,26 +16,12 @@ export interface CropState {
|
|
|
16
16
|
/** Quarter turns applied to the picture, clockwise: 0, 90, 180 or 270. */
|
|
17
17
|
rotation: number;
|
|
18
18
|
}
|
|
19
|
-
/** One offered output shape. `value` is width / height. */
|
|
20
|
-
export interface CropRatio {
|
|
21
|
-
label: string;
|
|
22
|
-
value: number;
|
|
23
|
-
}
|
|
24
|
-
/** The house set: square, portrait, landscape, wide. */
|
|
25
|
-
export declare const cropRatios: CropRatio[];
|
|
26
19
|
export interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
27
20
|
src: string;
|
|
28
21
|
/** Describes the picture for people who cannot see the stage. */
|
|
29
22
|
alt?: string;
|
|
30
|
-
/**
|
|
31
|
-
aspect
|
|
32
|
-
/**
|
|
33
|
-
* Offer the reader a choice of output shapes. Omit it — or pass one ratio —
|
|
34
|
-
* when the shape is fixed by the slot the picture is going into.
|
|
35
|
-
*/
|
|
36
|
-
ratios?: CropRatio[];
|
|
37
|
-
/** Fires when the reader picks a different shape. */
|
|
38
|
-
onAspectChange?: (aspect: number) => void;
|
|
23
|
+
/** Required output shape, width / height. Must be positive and finite. */
|
|
24
|
+
aspect: number;
|
|
39
25
|
/** Highest allowed zoom. Past 3 the result is mush on most uploads. */
|
|
40
26
|
maxZoom?: number;
|
|
41
27
|
/** Offer the quarter-turn controls. */
|
|
@@ -62,5 +48,5 @@ export interface CropToBlobOptions {
|
|
|
62
48
|
* Renders a `CropRect` to an image blob in the browser. Same-origin or
|
|
63
49
|
* CORS-enabled sources only — a tainted canvas cannot be exported.
|
|
64
50
|
*/
|
|
65
|
-
export declare function cropToBlob(src: string, crop: CropRect, { outputWidth, rotation, type, quality }?: CropToBlobOptions): Promise<Blob>;
|
|
51
|
+
export declare function cropToBlob(src: string, crop: CropRect, { outputWidth, rotation, type, quality, }?: CropToBlobOptions): Promise<Blob>;
|
|
66
52
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ImageCropper/index.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ImageCropper/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EAEpB,MAAM,OAAO,CAAC;AAMf,OAAO,aAAa,CAAC;AAErB,MAAM,WAAW,QAAQ;IACvB,kEAAkE;IAClE,CAAC,EAAE,MAAM,CAAC;IACV,iEAAiE;IACjE,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CACvD;AAKD;;;;;GAKG;AACH,eAAO,MAAM,YAAY,8GA6RxB,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,GAAG,YAAY,GAAG,YAAY,CAAC;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,QAAQ,EACd,EACE,WAAW,EACX,QAAY,EACZ,IAAkB,EAClB,OAAc,GACf,GAAE,iBAAsB,GACxB,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|