@buoy-gg/shared-ui 7.0.31 → 7.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/lib/commonjs/JsModal.js +13 -6
  2. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  3. package/lib/commonjs/storage/devToolsStorageKeys.js +12 -0
  4. package/lib/commonjs/ui/components/EmptyState.js +10 -2
  5. package/lib/commonjs/ui/components/ModalHeader.js +5 -0
  6. package/lib/commonjs/ui/components/ToolEmptyState.js +154 -0
  7. package/lib/commonjs/ui/components/WindowControls.js +8 -5
  8. package/lib/commonjs/ui/components/index.js +26 -0
  9. package/lib/commonjs/ui/components/windowDrag.js +45 -0
  10. package/lib/commonjs/ui/index.js +12 -0
  11. package/lib/commonjs/ui/otto/OttoSprite.js +510 -0
  12. package/lib/commonjs/ui/otto/index.js +38 -0
  13. package/lib/commonjs/ui/otto/ottoEngine.js +2734 -0
  14. package/lib/commonjs/ui/otto/ottoSheet.js +191 -0
  15. package/lib/module/JsModal.js +13 -6
  16. package/lib/module/hooks/safe-area-impl.js +1 -1
  17. package/lib/module/storage/devToolsStorageKeys.js +12 -0
  18. package/lib/module/ui/components/EmptyState.js +10 -2
  19. package/lib/module/ui/components/ModalHeader.js +5 -0
  20. package/lib/module/ui/components/ToolEmptyState.js +149 -0
  21. package/lib/module/ui/components/WindowControls.js +8 -5
  22. package/lib/module/ui/components/index.js +4 -0
  23. package/lib/module/ui/components/windowDrag.js +41 -0
  24. package/lib/module/ui/index.js +4 -1
  25. package/lib/module/ui/otto/OttoSprite.js +496 -0
  26. package/lib/module/ui/otto/index.js +20 -0
  27. package/lib/module/ui/otto/ottoEngine.js +2726 -0
  28. package/lib/module/ui/otto/ottoSheet.js +180 -0
  29. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  31. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +12 -0
  32. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/components/EmptyState.d.ts +14 -1
  34. package/lib/typescript/commonjs/ui/components/EmptyState.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/ui/components/ModalHeader.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/ui/components/ToolEmptyState.d.ts +113 -0
  37. package/lib/typescript/commonjs/ui/components/ToolEmptyState.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts +7 -1
  39. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/ui/components/index.d.ts +3 -0
  41. package/lib/typescript/commonjs/ui/components/index.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/ui/components/windowDrag.d.ts +39 -0
  43. package/lib/typescript/commonjs/ui/components/windowDrag.d.ts.map +1 -0
  44. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  45. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/ui/otto/OttoSprite.d.ts +103 -0
  47. package/lib/typescript/commonjs/ui/otto/OttoSprite.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/ui/otto/index.d.ts +19 -0
  49. package/lib/typescript/commonjs/ui/otto/index.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts +2205 -0
  51. package/lib/typescript/commonjs/ui/otto/ottoEngine.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/ui/otto/ottoSheet.d.ts +73 -0
  53. package/lib/typescript/commonjs/ui/otto/ottoSheet.d.ts.map +1 -0
  54. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  55. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  56. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +12 -0
  57. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  58. package/lib/typescript/module/ui/components/EmptyState.d.ts +14 -1
  59. package/lib/typescript/module/ui/components/EmptyState.d.ts.map +1 -1
  60. package/lib/typescript/module/ui/components/ModalHeader.d.ts.map +1 -1
  61. package/lib/typescript/module/ui/components/ToolEmptyState.d.ts +113 -0
  62. package/lib/typescript/module/ui/components/ToolEmptyState.d.ts.map +1 -0
  63. package/lib/typescript/module/ui/components/WindowControls.d.ts +7 -1
  64. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  65. package/lib/typescript/module/ui/components/index.d.ts +3 -0
  66. package/lib/typescript/module/ui/components/index.d.ts.map +1 -1
  67. package/lib/typescript/module/ui/components/windowDrag.d.ts +39 -0
  68. package/lib/typescript/module/ui/components/windowDrag.d.ts.map +1 -0
  69. package/lib/typescript/module/ui/index.d.ts +1 -0
  70. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  71. package/lib/typescript/module/ui/otto/OttoSprite.d.ts +103 -0
  72. package/lib/typescript/module/ui/otto/OttoSprite.d.ts.map +1 -0
  73. package/lib/typescript/module/ui/otto/index.d.ts +19 -0
  74. package/lib/typescript/module/ui/otto/index.d.ts.map +1 -0
  75. package/lib/typescript/module/ui/otto/ottoEngine.d.ts +2205 -0
  76. package/lib/typescript/module/ui/otto/ottoEngine.d.ts.map +1 -0
  77. package/lib/typescript/module/ui/otto/ottoSheet.d.ts +73 -0
  78. package/lib/typescript/module/ui/otto/ottoSheet.d.ts.map +1 -0
  79. package/package.json +4 -4
