Repository navigation
Feat/submit stepper - #32
Merged
Merged
Conversation
Groundwork for the submit stepper, separated so the feature diff is readable. No behaviour change. react-native-gesture-handler was the one rendering-layer package without a wrapper and without a firewall entry — installed, mounted in AppProviders, and importable from any screen with a clean lint. That was an oversight rather than a decision. It now has both, added together, because a wrapper without the rule is only a suggestion. Verified in both directions: a features/ file importing it fails lint, and app-providers.tsx still imports GestureHandlerRootView directly, which it must, since components/ is exempt and the root view has to sit outside HeroUINativeProvider. The wrapper's docblock carries the two things a caller gets wrong alone: a horizontal pan inside a ScrollView steals the scroll without activeOffsetX, and a gesture callback is a worklet, so touching React state needs runOnJS. motion.tsx gains runOnJS and interpolate — the first to commit a swipe from the UI thread, the second for the slide. Its docblock said "Two rules" and listed two; AGENTS.md §3 has always had three. The third, honour useReducedMotion, is now written where someone reaching for the wrapper will read it, along with the closes-over-a-prop-produces-a-step trap. view.tsx exposes accessibilityValue, which no wrapper did and a progressbar role is meaningless without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven controls on one page means a first-time contributor meets all of them at once, and the one that matters most — the pin — is the one they scroll past. Now: Where, Photo, Finding it, Details, grouped by the question being answered rather than by control type.⚠️ Steps are in-screen state and every one stays MOUNTED. That is forced, not chosen: (app)/_layout.tsx and pin-draft-store.ts both record that /submit cannot be unmounted while the full-screen placer is open, because picked photos are local file URIs. A step-per-route stepper would drop every photo the moment someone adjusts the pin. The track translates; nothing unmounts. Step 1 is location, and it reuses PinField unchanged — a static mini-map preview that opens the full-screen placer, which is what it already was. use-form-steps.ts holds the only new logic, and one rule is the point of it: the indicator can never be a skip button. Forward is refused while a step is unfinished; backward is always free. Each step yields a `missing` string, so FormMessage's `incomplete` slot keeps working and the button never looks dead without saying why. The indicator is assembled from three things already here rather than invented: geometry from password-strength.tsx, accessibility from SlotPips (one accessible container carrying "Step 2 of 4, Photo", never four dots each announcing themselves), motion from floating-tab-bar.tsx including its first-placement-jumps guard. HeroUI ships no Progress and no Stepper, and its Tabs announces as a tablist and springs at stiffness 1200 against the house 220 — it would visibly not match the tab bar below it. Swipe uses activeOffsetX([-12, 12]) so a vertical drag still reaches the ScrollView, rubber-bands when the way is blocked, and commits through runOnJS. Reduce Motion sets positions directly instead of animating.⚠️ Next stays in the scroll flow rather than a fixed footer. There is no KeyboardAvoidingView in this codebase and no keyboard-controller package; avoidsKeyboard is one iOS ScrollView prop that pads scroll CONTENT, so a pinned footer would sit under the keyboard exactly when step 3's text fields are open. The last step gains a recap, each row jumping back to the step that set it — a stepper otherwise hides its own answers, and the only way to check is to walk backwards and lose your place. It shows what is there and never a blank row, for the reason score-bar.tsx gives about empty scales. The quota line and the guest JoinBenefits block move to the last step, where they are relevant. /edit-restroom is untouched and stays one page. First screen test for /submit — there has never been one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
connection-store kept a single global fromCache flag that both campus listeners wrote to, so whichever reported last won. restrooms reporting a live snapshot set online, buildings reporting a cached one set offline, and with nothing further to deliver nothing ever set it back — an online phone stuck showing "You're offline" with its writes blocked, because useWriteBlock reads this. Shipped in v1.2.0. The comment that used to sit there claimed taking whichever spoke last was "self-healing rather than a race". It was exactly a race. Each listener now reports under its own name and the store is online if ANY source is server-backed, going offline only once every one of them is answering from cache.⚠️ Worth being straight about how this was found: I went looking for it because the emulator showed an offline banner, and it turned out NOT to be the cause — logging both listeners showed they were both reporting cached, because the app could not reach the Firestore emulator at all and was serving last session's disk cache. The banner was telling the truth. The race is real and worth closing on its own merits, but it is not what I saw. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The track was as tall as its tallest child, so step 1 — a heading and one button — inherited the height of step 4's chips and summary and sat in most of a screen of nothing, pushing Next far below the content. Only the active step renders now, sliding in from the side it came from.⚠️ That is safe, and the reason is worth writing down because the opposite looks true: every value lives in useRestroomForm, held by the screen, and PhotoPicker has no state of its own — so unmounting a step cannot drop a picked photo. The docblocks claiming otherwise were mine and overstated it. What genuinely must not unmount is /submit itself, which is why steps are screen state rather than routes. Verified on the emulator through all four steps: indicator, per-step gating, the placer round trip, the photo picker, and the recap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things were stale, one of them since before this branch. The numbered walkthrough described five items in one long scroll. It is now four steps — Where · Photo · Finding it · Details — with swiping and a recap on the last one, so the README says that.⚠️ And the claim "only the pin and a landmark are required" was in TWO places and had been false since photos became required. I broke it and did not fix it at the time. The screenshot beside the caption still shows the old single-page form. Reshooting it is outstanding — the caption was reworded to describe what is required rather than what the form looks like, so it is not a lie in the meantime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Verification
npm run typechecknpm run lintnpm testnpx expo-doctornpm run test:rules— required if security rules changedNative impact
app.json, a config plugin, or a native dependency(needs
npx expo prebuild --cleanand a fresh dev build)