@dloizides/ui-media 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - KEFI-PEOPLE-1 T10b "Organizer people editor: ui-media adoption gaps" (backwards compatible):
6
+ - `ImagePickerButton` / `useImageUpload` take a second generic `TResult` (default `void`): `upload` returns `Promise<TResult>` and the new `onUploaded(result)` receives it once the slot is `done`. A throwing `onUploaded` does not flip the slot to error.
7
+ - `PhotoFramingEditor` gains `anchor?: PhotoAnchor` (`center` default | `bottom`); `renderPreview` now also receives `anchor` and `objectPosition` (`center bottom` for kefi-landings `.amb-photo` cut-outs). New exports `PhotoAnchor`, `resolvePhotoAnchor`, `PhotoAnchorLayout`.
8
+
9
+ ## 0.2.0
10
+
11
+ - KEFI-PEOPLE-1 T12 "Organizer people editor: shared photo framing": `PHOTO_FRAMING_BOUNDS`,
12
+ `DEFAULT_PHOTO_FRAMING`, `clampFraming`, `isDefaultFraming` and the framing types are now
13
+ re-exported from `@dloizides/site-template-kit/framing` (new dependency, no new peers) — the
14
+ same module the kefi-landings renderer uses, so the two can no longer drift.
15
+
3
16
  ## 0.1.0
4
17
 
5
18
  - Initial release: ImagePickerButton, useImageUpload, PhotoFramingEditor, PHOTO_FRAMING_BOUNDS + clampFraming (parity with kefi-landings photoTransform), framework-free ./framing subpath.
@@ -1,47 +1,19 @@
1
+ import { PhotoFraming, PhotoFramingInput } from '@dloizides/site-template-kit/framing';
2
+ export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PhotoFraming, PhotoFramingInput, clampFraming, isDefaultFraming } from '@dloizides/site-template-kit/framing';
3
+
1
4
  /**
2
5
  * Photo framing: the `{x, y, scale}` an organiser sets on a performer card.
3
6
  *
4
- * The bounds MIRROR `kefi-landings/src/lib/config-to-site.ts` (`photoTransform`,
5
- * MAX_PHOTO_OFFSET_PCT / MIN_PHOTO_SCALE / MAX_PHOTO_SCALE), which is what the
6
- * public landing page renders. The editor must never offer a value the landing
7
- * page would silently clamp away, so the numbers here and there must stay equal.
8
- * KEFI-PEOPLE-1 T12 "import the bounds instead of hard-coding them" removes the copy.
7
+ * The bounds, default, clamp and identity check come from
8
+ * `@dloizides/site-template-kit/framing` — the same module the kefi-landings
9
+ * renderer builds its `--photo-tf` CSS from (KEFI-PEOPLE-1 T12 "Organizer people
10
+ * editor: shared photo framing"). One copy, so the editor can never offer a value
11
+ * the landing page would silently clamp away.
9
12
  *
10
- * This file imports nothing: it backs the framework-free `@dloizides/ui-media/framing`
11
- * subpath.
12
- */
13
- /** A framing value after clamping: every field finite and inside the bounds. */
14
- interface PhotoFraming {
15
- /** Horizontal nudge, % of the card. Negative = left. */
16
- readonly x: number;
17
- /** Vertical nudge, % of the card. Negative = up. */
18
- readonly y: number;
19
- /** Zoom factor. 1 = the photo's natural cover fit. */
20
- readonly scale: number;
21
- }
22
- /** What config / an API may hold: any field missing, null or non-finite. */
23
- interface PhotoFramingInput {
24
- x?: number | null;
25
- y?: number | null;
26
- scale?: number | null;
27
- }
28
- declare const PHOTO_FRAMING_BOUNDS: Readonly<{
29
- /** Widest nudge, as a percentage of the card, in either direction. */
30
- maxOffsetPct: 60;
31
- /** Below 1 the photo shrinks inside an already-roomy frame. */
32
- minScale: 0.5;
33
- /** Far above 2 the subject's head leaves the card entirely. */
34
- maxScale: 3;
35
- }>;
36
- declare const DEFAULT_PHOTO_FRAMING: PhotoFraming;
37
- /**
38
- * Normalises any stored framing: non-finite fields fall back to the default,
39
- * the rest are clamped to {@link PHOTO_FRAMING_BOUNDS}. Same rules as the
40
- * landing page's `photoTransform`, so what the editor previews is what ships.
13
+ * That subpath imports nothing (no ajv, no JSON Schema), so this file stays
14
+ * framework-free: it backs the `@dloizides/ui-media/framing` subpath.
41
15
  */
42
- declare function clampFraming(input?: PhotoFramingInput | null): PhotoFraming;
43
- /** True when the framing is the identity (the landing page emits no transform). */
44
- declare const isDefaultFraming: (framing: PhotoFraming) => boolean;
16
+
45
17
  /**
46
18
  * An RN `transform` array. Percent translates are relative to the element
47
19
  * itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.
@@ -92,4 +64,4 @@ declare function stepFraming(current: PhotoFramingInput | null | undefined, acti
92
64
  /** False when the press would change nothing (at a bound, or reset on the default). */
93
65
  declare function canStepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): boolean;
94
66
 
