@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.
Files changed (45) hide show
  1. package/README.md +107 -4
  2. package/UPGRADING.md +67 -1
  3. package/dist/components/ImageCropper/index.cjs +1 -1
  4. package/dist/components/ImageCropper/index.cjs.map +1 -1
  5. package/dist/components/ImageCropper/index.d.cts +3 -17
  6. package/dist/components/ImageCropper/index.d.cts.map +1 -1
  7. package/dist/components/ImageCropper/index.d.ts +3 -17
  8. package/dist/components/ImageCropper/index.d.ts.map +1 -1
  9. package/dist/components/ImageCropper/index.js +143 -180
  10. package/dist/components/ImageCropper/index.js.map +1 -1
  11. package/dist/components/ImageCropper/style.css +2 -94
  12. package/dist/components/Pagination/index.cjs +2 -0
  13. package/dist/components/Pagination/index.cjs.map +1 -0
  14. package/dist/components/Pagination/index.d.cts +24 -0
  15. package/dist/components/Pagination/index.d.cts.map +1 -0
  16. package/dist/components/Pagination/index.d.ts +24 -0
  17. package/dist/components/Pagination/index.d.ts.map +1 -0
  18. package/dist/components/Pagination/index.js +96 -0
  19. package/dist/components/Pagination/index.js.map +1 -0
  20. package/dist/components/Pagination/style.css +52 -0
  21. package/dist/components/Popover/index.cjs +1 -1
  22. package/dist/components/Popover/index.cjs.map +1 -1
  23. package/dist/components/Popover/index.d.cts +5 -1
  24. package/dist/components/Popover/index.d.cts.map +1 -1
  25. package/dist/components/Popover/index.d.ts +5 -1
  26. package/dist/components/Popover/index.d.ts.map +1 -1
  27. package/dist/components/Popover/index.js +25 -24
  28. package/dist/components/Popover/index.js.map +1 -1
  29. package/dist/components/Select/index.cjs +1 -1
  30. package/dist/components/Select/index.cjs.map +1 -1
  31. package/dist/components/Select/index.d.cts +3 -0
  32. package/dist/components/Select/index.d.cts.map +1 -1
  33. package/dist/components/Select/index.d.ts +3 -0
  34. package/dist/components/Select/index.d.ts.map +1 -1
  35. package/dist/components/Select/index.js +50 -48
  36. package/dist/components/Select/index.js.map +1 -1
  37. package/dist/components/Slider/style.css +14 -5
  38. package/dist/index.cjs +1 -1
  39. package/dist/index.d.cts +5 -3
  40. package/dist/index.d.cts.map +1 -1
  41. package/dist/index.d.ts +5 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +9 -8
  44. package/dist/styles.css +1 -0
  45. package/package.json +1 -1
@@ -1,117 +1,102 @@
1
1
  import { Button as e } from "../Button/index.js";
2
2
  import { ButtonGroup as t } from "../ButtonGroup/index.js";
3
- import { forwardRef as n, useCallback as r, useEffect as i, useLayoutEffect as a, useRef as o, useState as s } from "react";
4
- import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
3
+ import { Field as n } from "../Field/index.js";
4
+ import { Stack as r } from "../Grid/index.js";
5
+ import { Slider as i } from "../Slider/index.js";
6
+ import { forwardRef as a, useCallback as o, useEffect as s, useLayoutEffect as c, useMemo as l, useRef as u, useState as d } from "react";
7
+ import { Fragment as f, jsx as p, jsxs as m } from "react/jsx-runtime";
5
8
  //#region src/components/ImageCropper/index.tsx
