@dloizides/ui-media 0.2.0 → 0.4.0

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/dist/index.js CHANGED
@@ -16,30 +16,31 @@ var framingToTransform = (framing) => [
16
16
  ];
17
17
 
18
18
  // src/framing/FramingAction.ts
19
- var FramingAction = {
20
- ZoomIn: "zoomIn",
21
- ZoomOut: "zoomOut",
22
- MoveUp: "moveUp",
23
- MoveDown: "moveDown",
24
- MoveLeft: "moveLeft",
25
- MoveRight: "moveRight",
26
- Reset: "reset"
27
- };
19
+ var FramingAction = /* @__PURE__ */ ((FramingAction2) => {
20
+ FramingAction2["ZoomIn"] = "zoomIn";
21
+ FramingAction2["ZoomOut"] = "zoomOut";
22
+ FramingAction2["MoveUp"] = "moveUp";
23
+ FramingAction2["MoveDown"] = "moveDown";
24
+ FramingAction2["MoveLeft"] = "moveLeft";
25
+ FramingAction2["MoveRight"] = "moveRight";
26
+ FramingAction2["Reset"] = "reset";
27
+ return FramingAction2;
28
+ })(FramingAction || {});
28
29
 
29
30
  // src/framing/stepFraming.ts
30
31
  var PHOTO_FRAMING_STEPS = Object.freeze({ offsetPct: 5, scale: 0.1 });
31
32
  var PRECISION = 1e3;
32
33
  var round = (n) => Math.round(n * PRECISION) / PRECISION;
33
34
  var DIRECTIONS = {
34
- [FramingAction.ZoomIn]: [0, 0, 1],
35
- [FramingAction.ZoomOut]: [0, 0, -1],
36
- [FramingAction.MoveUp]: [0, -1, 0],
37
- [FramingAction.MoveDown]: [0, 1, 0],
38
- [FramingAction.MoveLeft]: [-1, 0, 0],
39
- [FramingAction.MoveRight]: [1, 0, 0]
35
+ ["zoomIn" /* ZoomIn */]: [0, 0, 1],
36
+ ["zoomOut" /* ZoomOut */]: [0, 0, -1],
37
+ ["moveUp" /* MoveUp */]: [0, -1, 0],
38
+ ["moveDown" /* MoveDown */]: [0, 1, 0],
39
+ ["moveLeft" /* MoveLeft */]: [-1, 0, 0],
40
+ ["moveRight" /* MoveRight */]: [1, 0, 0]
40
41
  };
41
42
  function stepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
42
- if (action === FramingAction.Reset) return framing.DEFAULT_PHOTO_FRAMING;
43
+ if (action === "reset" /* Reset */) return framing.DEFAULT_PHOTO_FRAMING;
43
44
  const base = framing.clampFraming(current);
44
45
  const [dx, dy, ds] = DIRECTIONS[action];
45
46
  return framing.clampFraming({
@@ -55,12 +56,13 @@ function canStepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
55
56
  }
56
57
 
57
58
  // src/ImagePickerButton/ImagePickerStatus.ts
58
- var ImagePickerStatus = {
59
- Idle: "idle",
60
- Uploading: "uploading",
61
- Error: "error",
62
- Done: "done"
63
- };
59
+ var ImagePickerStatus = /* @__PURE__ */ ((ImagePickerStatus2) => {
60
+ ImagePickerStatus2["Idle"] = "idle";
61
+ ImagePickerStatus2["Uploading"] = "uploading";
62
+ ImagePickerStatus2["Error"] = "error";
63
+ ImagePickerStatus2["Done"] = "done";
64
+ return ImagePickerStatus2;
65
+ })(ImagePickerStatus || {});
64
66
  var DEFAULT_IMAGE_ACCEPT = "image/*";
65
67
  function pickWebImage(accept = DEFAULT_IMAGE_ACCEPT) {
66
68
  if (typeof document === "undefined") return Promise.resolve(null);
@@ -77,10 +79,11 @@ function useImageUpload({
77
79
  upload,
78
80
  pickFile,
79
81
  hasImage = false,
80
- onError
82
+ onError,
83
+ onUploaded
81
84
  }) {
82
85
  const [status, setStatus] = react.useState(
83
- hasImage ? ImagePickerStatus.Done : ImagePickerStatus.Idle
86
+ hasImage ? "done" /* Done */ : "idle" /* Idle */
84
87
  );
85
88
  const busy = react.useRef(false);
86
89
  const mounted = react.useRef(true);
@@ -96,28 +99,30 @@ function useImageUpload({
96
99
  const pick = react.useCallback(async () => {
97
100
  if (busy.current) return;
98
101
  busy.current = true;
102
+ let uploaded = null;
99
103
  try {
100
104
  const file = await pickFile();
101
105
  if (file === null) return;
102
- settle(ImagePickerStatus.Uploading);
103
- await upload(file);
104
- settle(ImagePickerStatus.Done);
106
+ settle("uploading" /* Uploading */);
107
+ uploaded = { result: await upload(file) };
108
+ settle("done" /* Done */);
105
109
  } catch (error) {
106
- settle(ImagePickerStatus.Error);
110
+ settle("error" /* Error */);
107
111
  onError?.(error);
108
112
  } finally {
109
113
  busy.current = false;
110
114
  }
111
- }, [pickFile, upload, onError, settle]);
115
+ if (uploaded !== null) onUploaded?.(uploaded.result);
116
+ }, [pickFile, upload, onError, onUploaded, settle]);
112
117
  return { status, pick };
113
118
  }
114
119
  function resolvePickerView(status, labels) {
115
120
  switch (status) {
116
- case ImagePickerStatus.Uploading:
121
+ case "uploading" /* Uploading */:
117
122
  return { buttonLabel: labels.uploading, variant: "outline", statusText: null, isError: false };
118
- case ImagePickerStatus.Error:
123
+ case "error" /* Error */:
119
124
  return { buttonLabel: labels.retry, variant: "danger", statusText: labels.error, isError: true };
120
- case ImagePickerStatus.Done:
125
+ case "done" /* Done */:
121
126
  return { buttonLabel: labels.replace, variant: "outline", statusText: labels.done, isError: false };
122
127
  default:
123
128
  return { buttonLabel: labels.pick, variant: "primary", statusText: null, isError: false };
@@ -131,6 +136,7 @@ var styles = reactNative.StyleSheet.create({
131
136
  });
132
137
  function ImagePickerButton({
133
138
  upload,
139
+ onUploaded,
134
140
  labels,
135
141
  testID,
136
142
  pickFile,
@@ -141,9 +147,15 @@ function ImagePickerButton({
141
147
  }) {
142
148
  const { theme } = uiFeedback.useUi();
143
149
  const webPick = react.useCallback(() => pickWebImage(accept), [accept]);
144
- const { status, pick } = useImageUpload({ upload, pickFile: pickFile ?? webPick, hasImage, onError });
150
+ const { status, pick } = useImageUpload({
151
+ upload,
152
+ pickFile: pickFile ?? webPick,
153
+ hasImage,
154
+ onError,
155
+ onUploaded
156
+ });
145
157
  const view = react.useMemo(() => resolvePickerView(status, labels), [status, labels]);
146
- const isUploading = status === ImagePickerStatus.Uploading;
158
+ const isUploading = status === "uploading" /* Uploading */;
147
159
  const statusColor = view.isError ? theme.semantic.error["500"] : theme.colors.textSecondary;
148
160
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.root, testID, children: [
149
161
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -171,7 +183,7 @@ function ImagePickerButton({
171
183
  ) : null
172
184
  ] });
173
185
  }
174
- var TARGET = 44;
186
+ var TARGET = uiLayout.MIN_TARGET_PX;
175
187
  var GAP = 8;
176
188
  var SECTION_GAP = 16;
177
189
  var HEADING_SIZE = 13;
@@ -179,13 +191,13 @@ var READOUT_MIN_WIDTH = 56;
179
191
  var READOUT_SIZE = 15;
180
192
  var PERCENT = 100;
181
193
  var DEFAULT_GLYPHS = {
182
- [FramingAction.ZoomIn]: "+",
183
- [FramingAction.ZoomOut]: "\u2212",
184
- [FramingAction.MoveUp]: "\u2191",
185
- [FramingAction.MoveDown]: "\u2193",
186
- [FramingAction.MoveLeft]: "\u2190",
187
- [FramingAction.MoveRight]: "\u2192",
188
- [FramingAction.Reset]: "\u21BA"
194
+ ["zoomIn" /* ZoomIn */]: "+",
195
+ ["zoomOut" /* ZoomOut */]: "\u2212",
196
+ ["moveUp" /* MoveUp */]: "\u2191",
197
+ ["moveDown" /* MoveDown */]: "\u2193",
198
+ ["moveLeft" /* MoveLeft */]: "\u2190",
199
+ ["moveRight" /* MoveRight */]: "\u2192",
200
+ ["reset" /* Reset */]: "\u21BA"
189
201
  };
190
202
  var styles2 = reactNative.StyleSheet.create({
191
203
  root: { gap: SECTION_GAP },
@@ -223,31 +235,41 @@ var FramingControls = ({
223
235
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.section, children: [
224
236
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [styles2.heading, heading], children: labels.size }),
225
237
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.row, children: [
226
- control(FramingAction.ZoomOut),
238
+ control("zoomOut" /* ZoomOut */),
227
239
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.readout, { color: theme.colors.text }], testID: `${testID}-scale`, children: labels.scaleValue(Math.round(framing.scale * PERCENT)) }),
228
- control(FramingAction.ZoomIn)
240
+ control("zoomIn" /* ZoomIn */)
229
241
  ] })
230
242
  ] }),
