@dloizides/ui-media 0.1.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 +5 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/framing.d.mts +95 -0
- package/dist/framing.d.ts +95 -0
- package/dist/framing.js +80 -0
- package/dist/framing.js.map +1 -0
- package/dist/framing.mjs +70 -0
- package/dist/framing.mjs.map +1 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +313 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +123 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 dloizides
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @dloizides/ui-media
|
|
2
|
+
|
|
3
|
+
Themable React Native (RN-web) media controls for the dloizides.com UI kit.
|
|
4
|
+
|
|
5
|
+
- `ImagePickerButton` - one upload slot: idle / uploading / error / done. You pass the `upload(file)` function; the package has no network code. Default picker is the browser file dialog; pass `pickFile` on native.
|
|
6
|
+
- `PhotoFramingEditor` - live preview of your own card (`renderPreview({ framing, transform })`), zoom stepper and position d-pad (44px ui-buttons IconButtons). Value `{ x, y, scale }`.
|
|
7
|
+
- `PHOTO_FRAMING_BOUNDS`, `clampFraming`, `stepFraming`, `framingToTransform` - also importable from the framework-free `@dloizides/ui-media/framing` subpath (no react / react-native import).
|
|
8
|
+
|
|
9
|
+
All strings arrive through `labels` props (pass your FM() keys). Theme comes from `FeedbackUiProvider` (@dloizides/ui-feedback).
|
|
10
|
+
|
|
11
|
+
Peers: react, react-native, @dloizides/ui-buttons, @dloizides/ui-feedback, @dloizides/ui-layout.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo framing: the `{x, y, scale}` an organiser sets on a performer card.
|
|
3
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
41
|
+
*/
|
|
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;
|
|
45
|
+
/**
|
|
46
|
+
* An RN `transform` array. Percent translates are relative to the element
|
|
47
|
+
* itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.
|
|
48
|
+
* Typed locally so this module stays free of a react-native import.
|
|
49
|
+
*/
|
|
50
|
+
type PhotoFramingTransform = [
|
|
51
|
+
{
|
|
52
|
+
translateX: `${number}%`;
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
translateY: `${number}%`;
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
scale: number;
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
declare const framingToTransform: (framing: PhotoFraming) => PhotoFramingTransform;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The seven controls of the framing editor. An `as const` object rather than a
|
|
65
|
+
* TS `const enum`: a const enum shipped in a .d.ts cannot be read by consumers
|
|
66
|
+
* compiled with `isolatedModules` (every Expo / Babel portal).
|
|
67
|
+
*/
|
|
68
|
+
declare const FramingAction: {
|
|
69
|
+
readonly ZoomIn: "zoomIn";
|
|
70
|
+
readonly ZoomOut: "zoomOut";
|
|
71
|
+
readonly MoveUp: "moveUp";
|
|
72
|
+
readonly MoveDown: "moveDown";
|
|
73
|
+
readonly MoveLeft: "moveLeft";
|
|
74
|
+
readonly MoveRight: "moveRight";
|
|
75
|
+
readonly Reset: "reset";
|
|
76
|
+
};
|
|
77
|
+
type FramingAction = (typeof FramingAction)[keyof typeof FramingAction];
|
|
78
|
+
|
|
79
|
+
/** How far one press moves or zooms. */
|
|
80
|
+
interface PhotoFramingSteps {
|
|
81
|
+
/** Nudge per press, % of the card. */
|
|
82
|
+
offsetPct: number;
|
|
83
|
+
/** Zoom per press, as a scale delta. */
|
|
84
|
+
scale: number;
|
|
85
|
+
}
|
|
86
|
+
declare const PHOTO_FRAMING_STEPS: PhotoFramingSteps;
|
|
87
|
+
/**
|
|
88
|
+
* Applies one control press. The result is always clamped and rounded, so ten
|
|
89
|
+
* presses of +0.1 land on exactly 2, not 1.9999999999999998.
|
|
90
|
+
*/
|
|
91
|
+
declare function stepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): PhotoFraming;
|
|
92
|
+
/** False when the press would change nothing (at a bound, or reset on the default). */
|
|
93
|
+
declare function canStepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): boolean;
|
|
94
|
+
|
|
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 };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photo framing: the `{x, y, scale}` an organiser sets on a performer card.
|
|
3
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
41
|
+
*/
|
|
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;
|
|
45
|
+
/**
|
|
46
|
+
* An RN `transform` array. Percent translates are relative to the element
|
|
47
|
+
* itself on RN-web, matching the landing page's CSS `translate(x%, y%) scale(s)`.
|
|
48
|
+
* Typed locally so this module stays free of a react-native import.
|
|
49
|
+
*/
|
|
50
|
+
type PhotoFramingTransform = [
|
|
51
|
+
{
|
|
52
|
+
translateX: `${number}%`;
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
translateY: `${number}%`;
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
scale: number;
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
declare const framingToTransform: (framing: PhotoFraming) => PhotoFramingTransform;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The seven controls of the framing editor. An `as const` object rather than a
|
|
65
|
+
* TS `const enum`: a const enum shipped in a .d.ts cannot be read by consumers
|
|
66
|
+
* compiled with `isolatedModules` (every Expo / Babel portal).
|
|
67
|
+
*/
|
|
68
|
+
declare const FramingAction: {
|
|
69
|
+
readonly ZoomIn: "zoomIn";
|
|
70
|
+
readonly ZoomOut: "zoomOut";
|
|
71
|
+
readonly MoveUp: "moveUp";
|
|
72
|
+
readonly MoveDown: "moveDown";
|
|
73
|
+
readonly MoveLeft: "moveLeft";
|
|
74
|
+
readonly MoveRight: "moveRight";
|
|
75
|
+
readonly Reset: "reset";
|
|
76
|
+
};
|
|
77
|
+
type FramingAction = (typeof FramingAction)[keyof typeof FramingAction];
|
|
78
|
+
|
|
79
|
+
/** How far one press moves or zooms. */
|
|
80
|
+
interface PhotoFramingSteps {
|
|
81
|
+
/** Nudge per press, % of the card. */
|
|
82
|
+
offsetPct: number;
|
|
83
|
+
/** Zoom per press, as a scale delta. */
|
|
84
|
+
scale: number;
|
|
85
|
+
}
|
|
86
|
+
declare const PHOTO_FRAMING_STEPS: PhotoFramingSteps;
|
|
87
|
+
/**
|
|
88
|
+
* Applies one control press. The result is always clamped and rounded, so ten
|
|
89
|
+
* presses of +0.1 land on exactly 2, not 1.9999999999999998.
|
|
90
|
+
*/
|
|
91
|
+
declare function stepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): PhotoFraming;
|
|
92
|
+
/** False when the press would change nothing (at a bound, or reset on the default). */
|
|
93
|
+
declare function canStepFraming(current: PhotoFramingInput | null | undefined, action: FramingAction, steps?: PhotoFramingSteps): boolean;
|
|
94
|
+
|
|
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 };
|
package/dist/framing.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 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
|
+
var framingToTransform = (framing) => [
|
|
26
|
+
{ translateX: `${framing.x}%` },
|
|
27
|
+
{ translateY: `${framing.y}%` },
|
|
28
|
+
{ scale: framing.scale }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// src/framing/FramingAction.ts
|
|
32
|
+
var FramingAction = {
|
|
33
|
+
ZoomIn: "zoomIn",
|
|
34
|
+
ZoomOut: "zoomOut",
|
|
35
|
+
MoveUp: "moveUp",
|
|
36
|
+
MoveDown: "moveDown",
|
|
37
|
+
MoveLeft: "moveLeft",
|
|
38
|
+
MoveRight: "moveRight",
|
|
39
|
+
Reset: "reset"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/framing/stepFraming.ts
|
|
43
|
+
var PHOTO_FRAMING_STEPS = Object.freeze({ offsetPct: 5, scale: 0.1 });
|
|
44
|
+
var PRECISION = 1e3;
|
|
45
|
+
var round = (n) => Math.round(n * PRECISION) / PRECISION;
|
|
46
|
+
var DIRECTIONS = {
|
|
47
|
+
[FramingAction.ZoomIn]: [0, 0, 1],
|
|
48
|
+
[FramingAction.ZoomOut]: [0, 0, -1],
|
|
49
|
+
[FramingAction.MoveUp]: [0, -1, 0],
|
|
50
|
+
[FramingAction.MoveDown]: [0, 1, 0],
|
|
51
|
+
[FramingAction.MoveLeft]: [-1, 0, 0],
|
|
52
|
+
[FramingAction.MoveRight]: [1, 0, 0]
|
|
53
|
+
};
|
|
54
|
+
function stepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
|
|
55
|
+
if (action === FramingAction.Reset) return DEFAULT_PHOTO_FRAMING;
|
|
56
|
+
const base = clampFraming(current);
|
|
57
|
+
const [dx, dy, ds] = DIRECTIONS[action];
|
|
58
|
+
return clampFraming({
|
|
59
|
+
x: round(base.x + dx * steps.offsetPct),
|
|
60
|
+
y: round(base.y + dy * steps.offsetPct),
|
|
61
|
+
scale: round(base.scale + ds * steps.scale)
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function canStepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
|
|
65
|
+
const base = clampFraming(current);
|
|
66
|
+
const next = stepFraming(base, action, steps);
|
|
67
|
+
return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
exports.DEFAULT_PHOTO_FRAMING = DEFAULT_PHOTO_FRAMING;
|
|
71
|
+
exports.FramingAction = FramingAction;
|
|
72
|
+
exports.PHOTO_FRAMING_BOUNDS = PHOTO_FRAMING_BOUNDS;
|
|
73
|
+
exports.PHOTO_FRAMING_STEPS = PHOTO_FRAMING_STEPS;
|
|
74
|
+
exports.canStepFraming = canStepFraming;
|
|
75
|
+
exports.clampFraming = clampFraming;
|
|
76
|
+
exports.framingToTransform = framingToTransform;
|
|
77
|
+
exports.isDefaultFraming = isDefaultFraming;
|
|
78
|
+
exports.stepFraming = stepFraming;
|
|
79
|
+
//# sourceMappingURL=framing.js.map
|
|
80
|
+
//# sourceMappingURL=framing.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/framing.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 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
|
+
var framingToTransform = (framing) => [
|
|
24
|
+
{ translateX: `${framing.x}%` },
|
|
25
|
+
{ translateY: `${framing.y}%` },
|
|
26
|
+
{ scale: framing.scale }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// src/framing/FramingAction.ts
|
|
30
|
+
var FramingAction = {
|
|
31
|
+
ZoomIn: "zoomIn",
|
|
32
|
+
ZoomOut: "zoomOut",
|
|
33
|
+
MoveUp: "moveUp",
|
|
34
|
+
MoveDown: "moveDown",
|
|
35
|
+
MoveLeft: "moveLeft",
|
|
36
|
+
MoveRight: "moveRight",
|
|
37
|
+
Reset: "reset"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/framing/stepFraming.ts
|
|
41
|
+
var PHOTO_FRAMING_STEPS = Object.freeze({ offsetPct: 5, scale: 0.1 });
|
|
42
|
+
var PRECISION = 1e3;
|
|
43
|
+
var round = (n) => Math.round(n * PRECISION) / PRECISION;
|
|
44
|
+
var DIRECTIONS = {
|
|
45
|
+
[FramingAction.ZoomIn]: [0, 0, 1],
|
|
46
|
+
[FramingAction.ZoomOut]: [0, 0, -1],
|
|
47
|
+
[FramingAction.MoveUp]: [0, -1, 0],
|
|
48
|
+
[FramingAction.MoveDown]: [0, 1, 0],
|
|
49
|
+
[FramingAction.MoveLeft]: [-1, 0, 0],
|
|
50
|
+
[FramingAction.MoveRight]: [1, 0, 0]
|
|
51
|
+
};
|
|
52
|
+
function stepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
|
|
53
|
+
if (action === FramingAction.Reset) return DEFAULT_PHOTO_FRAMING;
|
|
54
|
+
const base = clampFraming(current);
|
|
55
|
+
const [dx, dy, ds] = DIRECTIONS[action];
|
|
56
|
+
return clampFraming({
|
|
57
|
+
x: round(base.x + dx * steps.offsetPct),
|
|
58
|
+
y: round(base.y + dy * steps.offsetPct),
|
|
59
|
+
scale: round(base.scale + ds * steps.scale)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function canStepFraming(current, action, steps = PHOTO_FRAMING_STEPS) {
|
|
63
|
+
const base = clampFraming(current);
|
|
64
|
+
const next = stepFraming(base, action, steps);
|
|
65
|
+
return next.x !== base.x || next.y !== base.y || next.scale !== base.scale;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { DEFAULT_PHOTO_FRAMING, FramingAction, PHOTO_FRAMING_BOUNDS, PHOTO_FRAMING_STEPS, canStepFraming, clampFraming, framingToTransform, isDefaultFraming, stepFraming };
|
|
69
|
+
//# sourceMappingURL=framing.mjs.map
|
|
70
|
+
//# sourceMappingURL=framing.mjs.map
|
|
@@ -0,0 +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"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
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';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ButtonVariant } from '@dloizides/ui-buttons';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The four states of an image upload slot (KEFI-PEOPLE-1 design board "upload
|
|
8
|
+
* slot"). An `as const` object, not a `const enum`, so Babel / isolatedModules
|
|
9
|
+
* consumers can read it from the published .d.ts.
|
|
10
|
+
*/
|
|
11
|
+
declare const ImagePickerStatus: {
|
|
12
|
+
readonly Idle: "idle";
|
|
13
|
+
readonly Uploading: "uploading";
|
|
14
|
+
readonly Error: "error";
|
|
15
|
+
readonly Done: "done";
|
|
16
|
+
};
|
|
17
|
+
type ImagePickerStatus = (typeof ImagePickerStatus)[keyof typeof ImagePickerStatus];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ImagePickerButton — one upload slot: pick an image, hand it to the consumer's
|
|
21
|
+
* `upload` function, show idle / uploading / error / done.
|
|
22
|
+
*
|
|
23
|
+
* No network code and no copy of its own: the transport is a prop and every
|
|
24
|
+
* string arrives through `labels`, so each portal passes its FM() keys.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
interface ImagePickerLabels {
|
|
28
|
+
/** Button, idle: "Add photo". */
|
|
29
|
+
pick: string;
|
|
30
|
+
/** Button, uploading: "Uploading...". */
|
|
31
|
+
uploading: string;
|
|
32
|
+
/** Button, done: "Replace photo". */
|
|
33
|
+
replace: string;
|
|
34
|
+
/** Button, error: "Try again". */
|
|
35
|
+
retry: string;
|
|
36
|
+
/** Status line after a successful upload: "Photo saved". */
|
|
37
|
+
done: string;
|
|
38
|
+
/** Status line after a failure: "Upload failed". */
|
|
39
|
+
error: string;
|
|
40
|
+
/** Accessibility hint for the button: "Opens a file picker to choose an image". */
|
|
41
|
+
hint: string;
|
|
42
|
+
}
|
|
43
|
+
interface ImagePickerButtonProps<TFile = File> {
|
|
44
|
+
upload: (file: TFile) => Promise<void>;
|
|
45
|
+
labels: ImagePickerLabels;
|
|
46
|
+
testID: string;
|
|
47
|
+
/** Defaults to the browser file dialog. Native consumers pass their own picker. */
|
|
48
|
+
pickFile?: () => Promise<TFile | null>;
|
|
49
|
+
/** `accept` for the default web picker. Default `image/*`. */
|
|
50
|
+
accept?: string;
|
|
51
|
+
/** Start in the `done` state (the slot already has an image). */
|
|
52
|
+
hasImage?: boolean;
|
|
53
|
+
onError?: (error: unknown) => void;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface PickerView {
|
|
57
|
+
buttonLabel: string;
|
|
58
|
+
variant: ButtonVariant;
|
|
59
|
+
statusText: string | null;
|
|
60
|
+
/** Paint the status line in the theme's error colour. */
|
|
61
|
+
isError: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Pure status -> copy/variant mapping (unit-tested; the component only paints it). */
|
|
64
|
+
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;
|
|
66
|
+
|
|
67
|
+
declare const DEFAULT_IMAGE_ACCEPT = "image/*";
|
|
68
|
+
/**
|
|
69
|
+
* Opens the browser's file dialog for ONE file. Resolves `null` on cancel, and
|
|
70
|
+
* where there is no DOM (native) — a native consumer passes its own `pickFile`.
|
|
71
|
+
*/
|
|
72
|
+
declare function pickWebImage(accept?: string): Promise<File | null>;
|
|
73
|
+
interface UseImageUploadOptions<TFile> {
|
|
74
|
+
/** Consumer-owned transport. Resolve = stored; reject = error state. No network code lives here. */
|
|
75
|
+
upload: (file: TFile) => Promise<void>;
|
|
76
|
+
/** Picks one file; `null` means the user cancelled. */
|
|
77
|
+
pickFile: () => Promise<TFile | null>;
|
|
78
|
+
/** Start in `done` when the slot already holds an image. */
|
|
79
|
+
hasImage?: boolean;
|
|
80
|
+
/** Told about a failed pick or upload (logging / toasts). */
|
|
81
|
+
onError?: (error: unknown) => void;
|
|
82
|
+
}
|
|
83
|
+
interface ImageUploadState {
|
|
84
|
+
status: ImagePickerStatus;
|
|
85
|
+
/** Pick then upload. A second call while one is in flight is ignored. */
|
|
86
|
+
pick: () => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The upload slot's state machine: idle -> uploading -> done | error, and from
|
|
90
|
+
* done or error back to uploading on the next pick. A cancelled pick leaves the
|
|
91
|
+
* state as it was, so cancelling "Replace" does not wipe a finished upload.
|
|
92
|
+
*/
|
|
93
|
+
declare function useImageUpload<TFile>({ upload, pickFile, hasImage, onError, }: UseImageUploadOptions<TFile>): ImageUploadState;
|
|
94
|
+
|
|
95
|
+
/** Name + hint of one control. `label` is the full phrase ("Move photo up"). */
|
|
96
|
+
interface FramingActionLabel {
|
|
97
|
+
label: string;
|
|
98
|
+
hint: string;
|
|
99
|
+
}
|
|
100
|
+
/** Every string the editor shows. The package has no copy of its own (FM-free). */
|
|
101
|
+
interface PhotoFramingLabels {
|
|
102
|
+
/** Heading over the zoom stepper: "Size". */
|
|
103
|
+
size: string;
|
|
104
|
+
/** Heading over the d-pad: "Position". */
|
|
105
|
+
position: string;
|
|
106
|
+
/** Readout between the zoom buttons, given a whole percent: (120) => "120%". */
|
|
107
|
+
scaleValue: (percent: number) => string;
|
|
108
|
+
actions: Record<FramingAction, FramingActionLabel>;
|
|
109
|
+
}
|
|
110
|
+
interface PhotoFramingPreviewArgs {
|
|
111
|
+
/** The clamped value. */
|
|
112
|
+
framing: PhotoFraming;
|
|
113
|
+
/** Ready to spread onto the photo: `style={{ transform }}`. */
|
|
114
|
+
transform: PhotoFramingTransform;
|
|
115
|
+
}
|
|
116
|
+
interface PhotoFramingEditorProps {
|
|
117
|
+
/** Stored framing; clamped before use, so raw config is safe to pass. */
|
|
118
|
+
value: PhotoFramingInput | null | undefined;
|
|
119
|
+
/** Receives the next clamped value after each press. */
|
|
120
|
+
onChange: (next: PhotoFraming) => void;
|
|
121
|
+
/** Renders the consumer's own card with the photo transformed — a live preview. */
|
|
122
|
+
renderPreview: (args: PhotoFramingPreviewArgs) => React.ReactNode;
|
|
123
|
+
labels: PhotoFramingLabels;
|
|
124
|
+
testID: string;
|
|
125
|
+
steps?: PhotoFramingSteps;
|
|
126
|
+
/** Override the default arrow / plus / minus glyphs (e.g. with ui-icons). */
|
|
127
|
+
renderGlyph?: (action: FramingAction, color: string, size: number) => React.ReactNode;
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* PhotoFramingEditor — the KEFI-PEOPLE-1 "A2" framing screen: a live preview of
|
|
133
|
+
* the consumer's own card (render prop) beside a zoom stepper and a position
|
|
134
|
+
* d-pad. The value is `{x, y, scale}`, clamped to PHOTO_FRAMING_BOUNDS, the same
|
|
135
|
+
* rules the public landing page applies — the preview is what ships.
|
|
136
|
+
*
|
|
137
|
+
* Narrow first: preview stacked over the controls; from ui-layout's
|
|
138
|
+
* LAYOUT_COLLAPSE_BREAKPOINT up they sit side by side.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
declare const PhotoFramingEditor: ({ value, onChange, renderPreview, labels, testID, steps, renderGlyph, disabled, }: PhotoFramingEditorProps) => React.ReactElement;
|
|
142
|
+
|
|
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 };
|