@pictor-sdk/ui 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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.d.mts +833 -0
- package/dist/index.d.ts +833 -0
- package/dist/index.js +4620 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4643 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
- package/src/Editor.tsx +276 -0
- package/src/ToolPanelHost.tsx +98 -0
- package/src/components/Slider.tsx +172 -0
- package/src/components/StraightenRuler.tsx +57 -0
- package/src/components/ToolRail.tsx +58 -0
- package/src/components/TopBar.tsx +53 -0
- package/src/composer/TextComposer.tsx +540 -0
- package/src/composer/keyboardLayout.ts +130 -0
- package/src/composer/useTextSession.ts +230 -0
- package/src/gestures/CropGestureSurface.tsx +270 -0
- package/src/gestures/CropGrid.tsx +24 -0
- package/src/gestures/DrawingSurface.tsx +77 -0
- package/src/gestures/EditorGestures.tsx +97 -0
- package/src/gestures/InteractionGuides.tsx +161 -0
- package/src/gestures/LayerGestureSurface.tsx +462 -0
- package/src/gestures/StoryMediaGestureSurface.tsx +517 -0
- package/src/gestures/TiltShiftGestureSurface.tsx +189 -0
- package/src/gestures/controller.ts +152 -0
- package/src/gestures/drawController.ts +104 -0
- package/src/gestures/interactionState.tsx +81 -0
- package/src/gestures/layerGestureWorklet.ts +198 -0
- package/src/gestures/mediaGestureWorklet.ts +159 -0
- package/src/gestures/routing.ts +42 -0
- package/src/gestures/settleCoordinator.ts +61 -0
- package/src/gestures/transformOwnership.ts +91 -0
- package/src/hostCallbacks.tsx +62 -0
- package/src/index.ts +72 -0
- package/src/panels/AddPhotoPanel.tsx +101 -0
- package/src/panels/AdjustPanel.tsx +158 -0
- package/src/panels/ColorPanel.tsx +193 -0
- package/src/panels/CropPanel.tsx +185 -0
- package/src/panels/DrawPanel.tsx +106 -0
- package/src/panels/FilterStrip.tsx +153 -0
- package/src/panels/LayersPanel.tsx +85 -0
- package/src/panels/LayoutPanel.tsx +55 -0
- package/src/panels/LuxPanel.tsx +122 -0
- package/src/panels/MediaPanel.tsx +73 -0
- package/src/panels/PerspectivePanel.tsx +157 -0
- package/src/panels/StickerPanel.tsx +78 -0
- package/src/panels/TextPanel.tsx +177 -0
- package/src/panels/TiltShiftPanel.tsx +146 -0
- package/src/panels/addPhotoController.ts +196 -0
- package/src/panels/cropMath.ts +37 -0
- package/src/panels/layoutMath.ts +58 -0
- package/src/panels/useTargetImageLayer.ts +41 -0
- package/src/theme.ts +61 -0
- package/src/uiState.tsx +57 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import * as _pictor_sdk_core from '@pictor-sdk/core';
|
|
2
|
+
import { Size, Layer, TextSpan, TextLayerData, EditorProfile, EditorPlugin, EditorDocument, ExportSpec, InsertableImage, ToolPanelProps, Dispatch, InsertImageOptions, InsertImageTransaction, Rect, EditorStore, LayerSnapState, Transform, Stroke } from '@pictor-sdk/core';
|
|
3
|
+
export { POST_PROFILE, STORY_PROFILE } from '@pictor-sdk/core';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import React__default, { ReactNode, ComponentType } from 'react';
|
|
6
|
+
import { PreviewLayerTransform } from '@pictor-sdk/skia';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One sticker offered by the panel.
|
|
10
|
+
*
|
|
11
|
+
* Supplied by the host: the library ships no sticker artwork, so what appears
|
|
12
|
+
* here is entirely the app's own.
|
|
13
|
+
*/
|
|
14
|
+
interface StickerAsset {
|
|
15
|
+
id: string;
|
|
16
|
+
/** image source (require() result, {uri}, or URL string) */
|
|
17
|
+
source: unknown;
|
|
18
|
+
intrinsic?: Size;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sticker tab. Sticker catalogs are host-supplied — pass them via
|
|
23
|
+
* <Editor stickers={…}> (or replace this panel through a plugin tool Panel).
|
|
24
|
+
* DECISION: no sticker assets ship with the library (same reasoning as LUTs).
|
|
25
|
+
*/
|
|
26
|
+
declare function StickerPanel(): React__default.JSX.Element;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Theming is one token object on context — no style props on individual
|
|
30
|
+
* components (ARCHITECTURE.md). Override any subset via <Editor theme={…}>.
|
|
31
|
+
*/
|
|
32
|
+
interface EditorTheme {
|
|
33
|
+
colors: {
|
|
34
|
+
background: string;
|
|
35
|
+
surface: string;
|
|
36
|
+
text: string;
|
|
37
|
+
textMuted: string;
|
|
38
|
+
accent: string;
|
|
39
|
+
danger: string;
|
|
40
|
+
track: string;
|
|
41
|
+
thumb: string;
|
|
42
|
+
};
|
|
43
|
+
radii: {
|
|
44
|
+
sm: number;
|
|
45
|
+
md: number;
|
|
46
|
+
lg: number;
|
|
47
|
+
};
|
|
48
|
+
spacing: {
|
|
49
|
+
xs: number;
|
|
50
|
+
sm: number;
|
|
51
|
+
md: number;
|
|
52
|
+
lg: number;
|
|
53
|
+
};
|
|
54
|
+
font: {
|
|
55
|
+
size: number;
|
|
56
|
+
sizeSm: number;
|
|
57
|
+
weight: '400' | '500' | '600' | '700';
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The bundled dark theme. Every value is overridable; see {@link mergeTheme}. */
|
|
61
|
+
declare const defaultTheme: EditorTheme;
|
|
62
|
+
/** A theme override — any subset of any group, the rest falling back to the default. */
|
|
63
|
+
type PartialTheme = {
|
|
64
|
+
[K in keyof EditorTheme]?: Partial<EditorTheme[K]>;
|
|
65
|
+
};
|
|
66
|
+
/** Apply an override onto the default theme, one group at a time. */
|
|
67
|
+
declare function mergeTheme(overrides?: PartialTheme): EditorTheme;
|
|
68
|
+
/** Carries the resolved theme. `<Editor>` provides it; components read it with {@link useTheme}. */
|
|
69
|
+
declare const ThemeContext: React.Context<EditorTheme>;
|
|
70
|
+
/** The theme in effect. Defaults outside an `<Editor>` rather than throwing. */
|
|
71
|
+
declare function useTheme(): EditorTheme;
|
|
72
|
+
|
|
73
|
+
interface ComposerInsets {
|
|
74
|
+
/** Distance from the bottom of the screen to place the control bar. */
|
|
75
|
+
bottom: number;
|
|
76
|
+
/** True while the keyboard occupies screen space. */
|
|
77
|
+
keyboardVisible: boolean;
|
|
78
|
+
/** Animation duration to match the platform's own, clamped to something sane. */
|
|
79
|
+
duration: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The state and commands behind a text-editing session.
|
|
84
|
+
*
|
|
85
|
+
* This is the whole contract a composer needs, so the default composer and any
|
|
86
|
+
* replacement are built on exactly the same surface — which is what makes the
|
|
87
|
+
* default one genuinely replaceable rather than merely swappable-in-principle.
|
|
88
|
+
*
|
|
89
|
+
* The session owns its history batch. The text tool has no bottom-sheet panel —
|
|
90
|
+
* the composer *is* its UI — so `ToolPanelHost` never opens one for it, and the
|
|
91
|
+
* session must, or every keystroke would be its own undo entry. Opening it here
|
|
92
|
+
* also fixes the ordering: the batch is open before the draft is created,
|
|
93
|
+
* which it would not be if a parent opened it in its own mount effect (effects
|
|
94
|
+
* run child-first).
|
|
95
|
+
*
|
|
96
|
+
* @experimental
|
|
97
|
+
*/
|
|
98
|
+
interface TextSessionController {
|
|
99
|
+
/** The layer being edited — a fresh draft, or the one that was selected. */
|
|
100
|
+
layer: Layer | undefined;
|
|
101
|
+
/** Whether this session created the layer, which decides blank-Done. */
|
|
102
|
+
kind: 'draft' | 'existing';
|
|
103
|
+
/** Current text. Mirrors the document; drives the input's value. */
|
|
104
|
+
value: string;
|
|
105
|
+
/** Replace the text. */
|
|
106
|
+
setValue(next: string): void;
|
|
107
|
+
/** The span the composer styles as a whole. */
|
|
108
|
+
span: TextSpan | undefined;
|
|
109
|
+
/** Full layer data, for alignment/fill/stroke/shadow controls. */
|
|
110
|
+
data: TextLayerData | undefined;
|
|
111
|
+
/** Patch the styled span — font, size, colour, weight, spacing. */
|
|
112
|
+
styleSpan(patch: Partial<TextSpan>): void;
|
|
113
|
+
/** Patch layer-level text data — alignment, fill, stroke, shadow, maxWidth. */
|
|
114
|
+
styleText(patch: Partial<TextLayerData>): void;
|
|
115
|
+
/** Set a normalized font size, held inside the supported range. */
|
|
116
|
+
setSize(size: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Finish. A draft that ended blank is discarded and records nothing.
|
|
119
|
+
*
|
|
120
|
+
* Returns the committed layer id, or `null` when nothing was kept.
|
|
121
|
+
*/
|
|
122
|
+
commit(): string | null;
|
|
123
|
+
/** Abandon. Restores the document to exactly its state when the tool opened. */
|
|
124
|
+
cancel(): void;
|
|
125
|
+
}
|
|
126
|
+
/** Hooks into the composing session — chiefly what to do when it ends. */
|
|
127
|
+
interface UseTextSessionOptions {
|
|
128
|
+
/**
|
|
129
|
+
* Edit this layer instead of resolving from the selection. Lets a custom UI
|
|
130
|
+
* request editing for a specific layer — the re-entry API.
|
|
131
|
+
*/
|
|
132
|
+
layerId?: string;
|
|
133
|
+
/** Called once the session has committed or been discarded. */
|
|
134
|
+
onFinished?: (layerId: string | null) => void;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Open a text-editing session for the current selection (or `layerId`).
|
|
138
|
+
*
|
|
139
|
+
* Creates its draft on mount so the canvas shows the layer immediately and the
|
|
140
|
+
* caret has something to sit on.
|
|
141
|
+
*/
|
|
142
|
+
declare function useTextSession(options?: UseTextSessionOptions): TextSessionController;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Props a text composer receives.
|
|
146
|
+
*
|
|
147
|
+
* The default composer is built on exactly this, so a replacement has
|
|
148
|
+
* everything the built-in one had — no privileged access to reach for.
|
|
149
|
+
*
|
|
150
|
+
* @experimental
|
|
151
|
+
*/
|
|
152
|
+
interface TextComposerProps {
|
|
153
|
+
/** The live session: layer, value, style commands, Done and Cancel. */
|
|
154
|
+
session: TextSessionController;
|
|
155
|
+
/** Where the controls should sit, given the keyboard. */
|
|
156
|
+
insets: ComposerInsets;
|
|
157
|
+
/** Finish and keep the text. */
|
|
158
|
+
onDone(): void;
|
|
159
|
+
/** Abandon, restoring the document. */
|
|
160
|
+
onCancel(): void;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The full-screen Story text composer.
|
|
164
|
+
*
|
|
165
|
+
* An absolute overlay above the canvas. The canvas keeps rendering the layer
|
|
166
|
+
* being typed — there is no second text renderer and no editing chrome inside
|
|
167
|
+
* `renderDocument` — so what is on screen while composing is what exports.
|
|
168
|
+
*
|
|
169
|
+
* The `TextInput` itself is invisible and stretched over the canvas: it owns
|
|
170
|
+
* the caret, the platform selection handles and the IME, while the *visible*
|
|
171
|
+
* text is the real Skia layer underneath. That is what keeps preview and export
|
|
172
|
+
* identical without giving up native text editing.
|
|
173
|
+
*
|
|
174
|
+
* @experimental
|
|
175
|
+
*/
|
|
176
|
+
declare function TextComposer({ session, insets, onDone, onCancel }: TextComposerProps): React__default.JSX.Element;
|
|
177
|
+
/**
|
|
178
|
+
* Mounts a text session and the composer for it, and keeps the keyboard inset
|
|
179
|
+
* up to date. This is what `<Editor>` renders while the text tool is active.
|
|
180
|
+
*
|
|
181
|
+
* @experimental
|
|
182
|
+
*/
|
|
183
|
+
declare function TextComposerHost({ Composer, }: {
|
|
184
|
+
/** Replace the default composer without replacing the canvas or editor. */
|
|
185
|
+
Composer?: React__default.ComponentType<TextComposerProps>;
|
|
186
|
+
}): React__default.JSX.Element;
|
|
187
|
+
|
|
188
|
+
/** What the top bar slot receives: the transaction actions and undo state. */
|
|
189
|
+
interface TopBarProps {
|
|
190
|
+
onCancel?: () => void;
|
|
191
|
+
onNext?: () => void;
|
|
192
|
+
/** true while an export is running (Next shows a spinner). */
|
|
193
|
+
busy?: boolean;
|
|
194
|
+
}
|
|
195
|
+
/** The default top bar: cancel, undo/redo, and the action that ends the session. */
|
|
196
|
+
declare function TopBar({ onCancel, onNext, busy }: TopBarProps): React__default.JSX.Element;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Every region of the default editor is a slot: pass your own component to
|
|
200
|
+
* replace it, or null to remove it. All slots are built exclusively on the
|
|
201
|
+
* public headless hooks — replacing one can never break the others.
|
|
202
|
+
*/
|
|
203
|
+
interface EditorSlots {
|
|
204
|
+
TopBar: ComponentType<TopBarProps> | null;
|
|
205
|
+
CanvasStage: ComponentType<CanvasStageProps> | null;
|
|
206
|
+
ToolRail: ComponentType | null;
|
|
207
|
+
ToolPanelHost: ComponentType | null;
|
|
208
|
+
/**
|
|
209
|
+
* The full-screen text composer, shown while the `text` tool is active.
|
|
210
|
+
*
|
|
211
|
+
* Replace it to ship your own composition UI without replacing the canvas or
|
|
212
|
+
* the editor; it receives everything the default one gets. `null` removes it,
|
|
213
|
+
* leaving text editing to a host-built surface.
|
|
214
|
+
*
|
|
215
|
+
* @experimental
|
|
216
|
+
*/
|
|
217
|
+
TextComposer: ComponentType<TextComposerProps> | null;
|
|
218
|
+
}
|
|
219
|
+
/** What the canvas slot receives. Children draw over the canvas, in its coordinates. */
|
|
220
|
+
interface CanvasStageProps {
|
|
221
|
+
children?: ReactNode;
|
|
222
|
+
/** Reported when a layer's image source fails to load. */
|
|
223
|
+
onError?: (error: unknown) => void;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The canvas plus whichever gesture surface the active tool calls for.
|
|
227
|
+
*
|
|
228
|
+
* Exactly one surface is mounted at a time — precedence lives in
|
|
229
|
+
* `resolveGestureSurface`, not in this JSX — so two recognisers can never
|
|
230
|
+
* arbitrate over the same touch.
|
|
231
|
+
*/
|
|
232
|
+
declare function CanvasStage({ children, onError }: CanvasStageProps): React__default.JSX.Element;
|
|
233
|
+
/**
|
|
234
|
+
* What `<Editor>` accepts: a profile, its plugins, a document or a source, and
|
|
235
|
+
* any slots or theme you want to replace.
|
|
236
|
+
*/
|
|
237
|
+
interface EditorProps {
|
|
238
|
+
profile: EditorProfile;
|
|
239
|
+
plugins?: EditorPlugin[];
|
|
240
|
+
/** Image source for a fresh document (or pass initialDocument). */
|
|
241
|
+
source?: unknown;
|
|
242
|
+
intrinsic?: Size;
|
|
243
|
+
initialDocument?: EditorDocument;
|
|
244
|
+
onExport?: (bytes: Uint8Array) => void | Promise<void>;
|
|
245
|
+
onCancel?: () => void;
|
|
246
|
+
onError?: (error: unknown) => void;
|
|
247
|
+
exportSpec?: Partial<ExportSpec>;
|
|
248
|
+
slots?: Partial<EditorSlots>;
|
|
249
|
+
theme?: PartialTheme;
|
|
250
|
+
/** Host-supplied sticker catalog for the built-in sticker panel. */
|
|
251
|
+
stickers?: StickerAsset[];
|
|
252
|
+
/**
|
|
253
|
+
* Asked to supply photos when the user presses the add-photo control.
|
|
254
|
+
*
|
|
255
|
+
* The library never touches the gallery: permissions, the picker and any
|
|
256
|
+
* HEIC/HEIF transcoding stay with the host, which is what keeps it free of
|
|
257
|
+
* Expo and of native code. Resolve `null` or `[]` for cancellation, which
|
|
258
|
+
* adds nothing. Omit it and the control renders unavailable rather than
|
|
259
|
+
* throwing.
|
|
260
|
+
*
|
|
261
|
+
* @experimental
|
|
262
|
+
*/
|
|
263
|
+
requestImages?: () => Promise<readonly InsertableImage[] | null>;
|
|
264
|
+
}
|
|
265
|
+
/** Merge slot overrides over the defaults. Pure — unit tested. */
|
|
266
|
+
declare function mergeSlots(overrides?: Partial<EditorSlots>): EditorSlots;
|
|
267
|
+
/**
|
|
268
|
+
* The batteries-included editor. `<Editor profile={POST_PROFILE} … />` gives
|
|
269
|
+
* the full IG-style flow; every part is replaceable via `slots`.
|
|
270
|
+
*/
|
|
271
|
+
declare function Editor({ profile, plugins, source, intrinsic, initialDocument, onExport, onCancel, onError, exportSpec, slots, theme, stickers, requestImages, }: EditorProps): React__default.JSX.Element;
|
|
272
|
+
|
|
273
|
+
/** The toolbar IS profile.tools — rendered from the registry, never hardcoded. */
|
|
274
|
+
declare function ToolRail(): React__default.JSX.Element;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Built-in panels for the built-in tool ids. Pure — unit tested.
|
|
278
|
+
*
|
|
279
|
+
* `text` deliberately has no panel: the text tool is served by the full-screen
|
|
280
|
+
* composer slot, which owns its own controls. Leaving a panel here would open a
|
|
281
|
+
* second history batch alongside the composer's and render a bottom sheet
|
|
282
|
+
* underneath it.
|
|
283
|
+
*/
|
|
284
|
+
declare function resolvePanel(toolId: string | null, pluginPanel: unknown, builtins?: Record<string, ComponentType>): ComponentType<Partial<ToolPanelProps>> | null;
|
|
285
|
+
/** Renders the active tool's panel — plugin-provided Panel wins over built-ins. */
|
|
286
|
+
declare function ToolPanelHost(): React__default.JSX.Element | null;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The base photo — the bottom-most image layer.
|
|
290
|
+
*
|
|
291
|
+
* Unchanged, and deliberately still available: some capabilities are genuinely
|
|
292
|
+
* about *the base photo* rather than about whichever photo is selected — Story
|
|
293
|
+
* media placement and the derived background are computed from it. Panels that
|
|
294
|
+
* edit "the current image" use {@link useTargetImageLayer} instead.
|
|
295
|
+
*/
|
|
296
|
+
declare function useBaseImageLayer(): _pictor_sdk_core.Layer | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* The LUT filter strip: live thumbnails, tap to apply, second tap on the
|
|
299
|
+
* active filter reveals the intensity slider.
|
|
300
|
+
*/
|
|
301
|
+
declare function FilterStrip(): React__default.JSX.Element | null;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The image an open panel edits, **pinned for the whole session**.
|
|
305
|
+
*
|
|
306
|
+
* Resolved once, from the selection at the moment the panel mounts, and then
|
|
307
|
+
* held by id. `ToolPanelHost` mounts a fresh panel per tool activation, so a
|
|
308
|
+
* mount *is* a session, and closing and reopening resolves again.
|
|
309
|
+
*
|
|
310
|
+
* The pinning is the point. Re-resolving each render would let a selection
|
|
311
|
+
* change mid-drag redirect the next slider movement to a different photo — an
|
|
312
|
+
* edit landing silently on the wrong image, which is worse than an edit that
|
|
313
|
+
* does nothing. So when the target is deleted, locked or hidden mid-session
|
|
314
|
+
* this returns `null` and the panel disables its controls; it never quietly
|
|
315
|
+
* falls back to the base.
|
|
316
|
+
*
|
|
317
|
+
* `useBaseImageLayer` is unchanged and still means *the base photo*. Use it
|
|
318
|
+
* where a capability is genuinely base-only — Story media layout, the derived
|
|
319
|
+
* background — rather than by inertia.
|
|
320
|
+
*
|
|
321
|
+
* @experimental
|
|
322
|
+
*/
|
|
323
|
+
declare function useTargetImageLayer(): Layer | null;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The add-photo control: one press, one picker, one undo entry.
|
|
327
|
+
*
|
|
328
|
+
* Nothing happens on mount. Opening a picker because a panel appeared would
|
|
329
|
+
* take the choice away from the user and make a stray tool tap launch the
|
|
330
|
+
* gallery, so acquisition waits for an explicit press.
|
|
331
|
+
*
|
|
332
|
+
* All the asynchronous behaviour lives in `createAddPhotoController`, which is
|
|
333
|
+
* testable without mounting anything. This is the shell.
|
|
334
|
+
*
|
|
335
|
+
* @experimental
|
|
336
|
+
*/
|
|
337
|
+
declare function AddPhotoPanel(): React__default.JSX.Element;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Acquiring photos from the host, then inserting them.
|
|
341
|
+
*
|
|
342
|
+
* Deliberately **not** a React component. Acquisition is asynchronous and has
|
|
343
|
+
* to survive a re-render, refuse a second request while one is in flight, and
|
|
344
|
+
* do nothing at all if the panel is gone by the time the host answers — none of
|
|
345
|
+
* which is expressible as render logic, and all of which is worth testing
|
|
346
|
+
* without mounting anything.
|
|
347
|
+
*
|
|
348
|
+
* Acquisition is also kept separate from insertion so a host that wants neither
|
|
349
|
+
* this controller nor the default panel can call `createImageLayers` and
|
|
350
|
+
* `insertImageCommands` directly.
|
|
351
|
+
*
|
|
352
|
+
* @experimental
|
|
353
|
+
*/
|
|
354
|
+
interface AddPhotoControllerOptions {
|
|
355
|
+
/**
|
|
356
|
+
* Supplied by the host. Resolving `null` or `[]` means the user cancelled,
|
|
357
|
+
* which must add nothing — no command, no selection change, no history entry.
|
|
358
|
+
*/
|
|
359
|
+
requestImages?: () => Promise<readonly InsertableImage[] | null>;
|
|
360
|
+
dispatch: Dispatch;
|
|
361
|
+
select: (ids: string[]) => void;
|
|
362
|
+
/** Anything the host's picker throws, or a result that is not usable. */
|
|
363
|
+
onError?: (error: unknown) => void;
|
|
364
|
+
/** Presentation and transaction options forwarded to the core helpers. */
|
|
365
|
+
insert?: InsertImageOptions & InsertImageTransaction;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* A live add-photo session.
|
|
369
|
+
*
|
|
370
|
+
* @experimental
|
|
371
|
+
*/
|
|
372
|
+
interface AddPhotoController {
|
|
373
|
+
/** Ask the host for photos and insert whatever comes back. */
|
|
374
|
+
request: () => Promise<void>;
|
|
375
|
+
/** True while a request is in flight; a second press is ignored. */
|
|
376
|
+
isPending: () => boolean;
|
|
377
|
+
/**
|
|
378
|
+
* Whether the host supplied a way to ask at all.
|
|
379
|
+
*
|
|
380
|
+
* **Session-scoped.** Captured when the controller is created, which for the
|
|
381
|
+
* bundled panel means when it mounts. A host that adds `requestImages` while
|
|
382
|
+
* the panel is open sees it unavailable until the tool is reopened — the
|
|
383
|
+
* mount is the session boundary everywhere else in this panel too, and a
|
|
384
|
+
* control that becomes live mid-session is a stranger surprise than one that
|
|
385
|
+
* waits for the next open.
|
|
386
|
+
*/
|
|
387
|
+
isAvailable: () => boolean;
|
|
388
|
+
/** Called when the panel unmounts: a late result is then discarded. */
|
|
389
|
+
dispose: () => void;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The host resolved with something that is not a list of insertable images.
|
|
393
|
+
*
|
|
394
|
+
* Reported rather than coerced: guessing at a malformed result would insert
|
|
395
|
+
* layers the host did not mean to add.
|
|
396
|
+
*
|
|
397
|
+
* @experimental
|
|
398
|
+
*/
|
|
399
|
+
declare class InvalidImageResultError extends Error {
|
|
400
|
+
constructor();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Wire a host's picker to the core insertion helpers.
|
|
404
|
+
*
|
|
405
|
+
* Acquisition stays separate from insertion, so a host that wants neither this
|
|
406
|
+
* controller nor the default panel can call `createImageLayers` and
|
|
407
|
+
* `insertImageCommands` itself.
|
|
408
|
+
*
|
|
409
|
+
* @experimental
|
|
410
|
+
*/
|
|
411
|
+
declare function createAddPhotoController({ requestImages, dispatch, select, onError, insert, }: AddPhotoControllerOptions): AddPhotoController;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Callbacks the host supplies to `<Editor>` that panels deep in the tree need.
|
|
415
|
+
*
|
|
416
|
+
* Held in a **ref** rather than state. A callback prop is usually an inline
|
|
417
|
+
* arrow recreated every render, so storing it in state would notify on every
|
|
418
|
+
* render and re-render forever; consumers read `.current` at call time instead
|
|
419
|
+
* and never re-render because the host re-created its closure.
|
|
420
|
+
*
|
|
421
|
+
* @experimental
|
|
422
|
+
*/
|
|
423
|
+
interface HostCallbacks {
|
|
424
|
+
/**
|
|
425
|
+
* Asked to supply photos when the user presses the add-photo control.
|
|
426
|
+
*
|
|
427
|
+
* The library never touches the gallery itself: permissions, the picker and
|
|
428
|
+
* any HEIC/HEIF transcoding are the host's, which is what keeps the library
|
|
429
|
+
* free of Expo and of native code. Resolve `null` or `[]` for cancellation.
|
|
430
|
+
*/
|
|
431
|
+
requestImages?: () => Promise<readonly InsertableImage[] | null>;
|
|
432
|
+
/** Where a failed request is reported. */
|
|
433
|
+
onError?: (error: unknown) => void;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Makes the host's callbacks reachable from panels deep in the tree.
|
|
437
|
+
*
|
|
438
|
+
* Mounted by `<Editor>`; a host composing its own editor from slots mounts it
|
|
439
|
+
* itself, or omits it and the controls render unavailable.
|
|
440
|
+
*
|
|
441
|
+
* @experimental
|
|
442
|
+
*/
|
|
443
|
+
declare function HostCallbacksProvider({ value, children, }: {
|
|
444
|
+
value: HostCallbacks;
|
|
445
|
+
children?: React__default.ReactNode;
|
|
446
|
+
}): React__default.JSX.Element;
|
|
447
|
+
/**
|
|
448
|
+
* The host's callbacks, as a stable ref.
|
|
449
|
+
*
|
|
450
|
+
* Returns an empty set outside an `<Editor>` rather than throwing — a panel
|
|
451
|
+
* rendered standalone should degrade to "unavailable", not crash.
|
|
452
|
+
*
|
|
453
|
+
* @experimental
|
|
454
|
+
*/
|
|
455
|
+
declare function useHostCallbacks(): {
|
|
456
|
+
current: HostCallbacks;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The Adjust tab: the 13 tools from profile/registry order, one centred
|
|
461
|
+
* slider with a detent at 0 (a 0 adjustment is a provable render no-op).
|
|
462
|
+
*/
|
|
463
|
+
declare function AdjustPanel(): React__default.JSX.Element | null;
|
|
464
|
+
|
|
465
|
+
/** Crop tool: aspect presets from the profile, 90° rotate, straighten dial. */
|
|
466
|
+
declare function CropPanel(): React__default.JSX.Element | null;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Pure crop math for the Crop tool (unit-tested in Node).
|
|
470
|
+
*
|
|
471
|
+
* Aspect selection: the document canvas takes the chosen aspect (constant
|
|
472
|
+
* width) and the image layer gets a maximal centred crop matching it.
|
|
473
|
+
*/
|
|
474
|
+
declare function canvasSizeForAspect(current: Size, aspect: number | 'free'): Size;
|
|
475
|
+
/** Maximal centred crop (normalized within the source) with the given aspect. */
|
|
476
|
+
declare function centeredCropForAspect(intrinsic: Size, aspect: number): Rect;
|
|
477
|
+
/** The straighten dial's range: ±45°, in radians. */
|
|
478
|
+
declare const STRAIGHTEN_LIMIT: number;
|
|
479
|
+
/** Straighten dial value (-1..1) ↔ radians (±45°). */
|
|
480
|
+
declare function straightenToRadians(value: number): number;
|
|
481
|
+
/** Radians to dial value (-1..1), clamped to the limit. */
|
|
482
|
+
declare function radiansToStraighten(rad: number): number;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A slider's value and range.
|
|
486
|
+
*
|
|
487
|
+
* `centered` is the adjustment shape — `-1..1` resting in the middle with a
|
|
488
|
+
* detent at zero. A quantity with no meaningful negative (Lux, tint strength,
|
|
489
|
+
* blur) uses `min: 0` and `centered: false` instead, which is the control shape
|
|
490
|
+
* measured on the reference editor.
|
|
491
|
+
*/
|
|
492
|
+
interface SliderProps {
|
|
493
|
+
/** -1..1 when centered, 0..1 otherwise */
|
|
494
|
+
value: number;
|
|
495
|
+
min?: number;
|
|
496
|
+
max?: number;
|
|
497
|
+
/** show a centre detent and snap to 0 within ±0.05 (adjustment sliders) */
|
|
498
|
+
centered?: boolean;
|
|
499
|
+
onStart?: () => void;
|
|
500
|
+
onChange: (value: number) => void;
|
|
501
|
+
onEnd?: () => void;
|
|
502
|
+
testID?: string;
|
|
503
|
+
/** Floating value label shown above the thumb. */
|
|
504
|
+
valueLabel?: string;
|
|
505
|
+
/**
|
|
506
|
+
* Inert and dimmed. For a control whose value has nothing meaningful to
|
|
507
|
+
* write yet — dragging it would record something untrue.
|
|
508
|
+
*/
|
|
509
|
+
disabled?: boolean;
|
|
510
|
+
}
|
|
511
|
+
/** Dependency-free horizontal slider with an optional centre detent. */
|
|
512
|
+
declare function Slider({ value, min, max, centered, onStart, onChange, onEnd, testID, valueLabel, disabled, }: SliderProps): React__default.JSX.Element;
|
|
513
|
+
|
|
514
|
+
/** The straighten dial: a value in -1..1 and the callbacks bracketing a drag. */
|
|
515
|
+
interface StraightenRulerProps {
|
|
516
|
+
value: number;
|
|
517
|
+
min?: number;
|
|
518
|
+
max?: number;
|
|
519
|
+
onStart?: () => void;
|
|
520
|
+
onChange(value: number): void;
|
|
521
|
+
onEnd?: () => void;
|
|
522
|
+
}
|
|
523
|
+
/** Fine, zero-snapping ruler for angle correction. Values are degrees. */
|
|
524
|
+
declare function StraightenRuler({ value, min, max, onStart, onChange, onEnd }: StraightenRulerProps): React__default.JSX.Element;
|
|
525
|
+
|
|
526
|
+
/** The fitted canvas's size in dp, for turning touches into document units. */
|
|
527
|
+
interface GestureMetrics {
|
|
528
|
+
/** canvas size in dp (the fitted preview) */
|
|
529
|
+
width: number;
|
|
530
|
+
height: number;
|
|
531
|
+
}
|
|
532
|
+
/** Emitted the moment a gesture snaps — the hook for haptics. */
|
|
533
|
+
interface GestureSnapEvent {
|
|
534
|
+
kind: 'rotation' | 'centerX' | 'centerY';
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Host callbacks a gesture controller may fire.
|
|
538
|
+
*
|
|
539
|
+
* Callbacks rather than behaviour: haptics would mean a native dependency, so
|
|
540
|
+
* the library reports the moment and the host decides what it means.
|
|
541
|
+
*/
|
|
542
|
+
interface GestureControllerOptions {
|
|
543
|
+
/** e.g. trigger haptics — optional callback, no haptics dependency here. */
|
|
544
|
+
onSnap?: (event: GestureSnapEvent) => void;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Pure gesture → command translation, shared by the RN gesture wiring and
|
|
548
|
+
* unit tests. Pan/pinch/rotate report cumulative deltas; the controller
|
|
549
|
+
* recomputes the transform from the base captured at gesture start and
|
|
550
|
+
* commits through a history batch so the whole gesture is ONE undo step.
|
|
551
|
+
*
|
|
552
|
+
* DECISION: shared values give 60fps feedback on the UI thread; document
|
|
553
|
+
* commits hop to JS per frame (cheap — one reducer pass at preview scale)
|
|
554
|
+
* inside the batch. True UI-thread document mutation would need a worklet
|
|
555
|
+
* store and is out of scope for v0.1.
|
|
556
|
+
*/
|
|
557
|
+
declare function createGestureController(store: Pick<EditorStore, 'getState' | 'dispatch' | 'select' | 'registry'>, getMetrics: () => GestureMetrics | null, options?: GestureControllerOptions): {
|
|
558
|
+
/** Pan/pinch/rotate lifecycles — call begin/end once per recognizer. */
|
|
559
|
+
begin: () => void;
|
|
560
|
+
end: () => void;
|
|
561
|
+
setPan(xDp: number, yDp: number): void;
|
|
562
|
+
setPinch(scale: number): void;
|
|
563
|
+
setRotate(rad: number): void;
|
|
564
|
+
/** Tap in canvas dp → hit-test → select (empty tap clears selection). */
|
|
565
|
+
tap(xDp: number, yDp: number): void;
|
|
566
|
+
/** true while a gesture is in flight (used to mute other UI). */
|
|
567
|
+
isActive: () => boolean;
|
|
568
|
+
};
|
|
569
|
+
/** Turns raw gesture input into layer transforms, with snapping. */
|
|
570
|
+
type GestureController = ReturnType<typeof createGestureController>;
|
|
571
|
+
|
|
572
|
+
/** Overrides forwarded to whichever gesture surface the active tool mounts. */
|
|
573
|
+
interface EditorGesturesProps {
|
|
574
|
+
/** Optional haptic hook — fired once per snap engagement. */
|
|
575
|
+
onSnap?: (event: GestureSnapEvent) => void;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Invisible gesture surface for <EditorCanvas children={…}/>: tap to select,
|
|
579
|
+
* pan/pinch/rotate the selected layer. Recognizers run simultaneously on the
|
|
580
|
+
* UI thread; commits are batched into a single undo step (see controller).
|
|
581
|
+
*/
|
|
582
|
+
declare function EditorGestures({ onSnap }: EditorGesturesProps): React__default.JSX.Element;
|
|
583
|
+
|
|
584
|
+
/** Overrides for overlay-layer gestures, defaulting to the profile's settings. */
|
|
585
|
+
interface LayerGestureSurfaceProps {
|
|
586
|
+
/** Fired once per snap engagement — hook up haptics here if you want them. */
|
|
587
|
+
onSnap?: (event: GestureSnapEvent) => void;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Direct manipulation of overlay layers: grab the top-most layer under the
|
|
591
|
+
* finger and pan, focal-pinch and rotate it on the UI thread.
|
|
592
|
+
*
|
|
593
|
+
* Three things make this feel right, and each is covered by tests:
|
|
594
|
+
*
|
|
595
|
+
* - **Focal composition.** Scale and rotation happen about the gesture
|
|
596
|
+
* centroid, so the document point under the fingers stays under them.
|
|
597
|
+
* - **Pointer-count re-basing.** Gesture Handler restarts its cumulative
|
|
598
|
+
* translation/scale/rotation when a finger lands or lifts. Every recognizer
|
|
599
|
+
* value is read relative to an origin captured at the last re-base, and the
|
|
600
|
+
* frame re-bases from the live transform whenever the pointer count changes
|
|
601
|
+
* — so two fingers going to one does not jump.
|
|
602
|
+
* - **One history entry.** The document is written exactly once, when the
|
|
603
|
+
* gesture settles, and not at all if nothing actually moved.
|
|
604
|
+
*/
|
|
605
|
+
declare function LayerGestureSurface({ onSnap }: LayerGestureSurfaceProps): React__default.JSX.Element;
|
|
606
|
+
|
|
607
|
+
/** The centre and rotation guides shown while a layer snaps. */
|
|
608
|
+
interface InteractionGuidesProps {
|
|
609
|
+
/** Label shown above the delete target. */
|
|
610
|
+
deleteLabel?: string;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Alignment guides and the drag-to-delete target.
|
|
614
|
+
*
|
|
615
|
+
* DECISION: these are plain React Native views layered over `<EditorCanvas>`,
|
|
616
|
+
* not Skia nodes inside `renderDocument`. Transient interaction chrome then
|
|
617
|
+
* has no way of reaching an export — stronger than an `isExport` check — and
|
|
618
|
+
* the single render path (invariant 1) stays untouched.
|
|
619
|
+
*
|
|
620
|
+
* Render as a child of `<EditorCanvas>`, which sizes its children to the
|
|
621
|
+
* fitted document box, so percentage offsets here are document-relative.
|
|
622
|
+
*/
|
|
623
|
+
declare function InteractionGuides({ deleteLabel }: InteractionGuidesProps): React__default.JSX.Element | null;
|
|
624
|
+
|
|
625
|
+
/** Overrides for base-media gestures, defaulting to the profile's settings. */
|
|
626
|
+
interface StoryMediaGestureSurfaceProps {
|
|
627
|
+
/** Fired when a media gesture starts and ends — for host haptics/chrome. */
|
|
628
|
+
onInteractionChange?: (active: boolean) => void;
|
|
629
|
+
}
|
|
630
|
+
/** Find the document's base media — the bottom-most layer, if it is an image. */
|
|
631
|
+
declare function useBaseMediaLayer(): Layer | undefined;
|
|
632
|
+
/**
|
|
633
|
+
* Direct manipulation of the Story base photo: one-finger reposition, focal
|
|
634
|
+
* pinch and rotation, held inside rotation-aware pan bounds with rubber-band
|
|
635
|
+
* resistance and a bounded settle.
|
|
636
|
+
*
|
|
637
|
+
* Mounted only while the `media` tool is active, so it cannot compete with
|
|
638
|
+
* overlay or drawing gestures. The transform is written to the document once
|
|
639
|
+
* per settled gesture; because the tool session already opened a history batch
|
|
640
|
+
* (see `ToolPanelHost`), a whole session of adjustments collapses into one undo
|
|
641
|
+
* entry, and a session that changed nothing records none.
|
|
642
|
+
*/
|
|
643
|
+
declare function StoryMediaGestureSurface({ onInteractionChange, }: StoryMediaGestureSurfaceProps): React__default.JSX.Element | null;
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Which gesture surface owns the canvas.
|
|
647
|
+
*
|
|
648
|
+
* Precedence is decided by the active tool and nothing else, so it is a pure
|
|
649
|
+
* function that can be tested without mounting anything — and so the rule is
|
|
650
|
+
* stated in one place rather than implied by JSX nesting.
|
|
651
|
+
*/
|
|
652
|
+
type GestureSurfaceKind = 'media' | 'draw' | 'crop' | 'tilt' | 'text' | 'overlay';
|
|
653
|
+
/**
|
|
654
|
+
* Resolve the surface for the active tool.
|
|
655
|
+
*
|
|
656
|
+
* - `text` — the full-screen composer owns the screen, so **no** canvas surface
|
|
657
|
+
* is mounted at all. Not mounting is the strongest form of "disabled": there
|
|
658
|
+
* is no recogniser to arbitrate with, so a tap meant for the caret cannot
|
|
659
|
+
* start a canvas gesture, and a drag begins immediately once composing ends.
|
|
660
|
+
* - `media` — the base photo tool owns the surface, so the photo moves and
|
|
661
|
+
* overlays are inert.
|
|
662
|
+
* - `draw` — drawing owns the surface, so strokes are never interpreted as
|
|
663
|
+
* layer drags.
|
|
664
|
+
* - `crop` — the Post crop surface.
|
|
665
|
+
* - `tilt` — placing the selective-focus region, which needs the whole canvas;
|
|
666
|
+
* overlay hit-testing would steal the drag to whatever sits under the finger.
|
|
667
|
+
* - anything else (including no active tool) — overlay hit-testing, which
|
|
668
|
+
* excludes the base photo, so dragging text or a sticker cannot move it.
|
|
669
|
+
*/
|
|
670
|
+
declare function resolveGestureSurface(activeTool: string | null | undefined): GestureSurfaceKind;
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* The base-photo tool panel.
|
|
674
|
+
*
|
|
675
|
+
* It is deliberately thin: the gestures live on the canvas surface, and the
|
|
676
|
+
* Cancel/Done transaction comes from `ToolPanelHost`, which opens a history
|
|
677
|
+
* batch for any tool with a panel. The panel only offers the affordances that
|
|
678
|
+
* are not gestures — a reset to the initial fit — and explains the interaction.
|
|
679
|
+
*/
|
|
680
|
+
declare function MediaPanel(): React__default.JSX.Element;
|
|
681
|
+
|
|
682
|
+
/** Overrides for crop gestures, defaulting to the profile's interaction settings. */
|
|
683
|
+
interface CropGestureSurfaceProps {
|
|
684
|
+
/** Override profile crop fit for this surface. */
|
|
685
|
+
fit?: 'contain' | 'cover';
|
|
686
|
+
maxScale?: number;
|
|
687
|
+
/** Enable free two-finger rotation; disabled by the default Post profile. */
|
|
688
|
+
rotationEnabled?: boolean;
|
|
689
|
+
/** Optional host haptic/motion callback. */
|
|
690
|
+
onInteractionChange?: (active: boolean) => void;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Fixed-viewport photo crop gestures. Pan and focal-point pinch run as shared
|
|
694
|
+
* values on the UI thread; one normalized transform is committed on release.
|
|
695
|
+
*/
|
|
696
|
+
declare function CropGestureSurface({ fit, maxScale, rotationEnabled, onInteractionChange }: CropGestureSurfaceProps): React__default.JSX.Element | null;
|
|
697
|
+
|
|
698
|
+
/** Rule-of-thirds overlay shown only while crop interaction is active. */
|
|
699
|
+
declare function CropGrid({ always }: {
|
|
700
|
+
always?: boolean;
|
|
701
|
+
}): React__default.JSX.Element | null;
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Transient interaction state shared between the gesture surfaces and the
|
|
705
|
+
* chrome. None of it is part of the document: it never reaches history,
|
|
706
|
+
* serialization or export.
|
|
707
|
+
*
|
|
708
|
+
* Per-frame transforms stay on the UI thread as shared values (`previews`).
|
|
709
|
+
* Only the discrete flags below cross to the JS thread, and only when they
|
|
710
|
+
* actually change — a drag starting, a guide engaging, the delete target being
|
|
711
|
+
* entered — so the render path is untouched while fingers move.
|
|
712
|
+
*/
|
|
713
|
+
interface InteractionState {
|
|
714
|
+
previews: readonly PreviewLayerTransform[];
|
|
715
|
+
setPreview(preview: PreviewLayerTransform | null): void;
|
|
716
|
+
cropActive: boolean;
|
|
717
|
+
setCropActive(active: boolean): void;
|
|
718
|
+
/** True while a layer is being dragged/scaled/rotated. */
|
|
719
|
+
layerDragActive: boolean;
|
|
720
|
+
setLayerDragActive(active: boolean): void;
|
|
721
|
+
/** True while the dragged layer is over the delete target. */
|
|
722
|
+
overDeleteTarget: boolean;
|
|
723
|
+
setOverDeleteTarget(over: boolean): void;
|
|
724
|
+
/** Which alignment guides are currently engaged. */
|
|
725
|
+
snap: LayerSnapState;
|
|
726
|
+
setSnap(snap: LayerSnapState): void;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Shares the in-flight interaction between the gesture surfaces and the canvas.
|
|
730
|
+
*
|
|
731
|
+
* How a live preview transform reaches the renderer without going through the
|
|
732
|
+
* document — which would mean dispatching every frame.
|
|
733
|
+
*/
|
|
734
|
+
declare function InteractionProvider({ children }: {
|
|
735
|
+
children?: React__default.ReactNode;
|
|
736
|
+
}): React__default.JSX.Element;
|
|
737
|
+
/** The current interaction. Throws outside an `<Editor>`, where there is none. */
|
|
738
|
+
declare function useInteractionState(): InteractionState;
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Text tab: add a text layer, then edit the selected one — content, font,
|
|
742
|
+
* size, color, alignment, and the fill treatment (solid / per-line
|
|
743
|
+
* @experimental). Editing happens in the panel for v0.2; DECISION:
|
|
744
|
+
* on-canvas edit-in-place needs a native text-input overlay pass, deferred.
|
|
745
|
+
*/
|
|
746
|
+
declare function TextPanel(): React__default.JSX.Element;
|
|
747
|
+
|
|
748
|
+
/** Draw tab: brush picker, color swatches + eyedropper, width slider. */
|
|
749
|
+
declare function DrawPanel(): React__default.JSX.Element;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Z-order UI: top-most first, tap to select, reorder and delete.
|
|
753
|
+
* (Drag-to-delete on canvas maps to the same layer/remove command.)
|
|
754
|
+
*/
|
|
755
|
+
declare function LayersPanel(): React__default.JSX.Element;
|
|
756
|
+
|
|
757
|
+
/** Layout grids: snap the image layers into the chosen arrangement. */
|
|
758
|
+
declare function LayoutPanel(): React__default.JSX.Element;
|
|
759
|
+
|
|
760
|
+
/** The bundled multi-photo arrangements, by cell count and split direction. */
|
|
761
|
+
type LayoutPreset = '1' | '2v' | '2h' | '3v' | '4grid';
|
|
762
|
+
/** One slot in a layout: where its photo sits, in normalized document units. */
|
|
763
|
+
interface LayoutCell {
|
|
764
|
+
/** centre + scale in normalized document units */
|
|
765
|
+
x: number;
|
|
766
|
+
y: number;
|
|
767
|
+
/** width of the cell as a fraction of document width */
|
|
768
|
+
scale: number;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Story layout grids: cell centres and scales for placing the first N image
|
|
772
|
+
* layers. Pure — unit tested. Cells are defined as document fractions so any
|
|
773
|
+
* canvas size works.
|
|
774
|
+
*/
|
|
775
|
+
declare function layoutCells(preset: LayoutPreset, _doc: Size): LayoutCell[];
|
|
776
|
+
/** The transform patch that moves a layer into a cell, leaving its other fields alone. */
|
|
777
|
+
declare function cellToTransformPatch(cell: LayoutCell): Partial<Transform>;
|
|
778
|
+
|
|
779
|
+
/** The active brush: kind, colour and width. Width is a fraction of document width. */
|
|
780
|
+
interface BrushConfig {
|
|
781
|
+
brush: Stroke['brush'];
|
|
782
|
+
color: string;
|
|
783
|
+
/** 0..1 of document width */
|
|
784
|
+
width: number;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Pure drawing controller: converts a pan gesture into a stroke on the
|
|
788
|
+
* top-most draw layer (created on first use), committed as ONE undo step.
|
|
789
|
+
* Live feedback comes from patching the stroke into the document on every
|
|
790
|
+
* move inside the history batch.
|
|
791
|
+
*/
|
|
792
|
+
declare function createDrawController(store: Pick<EditorStore, 'getState' | 'dispatch'>, getMetrics: () => GestureMetrics | null, getBrush: () => BrushConfig): {
|
|
793
|
+
begin(xDp: number, yDp: number): void;
|
|
794
|
+
move(xDp: number, yDp: number): void;
|
|
795
|
+
end(): void;
|
|
796
|
+
isDrawing: () => boolean;
|
|
797
|
+
};
|
|
798
|
+
/** Accumulates touch points into strokes and commits them to the document. */
|
|
799
|
+
type DrawController = ReturnType<typeof createDrawController>;
|
|
800
|
+
|
|
801
|
+
/** The brush a drawing session starts with. */
|
|
802
|
+
declare const DEFAULT_BRUSH: BrushConfig;
|
|
803
|
+
/**
|
|
804
|
+
* Freehand drawing capture, active while the draw tool is open. In
|
|
805
|
+
* eyedropper mode a tap samples the rendered pixel color into the brush.
|
|
806
|
+
*/
|
|
807
|
+
declare function DrawingSurface(): React__default.JSX.Element;
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* A tiny shared key/value store for panel ↔ canvas coordination (brush
|
|
811
|
+
* config, sticker catalogs, eyedropper mode). Ephemeral — never touches the
|
|
812
|
+
* document or history.
|
|
813
|
+
*/
|
|
814
|
+
interface UiStateStore {
|
|
815
|
+
get<T>(key: string, fallback: T): T;
|
|
816
|
+
set(key: string, value: unknown): void;
|
|
817
|
+
subscribe(listener: () => void): () => void;
|
|
818
|
+
}
|
|
819
|
+
/** A scratch store for UI state that is neither document nor history. */
|
|
820
|
+
declare function createUiStateStore(): UiStateStore;
|
|
821
|
+
/**
|
|
822
|
+
* Holds ephemeral panel state — which tab is open, which swatch is selected.
|
|
823
|
+
*
|
|
824
|
+
* Deliberately outside the document: none of it should be serialized, undoable,
|
|
825
|
+
* or survive the editor closing.
|
|
826
|
+
*/
|
|
827
|
+
declare function UiStateProvider({ children }: {
|
|
828
|
+
children?: React__default.ReactNode;
|
|
829
|
+
}): React__default.JSX.Element;
|
|
830
|
+
/** Read and write one key of the ephemeral UI state. Throws outside an `<Editor>`. */
|
|
831
|
+
declare function useUiState<T>(key: string, fallback: T): [T, (value: T) => void];
|
|
832
|
+
|
|
833
|
+
export { type AddPhotoController, type AddPhotoControllerOptions, AddPhotoPanel, AdjustPanel, type BrushConfig, CanvasStage, type CanvasStageProps, CropGestureSurface, type CropGestureSurfaceProps, CropGrid, CropPanel, DEFAULT_BRUSH, type DrawController, DrawPanel, DrawingSurface, Editor, EditorGestures, type EditorGesturesProps, type EditorProps, type EditorSlots, type EditorTheme, FilterStrip, type GestureController, type GestureControllerOptions, type GestureMetrics, type GestureSnapEvent, type GestureSurfaceKind, type HostCallbacks, HostCallbacksProvider, InteractionGuides, type InteractionGuidesProps, InteractionProvider, InvalidImageResultError, LayerGestureSurface, type LayerGestureSurfaceProps, LayersPanel, type LayoutCell, LayoutPanel, type LayoutPreset, MediaPanel, type PartialTheme, STRAIGHTEN_LIMIT, Slider, type SliderProps, type StickerAsset, StickerPanel, StoryMediaGestureSurface, type StoryMediaGestureSurfaceProps, StraightenRuler, type StraightenRulerProps, TextComposer, TextComposerHost, type TextComposerProps, TextPanel, type TextSessionController, ThemeContext, ToolPanelHost, ToolRail, TopBar, type TopBarProps, UiStateProvider, type UiStateStore, type UseTextSessionOptions, canvasSizeForAspect, cellToTransformPatch, centeredCropForAspect, createAddPhotoController, createDrawController, createGestureController, createUiStateStore, defaultTheme, layoutCells, mergeSlots, mergeTheme, radiansToStraighten, resolveGestureSurface, resolvePanel, straightenToRadians, useBaseImageLayer, useBaseMediaLayer, useHostCallbacks, useInteractionState, useTargetImageLayer, useTextSession, useTheme, useUiState };
|