95
- export { DEFAULT_PHOTO_FRAMING, FramingAction, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, type PhotoFraming, type PhotoFramingInput, type PhotoFramingSteps, type PhotoFramingTransform, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming };
67
+ export { FramingAction, PHOTO_FRAMING_STEPS, type PhotoFramingSteps, type PhotoFramingTransform, canStepFraming, framingToTransform, stepFraming };
package/dist/framing.d.ts CHANGED
@@ -1,47 +1,19 @@
1
+ import { PhotoFraming, PhotoFramingInput } from '@dloizides/site-template-kit/framing';
2
+ export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PhotoFraming, PhotoFramingInput, clampFraming, isDefaultFraming } from '@dloizides/site-template-kit/framing';
3
+
1
4
  /**
2
5
  * Photo framing: the `{x, y, scale}` an organiser sets on a performer card.
3
6
  *
4
- * The bounds MIRROR `kefi-landings/src/lib/config-to-site.ts` (`photoTransform`,
5
- * MAX_PHOTO_OFFSET_PCT / MIN_PHOTO_SCALE / MAX_PHOTO_SCALE), which is what the
6
- * public landing page renders. The editor must never offer a value the landing
7
- * page would silently clamp away, so the numbers here and there must stay equal.
8
- * KEFI-PEOPLE-1 T12 "import the bounds instead of hard-coding them" removes the copy.
7
+ * The bounds, default, clamp and identity check come from
8
+ * `@dloizides/site-template-kit/framing` — the same module the kefi-landings
9
+ * renderer builds its `--photo-tf` CSS from (KEFI-PEOPLE-1 T12 "Organizer people
10
+ * editor: shared photo framing"). One copy, so the editor can never offer a value
11
+ * the landing page would silently clamp away.
9
12
  *
10
- * This file imports nothing: it backs the framework-free `@dloizides/ui-media/framing`
11
- * subpath.
12
- */
13
- /** A framing value after clamping: every field finite and inside the bounds. */
14
- interface PhotoFraming {
15
- /** Horizontal nudge, % of the card. Negative = left. */
16
- readonly x: number;
17
- /** Vertical nudge, % of the card. Negative = up. */
18
- readonly y: number;
19
- /** Zoom factor. 1 = the photo's natural cover fit. */
20
- readonly scale: number;
21
- }
22
- /** What config / an API may hold: any field missing, null or non-finite. */
23
- interface PhotoFramingInput {
24
- x?: number | null;
25
- y?: number | null;
26
- scale?: number | null;
27
- }
28
- declare const PHOTO_FRAMING_BOUNDS: Readonly<{
29
- /** Widest nudge, as a percentage of the card, in either direction. */
30
- maxOffsetPct: 60;
31
- /** Below 1 the photo shrinks inside an already-roomy frame. */
32
- minScale: 0.5;
33
- /** Far above 2 the subject's head leaves the card entirely. */
34
- maxScale: 3;
35
- }>;
36
- declare const DEFAULT_PHOTO_FRAMING: PhotoFraming;
37
- /**
38
- * Normalises any stored framing: non-finite fields fall back to the default,
39
- * the rest are clamped to {@link PHOTO_FRAMING_BOUNDS}. Same rules as the
40
- * landing page's `photoTransform`, so what the editor previews is what ships.
13
+ * That subpath imports nothing (no ajv, no JSON Schema), so this file stays
14
+ * framework-free: it backs the `@dloizides/ui-media/framing` subpath.
41
15
  */
42
- declare function clampFraming(input?: PhotoFramingInput | null): PhotoFraming;
43
- /** True when the framing is the identity (the landing page emits no transform). */
44
- declare const isDefaultFraming: (framing: PhotoFraming) => boolean;
16
+
45
17
  /**
46
18
  * An RN `transform` array. Percent translates are relative to the element
47
19
  * itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.
@@ -92,4 +64,4 @@ declare function stepFraming(current: PhotoFramingInput | null | undefined, acti
92
64
  /** False when the press would change nothing (at a bound, or reset on the default). */
93
65
  declare function canStepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): boolean;
94
66
 
95
- export { DEFAULT_PHOTO_FRAMING, FramingAction, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, type PhotoFraming, type PhotoFramingInput, type PhotoFramingSteps, type PhotoFramingTransform, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming };
67
+ export { FramingAction, PHOTO_FRAMING_STEPS, type PhotoFramingSteps, type PhotoFramingTransform, canStepFraming, framingToTransform, stepFraming };
package/dist/framing.js CHANGED
@@ -1,27 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ var framing = require('@dloizides/site-template-kit/framing');
4
+
3
5
  // src/framing/photoFraming.ts
