From 4740291e25b1ef580ca874cad4f9e2597900f51a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Mon, 21 Sep 2026 00:36:29 -0400 Subject: [PATCH] feat: arrange field notes toolbar around occlusions --- example/src/FieldNotes.tsx | 123 ++++++++++++++++++++++++++++++------- website/docs/example.md | 10 +-- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/example/src/FieldNotes.tsx b/example/src/FieldNotes.tsx index 905dd59..0043af4 100644 --- a/example/src/FieldNotes.tsx +++ b/example/src/FieldNotes.tsx @@ -16,6 +16,35 @@ import Animated, { withTiming, } from 'react-native-reanimated'; +const sections = { + Journal: { + title: 'Room to wander.', + subtitle: 'A small journal for a wider world.', + label: 'THE WEEKEND EDIT', + number: '01', + heading: 'Take the\nlong way.', + body: 'Pine air. Still water. No particular hurry.', + }, + Map: { + title: 'Follow the quiet.', + subtitle: 'The lakeside loop, one step at a time.', + label: 'LAKESIDE LOOP', + number: '4.8', + heading: 'Kilometres\nto slow down.', + body: 'Easy terrain. Follow the shoreline back to camp.', + }, + Moments: { + title: 'Keep a little light.', + subtitle: 'Small memories from a day outside.', + label: 'TODAY’S MEMORY', + number: '03', + heading: 'The light\nbefore dusk.', + body: 'A quiet shore, a warm sky, and nowhere else to be.', + }, +}; +type Section = keyof typeof sections; +const sectionNames = Object.keys(sections) as Section[]; + export function FieldNotes({ onOpenLab }: { onOpenLab: () => void }) { const [viewportWidth, setViewportWidth] = useState(0); return ( @@ -46,9 +75,23 @@ function FieldNotesContent({ viewportWidth, onOpenLab }: { viewportWidth: number ]; }); } - const header = spaces.reduce((largest, space) => - space.end - space.start > largest.end - largest.start ? space : largest, - ); + const usableSpaces = spaces.filter((space) => space.end - space.start >= 44); + const totalSpace = usableSpaces.reduce((total, space) => total + space.end - space.start, 0); + let nextSection = 0; + const toolbar = usableSpaces.map((space, index) => { + const count = + index === usableSpaces.length - 1 + ? sectionNames.length - nextSection + : Math.min( + sectionNames.length - nextSection, + Math.max(1, Math.round((sectionNames.length * (space.end - space.start)) / totalSpace)), + ); + const items = sectionNames.slice(nextSection, nextSection + count); + nextSection += count; + return { ...space, items }; + }); + const [section, setSection] = useState
('Journal'); + const entry = sections[section]; const [preview, setPreview] = useState(false); const [width, setWidth] = useState(0); return ( @@ -65,14 +108,14 @@ function FieldNotesContent({ viewportWidth, onOpenLab }: { viewportWidth: number > VOL. 01 / THE OUTDOORS - Room to wander. - A small journal for a wider world. + {entry.title} + {entry.subtitle} {ready && occlusions.length > 0 && ( RESERVED SPACE · CONTROLS CLEAR )} setWidth(event.nativeEvent.layout.width)} style={styles.spread}> - + @@ -87,19 +130,38 @@ function FieldNotesContent({ viewportWidth, onOpenLab }: { viewportWidth: number > {preview ? 'Use native angle' : 'Preview motion'} ↗ - - react-native-hinges + react-native-reserved-regions - - {ready && header.end > header.start && ( - - {header.end - header.start >= 240 && FIELD NOTES} - - Sensor lab ↗ - + Sensor lab ↗ - )} + react-native-hinges + react-native-reserved-regions + + {ready && + toolbar.map((space, index) => ( + + {index === 0 && space.end - space.start >= 148 + space.items.length * 84 && ( + + FIELD NOTES + + )} + {space.items.map((name) => ( + setSection(name)} + > + + {name} + + + ))} + + ))} {occlusions.map(({ frame }, index) => ( - THE WEEKEND EDIT - 01 - Take the{'\n'}long way. + {entry.label} + {entry.number} + {entry.heading} - Pine air. Still water. No particular hurry. + {entry.body} WALK / PAUSE / REPEAT @@ -242,7 +305,22 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#172521', + gap: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#829c8d40', + }, + tab: { + flex: 1, + minWidth: 0, + minHeight: 44, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 10, + paddingHorizontal: 8, }, + selectedTab: { backgroundColor: '#edc278' }, + tabText: { color: '#c0cdb8', fontSize: 13, fontWeight: '600' }, + selectedTabText: { color: '#284031' }, occlusionOutline: { position: 'absolute', borderWidth: 2, @@ -252,7 +330,7 @@ const styles = StyleSheet.create({ }, occlusionCaption: { color: '#edc278', fontSize: 8, letterSpacing: 1.5, marginTop: 14 }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }, - brand: { color: '#e9eddb', fontSize: 12, letterSpacing: 3, fontWeight: '700' }, + brand: { flexShrink: 1, color: '#e9eddb', fontSize: 12, letterSpacing: 3, fontWeight: '700' }, edition: { color: '#829c8d', fontSize: 8, letterSpacing: 1.2 }, heading: { color: '#eef0e3', fontSize: 37, fontWeight: '500', letterSpacing: -1.8 }, subtitle: { color: '#a6b7a6', fontSize: 13, lineHeight: 20, marginTop: 8 }, @@ -315,7 +393,6 @@ const styles = StyleSheet.create({ buttonText: { color: '#284031', fontSize: 12, fontWeight: '600' }, labButton: { maxWidth: '100%', - marginLeft: 'auto', minHeight: 44, justifyContent: 'center', paddingHorizontal: 18, diff --git a/website/docs/example.md b/website/docs/example.md index 8337ae1..30e85f7 100644 --- a/website/docs/example.md +++ b/website/docs/example.md @@ -44,8 +44,10 @@ The example opens **Field Notes**, a two-page travel journal using both librarie - `react-native-reserved-regions` positions the pages around a native vertical division within the journal's bounded provider. Its caption changes to **TWO PAGES · NATIVE FOLD** when that division is present. -- A full-screen provider places the header controls beside native occlusion bounds, - with a 12-point gap. Amber outlines show the measured rectangles. +- A full-screen provider splits the Journal / Map / Moments toolbar around native + occlusion bounds, with a 12-point gap. The tabs use the clear spans on either + side; amber outlines show measured rectangles. The API does not identify what + causes an occlusion. - `useAnimatedHinges()` drives lighting and fades/slides secondary journal text between 120° and 60°. The pages stay geometrically flat; the physical screen supplies perspective. Reduced motion disables translation. When the angle is @@ -55,9 +57,7 @@ Native input is the default, labeled **NATIVE HINGE**. **Preview motion** genera an angle for the artwork and displays **SIMULATED ANGLE**; **Use native angle** returns to native observations. Preview does not generate reserved-region geometry. -The example installs the reserved-regions implementation from a pinned GitHub -commit while its functional npm release is pending. The workspace permits that -exact dependency revision's build during `pnpm install`. +The example installs `react-native-reserved-regions@0.1.0-alpha.2` from npm. [Watch the native Android emulator demo](https://github.com/user-attachments/assets/7f69106d-2649-4591-917c-9aa7743fd5fa). The recording uses the matching local alpha.2 candidate and the example Worklets patch; it is not a physical-device or npm-installed recording.