6
- var d = [
7
- {
8
- label: "1:1",
9
- value: 1
10
- },
11
- {
12
- label: "4:5",
13
- value: 4 / 5
14
- },
15
- {
16
- label: "4:3",
17
- value: 4 / 3
18
- },
19
- {
20
- label: "16:9",
21
- value: 16 / 9
22
- }
23
- ], f = 1, p = {
9
+ var h = 1, g = {
24
10
  offsetX: 0,
25
11
  offsetY: 0,
26
12
  zoom: 1,
27
13
  rotation: 0
28
- }, m = n(function({ src: n, alt: d = "", aspect: m = 1, ratios: h, onAspectChange: g, maxZoom: _ = 3, rotatable: v = !0, onChange: y, className: b, ...x }, S) {
29
- let C = o(null), [w, T] = s({
14
+ }, _ = a(function({ src: a, alt: _ = "", aspect: v, maxZoom: y = 3, rotatable: b = !0, onChange: x, className: S, ...C }, w) {
15
+ let T = u(null), [E, D] = d({
30
16
  width: 0,
31
17
  height: 0
32
- }), [E, D] = s({
18
+ }), [O, k] = d({
33
19
  width: 0,
34
20
  height: 0
35
- }), [O, k] = s(p), [A, j] = s(m), M = o(null), N = o(null);
36
- i(() => {
37
- j(m), k((e) => ({
38
- ...p,
21
+ }), [A, j] = d(g), M = u(null), N = u(null);
22
+ s(() => {
23
+ j((e) => ({
24
+ ...g,
39
25
  rotation: e.rotation
40
26
  }));
41
- }, [m]);
42
- let P = (e) => {
43
- j(e), k((e) => ({
44
- ...p,
45
- rotation: e.rotation
46
- })), g?.(e);
47
- }, F = (e) => k((t) => ({
48
- ...p,
27
+ }, [v]);
28
+ let P = (e) => j((t) => ({
29
+ ...g,
49
30
  rotation: (t.rotation + e + 360) % 360
50
- })), I = r((e) => {
51
- e.naturalWidth && (D({
31
+ })), F = o((e) => {
32
+ e.naturalWidth && (k({
52
33
  width: e.naturalWidth,
53
34
  height: e.naturalHeight
54
- }), k(p));
35
+ }), j(g));
55
36
  }, []);
56
- i(() => {
37
+ s(() => {
57
38
  let e = N.current;
58
- e?.complete && I(e);
59
- }, [I, n]);
60
- let L = O.rotation % 180 != 0, R = {
61
- width: L ? E.height : E.width,
62
- height: L ? E.width : E.height
63
- }, z = R.width && w.width ? Math.max(w.width / R.width, w.height / R.height) : 0, B = z * O.zoom, V = B ? Math.max(0, (R.width * B - w.width) / 2) : 0, H = B ? Math.max(0, (R.height * B - w.height) / 2) : 0;
64
- a(() => {
65
- let e = C.current;
39
+ e?.complete && F(e);
40
+ }, [F, a]);
41
+ let I = A.rotation % 180 != 0, L = {
42
+ width: I ? O.height : O.width,
43
+ height: I ? O.width : O.height
44
+ }, R = L.width && E.width ? Math.max(E.width / L.width, E.height / L.height) : 0;
45
+ c(() => {
46
+ let e = T.current;
66
47
  if (!e) return;
67
- let t = () => T({
48
+ let t = () => D({
68
49
  width: e.clientWidth,
69
50
  height: e.clientHeight
70
51
  });
71
52
  t();
72
53
  let n = new ResizeObserver(t);
73
54
  return n.observe(e), () => n.disconnect();
74
- }, []);
75
- let U = r((e) => {
76
- let t = z * e.zoom, n = t ? Math.max(0, (R.width * t - w.width) / 2) : 0, r = t ? Math.max(0, (R.height * t - w.height) / 2) : 0;
77
- return {
78
- rotation: e.rotation,
79
- zoom: Math.min(_, Math.max(f, e.zoom)),
80
- offsetX: Math.min(n, Math.max(-n, e.offsetX)),
81
- offsetY: Math.min(r, Math.max(-r, e.offsetY))
55
+ }, [v]);
56
+ let z = o((e) => {
57
+ let t = Math.min(Math.max(h, y), Math.max(h, e.zoom)), n = R * t, r = n ? Math.max(0, (L.width * n - E.width) / 2) : 0, i = n ? Math.max(0, (L.height * n - E.height) / 2) : 0, a = Math.min(r, Math.max(-r, e.offsetX)), o = Math.min(i, Math.max(-i, e.offsetY));
58
+ return t === e.zoom && a === e.offsetX && o === e.offsetY ? e : {
59
+ ...e,
60
+ zoom: t,
61
+ offsetX: a,
62
+ offsetY: o
82
63
  };
83
64
  }, [
84
- z,
85
- _,
86
- R.height,
87
- R.width,
88
- w.height,
89
- w.width
90
- ]);
91
- i(() => {
92
- if (!B || !w.width) return;
93
- let e = w.width / B, t = w.height / B;
94
- y?.({
95
- x: Math.round(R.width / 2 - O.offsetX / B - e / 2),
96
- y: Math.round(R.height / 2 - O.offsetY / B - t / 2),
97
- width: Math.round(e),
98
- height: Math.round(t)
99
- }, O);
65
+ R,
66
+ y,
67
+ L.height,
68
+ L.width,
69
+ E.height,
70
+ E.width
71
+ ]), B = l(() => z(A), [z, A]);
72
+ s(() => {
73
+ j(B);
74
+ }, [B]);
75
+ let V = R * B.zoom, H = V ? Math.max(0, (L.width * V - E.width) / 2) : 0, U = V ? Math.max(0, (L.height * V - E.height) / 2) : 0, W = `Drag the picture to reposition it. Nothing is written until you save — ${H === 0 && U === 0 ? "at this zoom the whole picture is in frame." : "the veiled edges are what gets trimmed."}`;
76
+ s(() => {
77
+ if (!V || !E.width) return;
78
+ let e = Math.min(L.width, Math.round(E.width / V)), t = Math.min(L.height, Math.round(E.height / V));
79
+ x?.({
80
+ x: Math.max(0, Math.min(L.width - e, Math.round(L.width / 2 - B.offsetX / V - E.width / V / 2))),
81
+ y: Math.max(0, Math.min(L.height - t, Math.round(L.height / 2 - B.offsetY / V - E.height / V / 2))),
82
+ width: e,
83
+ height: t
84
+ }, B);
100
85
  }, [
101
- B,
102
- w.width,
103
- w.height,
104
- R.width,
105
- R.height,
106
- O
86
+ V,
87
+ E.width,
88
+ E.height,
89
+ L.width,
90
+ L.height,
91
+ B
107
92
  ]);
108
- let W = (e) => {
93
+ let G = (e) => {
109
94
  e.button === 0 && (M.current = {
110
95
  pointerId: e.pointerId,
111
96
  x: e.clientX,
112
97
  y: e.clientY
113
98
  }, e.currentTarget.setPointerCapture(e.pointerId));
114
- }, G = (e) => {
99
+ }, K = (e) => {
115
100
  let t = M.current;
116
101
  if (!t || t.pointerId !== e.pointerId) return;
117
102
  let n = e.clientX - t.x, r = e.clientY - t.y;
@@ -119,129 +104,107 @@ var d = [
119
104
  ...t,
120
105
  x: e.clientX,
121
106
  y: e.clientY
122
- }, k((e) => U({
107
+ }, j((e) => z({
123
108
  ...e,
124
109
  offsetX: e.offsetX + n,
125
110
  offsetY: e.offsetY + r
126
111
  }));
127
- }, K = (e) => {
112
+ }, q = (e) => {
128
113
  M.current?.pointerId === e.pointerId && (M.current = null, e.currentTarget.releasePointerCapture?.(e.pointerId));
129
- }, q = (e, t) => k((n) => U({
114
+ }, J = (e, t) => j((n) => z({
130
115
  ...n,
131
116
  offsetX: n.offsetX + e,
132
117
  offsetY: n.offsetY + t
133
118
  }));
134
- return /* @__PURE__ */ u("div", {
135
- ref: S,
136
- className: ["ds-cropper", b].filter(Boolean).join(" "),
137
- ...x,
138
- children: [
139
- h && h.length > 1 && /* @__PURE__ */ l(t, {
140
- "aria-label": "Crop ratio",
141
- className: "ds-cropper__ratios",
142
- children: h.map((t) => /* @__PURE__ */ l(e, {
143
- size: "sm",
144
- variant: t.value === A ? "active" : "neutral",
145
- "aria-pressed": t.value === A,
146
- onClick: () => P(t.value),
147
- children: t.label
148
- }, t.label))
149
- }),
150
- /* @__PURE__ */ u("div", {
151
- role: "application",
152
- "aria-label": "Crop area — drag to reposition, arrow keys to nudge",
153
- tabIndex: 0,
154
- className: "ds-cropper__stage",
155
- style: { "--cropper-aspect": String(A) },
156
- onPointerDown: W,
157
- onPointerMove: G,
158
- onPointerUp: K,
159
- onPointerCancel: K,
160
- onKeyDown: (e) => {
161
- let t = e.shiftKey ? 24 : 8, n = {
162
- ArrowLeft: [-t, 0],
163
- ArrowRight: [t, 0],
164
- ArrowUp: [0, -t],
165
- ArrowDown: [0, t]
166
- }[e.key];
167
- n && (e.preventDefault(), q(n[0], n[1]));
168
- },
169
- children: [/* @__PURE__ */ l("img", {
170
- ref: N,
171
- src: n,
172
- alt: d,
173
- draggable: !1,
174
- className: "ds-cropper__image",
175
- onLoad: (e) => I(e.currentTarget),
119
+ return /* @__PURE__ */ m(r, {
120
+ ref: w,
121
+ className: ["ds-cropper", S].filter(Boolean).join(" "),
122
+ ...C,
123
+ children: [/* @__PURE__ */ m("div", {
124
+ role: "application",
125
+ "aria-label": "Crop area — drag to reposition, arrow keys to nudge",
126
+ tabIndex: 0,
127
+ className: "ds-cropper__stage",
128
+ style: { "--cropper-aspect": String(v) },
129
+ onPointerDown: G,
130
+ onPointerMove: K,
131
+ onPointerUp: q,
132
+ onPointerCancel: q,
133
+ onKeyDown: (e) => {
134
+ let t = e.shiftKey ? 24 : 8, n = {
135
+ ArrowLeft: [-t, 0],
136
+ ArrowRight: [t, 0],
137
+ ArrowUp: [0, -t],
138
+ ArrowDown: [0, t]
139
+ }[e.key];
140
+ n && (e.preventDefault(), J(n[0], n[1]));
141
+ },
142
+ children: [/* @__PURE__ */ p("img", {
143
+ ref: N,
144
+ src: a,
145
+ alt: _,
146
+ draggable: !1,
147
+ className: "ds-cropper__image",
148
+ onLoad: (e) => F(e.currentTarget),
149
+ style: {
150
+ inlineSize: O.width * V || void 0,
151
+ blockSize: O.height * V || void 0,
152
+ transform: `translate(calc(-50% + ${B.offsetX}px), calc(-50% + ${B.offsetY}px)) rotate(${B.rotation}deg)`
153
+ }
154
+ }), /* @__PURE__ */ p("div", {
155
+ className: "ds-cropper__frame",
156
+ ref: T,
157
+ "aria-hidden": "true",
158
+ children: /* @__PURE__ */ p("div", { className: "ds-cropper__guides" })
159
+ })]
160
+ }), /* @__PURE__ */ p(n, {
161
+ label: "Zoom",
162
+ hint: W,
163
+ children: (n) => /* @__PURE__ */ m(r, {
164
+ direction: "horizontal",
165
+ gap: "md",
166
+ align: "center",
167
+ wrap: !0,
168
+ children: [/* @__PURE__ */ p(r, {
176
169
  style: {
177
- inlineSize: E.width * B || void 0,
178
- blockSize: E.height * B || void 0,
179
- transform: `translate(calc(-50% + ${O.offsetX}px), calc(-50% + ${O.offsetY}px)) rotate(${O.rotation}deg)`
180
- }
181
- }), /* @__PURE__ */ l("div", {
182
- className: "ds-cropper__frame",
183
- ref: C,
184
- "aria-hidden": "true",
185
- children: /* @__PURE__ */ l("div", { className: "ds-cropper__guides" })
186
- })]
187
- }),
188
- /* @__PURE__ */ u("div", {
189
- className: "ds-cropper__controls",
190
- children: [/* @__PURE__ */ u("label", {
191
- className: "ds-cropper__zoom",
192
- children: [
193
- /* @__PURE__ */ l("span", {
194
- className: "ds-cropper__zoom-label",
195
- children: "Zoom"
196
- }),
197
- /* @__PURE__ */ l("input", {
198
- type: "range",
199
- min: f,
200
- max: _,
201
- step: .01,
202
- value: O.zoom,
203
- className: "ds-cropper__slider",
204
- onChange: (e) => k((t) => U({
205
- ...t,
206
- zoom: Number(e.target.value)
207
- }))
208
- }),
209
- /* @__PURE__ */ u("span", {
210
- className: "ds-cropper__value",
211
- children: [O.zoom.toFixed(2), "×"]
212
- })
213
- ]
214
- }), /* @__PURE__ */ u(t, {
170
+ flex: "1 1 50%",
171
+ minInlineSize: 0
172
+ },
173
+ children: /* @__PURE__ */ p(i, {
174
+ ...n,
175
+ min: h,
176
+ max: Math.max(h, y),
177
+ step: .01,
178
+ value: B.zoom,
179
+ suffix: "×",
180
+ onValueChange: (e) => j((t) => z({
181
+ ...t,
182
+ zoom: e
183
+ }))
184
+ })
185
+ }), /* @__PURE__ */ m(t, {
215
186
  "aria-label": "Crop actions",
216
- children: [v && /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(e, {
187
+ children: [b && /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(e, {
217
188
  size: "sm",
218
189
  iconStart: "rotate-ccw",
219
190
  "aria-label": "Rotate left",
220
- onClick: () => F(-90)
221
- }), /* @__PURE__ */ l(e, {
191
+ onClick: () => P(-90)
192
+ }), /* @__PURE__ */ p(e, {
222
193
  size: "sm",
223
194
  iconStart: "rotate-cw",
224
195
  "aria-label": "Rotate right",
225
- onClick: () => F(90)
226
- })] }), /* @__PURE__ */ l(e, {
196
+ onClick: () => P(90)
197
+ })] }), /* @__PURE__ */ p(e, {
227
198
  size: "sm",
228
- onClick: () => k(p),
199
+ onClick: () => j(g),
229
200
  children: "Reset"
230
201
  })]
231
202
  })]
232
- }),
233
- /* @__PURE__ */ u("p", {
234
- className: "ds-cropper__hint",
235
- children: [
236
- "Drag the picture to reposition it. Nothing is written until you save —",
237
- " ",
238
- V === 0 && H === 0 ? "at this zoom the whole picture is in frame." : "the veiled edges are what gets trimmed."
239
- ]
240
203
  })
241
- ]
204
+ })]
242
205
  });
243
206
  });
244
- async function h(e, t, { outputWidth: n, rotation: r = 0, type: i = "image/png", quality: a = .92 } = {}) {
207
+ async function v(e, t, { outputWidth: n, rotation: r = 0, type: i = "image/png", quality: a = .92 } = {}) {
245
208
  let o = new Image();
246
209
  o.crossOrigin = "anonymous", o.src = e, await o.decode();
247
210
  let s = r % 180 != 0, c = document.createElement("canvas");
@@ -258,6 +221,6 @@ async function h(e, t, { outputWidth: n, rotation: r = 0, type: i = "image/png",
258
221
  });
259
222
  }
260
223
  //#endregion
261
- export { m as ImageCropper, d as cropRatios, h as cropToBlob };
224
+ export { _ as ImageCropper, v as cropToBlob };
262
225
 
263
226
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_Fragment","Fragment$1","cropRatios","MIN_ZOOM","REST","ImageCropper","forwardRef","rest","frameRef","useRef","frame","setFrame","useState","natural","setNatural","state","setState","ratio","setRatio","aspect","dragRef","imageRef","prev","chooseRatio","next","onAspectChange","turn","by","adopt","useCallback","img","src","quarter","view","baseScale","scale","maxOffsetX","maxOffsetY","node","measure","observer","clamp","nextScale","limitX","limitY","maxZoom","width","height","onPointerDown","event","onPointerMove","drag","dx","dy","endDrag","nudge","className","ratios","ButtonGroup","Button","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.js","names":["_Fragment","Fragment$1","MIN_ZOOM","REST","ImageCropper","forwardRef","rest","frameRef","useRef","frame","setFrame","useState","natural","setNatural","state","setState","dragRef","imageRef","prev","aspect","turn","by","adopt","useCallback","img","src","quarter","view","baseScale","node","measure","observer","clamp","zoom","maxZoom","next","nextScale","limitX","limitY","offsetX","offsetY","bounded","useMemo","scale","maxOffsetX","maxOffsetY","hint","width","height","onPointerDown","event","onPointerMove","drag","dx","dy","endDrag","nudge","Stack","className","step","move","Field","Slider","props","ButtonGroup","rotatable","Button","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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"}
@@ -1,14 +1,4 @@
1
1
  @layer components {
2
- .ds-cropper {
3
- display: flex;
4
- flex-direction: column;
5
- gap: var(--space-3);
6
- }
7
-
8
- .ds-cropper__ratios {
9
- align-self: start;
10
- }
11
-
12
2
  /* The stage wears the theme: a diagonal stripe ground so the bleed outside
13
3
  * the crop window is visible but never competes with the image. */
14
4
  .ds-cropper__stage {
@@ -53,10 +43,10 @@
53
43
  inset-block-start: 50%;
54
44
  inset-inline-start: 50%;
55
45
  translate: -50% -50%;
56
- aspect-ratio: var(--cropper-aspect, 1);
46
+ aspect-ratio: var(--cropper-aspect);
57
47
  inline-size: min(
58
48
  calc(100% - var(--space-10)),
59
- calc((var(--cropper-stage-height) - var(--space-10)) * var(--cropper-aspect, 1))
49
+ calc((var(--cropper-stage-height) - var(--space-10)) * var(--cropper-aspect))
60
50
  );
61
51
  pointer-events: none;
62
52
  box-shadow: 0 0 0 100vmax var(--cropper-veil);
@@ -75,86 +65,4 @@
75
65
  linear-gradient(to bottom, transparent calc(66.666% - (var(--rule-width) / 2)), var(--cropper-grid-color) calc(66.666% - (var(--rule-width) / 2)), var(--cropper-grid-color) calc(66.666% + (var(--rule-width) / 2)), transparent calc(66.666% + (var(--rule-width) / 2)));
76
66
  }
77
67
 
78
-
79
- .ds-cropper__controls {
80
- display: flex;
81
- align-items: center;
82
- justify-content: space-between;
83
- gap: var(--space-4);
84
- flex-wrap: wrap;
85
- }
86
-
87
- .ds-cropper__zoom {
88
- display: flex;
89
- align-items: center;
90
- gap: var(--space-3);
91
- flex: 1 1 16rem;
92
- min-inline-size: 0;
93
- }
94
-
95
- .ds-cropper__zoom-label {
96
- font-size: var(--font-size-sm);
97
- font-weight: var(--font-weight-bold);
98
- }
99
-
100
- .ds-cropper__value {
101
- font-family: var(--font-mono);
102
- font-size: var(--font-size-xs);
103
- color: var(--color-text-muted);
104
- min-inline-size: 3.5rem;
105
- text-align: end;
106
- }
107
-
108
- .ds-cropper__slider {
109
- flex: 1 1 auto;
110
- min-inline-size: 0;
111
- appearance: none;
112
- background: transparent;
113
- block-size: var(--space-6);
114
- }
115
-
116
- .ds-cropper__slider::-webkit-slider-runnable-track {
117
- block-size: var(--control-border-width);
118
- background: var(--control-border-color);
119
- border-radius: var(--radius-pill);
120
- }
121
-
122
- .ds-cropper__slider::-moz-range-track {
123
- block-size: var(--control-border-width);
124
- background: var(--control-border-color);
125
- border-radius: var(--radius-pill);
126
- }
127
-
128
- .ds-cropper__slider::-webkit-slider-thumb {
129
- appearance: none;
130
- inline-size: var(--icon-md);
131
- block-size: var(--icon-md);
132
- margin-block-start: calc((var(--control-border-width) - var(--icon-md)) / 2);
133
- border: var(--control-border-width) solid var(--control-border-color);
134
- border-radius: var(--radius-pill);
135
- background: var(--color-primary);
136
- cursor: pointer;
137
- }
138
-
139
- .ds-cropper__slider::-moz-range-thumb {
140
- inline-size: var(--icon-md);
141
- block-size: var(--icon-md);
142
- border: var(--control-border-width) solid var(--control-border-color);
143
- border-radius: var(--radius-pill);
144
- background: var(--color-primary);
145
- cursor: pointer;
146
- }
147
-
148
- .ds-cropper__slider:focus-visible {
149
- outline: var(--control-border-width) solid var(--color-focus);
150
- outline-offset: 2px;
151
- border-radius: var(--radius-pill);
152
- }
153
-
154
- .ds-cropper__hint {
155
- margin: 0;
156
- font-size: var(--font-size-sm);
157
- line-height: var(--line-height-body);
158
- color: var(--color-text-muted);
159
- }
160
68
  }
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../Button/index.cjs"),t=require("../Select/index.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.forwardRef)(function({totalResults:i,page:a,pageSize:o,onChange:s,pageSizeOptions:c=[10,25,50,100],size:l=`sm`,isDisabled:u=!1,showLabels:d=!0,menuPlacement:f=`bottom`,className:p,...m},h){let g=(0,n.useId)(),_=Math.max(1,Math.ceil(i/o)),v=Math.min(Math.max(1,a),_),y=u||i===0,b=y||v===1,x=y||v===_,S=(v-1)*o+1,C=Math.min(v*o,i),w=e=>{let t=Math.min(Math.max(1,e),_);t!==v&&s({page:t,pageSize:o})};return(0,r.jsxs)(`nav`,{ref:h,"aria-label":`Pagination`,"data-size":l,className:[`ds-pagination`,p].filter(Boolean).join(` `),...m,children:[(0,r.jsx)(`div`,{className:`ds-pagination__actions`,children:(0,r.jsx)(e.Button,{variant:`neutral`,size:l,iconStart:`chevron-left`,isIconOnly:!d,"aria-label":`Previous page`,isDisabled:b,onClick:()=>w(v-1),children:`Previous`})}),(0,r.jsxs)(`div`,{className:`ds-pagination__centre`,children:[(0,r.jsxs)(`div`,{className:`ds-pagination__per-page`,children:[(0,r.jsx)(`span`,{id:g,children:`Per page`}),(0,r.jsx)(`div`,{className:`ds-pagination__select`,children:(0,r.jsx)(t.Select,{"aria-labelledby":g,value:String(o),options:c.map(e=>({value:String(e),label:String(e)})),size:l,menuPlacement:f,isDisabled:y||i<=Math.min(...c),onValueChange:e=>{let t=Number(e);if(t===o)return;let n=Math.floor((v-1)*o/t)+1,r=Math.max(1,Math.ceil(i/t));s({page:Math.min(n,r),pageSize:t})}})})]}),(0,r.jsx)(`span`,{className:`ds-pagination__range`,"aria-live":`polite`,"aria-atomic":`true`,children:i===0?`No results`:`${S.toLocaleString(`en-US`)}–${C.toLocaleString(`en-US`)} of ${i.toLocaleString(`en-US`)}`})]}),(0,r.jsx)(`div`,{className:`ds-pagination__actions ds-pagination__actions--next`,children:(0,r.jsx)(e.Button,{variant:`neutral`,size:l,iconStart:d?void 0:`chevron-right`,iconEnd:d?`chevron-right`:void 0,isIconOnly:!d,"aria-label":`Next page`,isDisabled:x,onClick:()=>w(v+1),children:`Next`})})]})});exports.Pagination=i;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["forwardRef","useId","Button","Select","require_components_Button_index","require_components_Select_index","react","react_jsx_runtime","Pagination","rest","labelId","pageCount","totalResults","pageSize","currentPage","page","disabled","isDisabled","atStart","atEnd","start","end","go","next","nextPage","onChange","size","className","showLabels","pageSizeOptions","value","nextSize","nextPageCount"],"sources":["../../../src/components/Pagination/index.tsx"],"sourcesContent":["import { forwardRef, useId, type HTMLAttributes } from \"react\";\nimport { Button } from \"../Button\";\nimport { Select } from \"../Select\";\nimport type { PopoverPlacement } from \"../Popover\";\nimport \"./style.css\";\n\nexport type PaginationSize = \"sm\" | \"md\";\n\nexport interface PaginationState {\n /** One-based page number. */\n page: number;\n /** Positive integer results per page. */\n pageSize: number;\n}\n\nexport interface PaginationProps\n extends\n Omit<HTMLAttributes<HTMLElement>, \"children\" | \"onChange\">,\n PaginationState {\n /** Total number of matching results, before pagination. */\n totalResults: number;\n /** Emitted once per user change, with both values ready to apply together. */\n onChange: (next: PaginationState) => void;\n /** Nonempty list of positive integers containing the current pageSize. */\n pageSizeOptions?: number[];\n size?: PaginationSize;\n isDisabled?: boolean;\n showLabels?: boolean;\n menuPlacement?: PopoverPlacement;\n}\n\n/** Controlled navigation for a result set. Data loading and slicing belong to the application. */\nexport const Pagination = forwardRef<HTMLElement, PaginationProps>(\n function Pagination(\n {\n totalResults,\n page,\n pageSize,\n onChange,\n pageSizeOptions = [10, 25, 50, 100],\n size = \"sm\",\n isDisabled = false,\n showLabels = true,\n menuPlacement = \"bottom\",\n className,\n ...rest\n },\n ref,\n ) {\n const labelId = useId();\n const pageCount = Math.max(1, Math.ceil(totalResults / pageSize));\n const currentPage = Math.min(Math.max(1, page), pageCount);\n const disabled = isDisabled || totalResults === 0;\n const atStart = disabled || currentPage === 1;\n const atEnd = disabled || currentPage === pageCount;\n const start = (currentPage - 1) * pageSize + 1;\n const end = Math.min(currentPage * pageSize, totalResults);\n\n const go = (nextPage: number) => {\n const next = Math.min(Math.max(1, nextPage), pageCount);\n if (next !== currentPage) onChange({ page: next, pageSize });\n };\n\n return (\n <nav\n ref={ref}\n aria-label=\"Pagination\"\n data-size={size}\n className={[\"ds-pagination\", className].filter(Boolean).join(\" \")}\n {...rest}\n >\n <div className=\"ds-pagination__actions\">\n <Button\n variant=\"neutral\"\n size={size}\n iconStart=\"chevron-left\"\n isIconOnly={!showLabels}\n aria-label=\"Previous page\"\n isDisabled={atStart}\n onClick={() => go(currentPage - 1)}\n >\n Previous\n </Button>\n </div>\n\n <div className=\"ds-pagination__centre\">\n <div className=\"ds-pagination__per-page\">\n <span id={labelId}>Per page</span>\n <div className=\"ds-pagination__select\">\n <Select\n aria-labelledby={labelId}\n value={String(pageSize)}\n options={pageSizeOptions.map((value) => ({\n value: String(value),\n label: String(value),\n }))}\n size={size}\n menuPlacement={menuPlacement}\n isDisabled={\n disabled || totalResults <= Math.min(...pageSizeOptions)\n }\n onValueChange={(value) => {\n const nextSize = Number(value);\n if (nextSize === pageSize) return;\n const nextPage =\n Math.floor(((currentPage - 1) * pageSize) / nextSize) + 1;\n const nextPageCount = Math.max(\n 1,\n Math.ceil(totalResults / nextSize),\n );\n onChange({\n page: Math.min(nextPage, nextPageCount),\n pageSize: nextSize,\n });\n }}\n />\n </div>\n </div>\n <span\n className=\"ds-pagination__range\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n {totalResults === 0\n ? \"No results\"\n : `${start.toLocaleString(\"en-US\")}–${end.toLocaleString(\"en-US\")} of ${totalResults.toLocaleString(\"en-US\")}`}\n </span>\n </div>\n\n <div className=\"ds-pagination__actions ds-pagination__actions--next\">\n <Button\n variant=\"neutral\"\n size={size}\n iconStart={showLabels ? undefined : \"chevron-right\"}\n iconEnd={showLabels ? \"chevron-right\" : undefined}\n isIconOnly={!showLabels}\n aria-label=\"Next page\"\n isDisabled={atEnd}\n onClick={() => go(currentPage + 1)}\n >\n Next\n </Button>\n </div>\n </nav>\n );\n },\n);\n"],"mappings":"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"}