4
- var PHOTO_FRAMING_BOUNDS = Object.freeze({
5
- /** Widest nudge, as a percentage of the card, in either direction. */
6
- maxOffsetPct: 60,
7
- /** Below 1 the photo shrinks inside an already-roomy frame. */
8
- minScale: 0.5,
9
- /** Far above 2 the subject's head leaves the card entirely. */
10
- maxScale: 3
11
- });
12
- var DEFAULT_PHOTO_FRAMING = Object.freeze({ x: 0, y: 0, scale: 1 });
13
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
14
- var finiteOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
15
- function clampFraming(input) {
16
- if (!input) return DEFAULT_PHOTO_FRAMING;
17
- const { maxOffsetPct, minScale, maxScale } = PHOTO_FRAMING_BOUNDS;
18
- return {
19
- x: clamp(finiteOr(input.x, DEFAULT_PHOTO_FRAMING.x), -maxOffsetPct, maxOffsetPct),
20
- y: clamp(finiteOr(input.y, DEFAULT_PHOTO_FRAMING.y), -maxOffsetPct, maxOffsetPct),
21
- scale: clamp(finiteOr(input.scale, DEFAULT_PHOTO_FRAMING.scale), minScale, maxScale)
22
- };
23
- }
24
- var isDefaultFraming = (framing) => framing.x === DEFAULT_PHOTO_FRAMING.x && framing.y === DEFAULT_PHOTO_FRAMING.y && framing.scale === DEFAULT_PHOTO_FRAMING.scale;
25
6
  var framingToTransform = (framing) => [
26
7
  { translateX: `${framing.x}%` },
27
8
  { translateY: `${framing.y}%` },
@@ -52,29 +33,41 @@ var DIRECTIONS = {
52
33
  [FramingAction.MoveRight]: [1, 0, 0]
53
34
  };
54
35
  function stepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
55
- if (action === FramingAction.Reset) return DEFAULT_PHOTO_FRAMING;
56
- const base = clampFraming(current);
36
+ if (action === FramingAction.Reset) return framing.DEFAULT_PHOTO_FRAMING;
37
+ const base = framing.clampFraming(current);
57
38
  const [dx, dy, ds] = DIRECTIONS[action];
58
- return clampFraming({
39
+ return framing.clampFraming({
59
40
  x: round(base.x + dx * steps.offsetPct),
60
41
  y: round(base.y + dy * steps.offsetPct),
61
42
  scale: round(base.scale + ds * steps.scale)
62
43
  });
63
44
  }
64
45
  function canStepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
65
- const base = clampFraming(current);
46
+ const base = framing.clampFraming(current);
66
47
  const next = stepFraming(base, action, steps);
67
48
  return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;
68
49
  }
69
50
 
70
- exports.DEFAULT_PHOTO_FRAMING = DEFAULT_PHOTO_FRAMING;
51
+ Object.defineProperty(exports, "DEFAULT_PHOTO_FRAMING", {
52
+ enumerable: true,
53
+ get: function () { return framing.DEFAULT_PHOTO_FRAMING; }
54
+ });
55
+ Object.defineProperty(exports, "PHOTO_FRAMING_BOUNDS", {
56
+ enumerable: true,
57
+ get: function () { return framing.PHOTO_FRAMING_BOUNDS; }
58
+ });
59
+ Object.defineProperty(exports, "clampFraming", {
60
+ enumerable: true,
61
+ get: function () { return framing.clampFraming; }
62
+ });
63
+ Object.defineProperty(exports, "isDefaultFraming", {
64
+ enumerable: true,
65
+ get: function () { return framing.isDefaultFraming; }
66
+ });
71
67
  exports.FramingAction = FramingAction;
72
- exports.PHOTO_FRAMING_BOUNDS = PHOTO_FRAMING_BOUNDS;
73
68
  exports.PHOTO_FRAMING_STEPS = PHOTO_FRAMING_STEPS;
74
69
  exports.canStepFraming = canStepFraming;
75
- exports.clampFraming = clampFraming;
76
70
  exports.framingToTransform = framingToTransform;
77
- exports.isDefaultFraming = isDefaultFraming;
78
71
  exports.stepFraming = stepFraming;
79
72
  //# sourceMappingURL=framing.js.map
80
73
  //# sourceMappingURL=framing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts"],"names":[],"mappings":"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","file":"framing.js","sourcesContent":["/**\n * Photo framing: the `{x, y, scale}` an organiser sets on a performer card.\n *\n * The bounds MIRROR `kefi-landings/src/lib/config-to-site.ts` (`photoTransform`,\n * MAX_PHOTO_OFFSET_PCT / MIN_PHOTO_SCALE / MAX_PHOTO_SCALE), which is what the\n * public landing page renders. The editor must never offer a value the landing\n * page would silently clamp away, so the numbers here and there must stay equal.\n * KEFI-PEOPLE-1 T12 \"import the bounds instead of hard-coding them\" removes the copy.\n *\n * This file imports nothing: it backs the framework-free `@dloizides/ui-media/framing`\n * subpath.\n */\n\n/** A framing value after clamping: every field finite and inside the bounds. */\nexport interface PhotoFraming {\n /** Horizontal nudge, % of the card. Negative = left. */\n readonly x: number;\n /** Vertical nudge, % of the card. Negative = up. */\n readonly y: number;\n /** Zoom factor. 1 = the photo's natural cover fit. */\n readonly scale: number;\n}\n\n/** What config / an API may hold: any field missing, null or non-finite. */\nexport interface PhotoFramingInput {\n x?: number | null;\n y?: number | null;\n scale?: number | null;\n}\n\nexport const PHOTO_FRAMING_BOUNDS = Object.freeze({\n /** Widest nudge, as a percentage of the card, in either direction. */\n maxOffsetPct: 60,\n /** Below 1 the photo shrinks inside an already-roomy frame. */\n minScale: 0.5,\n /** Far above 2 the subject's head leaves the card entirely. */\n maxScale: 3,\n});\n\nexport const DEFAULT_PHOTO_FRAMING: PhotoFraming = Object.freeze({ x: 0, y: 0, scale: 1 });\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));\n\nconst finiteOr = (value: number | null | undefined, fallback: number): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n\n/**\n * Normalises any stored framing: non-finite fields fall back to the default,\n * the rest are clamped to {@link PHOTO_FRAMING_BOUNDS}. Same rules as the\n * landing page's `photoTransform`, so what the editor previews is what ships.\n */\nexport function clampFraming(input?: PhotoFramingInput | null): PhotoFraming {\n if (!input) return DEFAULT_PHOTO_FRAMING;\n const { maxOffsetPct, minScale, maxScale } = PHOTO_FRAMING_BOUNDS;\n return {\n x: clamp(finiteOr(input.x, DEFAULT_PHOTO_FRAMING.x), -maxOffsetPct, maxOffsetPct),\n y: clamp(finiteOr(input.y, DEFAULT_PHOTO_FRAMING.y), -maxOffsetPct, maxOffsetPct),\n scale: clamp(finiteOr(input.scale, DEFAULT_PHOTO_FRAMING.scale), minScale, maxScale),\n };\n}\n\n/** True when the framing is the identity (the landing page emits no transform). */\nexport const isDefaultFraming = (framing: PhotoFraming): boolean =>\n framing.x === DEFAULT_PHOTO_FRAMING.x &&\n framing.y === DEFAULT_PHOTO_FRAMING.y &&\n framing.scale === DEFAULT_PHOTO_FRAMING.scale;\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"]}
1
+ {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts"],"names":["DEFAULT_PHOTO_FRAMING","clampFraming"],"mappings":";;;;;AAiCO,IAAM,kBAAA,GAAqB,CAAC,OAAA,KAAiD;AAAA,EAClF,EAAE,UAAA,EAAY,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,EAAI;AAAA,EAC9B,EAAE,UAAA,EAAY,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,EAAI;AAAA,EAC9B,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA;AACnB;;;AChCO,IAAM,aAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,QAAA,EAAU,UAAA;AAAA,EACV,SAAA,EAAW,WAAA;AAAA,EACX,KAAA,EAAO;AACT;;;ACDO,IAAM,mBAAA,GAAyC,OAAO,MAAA,CAAO,EAAE,WAAW,CAAA,EAAG,KAAA,EAAO,KAAK;AAGhG,IAAM,SAAA,GAAY,GAAA;AAClB,IAAM,QAAQ,CAAC,CAAA,KAAsB,KAAK,KAAA,CAAM,CAAA,GAAI,SAAS,CAAA,GAAI,SAAA;AAKjE,IAAM,UAAA,GAAoE;AAAA,EACxE,CAAC,aAAA,CAAc,MAAM,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAChC,CAAC,aAAA,CAAc,OAAO,GAAG,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAClC,CAAC,aAAA,CAAc,MAAM,GAAG,CAAC,CAAA,EAAG,IAAI,CAAC,CAAA;AAAA,EACjC,CAAC,aAAA,CAAc,QAAQ,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAClC,CAAC,aAAA,CAAc,QAAQ,GAAG,CAAC,EAAA,EAAI,GAAG,CAAC,CAAA;AAAA,EACnC,CAAC,aAAA,CAAc,SAAS,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC;AACrC,CAAA;AAMO,SAAS,WAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,GAA2B,mBAAA,EACb;AACd,EAAA,IAAI,MAAA,KAAW,aAAA,CAAc,KAAA,EAAO,OAAOA,6BAAA;AAC3C,EAAA,MAAM,IAAA,GAAOC,qBAAa,OAAO,CAAA;AACjC,EAAA,MAAM,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA,GAAI,WAAW,MAAM,CAAA;AACtC,EAAA,OAAOA,oBAAA,CAAa;AAAA,IAClB,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,GAAI,EAAA,GAAK,MAAM,SAAS,CAAA;AAAA,IACtC,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,GAAI,EAAA,GAAK,MAAM,SAAS,CAAA;AAAA,IACtC,OAAO,KAAA,CAAM,IAAA,CAAK,KAAA,GAAQ,EAAA,GAAK,MAAM,KAAK;AAAA,GAC3C,CAAA;AACH;AAGO,SAAS,cAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,GAA2B,mBAAA,EAClB;AACT,EAAA,MAAM,IAAA,GAAOA,qBAAa,OAAO,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,IAAA,EAAM,MAAA,EAAQ,KAAK,CAAA;AAC5C,EAAA,OAAO,IAAA,CAAK,CAAA,KAAM,IAAA,CAAK,CAAA,IAAK,IAAA,CAAK,MAAM,IAAA,CAAK,CAAA,IAAK,IAAA,CAAK,KAAA,KAAU,IAAA,CAAK,KAAA;AACvE","file":"framing.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"]}
package/dist/framing.mjs CHANGED
@@ -1,25 +1,7 @@
1
+ import { DEFAULT_PHOTO_FRAMING, clampFraming } from '@dloizides/site-template-kit/framing';
2
+ export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, clampFraming, isDefaultFraming } from '@dloizides/site-template-kit/framing';
3
+
1
4
  // src/framing/photoFraming.ts
2
- var PHOTO_FRAMING_BOUNDS = Object.freeze({
3
- /** Widest nudge, as a percentage of the card, in either direction. */
4
- maxOffsetPct: 60,
5
- /** Below 1 the photo shrinks inside an already-roomy frame. */
6
- minScale: 0.5,
7
- /** Far above 2 the subject's head leaves the card entirely. */
8
- maxScale: 3
9
- });
10
- var DEFAULT_PHOTO_FRAMING = Object.freeze({ x: 0, y: 0, scale: 1 });
11
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
12
- var finiteOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
13
- function clampFraming(input) {
14
- if (!input) return DEFAULT_PHOTO_FRAMING;
15
- const { maxOffsetPct, minScale, maxScale } = PHOTO_FRAMING_BOUNDS;
16
- return {
17
- x: clamp(finiteOr(input.x, DEFAULT_PHOTO_FRAMING.x), -maxOffsetPct, maxOffsetPct),
18
- y: clamp(finiteOr(input.y, DEFAULT_PHOTO_FRAMING.y), -maxOffsetPct, maxOffsetPct),
19
- scale: clamp(finiteOr(input.scale, DEFAULT_PHOTO_FRAMING.scale), minScale, maxScale)
20
- };
21
- }
22
- var isDefaultFraming = (framing) => framing.x === DEFAULT_PHOTO_FRAMING.x && framing.y === DEFAULT_PHOTO_FRAMING.y && framing.scale === DEFAULT_PHOTO_FRAMING.scale;
23
5
  var framingToTransform = (framing) => [
24
6
  { translateX: `${framing.x}%` },
25
7
  { translateY: `${framing.y}%` },
@@ -65,6 +47,6 @@ function canStepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
65
47
  return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;
66
48
  }
67
49
 
68
- export { DEFAULT_PHOTO_FRAMING, FramingAction, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming };
50
+ export { FramingAction, PHOTO_FRAMING_STEPS, canStepFraming, framingToTransform, stepFraming };
69
51
  //# sourceMappingURL=framing.mjs.map
70
52
  //# sourceMappingURL=framing.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts"],"names":[],"mappings":"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","file":"framing.mjs","sourcesContent":["/**\n * Photo framing: the `{x, y, scale}` an organiser sets on a performer card.\n *\n * The bounds MIRROR `kefi-landings/src/lib/config-to-site.ts` (`photoTransform`,\n * MAX_PHOTO_OFFSET_PCT / MIN_PHOTO_SCALE / MAX_PHOTO_SCALE), which is what the\n * public landing page renders. The editor must never offer a value the landing\n * page would silently clamp away, so the numbers here and there must stay equal.\n * KEFI-PEOPLE-1 T12 \"import the bounds instead of hard-coding them\" removes the copy.\n *\n * This file imports nothing: it backs the framework-free `@dloizides/ui-media/framing`\n * subpath.\n */\n\n/** A framing value after clamping: every field finite and inside the bounds. */\nexport interface PhotoFraming {\n /** Horizontal nudge, % of the card. Negative = left. */\n readonly x: number;\n /** Vertical nudge, % of the card. Negative = up. */\n readonly y: number;\n /** Zoom factor. 1 = the photo's natural cover fit. */\n readonly scale: number;\n}\n\n/** What config / an API may hold: any field missing, null or non-finite. */\nexport interface PhotoFramingInput {\n x?: number | null;\n y?: number | null;\n scale?: number | null;\n}\n\nexport const PHOTO_FRAMING_BOUNDS = Object.freeze({\n /** Widest nudge, as a percentage of the card, in either direction. */\n maxOffsetPct: 60,\n /** Below 1 the photo shrinks inside an already-roomy frame. */\n minScale: 0.5,\n /** Far above 2 the subject's head leaves the card entirely. */\n maxScale: 3,\n});\n\nexport const DEFAULT_PHOTO_FRAMING: PhotoFraming = Object.freeze({ x: 0, y: 0, scale: 1 });\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));\n\nconst finiteOr = (value: number | null | undefined, fallback: number): number =>\n typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n\n/**\n * Normalises any stored framing: non-finite fields fall back to the default,\n * the rest are clamped to {@link PHOTO_FRAMING_BOUNDS}. Same rules as the\n * landing page's `photoTransform`, so what the editor previews is what ships.\n */\nexport function clampFraming(input?: PhotoFramingInput | null): PhotoFraming {\n if (!input) return DEFAULT_PHOTO_FRAMING;\n const { maxOffsetPct, minScale, maxScale } = PHOTO_FRAMING_BOUNDS;\n return {\n x: clamp(finiteOr(input.x, DEFAULT_PHOTO_FRAMING.x), -maxOffsetPct, maxOffsetPct),\n y: clamp(finiteOr(input.y, DEFAULT_PHOTO_FRAMING.y), -maxOffsetPct, maxOffsetPct),\n scale: clamp(finiteOr(input.scale, DEFAULT_PHOTO_FRAMING.scale), minScale, maxScale),\n };\n}\n\n/** True when the framing is the identity (the landing page emits no transform). */\nexport const isDefaultFraming = (framing: PhotoFraming): boolean =>\n framing.x === DEFAULT_PHOTO_FRAMING.x &&\n framing.y === DEFAULT_PHOTO_FRAMING.y &&\n framing.scale === DEFAULT_PHOTO_FRAMING.scale;\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"]}
1
+ {"version":3,"sources":["../src/framing/photoFraming.ts","../src/framing/FramingAction.ts","../src/framing/stepFraming.ts"],"names":[],"mappings":";;;;AAiCO,IAAM,kBAAA,GAAqB,CAAC,OAAA,KAAiD;AAAA,EAClF,EAAE,UAAA,EAAY,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,EAAI;AAAA,EAC9B,EAAE,UAAA,EAAY,CAAA,EAAG,OAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,EAAI;AAAA,EAC9B,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA;AACnB;;;AChCO,IAAM,aAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,QAAA;AAAA,EACR,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,QAAA,EAAU,UAAA;AAAA,EACV,SAAA,EAAW,WAAA;AAAA,EACX,KAAA,EAAO;AACT;;;ACDO,IAAM,mBAAA,GAAyC,OAAO,MAAA,CAAO,EAAE,WAAW,CAAA,EAAG,KAAA,EAAO,KAAK;AAGhG,IAAM,SAAA,GAAY,GAAA;AAClB,IAAM,QAAQ,CAAC,CAAA,KAAsB,KAAK,KAAA,CAAM,CAAA,GAAI,SAAS,CAAA,GAAI,SAAA;AAKjE,IAAM,UAAA,GAAoE;AAAA,EACxE,CAAC,aAAA,CAAc,MAAM,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAChC,CAAC,aAAA,CAAc,OAAO,GAAG,CAAC,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAClC,CAAC,aAAA,CAAc,MAAM,GAAG,CAAC,CAAA,EAAG,IAAI,CAAC,CAAA;AAAA,EACjC,CAAC,aAAA,CAAc,QAAQ,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAClC,CAAC,aAAA,CAAc,QAAQ,GAAG,CAAC,EAAA,EAAI,GAAG,CAAC,CAAA;AAAA,EACnC,CAAC,aAAA,CAAc,SAAS,GAAG,CAAC,CAAA,EAAG,GAAG,CAAC;AACrC,CAAA;AAMO,SAAS,WAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,GAA2B,mBAAA,EACb;AACd,EAAA,IAAI,MAAA,KAAW,aAAA,CAAc,KAAA,EAAO,OAAO,qBAAA;AAC3C,EAAA,MAAM,IAAA,GAAO,aAAa,OAAO,CAAA;AACjC,EAAA,MAAM,CAAC,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA,GAAI,WAAW,MAAM,CAAA;AACtC,EAAA,OAAO,YAAA,CAAa;AAAA,IAClB,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,GAAI,EAAA,GAAK,MAAM,SAAS,CAAA;AAAA,IACtC,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,GAAI,EAAA,GAAK,MAAM,SAAS,CAAA;AAAA,IACtC,OAAO,KAAA,CAAM,IAAA,CAAK,KAAA,GAAQ,EAAA,GAAK,MAAM,KAAK;AAAA,GAC3C,CAAA;AACH;AAGO,SAAS,cAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,GAA2B,mBAAA,EAClB;AACT,EAAA,MAAM,IAAA,GAAO,aAAa,OAAO,CAAA;AACjC,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,IAAA,EAAM,MAAA,EAAQ,KAAK,CAAA;AAC5C,EAAA,OAAO,IAAA,CAAK,CAAA,KAAM,IAAA,CAAK,CAAA,IAAK,IAAA,CAAK,MAAM,IAAA,CAAK,CAAA,IAAK,IAAA,CAAK,KAAA,KAAU,IAAA,CAAK,KAAA;AACvE","file":"framing.mjs","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"]}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,9 @@
1
- import { PhotoFramingInput, PhotoFraming, PhotoFramingTransform, FramingAction, PhotoFramingSteps } from './framing.mjs';
2
- export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming } from './framing.mjs';
1
+ import { PhotoFramingTransform, FramingAction, PhotoFramingSteps } from './framing.mjs';
2
+ export { PHOTO_FRAMING_STEPS, canStepFraming, framingToTransform, stepFraming } from './framing.mjs';
3
3
  import React from 'react';