@@ -0,0 +1,496 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Otto in React Native — core APIs only, so the same file runs in Expo, in a
5
+ * bare RN CLI app, and on the web through react-native-web.
6
+ *
7
+ * <Otto state="thinking" pixelSize={4} /> the shipping sprite
8
+ * <Otto state="reel" /> plays the demo schedule
9
+ * <OttoStill pixelSize={1} /> frame 0, no clock (icons, stills)
10
+ * <OttoPixels grid={…} pixelSize={…} /> paint any frame as Views
11
+ * useOttoAnchorMotion(state, …) move a prop in lock-step with
12
+ * Otto's hands (the tool scenes)
13
+ *
14
+ * How the shipping sprite works (winner of the buoy-optimize rounds, see
15
+ * OTTO_PERF_PLAN.md): each layer of a state's animation is a STRIP of its
16
+ * distinct frames, mounted once as static Views (merged rectangles), inside
17
+ * an overflow-hidden window one frame wide. A clock slides the strip:
18
+ *
19
+ * native core `Animated` on the native driver — one looping linear timing
20
+ * mapped to strip offsets by a step interpolation
21
+ * web CSS keyframes with `step-end` timing (react-native-web's
22
+ * `animationKeyframes` style), the zero-JS path in a browser
23
+ *
24
+ * Zero JS per tick on both, no React commits, and it keeps moving while the
25
+ * JS thread is blocked — exactly when a "working" Otto is on screen. On the
26
+ * iOS sim: 60 UI / 59 JS FPS at one sprite (the page's own floor) where the
27
+ * old per-tick renderer dropped UI frames.
28
+ *
29
+ * ONE TIMEBASE. Body, water and any anchored prop derive from the same
30
+ * `units` span (see `motionUnits`) at the same duration and start in the same
31
+ * commit, so they can never drift out of phase with each other.
32
+ */
33
+ import { useEffect, useRef, useState } from "react";
34
+ import { Animated, Easing, Platform, StyleSheet, View } from "react-native";
35
+ import { OTTO_ANCHORS, OTTO_GRID, OTTO_REEL, ottoRuns, renderOttoFrame } from "./ottoEngine.js";
36
+ import { WATER_FRAMES, WATER_ROW0, WATER_ROWS, ottoSheets } from "./ottoSheet.js";
37
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
38
+ const IS_WEB = Platform.OS === "web";
39
+
40
+ /** Every anchor in the engine repeats within 16 ticks. */
41
+ const ANCHOR_PERIOD = 16;
42
+ /** The water wave holds each frame for 2 ticks. */
43
+ const WATER_UNITS_PER_STEP = 2;
44
+ function gcd(a, b) {
45
+ return b === 0 ? a : gcd(b, a % b);
46
+ }
47
+ function lcm(a, b) {
48
+ return a * b / gcd(a, b);
49
+ }
50
+
51
+ /**
52
+ * The tick span every layer of a state loops over. It is a common multiple of
53
+ * the body cycle and the anchor cycle, so a prop held in Otto's hand and the
54
+ * hand itself repeat together.
55
+ */
56
+ export function motionUnits(state, keyline = true) {
57
+ return lcm(ottoSheets(state, keyline).period, ANCHOR_PERIOD);
58
+ }
59
+
60
+ // ---------------------------------------------------------------- pixels --
61
+
62
+ /** Pure renderer: a frame → coloured run Views. Fine for stills; costly per tick. */
63
+ export function OttoPixels({
64
+ grid,
65
+ pixelSize,
66
+ style
67
+ }) {
68
+ const side = OTTO_GRID * pixelSize;
69
+ const runs = ottoRuns(grid);
70
+ const spans = [];
71
+ let rowY = -1;
72
+ let rowIndex = 0;
73
+ for (const r of runs) {
74
+ if (r.y !== rowY) {
75
+ rowY = r.y;
76
+ rowIndex = 0;
77
+ }
78
+ spans.push(/*#__PURE__*/_jsx(View, {
79
+ style: {
80
+ position: "absolute",
81
+ left: r.x * pixelSize,
82
+ top: r.y * pixelSize,
83
+ width: r.w * pixelSize,
84
+ height: pixelSize,
85
+ backgroundColor: r.color
86
+ }
87
+ }, `${r.y}:${rowIndex++}`));
88
+ }
89
+ return /*#__PURE__*/_jsx(View, {
90
+ pointerEvents: "none",
91
+ style: [{
92
+ width: side,
93
+ height: side
94
+ }, style],
95
+ children: spans
96
+ });
97
+ }
98
+
99
+ // ---------------------------------------------------------------- still ---
100
+
101
+ /** Frame 0 of a state, no clock — safe on screens that must not re-render. */
102
+ export function OttoStill({
103
+ state = "static",
104
+ pixelSize = 1,
105
+ water,
106
+ keyline,
107
+ style
108
+ }) {
109
+ const grid = renderOttoFrame(state, 0, {
110
+ water,
111
+ keyline
112
+ });
113
+ return /*#__PURE__*/_jsx(OttoPixels, {
114
+ grid: grid,
115
+ pixelSize: pixelSize,
116
+ style: style
117
+ });
118
+ }
119
+
120
+ // ---------------------------------------------------------------- reel ----
121
+
122
+ /**
123
+ * Walks the demo reel: which state plays now. One React state change per
124
+ * schedule entry (every few seconds), nothing per tick.
125
+ */
126
+ export function useOttoReel(enabled, tickMs = 140, active = true) {
127
+ const [idx, setIdx] = useState(0);
128
+ useEffect(() => {
129
+ if (!enabled || !active) return;
130
+ const len = OTTO_REEL[idx % OTTO_REEL.length][1];
131
+ const id = setTimeout(() => setIdx(i => (i + 1) % OTTO_REEL.length), len * tickMs);
132
+ return () => clearTimeout(id);
133
+ }, [enabled, active, idx, tickMs]);
134
+ return OTTO_REEL[idx % OTTO_REEL.length][0];
135
+ }
136
+
137
+ // ---------------------------------------------------------------- clock ---
138
+
139
+ /** A style that moves a view through offsets in lock-step with Otto's clock. */
140
+
141
+ const EPS = 0.0001;
142
+
143
+ /**
144
+ * The layer the clock moves.
145
+ *
146
+ * On native this has to be an `Animated.View` — it carries interpolated
147
+ * values. On web the motion is a COMPILED CSS class (see `webStepStyle`), and
148
+ * `Animated.View` flattens its style prop into a fresh object, which loses the
149
+ * identity react-native-web looks the compiled rules up by; the keyframes are
150
+ * then silently dropped and the sprite sits on frame 0. So the web path takes
151
+ * a plain View, which passes the class straight through.
152
+ */
153
+ const OttoMotionView = IS_WEB ? View : Animated.View;
154
+ export { OttoMotionView };
155
+
156
+ /**
157
+ * Web keyframes, compiled once per motion pattern.
158
+ *
159
+ * react-native-web only turns `animationKeyframes` into real `@keyframes`
160
+ * rules through `StyleSheet.create` — its inline-style path drops the prop
161
+ * without a warning. Patterns repeat heavily across a page of sprites (every
162
+ * Otto shares one water wave, every state of a group shares body cycles), so
163
+ * the cache keeps the sheet small and the render cheap.
164
+ */
165
+ const webStyleCache = new Map();
166
+ function webStepStyle(offsets, unitsPerStep, units, tickMs, steps, at) {
167
+ const key = `${units}/${unitsPerStep}/${tickMs}/${offsets.map(o => `${o.x},${o.y}`).join("|")}`;
168
+ const cached = webStyleCache.get(key);
169
+ if (cached) return cached;
170
+
171
+ // Keyframe blocks skip react-native-web's style preprocessor, so the value
172
+ // has to be CSS already — an RN transform ARRAY compiles to an empty block.
173
+ const css = o => `translateX(${o.x}px) translateY(${o.y}px)`;
174
+ const keyframes = {};
175
+ for (let k = 0; k < steps; k++) {
176
+ const pct = k * unitsPerStep / units * 100;
177
+ keyframes[`${pct.toFixed(4)}%`] = {
178
+ transform: css(at(k))
179
+ };
180
+ }
181
+ keyframes["100%"] = {
182
+ transform: css(at(steps - 1))
183
+ };
184
+
185
+ // react-native-web's CSS-animation style props (not part of RN's ViewStyle).
186
+ const style = StyleSheet.create({
187
+ motion: {
188
+ animationKeyframes: [keyframes],
189
+ animationDuration: `${units * tickMs}ms`,
190
+ animationTimingFunction: "step-end",
191
+ animationIterationCount: "infinite",
192
+ animationFillMode: "both"
193
+ }
194
+ }).motion;
195
+ webStyleCache.set(key, style);
196
+ return style;
197
+ }
198
+
199
+ /**
200
+ * Otto's timebase: a clock that runs `units` ticks and loops.
201
+ *
202
+ * native → one `Animated.Value` on the native driver (zero JS per tick)
203
+ * web → nothing to drive; CSS keyframes carry the motion (zero JS at all)
204
+ *
205
+ * Every consumer of the SAME state builds the same `units` and duration and
206
+ * starts in the same commit, which is what keeps layers in phase.
207
+ */
208
+ function useOttoMotionClock(units, tickMs, active, restartKey) {
209
+ const value = useRef(new Animated.Value(0)).current;
210
+ useEffect(() => {
211
+ if (IS_WEB) return;
212
+ value.setValue(0);
213
+ if (!active) return;
214
+ const loop = Animated.loop(Animated.timing(value, {
215
+ toValue: units,
216
+ duration: units * tickMs,
217
+ easing: Easing.linear,
218
+ useNativeDriver: true,
219
+ isInteraction: false
220
+ }), {
221
+ resetBeforeIteration: true
222
+ });
223
+ loop.start();
224
+ return () => loop.stop();
225
+ }, [value, units, tickMs, active, restartKey]);
226
+ return value;
227
+ }
228
+
229
+ /**
230
+ * Turn a repeating offset pattern into a style on that clock. `offsets[k]` is
231
+ * where the view sits during tick `k·unitsPerStep`; the pattern repeats until
232
+ * `units` is filled, so patterns of different lengths share one timebase.
233
+ */
234
+ function stepStyle(value, offsets, unitsPerStep, units, tickMs, active) {
235
+ const steps = Math.max(1, Math.round(units / unitsPerStep));
236
+ const at = k => offsets[k % offsets.length] ?? {
237
+ x: 0,
238
+ y: 0
239
+ };
240
+ if (!active || offsets.length <= 1) {
241
+ const o = at(0);
242
+ return {
243
+ transform: [{
244
+ translateX: o.x
245
+ }, {
246
+ translateY: o.y
247
+ }]
248
+ };
249
+ }
250
+ if (IS_WEB) return webStepStyle(offsets, unitsPerStep, units, tickMs, steps, at);
251
+ const inputRange = [];
252
+ const xs = [];
253
+ const ys = [];
254
+ for (let k = 0; k < steps; k++) {
255
+ const o = at(k);
256
+ inputRange.push(k * unitsPerStep, (k + 1) * unitsPerStep - EPS);
257
+ xs.push(o.x, o.x);
258
+ ys.push(o.y, o.y);
259
+ }
260
+ return {
261
+ transform: [{
262
+ translateX: value.interpolate({
263
+ inputRange,
264
+ outputRange: xs,
265
+ extrapolate: "clamp"
266
+ })
267
+ }, {
268
+ translateY: value.interpolate({
269
+ inputRange,
270
+ outputRange: ys,
271
+ extrapolate: "clamp"
272
+ })
273
+ }]
274
+ };
275
+ }
276
+ /**
277
+ * Move a prop in lock-step with Otto's hands. `OTTO_ANCHORS[state]` publishes
278
+ * where the hand is each tick; this turns that into the same kind of clocked
279
+ * style the sprite itself uses, on the same timebase, so the two can't drift.
280
+ *
281
+ * Returns `null` for a state that has no anchor.
282
+ */
283
+ export function useOttoAnchorMotion(state, pixelSize, tickMs = 140, active = true, keyline = true) {
284
+ const anchorFn = OTTO_ANCHORS[state];
285
+ const units = motionUnits(state, keyline);
286
+ const value = useOttoMotionClock(units, tickMs, active && !!anchorFn, state);
287
+ if (!anchorFn) return null;
288
+ const first = anchorFn(0);
289
+ const offsets = [];
290
+ for (let t = 0; t < ANCHOR_PERIOD; t++) {
291
+ const a = anchorFn(t);
292
+ offsets.push({
293
+ x: (a.x - first.x) * pixelSize,
294
+ y: (a.y - first.y) * pixelSize
295
+ });
296
+ }
297
+ return {
298
+ style: stepStyle(value, offsets, 1, units, tickMs, active),
299
+ over: first.over,
300
+ origin: {
301
+ x: first.x,
302
+ y: first.y
303
+ }
304
+ };
305
+ }
306
+
307
+ // ---------------------------------------------------------------- otto ----
308
+
309
+ /**
310
+ * The shipping sprite.
311
+ *
312
+ * Mounting is deferred on purpose. The strips are ~500 static Views for a
313
+ * typical state, and creating those is native COMMIT cost: measured at 128 ms
314
+ * for one sprite, against 28 ms for a still. A mascot almost always appears
315
+ * while something else is animating (a panel opening, a list settling), and
316
+ * 128 ms of commit dropped into that is a visible hitch.
317
+ *
318
+ * So the first commit paints `OttoStill` — frame 0, ~36 Views, the exact
319
+ * pixels the animation starts on — and the strips mount once the interaction
320
+ * has settled (an idle callback). Nothing pops, because the still IS frame 0.
321
+ * Pass `eager` when the sprite is the thing the user is waiting for.
322
+ *
323
+ * The two layers are siblings that each gate themselves with `return null`
324
+ * rather than a ternary swap, so this component's child slots never change
325
+ * shape (Fabric view-index safety).
326
+ */
327
+ export function Otto({
328
+ state,
329
+ pixelSize = 8,
330
+ water = true,
331
+ keyline = true,
332
+ tickMs = 140,
333
+ active = true,
334
+ eager = false,
335
+ style
336
+ }) {
337
+ const reelState = useOttoReel(state === "reel", tickMs, active);
338
+ const resolved = state === "reel" ? reelState : state;
339
+ const side = OTTO_GRID * pixelSize;
340
+ const [mounted, setMounted] = useState(eager);
341
+ useEffect(() => {
342
+ if (eager || mounted) return;
343
+ // requestIdleCallback, not InteractionManager: RN 0.85 deprecates the
344
+ // latter and warns on every use, and this runs in every host we support.
345
+ // The setTimeout fallback covers runtimes without the idle callback.
346
+ const idle = globalThis.requestIdleCallback;
347
+ const cancelIdle = globalThis.cancelIdleCallback;
348
+ if (idle) {
349
+ const handle = idle(() => setMounted(true));
350
+ return () => cancelIdle?.(handle);
351
+ }
352
+ const timer = setTimeout(() => setMounted(true), 0);
353
+ return () => clearTimeout(timer);
354
+ }, [eager, mounted]);
355
+ return /*#__PURE__*/_jsxs(View, {
356
+ pointerEvents: "none",
357
+ style: [{
358
+ width: side,
359
+ height: side
360
+ }, style],
361
+ children: [/*#__PURE__*/_jsx(OttoStillLayer, {
362
+ show: !mounted,
363
+ state: resolved,
364
+ pixelSize: pixelSize,
365
+ water: water,
366
+ keyline: keyline
367
+ }), /*#__PURE__*/_jsx(OttoStrips, {
368
+ show: mounted,
369
+ state: resolved,
370
+ pixelSize: pixelSize,
371
+ water: water,
372
+ keyline: keyline,
373
+ tickMs: tickMs,
374
+ active: active
375
+ })]
376
+ });
377
+ }
378
+
379
+ /** Frame 0 while the strips are still coming. */
380
+ function OttoStillLayer({
381
+ show,
382
+ state,
383
+ pixelSize,
384
+ water,
385
+ keyline
386
+ }) {
387
+ if (!show) return null;
388
+ return /*#__PURE__*/_jsx(OttoStill, {
389
+ state: state,
390
+ pixelSize: pixelSize,
391
+ water: water,
392
+ keyline: keyline
393
+ });
394
+ }
395
+
396
+ /** The animated strips and their clock. */
397
+ function OttoStrips({
398
+ show,
399
+ state,
400
+ pixelSize,
401
+ water,
402
+ keyline,
403
+ tickMs,
404
+ active
405
+ }) {
406
+ const sheets = ottoSheets(state, keyline);
407
+ const side = OTTO_GRID * pixelSize;
408
+ const waterH = WATER_ROWS * pixelSize;
409
+ const units = lcm(sheets.period, ANCHOR_PERIOD);
410
+ const value = useOttoMotionClock(units, tickMs, active && show, state);
411
+ const bodyStyle = stepStyle(value, stripOffsets(sheets.body, side), 1, units, tickMs, active);
412
+ const waterStyle = stepStyle(value, stripOffsets(sheets.water, side), WATER_UNITS_PER_STEP, units, tickMs, active);
413
+ if (!show) return null;
414
+ return /*#__PURE__*/_jsxs(_Fragment, {
415
+ children: [/*#__PURE__*/_jsx(StripWindow, {
416
+ top: WATER_ROW0 * pixelSize,
417
+ width: side,
418
+ height: waterH,
419
+ strip: sheets.water,
420
+ px: pixelSize,
421
+ frameW: side,
422
+ motion: waterStyle,
423
+ visible: water
424
+ }), /*#__PURE__*/_jsx(StripWindow, {
425
+ top: 0,
426
+ width: side,
427
+ height: side,
428
+ strip: sheets.body,
429
+ px: pixelSize,
430
+ frameW: side,
431
+ motion: bodyStyle,
432
+ visible: true
433
+ })]
434
+ });
435
+ }
436
+ function stripOffsets(strip, frameW) {
437
+ return strip.colOf.map(c => ({
438
+ x: -c * frameW,
439
+ y: 0
440
+ }));
441
+ }
442
+ /** Pure UI: an overflow-hidden window over a strip of frames. */
443
+ function StripWindow({
444
+ top,
445
+ width,
446
+ height,
447
+ strip,
448
+ px,
449
+ frameW,
450
+ motion,
451
+ visible
452
+ }) {
453
+ const stripW = strip.frames.length * frameW;
454
+ return /*#__PURE__*/_jsx(View, {
455
+ style: {
456
+ position: "absolute",
457
+ left: 0,
458
+ top,
459
+ width,
460
+ height,
461
+ overflow: "hidden"
462
+ },
463
+ children: /*#__PURE__*/_jsx(OttoMotionView, {
464
+ style: [{
465
+ width: stripW,
466
+ height
467
+ }, motion],
468
+ children: visible ? strip.frames.map((rects, i) => /*#__PURE__*/_jsx(FrameViews, {
469
+ rects: rects,
470
+ px: px,
471
+ offsetX: i * frameW
472
+ }, i)) : null
473
+ })
474
+ });
475
+ }
476
+
477
+ /** One frame as absolutely positioned rectangle Views. Static after mount. */
478
+ function FrameViews({
479
+ rects,
480
+ px,
481
+ offsetX
482
+ }) {
483
+ return /*#__PURE__*/_jsx(_Fragment, {
484
+ children: rects.map((r, i) => /*#__PURE__*/_jsx(View, {
485
+ style: {
486
+ position: "absolute",
487
+ left: offsetX + r.x * px,
488
+ top: r.y * px,
489
+ width: r.w * px,
490
+ height: r.h * px,
491
+ backgroundColor: r.color
492
+ }
493
+ }, i))
494
+ });
495
+ }
496
+ export { WATER_FRAMES };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Otto — the Buoy mascot, as a shared UI primitive.
5
+ *
6
+ * One 16×16 pixel character with a state per product moment. `OTTO_USES` says
7
+ * what each state is for; place him only where the alternative is nothing
8
+ * (empty bodies, waiting states) and only in states that are actually true.
9
+ * See OTTO_PLACEMENT_PLAN.md.
10
+ *
11
+ * The renderer is core-RN only (no Reanimated, no expo-image), so it runs in
12
+ * Expo, in a bare RN CLI host, and on react-native-web. See OTTO_PERF_PLAN.md
13
+ * for the measurements behind that.
14
+ *
15
+ * This is the ONE copy. The example app's review page and the website's lab
16
+ * re-export from here rather than keeping forks.
17
+ */
18
+ export * from "./ottoEngine.js";
19
+ export * from "./ottoSheet.js";
20
+ export * from "./OttoSprite.js";