231
243
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.section, children: [
232
244
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [styles2.heading, heading], children: labels.position }),
233
245
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.row, children: [
234
246
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.spacer }),
235
- control(FramingAction.MoveUp),
247
+ control("moveUp" /* MoveUp */),
236
248
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.spacer })
237
249
  ] }),
238
250
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.row, children: [
239
- control(FramingAction.MoveLeft),
240
- control(FramingAction.Reset),
241
- control(FramingAction.MoveRight)
251
+ control("moveLeft" /* MoveLeft */),
252
+ control("reset" /* Reset */),
253
+ control("moveRight" /* MoveRight */)
242
254
  ] }),
243
255
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.row, children: [
244
256
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.spacer }),
245
- control(FramingAction.MoveDown),
257
+ control("moveDown" /* MoveDown */),
246
258
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.spacer })
247
259
  ] })
248
260
  ] })
249
261
  ] });
250
262
  };
263
+
264
+ // src/PhotoFramingEditor/PhotoAnchor.ts
265
+ var PhotoAnchor = /* @__PURE__ */ ((PhotoAnchor3) => {
266
+ PhotoAnchor3["Center"] = "center";
267
+ PhotoAnchor3["Bottom"] = "bottom";
268
+ return PhotoAnchor3;
269
+ })(PhotoAnchor || {});
270
+ function resolvePhotoAnchor(anchor) {
271
+ return anchor === "bottom" /* Bottom */ ? { justifyContent: "flex-end", objectPosition: "center bottom" } : { justifyContent: "center", objectPosition: "center center" };
272
+ }
251
273
  var GAP2 = 16;
252
274
  var styles3 = reactNative.StyleSheet.create({
253
275
  root: { flexDirection: "column", gap: GAP2, alignItems: "stretch" },
@@ -263,18 +285,27 @@ var PhotoFramingEditor = ({
263
285
  testID,
264
286
  steps,
265
287
  renderGlyph,
266
- disabled = false
288
+ disabled = false,
289
+ anchor = "center" /* Center */
267
290
  }) => {
268
291
  const { width } = reactNative.useWindowDimensions();
269
292
  const wide = width >= uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
270
293
  const framing$1 = react.useMemo(() => framing.clampFraming(value), [value]);
271
294
  const transform = react.useMemo(() => framingToTransform(framing$1), [framing$1]);
295
+ const anchorLayout = react.useMemo(() => resolvePhotoAnchor(anchor), [anchor]);
272
296
  const onAction = react.useCallback(
273
297
  (action) => onChange(stepFraming(framing$1, action, steps)),
274
298
  [framing$1, onChange, steps]
275
299
  );
276
300
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles3.root, wide ? styles3.rootWide : null], testID, children: [
277
- /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.preview, wide ? styles3.previewWide : null], testID: `${testID}-preview`, children: renderPreview({ framing: framing$1, transform }) }),
301
+ /* @__PURE__ */ jsxRuntime.jsx(
302
+ reactNative.View,
303
+ {
304
+ style: [styles3.preview, { justifyContent: anchorLayout.justifyContent }, wide ? styles3.previewWide : null],
305
+ testID: `${testID}-preview`,
306
+ children: renderPreview({ framing: framing$1, transform, anchor, objectPosition: anchorLayout.objectPosition })
307
+ }
308
+ ),
278
309
  /* @__PURE__ */ jsxRuntime.jsx(
279
310
  FramingControls,
280
311
  {
@@ -290,6 +321,57 @@ var PhotoFramingEditor = ({
290
321
  ] });
291
322
  };
292
323
 