4
4
  import { ButtonVariant } from '@dloizides/ui-buttons';
5
+ import { PhotoFramingInput, PhotoFraming } from '@dloizides/site-template-kit/framing';
6
+ export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PhotoFraming, PhotoFramingInput, clampFraming, isDefaultFraming } from '@dloizides/site-template-kit/framing';
5
7
 
6
8
  /**
7
9
  * The four states of an image upload slot (KEFI-PEOPLE-1 design board "upload
@@ -40,8 +42,11 @@ interface ImagePickerLabels {
40
42
  /** Accessibility hint for the button: "Opens a file picker to choose an image". */
41
43
  hint: string;
42
44
  }
43
- interface ImagePickerButtonProps<TFile = File> {
44
- upload: (file: TFile) => Promise<void>;
45
+ interface ImagePickerButtonProps<TFile = File, TResult = void> {
46
+ /** Consumer transport; whatever it resolves with (e.g. the stored URL) goes to `onUploaded`. */
47
+ upload: (file: TFile) => Promise<TResult>;
48
+ /** Called with `upload`'s result once the slot is `done`. */
49
+ onUploaded?: (result: TResult) => void;
45
50
  labels: ImagePickerLabels;
46
51
  testID: string;
47
52
  /** Defaults to the browser file dialog. Native consumers pass their own picker. */
@@ -62,7 +67,7 @@ interface PickerView {
62
67
  }
63
68
  /** Pure status -> copy/variant mapping (unit-tested; the component only paints it). */
64
69
  declare function resolvePickerView(status: ImagePickerStatus, labels: ImagePickerLabels): PickerView;
65
- declare function ImagePickerButton<TFile = File>({ upload, labels, testID, pickFile, accept, hasImage, onError, disabled, }: ImagePickerButtonProps<TFile>): React.ReactElement;
70
+ declare function ImagePickerButton<TFile = File, TResult = void>({ upload, onUploaded, labels, testID, pickFile, accept, hasImage, onError, disabled, }: ImagePickerButtonProps<TFile, TResult>): React.ReactElement;
66
71
 
67
72
  declare const DEFAULT_IMAGE_ACCEPT = "image/*";
68
73
  /**
@@ -70,9 +75,11 @@ declare const DEFAULT_IMAGE_ACCEPT = "image/*";
70
75
  * where there is no DOM (native) — a native consumer passes its own `pickFile`.
71
76
  */
72
77
  declare function pickWebImage(accept?: string): Promise<File | null>;
73
- interface UseImageUploadOptions<TFile> {
78
+ interface UseImageUploadOptions<TFile, TResult = void> {
74
79
  /** Consumer-owned transport. Resolve = stored; reject = error state. No network code lives here. */
75
- upload: (file: TFile) => Promise<void>;
80
+ upload: (file: TFile) => Promise<TResult>;
81
+ /** Given what `upload` resolved with (e.g. the stored URL), once the slot is `done`. */
82
+ onUploaded?: (result: TResult) => void;
76
83
  /** Picks one file; `null` means the user cancelled. */
77
84
  pickFile: () => Promise<TFile | null>;
78
85
  /** Start in `done` when the slot already holds an image. */
@@ -90,7 +97,27 @@ interface ImageUploadState {
90
97
  * done or error back to uploading on the next pick. A cancelled pick leaves the
91
98
  * state as it was, so cancelling "Replace" does not wipe a finished upload.
92
99
  */
93
- declare function useImageUpload<TFile>({ upload, pickFile, hasImage, onError, }: UseImageUploadOptions<TFile>): ImageUploadState;
100
+ declare function useImageUpload<TFile, TResult = void>({ upload, pickFile, hasImage, onError, onUploaded, }: UseImageUploadOptions<TFile, TResult>): ImageUploadState;
101
+
102
+ /**
103
+ * Where the photo sits inside the preview frame. `Bottom` matches a kefi-landings
104
+ * cut-out (`.amb-photo img`: 4:5, `object-fit: contain`, `object-position: center
105
+ * bottom`) — feet on the floor of the card. An `as const` object rather than a TS
106
+ * `const enum` for the same isolatedModules reason as FramingAction.
107
+ */
108
+ declare const PhotoAnchor: {
109
+ readonly Center: "center";
110
+ readonly Bottom: "bottom";
111
+ };
112
+ type PhotoAnchor = (typeof PhotoAnchor)[keyof typeof PhotoAnchor];
113
+ interface PhotoAnchorLayout {
114
+ /** Applied to the editor's preview column: where the consumer's card sits vertically. */
115
+ justifyContent: 'center' | 'flex-end';
116
+ /** CSS `object-position` the consumer spreads onto its `<img>` / RN-web image. */
117
+ objectPosition: 'center center' | 'center bottom';
118
+ }
119
+ /** Pure anchor -> layout mapping (unit-tested; the editor only applies it). */
120
+ declare function resolvePhotoAnchor(anchor: PhotoAnchor): PhotoAnchorLayout;
94
121
 
95
122
  /** Name + hint of one control. `label` is the full phrase ("Move photo up"). */
96
123
  interface FramingActionLabel {
@@ -112,6 +139,10 @@ interface PhotoFramingPreviewArgs {
112
139
  framing: PhotoFraming;
113
140
  /** Ready to spread onto the photo: `style={{ transform }}`. */
114
141
  transform: PhotoFramingTransform;
142
+ /** The editor's `anchor` prop (default `center`). */
143
+ anchor: PhotoAnchor;
144
+ /** CSS `object-position` for that anchor, ready for the photo's style. */
145
+ objectPosition: string;
115
146
  }
116
147
  interface PhotoFramingEditorProps {
117
148
  /** Stored framing; clamped before use, so raw config is safe to pass. */
@@ -126,6 +157,8 @@ interface PhotoFramingEditorProps {
126
157
  /** Override the default arrow / plus / minus glyphs (e.g. with ui-icons). */
127
158
  renderGlyph?: (action: FramingAction, color: string, size: number) => React.ReactNode;
128
159
  disabled?: boolean;
160
+ /** Where the photo sits in the preview. `bottom` for cut-outs (kefi `.amb-photo`). Default `center`. */
161
+ anchor?: PhotoAnchor;
129
162
  }
130
163
 
131
164
  /**
@@ -138,6 +171,6 @@ interface PhotoFramingEditorProps {
138
171
  * LAYOUT_COLLAPSE_BREAKPOINT up they sit side by side.
139
172
  */
140
173
 
141
- declare const PhotoFramingEditor: ({ value, onChange, renderPreview, labels, testID, steps, renderGlyph, disabled, }: PhotoFramingEditorProps) => React.ReactElement;
174
+ declare const PhotoFramingEditor: ({ value, onChange, renderPreview, labels, testID, steps, renderGlyph, disabled, anchor, }: PhotoFramingEditorProps) => React.ReactElement;
142
175
 
143
- export { DEFAULT_IMAGE_ACCEPT, FramingAction, type FramingActionLabel, ImagePickerButton, type ImagePickerButtonProps, type ImagePickerLabels, ImagePickerStatus, type ImageUploadState, PhotoFraming, PhotoFramingEditor, type PhotoFramingEditorProps, PhotoFramingInput, type PhotoFramingLabels, type PhotoFramingPreviewArgs, PhotoFramingSteps, PhotoFramingTransform, type PickerView, type UseImageUploadOptions, pickWebImage, resolvePickerView, useImageUpload };
176
+ export { DEFAULT_IMAGE_ACCEPT, FramingAction, type FramingActionLabel, ImagePickerButton, type ImagePickerButtonProps, type ImagePickerLabels, ImagePickerStatus, type ImageUploadState, PhotoAnchor, type PhotoAnchorLayout, PhotoFramingEditor, type PhotoFramingEditorProps, type PhotoFramingLabels, type PhotoFramingPreviewArgs, PhotoFramingSteps, PhotoFramingTransform, type PickerView, type UseImageUploadOptions, pickWebImage, resolvePhotoAnchor, resolvePickerView, useImageUpload };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { PhotoFramingInput, PhotoFraming, PhotoFramingTransform, FramingAction, PhotoFramingSteps } from './framing.js';
2
- export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming } from './framing.js';
1
+ import { PhotoFramingTransform, FramingAction, PhotoFramingSteps } from './framing.js';
2
+ export { PHOTO_FRAMING_STEPS, canStepFraming, framingToTransform, stepFraming } from './framing.js';
3
3
  import React from 'react';
4
4
  import { ButtonVariant } from '@dloizides/ui-buttons';
5
+ import { PhotoFramingInput, PhotoFraming } from '@dloizides/site-template-kit/framing';
6
+ export { DEFAULT_PHOTO_FRAMING, PHOTO_FRAMING_BOUNDS, PhotoFraming, PhotoFramingInput, clampFraming, isDefaultFraming } from '@dloizides/site-template-kit/framing';
5
7
 
6
8
  /**
7
9
  * The four states of an image upload slot (KEFI-PEOPLE-1 design board "upload
@@ -40,8 +42,11 @@ interface ImagePickerLabels {
40
42
  /** Accessibility hint for the button: "Opens a file picker to choose an image". */
41
43
  hint: string;
42
44
  }
43
- interface ImagePickerButtonProps<TFile = File> {
44
- upload: (file: TFile) => Promise<void>;
45
+ interface ImagePickerButtonProps<TFile = File, TResult = void> {
46
+ /** Consumer transport; whatever it resolves with (e.g. the stored URL) goes to `onUploaded`. */
47
+ upload: (file: TFile) => Promise<TResult>;
48
+ /** Called with `upload`'s result once the slot is `done`. */
49
+ onUploaded?: (result: TResult) => void;
45
50
  labels: ImagePickerLabels;
46
51
  testID: string;
47
52
  /** Defaults to the browser file dialog. Native consumers pass their own picker. */
@@ -62,7 +67,7 @@ interface PickerView {
62
67
  }
63
68
  /** Pure status -> copy/variant mapping (unit-tested; the component only paints it). */
64
69
  declare function resolvePickerView(status: ImagePickerStatus, labels: ImagePickerLabels): PickerView;
65
- declare function ImagePickerButton<TFile = File>({ upload, labels, testID, pickFile, accept, hasImage, onError, disabled, }: ImagePickerButtonProps<TFile>): React.ReactElement;
70
+ declare function ImagePickerButton<TFile = File, TResult = void>({ upload, onUploaded, labels, testID, pickFile, accept, hasImage, onError, disabled, }: ImagePickerButtonProps<TFile, TResult>): React.ReactElement;
66
71
 
67
72
  declare const DEFAULT_IMAGE_ACCEPT = "image/*";
68
73
  /**
@@ -70,9 +75,11 @@ declare const DEFAULT_IMAGE_ACCEPT = "image/*";
70
75
  * where there is no DOM (native) — a native consumer passes its own `pickFile`.
71
76
  */
72
77
  declare function pickWebImage(accept?: string): Promise<File | null>;
73
- interface UseImageUploadOptions<TFile> {
78
+ interface UseImageUploadOptions<TFile, TResult = void> {
74
79
  /** Consumer-owned transport. Resolve = stored; reject = error state. No network code lives here. */
75
- upload: (file: TFile) => Promise<void>;
80
+ upload: (file: TFile) => Promise<TResult>;
81
+ /** Given what `upload` resolved with (e.g. the stored URL), once the slot is `done`. */
82
+ onUploaded?: (result: TResult) => void;
76
83
  /** Picks one file; `null` means the user cancelled. */
77
84
  pickFile: () => Promise<TFile | null>;
78
85
  /** Start in `done` when the slot already holds an image. */
@@ -90,7 +97,27 @@ interface ImageUploadState {
90
97
  * done or error back to uploading on the next pick. A cancelled pick leaves the
91
98
  * state as it was, so cancelling "Replace" does not wipe a finished upload.
92
99
  */
93
- declare function useImageUpload<TFile>({ upload, pickFile, hasImage, onError, }: UseImageUploadOptions<TFile>): ImageUploadState;
100
+ declare function useImageUpload<TFile, TResult = void>({ upload, pickFile, hasImage, onError, onUploaded, }: UseImageUploadOptions<TFile, TResult>): ImageUploadState;
101
+
102
+ /**
103
+ * Where the photo sits inside the preview frame. `Bottom` matches a kefi-landings
104
+ * cut-out (`.amb-photo img`: 4:5, `object-fit: contain`, `object-position: center
105
+ * bottom`) — feet on the floor of the card. An `as const` object rather than a TS
106
+ * `const enum` for the same isolatedModules reason as FramingAction.
107
+ */
108
+ declare const PhotoAnchor: {
109
+ readonly Center: "center";
110
+ readonly Bottom: "bottom";
111
+ };
112
+ type PhotoAnchor = (typeof PhotoAnchor)[keyof typeof PhotoAnchor];
113
+ interface PhotoAnchorLayout {
114
+ /** Applied to the editor's preview column: where the consumer's card sits vertically. */
115
+ justifyContent: 'center' | 'flex-end';
116
+ /** CSS `object-position` the consumer spreads onto its `<img>` / RN-web image. */
117
+ objectPosition: 'center center' | 'center bottom';
118
+ }
119
+ /** Pure anchor -> layout mapping (unit-tested; the editor only applies it). */
120
+ declare function resolvePhotoAnchor(anchor: PhotoAnchor): PhotoAnchorLayout;
94
121
 
95
122
  /** Name + hint of one control. `label` is the full phrase ("Move photo up"). */
96
123
  interface FramingActionLabel {
@@ -112,6 +139,10 @@ interface PhotoFramingPreviewArgs {
112
139
  framing: PhotoFraming;
113
140
  /** Ready to spread onto the photo: `style={{ transform }}`. */
114
141
  transform: PhotoFramingTransform;
142
+ /** The editor's `anchor` prop (default `center`). */
143
+ anchor: PhotoAnchor;
144
+ /** CSS `object-position` for that anchor, ready for the photo's style. */
145
+ objectPosition: string;
115
146
  }
116
147
  interface PhotoFramingEditorProps {
117
148
  /** Stored framing; clamped before use, so raw config is safe to pass. */
@@ -126,6 +157,8 @@ interface PhotoFramingEditorProps {
126
157
  /** Override the default arrow / plus / minus glyphs (e.g. with ui-icons). */
127
158
  renderGlyph?: (action: FramingAction, color: string, size: number) => React.ReactNode;
128
159
  disabled?: boolean;
160
+ /** Where the photo sits in the preview. `bottom` for cut-outs (kefi `.amb-photo`). Default `center`. */
161
+ anchor?: PhotoAnchor;
129
162
  }
130
163
 
131
164
  /**
@@ -138,6 +171,6 @@ interface PhotoFramingEditorProps {
138
171
  * LAYOUT_COLLAPSE_BREAKPOINT up they sit side by side.
139
172
  */
140
173
 
141
- declare const PhotoFramingEditor: ({ value, onChange, renderPreview, labels, testID, steps, renderGlyph, disabled, }: PhotoFramingEditorProps) => React.ReactElement;
174
+ declare const PhotoFramingEditor: ({ value, onChange, renderPreview, labels, testID, steps, renderGlyph, disabled, anchor, }: PhotoFramingEditorProps) => React.ReactElement;
142
175
 
143
- export { DEFAULT_IMAGE_ACCEPT, FramingAction, type FramingActionLabel, ImagePickerButton, type ImagePickerButtonProps, type ImagePickerLabels, ImagePickerStatus, type ImageUploadState, PhotoFraming, PhotoFramingEditor, type PhotoFramingEditorProps, PhotoFramingInput, type PhotoFramingLabels, type PhotoFramingPreviewArgs, PhotoFramingSteps, PhotoFramingTransform, type PickerView, type UseImageUploadOptions, pickWebImage, resolvePickerView, useImageUpload };
176
+ export { DEFAULT_IMAGE_ACCEPT, FramingAction, type FramingActionLabel, ImagePickerButton, type ImagePickerButtonProps, type ImagePickerLabels, ImagePickerStatus, type ImageUploadState, PhotoAnchor, type PhotoAnchorLayout, PhotoFramingEditor, type PhotoFramingEditorProps, type PhotoFramingLabels, type PhotoFramingPreviewArgs, PhotoFramingSteps, PhotoFramingTransform, type PickerView, type UseImageUploadOptions, pickWebImage, resolvePhotoAnchor, resolvePickerView, useImageUpload };