324
+ // src/AnchoredPhoto/resolveAnchoredPhotoLayout.ts
325
+ function resolveAnchoredPhotoLayout(anchor, aspectRatio) {
326
+ const { justifyContent } = resolvePhotoAnchor(anchor);
327
+ const usable = aspectRatio !== null && Number.isFinite(aspectRatio) && aspectRatio > 0;
328
+ return { justifyContent, aspectRatio: usable ? aspectRatio : null };
329
+ }
330
+ function useImageAspectRatio(uri) {
331
+ const [measured, setMeasured] = react.useState(null);
332
+ react.useEffect(() => {
333
+ if (uri === "") return void 0;
334
+ let active = true;
335
+ reactNative.Image.getSize(
336
+ uri,
337
+ (width, height) => {
338
+ if (active && width > 0 && height > 0) setMeasured({ uri, ratio: width / height });
339
+ },
340
+ () => void 0
341
+ );
342
+ return () => {
343
+ active = false;
344
+ };
345
+ }, [uri]);
346
+ return measured?.uri === uri ? measured.ratio : null;
347
+ }
348
+ var styles4 = reactNative.StyleSheet.create({
349
+ frame: { ...reactNative.StyleSheet.absoluteFillObject, alignItems: "center" },
350
+ fill: { height: "100%", width: "100%" },
351
+ sized: { maxHeight: "100%", width: "100%" }
352
+ });
353
+ var AnchoredPhoto = ({
354
+ uri,
355
+ anchor = "center" /* Center */,
356
+ framing: framing$1,
357
+ style,
358
+ accessibilityLabel,
359
+ testID
360
+ }) => {
361
+ const layout = resolveAnchoredPhotoLayout(anchor, useImageAspectRatio(uri));
362
+ const transform = framingToTransform(framing.clampFraming(framing$1));
363
+ const imageStyle = layout.aspectRatio === null ? styles4.fill : [styles4.sized, { aspectRatio: layout.aspectRatio }];
364
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.frame, { justifyContent: layout.justifyContent, transform }, style], testID, children: /* @__PURE__ */ jsxRuntime.jsx(
365
+ reactNative.Image,
366
+ {
367
+ accessibilityLabel,
368
+ resizeMode: "contain",
369
+ source: { uri },
370
+ style: imageStyle
371
+ }
372
+ ) });
373
+ };
374
+
293
375
  Object.defineProperty(exports, "DEFAULT_PHOTO_FRAMING", {
294
376
  enumerable: true,
295
377
  get: function () { return framing.DEFAULT_PHOTO_FRAMING; }
@@ -306,17 +388,22 @@ Object.defineProperty(exports, "isDefaultFraming", {
306
388
  enumerable: true,
307
389
  get: function () { return framing.isDefaultFraming; }
308
390
  });
391
+ exports.AnchoredPhoto = AnchoredPhoto;
309
392
  exports.DEFAULT_IMAGE_ACCEPT = DEFAULT_IMAGE_ACCEPT;
310
393
  exports.FramingAction = FramingAction;
311
394
  exports.ImagePickerButton = ImagePickerButton;
312
395
  exports.ImagePickerStatus = ImagePickerStatus;
313
396
  exports.PHOTO_FRAMING_STEPS = PHOTO_FRAMING_STEPS;
397
+ exports.PhotoAnchor = PhotoAnchor;
314
398
  exports.PhotoFramingEditor = PhotoFramingEditor;
315
399
  exports.canStepFraming = canStepFraming;
316
400
  exports.framingToTransform = framingToTransform;
317
401
  exports.pickWebImage = pickWebImage;
402
+ exports.resolveAnchoredPhotoLayout = resolveAnchoredPhotoLayout;
403
+ exports.resolvePhotoAnchor = resolvePhotoAnchor;
318
404
  exports.resolvePickerView = resolvePickerView;
319
405
  exports.stepFraming = stepFraming;
406
+ exports.useImageAspectRatio = useImageAspectRatio;
320
407
  exports.useImageUpload = useImageUpload;
321
408
  //# sourceMappingURL=index.js.map
322
409
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts","../src/ImagePickerButton/ImagePickerStatus.ts","../src/ImagePickerButton/useImageUpload.ts","../src/ImagePickerButton/ImagePickerButton.tsx","../src/PhotoFramingEditor/FramingControls.tsx","../src/PhotoFramingEditor/PhotoFramingEditor.tsx"],"names":["DEFAULT_PHOTO_FRAMING","clampFraming","useState","useRef","useEffect","useCallback","StyleSheet","useUi","useMemo","jsxs","View","jsx","Button","Text","styles","IconButton","GAP","useWindowDimensions","LAYOUT_COLLAPSE_BREAKPOINT","framing"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Photo framing: the `{x, y, scale}` an organiser sets on a performer card.\n *\n * The bounds, default, clamp and identity check come from\n * `@dloizides/site-template-kit/framing` — the same module the kefi-landings\n * renderer builds its `--photo-tf` CSS from (KEFI-PEOPLE-1 T12 \"Organizer people\n * editor: shared photo framing\"). One copy, so the editor can never offer a value\n * the landing page would silently clamp away.\n *\n * That subpath imports nothing (no ajv, no JSON Schema), so this file stays\n * framework-free: it backs the `@dloizides/ui-media/framing` subpath.\n */\nimport type { PhotoFraming } from '@dloizides/site-template-kit/framing';\n\nexport {\n PHOTO_FRAMING_BOUNDS,\n DEFAULT_PHOTO_FRAMING,\n clampFraming,\n isDefaultFraming,\n} from '@dloizides/site-template-kit/framing';\nexport type { PhotoFraming, PhotoFramingInput } from '@dloizides/site-template-kit/framing';\n\n/**\n * An RN `transform` array. Percent translates are relative to the element\n * itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.\n * Typed locally so this module stays free of a react-native import.\n */\nexport type PhotoFramingTransform = [\n { translateX: `${number}%` },\n { translateY: `${number}%` },\n { scale: number },\n];\n\nexport const framingToTransform = (framing: PhotoFraming): PhotoFramingTransform => [\n { translateX: `${framing.x}%` },\n { translateY: `${framing.y}%` },\n { scale: framing.scale },\n];\n","/**\n * The seven controls of the framing editor. An `as const` object rather than a\n * TS `const enum`: a const enum shipped in a .d.ts cannot be read by consumers\n * compiled with `isolatedModules` (every Expo / Babel portal).\n */\nexport const FramingAction = {\n ZoomIn: 'zoomIn',\n ZoomOut: 'zoomOut',\n MoveUp: 'moveUp',\n MoveDown: 'moveDown',\n MoveLeft: 'moveLeft',\n MoveRight: 'moveRight',\n Reset: 'reset',\n} as const;\n\nexport type FramingAction = (typeof FramingAction)[keyof typeof FramingAction];\n","import { FramingAction } from './FramingAction';\nimport type { PhotoFraming, PhotoFramingInput } from './photoFraming';\nimport { DEFAULT_PHOTO_FRAMING, clampFraming } from './photoFraming';\n\n/** How far one press moves or zooms. */\nexport interface PhotoFramingSteps {\n /** Nudge per press, % of the card. */\n offsetPct: number;\n /** Zoom per press, as a scale delta. */\n scale: number;\n}\n\nexport const PHOTO_FRAMING_STEPS: PhotoFramingSteps = Object.freeze({ offsetPct: 5, scale: 0.1 });\n\n/** Three decimals, the same precision the landing page writes into its CSS. */\nconst PRECISION = 1000;\nconst round = (n: number): number => Math.round(n * PRECISION) / PRECISION;\n\ntype MoveAction = Exclude<FramingAction, typeof FramingAction.Reset>;\n\n/** [x, y, scale] direction of each non-reset action. */\nconst DIRECTIONS: Record<MoveAction, readonly [number, number, number]> = {\n [FramingAction.ZoomIn]: [0, 0, 1],\n [FramingAction.ZoomOut]: [0, 0, -1],\n [FramingAction.MoveUp]: [0, -1, 0],\n [FramingAction.MoveDown]: [0, 1, 0],\n [FramingAction.MoveLeft]: [-1, 0, 0],\n [FramingAction.MoveRight]: [1, 0, 0],\n};\n\n/**\n * Applies one control press. The result is always clamped and rounded, so ten\n * presses of +0.1 land on exactly 2, not 1.9999999999999998.\n */\nexport function stepFraming(\n current: PhotoFramingInput | null | undefined,\n action: FramingAction,\n steps: PhotoFramingSteps = PHOTO_FRAMING_STEPS,\n): PhotoFraming {\n if (action === FramingAction.Reset) return DEFAULT_PHOTO_FRAMING;\n const base = clampFraming(current);\n const [dx, dy, ds] = DIRECTIONS[action];\n return clampFraming({\n x: round(base.x + dx * steps.offsetPct),\n y: round(base.y + dy * steps.offsetPct),\n scale: round(base.scale + ds * steps.scale),\n });\n}\n\n/** False when the press would change nothing (at a bound, or reset on the default). */\nexport function canStepFraming(\n current: PhotoFramingInput | null | undefined,\n action: FramingAction,\n steps: PhotoFramingSteps = PHOTO_FRAMING_STEPS,\n): boolean {\n const base = clampFraming(current);\n const next = stepFraming(base, action, steps);\n return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;\n}\n","/**\n * The four states of an image upload slot (KEFI-PEOPLE-1 design board \"upload\n * slot\"). An `as const` object, not a `const enum`, so Babel / isolatedModules\n * consumers can read it from the published .d.ts.\n */\nexport const ImagePickerStatus = {\n Idle: 'idle',\n Uploading: 'uploading',\n Error: 'error',\n Done: 'done',\n} as const;\n\nexport type ImagePickerStatus = (typeof ImagePickerStatus)[keyof typeof ImagePickerStatus];\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { ImagePickerStatus } from './ImagePickerStatus';\n\nexport const DEFAULT_IMAGE_ACCEPT = 'image/*';\n\n/**\n * Opens the browser's file dialog for ONE file. Resolves `null` on cancel, and\n * where there is no DOM (native) — a native consumer passes its own `pickFile`.\n */\nexport function pickWebImage(accept: string = DEFAULT_IMAGE_ACCEPT): Promise<File | null> {\n if (typeof document === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.addEventListener('change', () => resolve(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => resolve(null), { once: true });\n input.click();\n });\n}\n\nexport interface UseImageUploadOptions<TFile> {\n /** Consumer-owned transport. Resolve = stored; reject = error state. No network code lives here. */\n upload: (file: TFile) => Promise<void>;\n /** Picks one file; `null` means the user cancelled. */\n pickFile: () => Promise<TFile | null>;\n /** Start in `done` when the slot already holds an image. */\n hasImage?: boolean;\n /** Told about a failed pick or upload (logging / toasts). */\n onError?: (error: unknown) => void;\n}\n\nexport interface ImageUploadState {\n status: ImagePickerStatus;\n /** Pick then upload. A second call while one is in flight is ignored. */\n pick: () => Promise<void>;\n}\n\n/**\n * The upload slot's state machine: idle -> uploading -> done | error, and from\n * done or error back to uploading on the next pick. A cancelled pick leaves the\n * state as it was, so cancelling \"Replace\" does not wipe a finished upload.\n */\nexport function useImageUpload<TFile>({\n upload,\n pickFile,\n hasImage = false,\n onError,\n}: UseImageUploadOptions<TFile>): ImageUploadState {\n const [status, setStatus] = useState<ImagePickerStatus>(\n hasImage ? ImagePickerStatus.Done : ImagePickerStatus.Idle,\n );\n const busy = useRef(false);\n const mounted = useRef(true);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const settle = useCallback((next: ImagePickerStatus): void => {\n if (mounted.current) setStatus(next);\n }, []);\n\n const pick = useCallback(async (): Promise<void> => {\n if (busy.current) return;\n busy.current = true;\n try {\n const file = await pickFile();\n if (file === null) return;\n settle(ImagePickerStatus.Uploading);\n await upload(file);\n settle(ImagePickerStatus.Done);\n } catch (error) {\n settle(ImagePickerStatus.Error);\n onError?.(error);\n } finally {\n busy.current = false;\n }\n }, [pickFile, upload, onError, settle]);\n\n return { status, pick };\n}\n","/**\r\n * ImagePickerButton — one upload slot: pick an image, hand it to the consumer's\r\n * `upload` function, show idle / uploading / error / done.\r\n *\r\n * No network code and no copy of its own: the transport is a prop and every\r\n * string arrives through `labels`, so each portal passes its FM() keys.\r\n */\r\nimport React, { useCallback, useMemo } from 'react';\r\n\r\nimport { StyleSheet, Text, View } from 'react-native';\r\n\r\nimport { Button } from '@dloizides/ui-buttons';\r\nimport type { ButtonVariant } from '@dloizides/ui-buttons';\r\nimport { useUi } from '@dloizides/ui-feedback';\r\n\r\nimport { ImagePickerStatus } from './ImagePickerStatus';\r\nimport { DEFAULT_IMAGE_ACCEPT, pickWebImage, useImageUpload } from './useImageUpload';\r\n\r\nexport interface ImagePickerLabels {\r\n /** Button, idle: \"Add photo\". */\r\n pick: string;\r\n /** Button, uploading: \"Uploading...\". */\r\n uploading: string;\r\n /** Button, done: \"Replace photo\". */\r\n replace: string;\r\n /** Button, error: \"Try again\". */\r\n retry: string;\r\n /** Status line after a successful upload: \"Photo saved\". */\r\n done: string;\r\n /** Status line after a failure: \"Upload failed\". */\r\n error: string;\r\n /** Accessibility hint for the button: \"Opens a file picker to choose an image\". */\r\n hint: string;\r\n}\r\n\r\nexport interface ImagePickerButtonProps<TFile = File> {\r\n upload: (file: TFile) => Promise<void>;\r\n labels: ImagePickerLabels;\r\n testID: string;\r\n /** Defaults to the browser file dialog. Native consumers pass their own picker. */\r\n pickFile?: () => Promise<TFile | null>;\r\n /** `accept` for the default web picker. Default `image/*`. */\r\n accept?: string;\r\n /** Start in the `done` state (the slot already has an image). */\r\n hasImage?: boolean;\r\n onError?: (error: unknown) => void;\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface PickerView {\r\n buttonLabel: string;\r\n variant: ButtonVariant;\r\n statusText: string | null;\r\n /** Paint the status line in the theme's error colour. */\r\n isError: boolean;\r\n}\r\n\r\n/** Pure status -> copy/variant mapping (unit-tested; the component only paints it). */\r\nexport function resolvePickerView(status: ImagePickerStatus, labels: ImagePickerLabels): PickerView {\r\n switch (status) {\r\n case ImagePickerStatus.Uploading:\r\n return { buttonLabel: labels.uploading, variant: 'outline', statusText: null, isError: false };\r\n case ImagePickerStatus.Error:\r\n return { buttonLabel: labels.retry, variant: 'danger', statusText: labels.error, isError: true };\r\n case ImagePickerStatus.Done:\r\n return { buttonLabel: labels.replace, variant: 'outline', statusText: labels.done, isError: false };\r\n default:\r\n return { buttonLabel: labels.pick, variant: 'primary', statusText: null, isError: false };\r\n }\r\n}\r\n\r\nconst STATUS_GAP = 6;\r\nconst STATUS_FONT_SIZE = 13;\r\n\r\nconst styles = StyleSheet.create({\r\n root: { alignItems: 'flex-start', gap: STATUS_GAP },\r\n status: { fontSize: STATUS_FONT_SIZE },\r\n});\r\n\r\nexport function ImagePickerButton<TFile = File>({\r\n upload,\r\n labels,\r\n testID,\r\n pickFile,\r\n accept = DEFAULT_IMAGE_ACCEPT,\r\n hasImage,\r\n onError,\r\n disabled = false,\r\n}: ImagePickerButtonProps<TFile>): React.ReactElement {\r\n const { theme } = useUi();\r\n // The default picker yields a DOM File; a consumer with another TFile supplies `pickFile`.\r\n const webPick = useCallback(() => pickWebImage(accept) as Promise<TFile | null>, [accept]);\r\n const { status, pick } = useImageUpload<TFile>({ upload, pickFile: pickFile ?? webPick, hasImage, onError });\r\n const view = useMemo(() => resolvePickerView(status, labels), [status, labels]);\r\n const isUploading = status === ImagePickerStatus.Uploading;\r\n const statusColor = view.isError ? theme.semantic.error['500'] : theme.colors.textSecondary;\r\n\r\n return (\r\n <View style={styles.root} testID={testID}>\r\n <Button\r\n accessibilityHint={labels.hint}\r\n accessibilityLabel={view.buttonLabel}\r\n autoLoading={false}\r\n disabled={disabled || isUploading}\r\n label={view.buttonLabel}\r\n loading={isUploading}\r\n testID={`${testID}-button`}\r\n variant={view.variant}\r\n onPress={pick}\r\n />\r\n {view.statusText !== null ? (\r\n <Text\r\n accessibilityLiveRegion=\"polite\"\r\n style={[styles.status, { color: statusColor }]}\r\n testID={`${testID}-status`}\r\n >\r\n {view.statusText}\r\n </Text>\r\n ) : null}\r\n </View>\r\n );\r\n}\r\n","/**\n * The editor's controls: a zoom stepper (minus, readout, plus) and a 3x3\n * position d-pad with reset in the middle. Every control is a 44px ui-buttons\n * IconButton, disabled when the press would change nothing (at a bound).\n */\nimport React from 'react';\n\nimport { StyleSheet, Text, View } from 'react-native';\n\nimport { IconButton } from '@dloizides/ui-buttons';\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport { FramingAction } from '../framing/FramingAction';\nimport type { PhotoFraming } from '../framing/photoFraming';\nimport type { PhotoFramingSteps } from '../framing/stepFraming';\nimport { canStepFraming } from '../framing/stepFraming';\nimport type { PhotoFramingLabels } from './types';\n\n/** WCAG AA touch target; IconButton `md` is this size, the d-pad spacers match it. */\nconst TARGET = 44;\nconst GAP = 8;\nconst SECTION_GAP = 16;\nconst HEADING_SIZE = 13;\nconst READOUT_MIN_WIDTH = 56;\nconst READOUT_SIZE = 15;\nconst PERCENT = 100;\n\nconst DEFAULT_GLYPHS: Record<FramingAction, string> = {\n [FramingAction.ZoomIn]: '+',\n [FramingAction.ZoomOut]: '−',\n [FramingAction.MoveUp]: '↑',\n [FramingAction.MoveDown]: '↓',\n [FramingAction.MoveLeft]: '←',\n [FramingAction.MoveRight]: '→',\n [FramingAction.Reset]: '↺',\n};\n\nconst styles = StyleSheet.create({\n root: { gap: SECTION_GAP },\n section: { gap: GAP },\n heading: { fontSize: HEADING_SIZE, fontWeight: '600' },\n row: { flexDirection: 'row', alignItems: 'center', gap: GAP },\n readout: { minWidth: READOUT_MIN_WIDTH, textAlign: 'center', fontSize: READOUT_SIZE },\n spacer: { width: TARGET, height: TARGET },\n});\n\ninterface FramingControlsProps {\n framing: PhotoFraming;\n steps?: PhotoFramingSteps;\n labels: PhotoFramingLabels;\n onAction: (action: FramingAction) => void;\n renderGlyph?: (action: FramingAction, color: string, size: number) => React.ReactNode;\n disabled: boolean;\n testID: string;\n}\n\nexport const FramingControls = ({\n framing,\n steps,\n labels,\n onAction,\n renderGlyph,\n disabled,\n testID,\n}: FramingControlsProps): React.ReactElement => {\n const { theme } = useUi();\n\n const control = (action: FramingAction): React.ReactElement => (\n <IconButton\n accessibilityHint={labels.actions[action].hint}\n actionLabel={labels.actions[action].label}\n autoLoading={false}\n disabled={disabled || !canStepFraming(framing, action, steps)}\n renderIcon={(color, size) =>\n renderGlyph ? (\n renderGlyph(action, color, size)\n ) : (\n <Text accessible={false} style={{ color, fontSize: size }}>\n {DEFAULT_GLYPHS[action]}\n </Text>\n )\n }\n showLabel={false}\n testID={`${testID}-${action}`}\n onPress={() => onAction(action)}\n />\n );\n const heading = { color: theme.colors.textSecondary };\n\n return (\n <View style={styles.root}>\n <View style={styles.section}>\n <Text accessibilityRole=\"header\" style={[styles.heading, heading]}>\n {labels.size}\n </Text>\n <View style={styles.row}>\n {control(FramingAction.ZoomOut)}\n <Text style={[styles.readout, { color: theme.colors.text }]} testID={`${testID}-scale`}>\n {labels.scaleValue(Math.round(framing.scale * PERCENT))}\n </Text>\n {control(FramingAction.ZoomIn)}\n </View>\n </View>\n <View style={styles.section}>\n <Text accessibilityRole=\"header\" style={[styles.heading, heading]}>\n {labels.position}\n </Text>\n <View style={styles.row}>\n <View style={styles.spacer} />\n {control(FramingAction.MoveUp)}\n <View style={styles.spacer} />\n </View>\n <View style={styles.row}>\n {control(FramingAction.MoveLeft)}\n {control(FramingAction.Reset)}\n {control(FramingAction.MoveRight)}\n </View>\n <View style={styles.row}>\n <View style={styles.spacer} />\n {control(FramingAction.MoveDown)}\n <View style={styles.spacer} />\n </View>\n </View>\n </View>\n );\n};\n","/**\n * PhotoFramingEditor — the KEFI-PEOPLE-1 \"A2\" framing screen: a live preview of\n * the consumer's own card (render prop) beside a zoom stepper and a position\n * d-pad. The value is `{x, y, scale}`, clamped to PHOTO_FRAMING_BOUNDS, the same\n * rules the public landing page applies — the preview is what ships.\n *\n * Narrow first: preview stacked over the controls; from ui-layout's\n * LAYOUT_COLLAPSE_BREAKPOINT up they sit side by side.\n */\nimport React, { useCallback, useMemo } from 'react';\n\nimport { StyleSheet, View, useWindowDimensions } from 'react-native';\n\nimport { LAYOUT_COLLAPSE_BREAKPOINT } from '@dloizides/ui-layout';\n\nimport type { FramingAction } from '../framing/FramingAction';\nimport { clampFraming, framingToTransform } from '../framing/photoFraming';\nimport { stepFraming } from '../framing/stepFraming';\nimport { FramingControls } from './FramingControls';\nimport type { PhotoFramingEditorProps } from './types';\n\nconst GAP = 16;\n\nconst styles = StyleSheet.create({\n root: { flexDirection: 'column', gap: GAP, alignItems: 'stretch' },\n rootWide: { flexDirection: 'row', alignItems: 'flex-start' },\n preview: { alignItems: 'center', overflow: 'hidden' },\n previewWide: { flexShrink: 1 },\n});\n\nexport const PhotoFramingEditor = ({\n value,\n onChange,\n renderPreview,\n labels,\n testID,\n steps,\n renderGlyph,\n disabled = false,\n}: PhotoFramingEditorProps): React.ReactElement => {\n const { width } = useWindowDimensions();\n const wide = width >= LAYOUT_COLLAPSE_BREAKPOINT;\n const framing = useMemo(() => clampFraming(value), [value]);\n const transform = useMemo(() => framingToTransform(framing), [framing]);\n\n const onAction = useCallback(\n (action: FramingAction): void => onChange(stepFraming(framing, action, steps)),\n [framing, onChange, steps],\n );\n\n return (\n <View style={[styles.root, wide ? styles.rootWide : null]} testID={testID}>\n <View style={[styles.preview, wide ? styles.previewWide : null]} testID={`${testID}-preview`}>\n {renderPreview({ framing, transform })}\n </View>\n <FramingControls\n disabled={disabled}\n framing={framing}\n labels={labels}\n renderGlyph={renderGlyph}\n steps={steps}\n testID={testID}\n onAction={onAction}\n />\n </View>\n );\n};\n"]}
1
+ {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts","../src/ImagePickerButton/ImagePickerStatus.ts","../src/ImagePickerButton/useImageUpload.ts","../src/ImagePickerButton/ImagePickerButton.tsx","../src/PhotoFramingEditor/FramingControls.tsx","../src/PhotoFramingEditor/PhotoAnchor.ts","../src/PhotoFramingEditor/PhotoFramingEditor.tsx","../src/AnchoredPhoto/resolveAnchoredPhotoLayout.ts","../src/AnchoredPhoto/useImageAspectRatio.ts","../src/AnchoredPhoto/AnchoredPhoto.tsx"],"names":["FramingAction","DEFAULT_PHOTO_FRAMING","clampFraming","ImagePickerStatus","useState","useRef","useEffect","useCallback","StyleSheet","useUi","useMemo","jsxs","View","jsx","Button","Text","MIN_TARGET_PX","styles","IconButton","PhotoAnchor","GAP","useWindowDimensions","LAYOUT_COLLAPSE_BREAKPOINT","framing","Image"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Photo framing: the `{x, y, scale}` an editor sets on a photo card.\n *\n * The bounds, default, clamp and identity check come from\n * `@dloizides/site-template-kit/framing` — the same module the static landing\n * renderer builds its `--photo-tf` CSS from. One copy, so the editor can never offer a value\n * the landing page would silently clamp away.\n *\n * That subpath imports nothing (no ajv, no JSON Schema), so this file stays\n * framework-free: it backs the `@dloizides/ui-media/framing` subpath.\n */\nimport type { PhotoFraming } from '@dloizides/site-template-kit/framing';\n\nexport {\n PHOTO_FRAMING_BOUNDS,\n DEFAULT_PHOTO_FRAMING,\n clampFraming,\n isDefaultFraming,\n} from '@dloizides/site-template-kit/framing';\nexport type { PhotoFraming, PhotoFramingInput } from '@dloizides/site-template-kit/framing';\n\n/**\n * An RN `transform` array. Percent translates are relative to the element\n * itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.\n * Typed locally so this module stays free of a react-native import.\n */\nexport type PhotoFramingTransform = [\n { translateX: `${number}%` },\n { translateY: `${number}%` },\n { scale: number },\n];\n\nexport const framingToTransform = (framing: PhotoFraming): PhotoFramingTransform => [\n { translateX: `${framing.x}%` },\n { translateY: `${framing.y}%` },\n { scale: framing.scale },\n];\n","/** The seven controls of the framing editor. */\nexport const enum FramingAction {\n ZoomIn = 'zoomIn',\n ZoomOut = 'zoomOut',\n MoveUp = 'moveUp',\n MoveDown = 'moveDown',\n MoveLeft = 'moveLeft',\n MoveRight = 'moveRight',\n Reset = 'reset',\n}\n","import { FramingAction } from './FramingAction';\nimport type { PhotoFraming, PhotoFramingInput } from './photoFraming';\nimport { DEFAULT_PHOTO_FRAMING, clampFraming } from './photoFraming';\n\n/** How far one press moves or zooms. */\nexport interface PhotoFramingSteps {\n /** Nudge per press, % of the card. */\n offsetPct: number;\n /** Zoom per press, as a scale delta. */\n scale: number;\n}\n\nexport const PHOTO_FRAMING_STEPS: PhotoFramingSteps = Object.freeze({ offsetPct: 5, scale: 0.1 });\n\n/** Three decimals, the same precision the landing page writes into its CSS. */\nconst PRECISION = 1000;\nconst round = (n: number): number => Math.round(n * PRECISION) / PRECISION;\n\ntype MoveAction = Exclude<FramingAction, FramingAction.Reset>;\n\n/** [x, y, scale] direction of each non-reset action. */\nconst DIRECTIONS: Record<MoveAction, readonly [number, number, number]> = {\n [FramingAction.ZoomIn]: [0, 0, 1],\n [FramingAction.ZoomOut]: [0, 0, -1],\n [FramingAction.MoveUp]: [0, -1, 0],\n [FramingAction.MoveDown]: [0, 1, 0],\n [FramingAction.MoveLeft]: [-1, 0, 0],\n [FramingAction.MoveRight]: [1, 0, 0],\n};\n\n/**\n * Applies one control press. The result is always clamped and rounded, so ten\n * presses of +0.1 land on exactly 2, not 1.9999999999999998.\n */\nexport function stepFraming(\n current: PhotoFramingInput | null | undefined,\n action: FramingAction,\n steps: PhotoFramingSteps = PHOTO_FRAMING_STEPS,\n): PhotoFraming {\n if (action === FramingAction.Reset) return DEFAULT_PHOTO_FRAMING;\n const base = clampFraming(current);\n const [dx, dy, ds] = DIRECTIONS[action];\n return clampFraming({\n x: round(base.x + dx * steps.offsetPct),\n y: round(base.y + dy * steps.offsetPct),\n scale: round(base.scale + ds * steps.scale),\n });\n}\n\n/** False when the press would change nothing (at a bound, or reset on the default). */\nexport function canStepFraming(\n current: PhotoFramingInput | null | undefined,\n action: FramingAction,\n steps: PhotoFramingSteps = PHOTO_FRAMING_STEPS,\n): boolean {\n const base = clampFraming(current);\n const next = stepFraming(base, action, steps);\n return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;\n}\n","/** The four states of an image upload slot. */\nexport const enum ImagePickerStatus {\n Idle = 'idle',\n Uploading = 'uploading',\n Error = 'error',\n Done = 'done',\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { ImagePickerStatus } from './ImagePickerStatus';\n\nexport const DEFAULT_IMAGE_ACCEPT = 'image/*';\n\n/**\n * Opens the browser's file dialog for ONE file. Resolves `null` on cancel, and\n * where there is no DOM (native) — a native consumer passes its own `pickFile`.\n */\nexport function pickWebImage(accept: string = DEFAULT_IMAGE_ACCEPT): Promise<File | null> {\n if (typeof document === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const input = document.createElement('input');\n input.type = 'file';\n input.accept = accept;\n input.addEventListener('change', () => resolve(input.files?.[0] ?? null), { once: true });\n input.addEventListener('cancel', () => resolve(null), { once: true });\n input.click();\n });\n}\n\nexport interface UseImageUploadOptions<TFile, TResult = void> {\n /** Consumer-owned transport. Resolve = stored; reject = error state. No network code lives here. */\n upload: (file: TFile) => Promise<TResult>;\n /** Given what `upload` resolved with (e.g. the stored URL), once the slot is `done`. */\n onUploaded?: (result: TResult) => void;\n /** Picks one file; `null` means the user cancelled. */\n pickFile: () => Promise<TFile | null>;\n /** Start in `done` when the slot already holds an image. */\n hasImage?: boolean;\n /** Told about a failed pick or upload (logging / toasts). */\n onError?: (error: unknown) => void;\n}\n\nexport interface ImageUploadState {\n status: ImagePickerStatus;\n /** Pick then upload. A second call while one is in flight is ignored. */\n pick: () => Promise<void>;\n}\n\n/**\n * The upload slot's state machine: idle -> uploading -> done | error, and from\n * done or error back to uploading on the next pick. A cancelled pick leaves the\n * state as it was, so cancelling \"Replace\" does not wipe a finished upload.\n */\nexport function useImageUpload<TFile, TResult = void>({\n upload,\n pickFile,\n hasImage = false,\n onError,\n onUploaded,\n}: UseImageUploadOptions<TFile, TResult>): ImageUploadState {\n const [status, setStatus] = useState<ImagePickerStatus>(\n hasImage ? ImagePickerStatus.Done : ImagePickerStatus.Idle,\n );\n const busy = useRef(false);\n const mounted = useRef(true);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const settle = useCallback((next: ImagePickerStatus): void => {\n if (mounted.current) setStatus(next);\n }, []);\n\n const pick = useCallback(async (): Promise<void> => {\n if (busy.current) return;\n busy.current = true;\n // Boxed so a falsy / void result still counts as uploaded.\n let uploaded: { result: TResult } | null = null;\n try {\n const file = await pickFile();\n if (file === null) return;\n settle(ImagePickerStatus.Uploading);\n uploaded = { result: await upload(file) };\n settle(ImagePickerStatus.Done);\n } catch (error) {\n settle(ImagePickerStatus.Error);\n onError?.(error);\n } finally {\n busy.current = false;\n }\n // Outside the try: a throwing consumer callback must not repaint a stored upload as failed.\n if (uploaded !== null) onUploaded?.(uploaded.result);\n }, [pickFile, upload, onError, onUploaded, settle]);\n\n return { status, pick };\n}\n","/**\r\n * ImagePickerButton — one upload slot: pick an image, hand it to the consumer's\r\n * `upload` function, show idle / uploading / error / done.\r\n *\r\n * No network code and no copy of its own: the transport is a prop and every\r\n * string arrives through `labels`, so each portal passes its FM() keys.\r\n */\r\nimport React, { useCallback, useMemo } from 'react';\r\n\r\nimport { StyleSheet, Text, View } from 'react-native';\r\n\r\nimport { Button } from '@dloizides/ui-buttons';\r\nimport type { ButtonVariant } from '@dloizides/ui-buttons';\r\nimport { useUi } from '@dloizides/ui-feedback';\r\n\r\nimport { ImagePickerStatus } from './ImagePickerStatus';\r\nimport { DEFAULT_IMAGE_ACCEPT, pickWebImage, useImageUpload } from './useImageUpload';\r\n\r\nexport interface ImagePickerLabels {\r\n /** Button, idle: \"Add photo\". */\r\n pick: string;\r\n /** Button, uploading: \"Uploading...\". */\r\n uploading: string;\r\n /** Button, done: \"Replace photo\". */\r\n replace: string;\r\n /** Button, error: \"Try again\". */\r\n retry: string;\r\n /** Status line after a successful upload: \"Photo saved\". */\r\n done: string;\r\n /** Status line after a failure: \"Upload failed\". */\r\n error: string;\r\n /** Accessibility hint for the button: \"Opens a file picker to choose an image\". */\r\n hint: string;\r\n}\r\n\r\nexport interface ImagePickerButtonProps<TFile = File, TResult = void> {\r\n /** Consumer transport; whatever it resolves with (e.g. the stored URL) goes to `onUploaded`. */\r\n upload: (file: TFile) => Promise<TResult>;\r\n /** Called with `upload`'s result once the slot is `done`. */\r\n onUploaded?: (result: TResult) => void;\r\n labels: ImagePickerLabels;\r\n testID: string;\r\n /** Defaults to the browser file dialog. Native consumers pass their own picker. */\r\n pickFile?: () => Promise<TFile | null>;\r\n /** `accept` for the default web picker. Default `image/*`. */\r\n accept?: string;\r\n /** Start in the `done` state (the slot already has an image). */\r\n hasImage?: boolean;\r\n onError?: (error: unknown) => void;\r\n disabled?: boolean;\r\n}\r\n\r\nexport interface PickerView {\r\n buttonLabel: string;\r\n variant: ButtonVariant;\r\n statusText: string | null;\r\n /** Paint the status line in the theme's error colour. */\r\n isError: boolean;\r\n}\r\n\r\n/** Pure status -> copy/variant mapping (unit-tested; the component only paints it). */\r\nexport function resolvePickerView(status: ImagePickerStatus, labels: ImagePickerLabels): PickerView {\r\n switch (status) {\r\n case ImagePickerStatus.Uploading:\r\n return { buttonLabel: labels.uploading, variant: 'outline', statusText: null, isError: false };\r\n case ImagePickerStatus.Error:\r\n return { buttonLabel: labels.retry, variant: 'danger', statusText: labels.error, isError: true };\r\n case ImagePickerStatus.Done:\r\n return { buttonLabel: labels.replace, variant: 'outline', statusText: labels.done, isError: false };\r\n default:\r\n return { buttonLabel: labels.pick, variant: 'primary', statusText: null, isError: false };\r\n }\r\n}\r\n\r\nconst STATUS_GAP = 6;\r\nconst STATUS_FONT_SIZE = 13;\r\n\r\nconst styles = StyleSheet.create({\r\n root: { alignItems: 'flex-start', gap: STATUS_GAP },\r\n status: { fontSize: STATUS_FONT_SIZE },\r\n});\r\n\r\nexport function ImagePickerButton<TFile = File, TResult = void>({\r\n upload,\r\n onUploaded,\r\n labels,\r\n testID,\r\n pickFile,\r\n accept = DEFAULT_IMAGE_ACCEPT,\r\n hasImage,\r\n onError,\r\n disabled = false,\r\n}: ImagePickerButtonProps<TFile, TResult>): React.ReactElement {\r\n const { theme } = useUi();\r\n // The default picker yields a DOM File; a consumer with another TFile supplies `pickFile`.\r\n const webPick = useCallback(() => pickWebImage(accept) as Promise<TFile | null>, [accept]);\r\n const { status, pick } = useImageUpload<TFile, TResult>({\r\n upload,\r\n pickFile: pickFile ?? webPick,\r\n hasImage,\r\n onError,\r\n onUploaded,\r\n });\r\n const view = useMemo(() => resolvePickerView(status, labels), [status, labels]);\r\n const isUploading = status === ImagePickerStatus.Uploading;\r\n const statusColor = view.isError ? theme.semantic.error['500'] : theme.colors.textSecondary;\r\n\r\n return (\r\n <View style={styles.root} testID={testID}>\r\n <Button\r\n accessibilityHint={labels.hint}\r\n accessibilityLabel={view.buttonLabel}\r\n autoLoading={false}\r\n disabled={disabled || isUploading}\r\n label={view.buttonLabel}\r\n loading={isUploading}\r\n testID={`${testID}-button`}\r\n variant={view.variant}\r\n onPress={pick}\r\n />\r\n {view.statusText !== null ? (\r\n <Text\r\n accessibilityLiveRegion=\"polite\"\r\n style={[styles.status, { color: statusColor }]}\r\n testID={`${testID}-status`}\r\n >\r\n {view.statusText}\r\n </Text>\r\n ) : null}\r\n </View>\r\n );\r\n}\r\n","/**\n * The editor's controls: a zoom stepper (minus, readout, plus) and a 3x3\n * position d-pad with reset in the middle. Every control is a 44px ui-buttons\n * IconButton, disabled when the press would change nothing (at a bound).\n */\nimport React from 'react';\n\nimport { StyleSheet, Text, View } from 'react-native';\n\nimport { IconButton } from '@dloizides/ui-buttons';\nimport { useUi } from '@dloizides/ui-feedback';\nimport { MIN_TARGET_PX } from '@dloizides/ui-layout';\n\nimport { FramingAction } from '../framing/FramingAction';\nimport type { PhotoFraming } from '../framing/photoFraming';\nimport type { PhotoFramingSteps } from '../framing/stepFraming';\nimport { canStepFraming } from '../framing/stepFraming';\nimport type { PhotoFramingLabels } from './types';\n\n/** IconButton `md` is ui-layout's minimum hit box; the d-pad spacers match it. */\nconst TARGET = MIN_TARGET_PX;\nconst GAP = 8;\nconst SECTION_GAP = 16;\nconst HEADING_SIZE = 13;\nconst READOUT_MIN_WIDTH = 56;\nconst READOUT_SIZE = 15;\nconst PERCENT = 100;\n\nconst DEFAULT_GLYPHS: Record<FramingAction, string> = {\n [FramingAction.ZoomIn]: '+',\n [FramingAction.ZoomOut]: '−',\n [FramingAction.MoveUp]: '↑',\n [FramingAction.MoveDown]: '↓',\n [FramingAction.MoveLeft]: '←',\n [FramingAction.MoveRight]: '→',\n [FramingAction.Reset]: '↺',\n};\n\nconst styles = StyleSheet.create({\n root: { gap: SECTION_GAP },\n section: { gap: GAP },\n heading: { fontSize: HEADING_SIZE, fontWeight: '600' },\n row: { flexDirection: 'row', alignItems: 'center', gap: GAP },\n readout: { minWidth: READOUT_MIN_WIDTH, textAlign: 'center', fontSize: READOUT_SIZE },\n spacer: { width: TARGET, height: TARGET },\n});\n\ninterface FramingControlsProps {\n framing: PhotoFraming;\n steps?: PhotoFramingSteps;\n labels: PhotoFramingLabels;\n onAction: (action: FramingAction) => void;\n renderGlyph?: (action: FramingAction, color: string, size: number) => React.ReactNode;\n disabled: boolean;\n testID: string;\n}\n\nexport const FramingControls = ({\n framing,\n steps,\n labels,\n onAction,\n renderGlyph,\n disabled,\n testID,\n}: FramingControlsProps): React.ReactElement => {\n const { theme } = useUi();\n\n const control = (action: FramingAction): React.ReactElement => (\n <IconButton\n accessibilityHint={labels.actions[action].hint}\n actionLabel={labels.actions[action].label}\n autoLoading={false}\n disabled={disabled || !canStepFraming(framing, action, steps)}\n renderIcon={(color, size) =>\n renderGlyph ? (\n renderGlyph(action, color, size)\n ) : (\n <Text accessible={false} style={{ color, fontSize: size }}>\n {DEFAULT_GLYPHS[action]}\n </Text>\n )\n }\n showLabel={false}\n testID={`${testID}-${action}`}\n onPress={() => onAction(action)}\n />\n );\n const heading = { color: theme.colors.textSecondary };\n\n return (\n <View style={styles.root}>\n <View style={styles.section}>\n <Text accessibilityRole=\"header\" style={[styles.heading, heading]}>\n {labels.size}\n </Text>\n <View style={styles.row}>\n {control(FramingAction.ZoomOut)}\n <Text style={[styles.readout, { color: theme.colors.text }]} testID={`${testID}-scale`}>\n {labels.scaleValue(Math.round(framing.scale * PERCENT))}\n </Text>\n {control(FramingAction.ZoomIn)}\n </View>\n </View>\n <View style={styles.section}>\n <Text accessibilityRole=\"header\" style={[styles.heading, heading]}>\n {labels.position}\n </Text>\n <View style={styles.row}>\n <View style={styles.spacer} />\n {control(FramingAction.MoveUp)}\n <View style={styles.spacer} />\n </View>\n <View style={styles.row}>\n {control(FramingAction.MoveLeft)}\n {control(FramingAction.Reset)}\n {control(FramingAction.MoveRight)}\n </View>\n <View style={styles.row}>\n <View style={styles.spacer} />\n {control(FramingAction.MoveDown)}\n <View style={styles.spacer} />\n </View>\n </View>\n </View>\n );\n};\n","/**\n * Where the photo sits inside its frame. `Bottom` is for cut-out portraits\n * (`object-fit: contain; object-position: center bottom`): feet on the floor of\n * the card.\n */\nexport const enum PhotoAnchor {\n Center = 'center',\n Bottom = 'bottom',\n}\n\nexport interface PhotoAnchorLayout {\n /** Applied to the editor's preview column: where the consumer's card sits vertically. */\n justifyContent: 'center' | 'flex-end';\n /** CSS `object-position` the consumer spreads onto its `<img>` / RN-web image. */\n objectPosition: 'center center' | 'center bottom';\n}\n\n/** Pure anchor -> layout mapping (unit-tested; the editor only applies it). */\nexport function resolvePhotoAnchor(anchor: PhotoAnchor): PhotoAnchorLayout {\n return anchor === PhotoAnchor.Bottom\n ? { justifyContent: 'flex-end', objectPosition: 'center bottom' }\n : { justifyContent: 'center', objectPosition: 'center center' };\n}\n","/**\n * PhotoFramingEditor — a photo framing screen: a live preview of\n * the consumer's own card (render prop) beside a zoom stepper and a position\n * d-pad. The value is `{x, y, scale}`, clamped to PHOTO_FRAMING_BOUNDS, the same\n * rules the public landing page applies — the preview is what ships.\n *\n * Narrow first: preview stacked over the controls; from ui-layout's\n * LAYOUT_COLLAPSE_BREAKPOINT up they sit side by side.\n */\nimport React, { useCallback, useMemo } from 'react';\n\nimport { StyleSheet, View, useWindowDimensions } from 'react-native';\n\nimport { LAYOUT_COLLAPSE_BREAKPOINT } from '@dloizides/ui-layout';\n\nimport type { FramingAction } from '../framing/FramingAction';\nimport { clampFraming, framingToTransform } from '../framing/photoFraming';\nimport { stepFraming } from '../framing/stepFraming';\nimport { FramingControls } from './FramingControls';\nimport { PhotoAnchor, resolvePhotoAnchor } from './PhotoAnchor';\nimport type { PhotoFramingEditorProps } from './types';\n\nconst GAP = 16;\n\nconst styles = StyleSheet.create({\n root: { flexDirection: 'column', gap: GAP, alignItems: 'stretch' },\n rootWide: { flexDirection: 'row', alignItems: 'flex-start' },\n preview: { alignItems: 'center', overflow: 'hidden' },\n previewWide: { flexShrink: 1 },\n});\n\nexport const PhotoFramingEditor = ({\n value,\n onChange,\n renderPreview,\n labels,\n testID,\n steps,\n renderGlyph,\n disabled = false,\n anchor = PhotoAnchor.Center,\n}: PhotoFramingEditorProps): React.ReactElement => {\n const { width } = useWindowDimensions();\n const wide = width >= LAYOUT_COLLAPSE_BREAKPOINT;\n const framing = useMemo(() => clampFraming(value), [value]);\n const transform = useMemo(() => framingToTransform(framing), [framing]);\n const anchorLayout = useMemo(() => resolvePhotoAnchor(anchor), [anchor]);\n\n const onAction = useCallback(\n (action: FramingAction): void => onChange(stepFraming(framing, action, steps)),\n [framing, onChange, steps],\n );\n\n return (\n <View style={[styles.root, wide ? styles.rootWide : null]} testID={testID}>\n <View\n style={[styles.preview, { justifyContent: anchorLayout.justifyContent }, wide ? styles.previewWide : null]}\n testID={`${testID}-preview`}\n >\n {renderPreview({ framing, transform, anchor, objectPosition: anchorLayout.objectPosition })}\n </View>\n <FramingControls\n disabled={disabled}\n framing={framing}\n labels={labels}\n renderGlyph={renderGlyph}\n steps={steps}\n testID={testID}\n onAction={onAction}\n />\n </View>\n );\n};\n","import { PhotoAnchor, resolvePhotoAnchor } from '../PhotoFramingEditor/PhotoAnchor';\nimport type { PhotoAnchorLayout } from '../PhotoFramingEditor/PhotoAnchor';\n\nexport interface AnchoredPhotoLayout {\n /** Main-axis placement of the photo inside its full-box frame. */\n justifyContent: PhotoAnchorLayout['justifyContent'];\n /**\n * `null` = fill the box (size unknown yet, image stays centred by `contain`);\n * a number = size the image to that width / height so the frame can anchor it.\n */\n aspectRatio: number | null;\n}\n\n/** Pure anchor + measured aspect -> layout (unit-tested; AnchoredPhoto only paints it). */\nexport function resolveAnchoredPhotoLayout(\n anchor: PhotoAnchor,\n aspectRatio: number | null,\n): AnchoredPhotoLayout {\n const { justifyContent } = resolvePhotoAnchor(anchor);\n const usable = aspectRatio !== null && Number.isFinite(aspectRatio) && aspectRatio > 0;\n return { justifyContent, aspectRatio: usable ? aspectRatio : null };\n}\n","/**\n * Natural width / height of a remote image, or null until it is known (or the\n * size lookup fails). AnchoredPhoto needs it to anchor a contained photo: RN-web's\n * Image paints a centred background, so `object-position` cannot move it, but an\n * element sized to the photo's own aspect can be pushed to an edge by flexbox.\n */\nimport { useEffect, useState } from 'react';\n\nimport { Image } from 'react-native';\n\nexport function useImageAspectRatio(uri: string): number | null {\n const [measured, setMeasured] = useState<{ uri: string; ratio: number } | null>(null);\n\n useEffect(() => {\n if (uri === '') return undefined;\n let active = true;\n Image.getSize(\n uri,\n (width, height) => {\n if (active && width > 0 && height > 0) setMeasured({ uri, ratio: width / height });\n },\n () => undefined,\n );\n return () => {\n active = false;\n };\n }, [uri]);\n\n return measured?.uri === uri ? measured.ratio : null;\n}\n","/**\n * AnchoredPhoto — a contained photo that fills its parent box, framed by a\n * `{x, y, scale}` value and anchored to the centre or the bottom edge.\n *\n * Why not `object-position`: RN-web's Image paints a centred background image,\n * so `center bottom` has no effect. Instead the image is sized to its own aspect\n * ratio (measured with `Image.getSize`) and pushed down by flexbox. The framing\n * transform sits on a full-box layer, so its percentages mean the same as the\n * CSS `translate(x%, y%) scale(s)` a static page applies to its `<img>`.\n *\n * The parent owns the box (aspect, radius, background, empty state); this renders\n * only the photo layer. No strings of its own: `accessibilityLabel` is a prop.\n */\nimport React from 'react';\n\nimport { Image, StyleSheet, View } from 'react-native';\nimport type { StyleProp, ViewStyle } from 'react-native';\n\nimport { clampFraming, framingToTransform } from '../framing/photoFraming';\nimport type { PhotoFramingInput } from '../framing/photoFraming';\nimport { PhotoAnchor } from '../PhotoFramingEditor/PhotoAnchor';\nimport { resolveAnchoredPhotoLayout } from './resolveAnchoredPhotoLayout';\nimport { useImageAspectRatio } from './useImageAspectRatio';\n\nexport interface AnchoredPhotoProps {\n uri: string;\n /** `bottom` for cut-out portraits. Default `center`. */\n anchor?: PhotoAnchor;\n /** Stored or live framing; clamped before use, so raw config is safe. */\n framing?: PhotoFramingInput | null;\n /** Extra style for the full-box frame layer. */\n style?: StyleProp<ViewStyle>;\n accessibilityLabel: string;\n testID?: string;\n}\n\nconst styles = StyleSheet.create({\n frame: { ...StyleSheet.absoluteFillObject, alignItems: 'center' },\n fill: { height: '100%', width: '100%' },\n sized: { maxHeight: '100%', width: '100%' },\n});\n\nexport const AnchoredPhoto = ({\n uri,\n anchor = PhotoAnchor.Center,\n framing,\n style,\n accessibilityLabel,\n testID,\n}: AnchoredPhotoProps): React.ReactElement => {\n const layout = resolveAnchoredPhotoLayout(anchor, useImageAspectRatio(uri));\n const transform = framingToTransform(clampFraming(framing));\n const imageStyle = layout.aspectRatio === null\n ? styles.fill\n : [styles.sized, { aspectRatio: layout.aspectRatio }];\n\n return (\n <View style={[styles.frame, { justifyContent: layout.justifyContent, transform }, style]} testID={testID}>\n <Image\n accessibilityLabel={accessibilityLabel}\n resizeMode=\"contain\"\n source={{ uri }}\n style={imageStyle}\n />\n </View>\n );\n};\n"]}