Commits
The next-card transition glitched: after a decision the card flung off via the shared dragX, which the fling callback reset to 0 before the deck advanced — so the old card snapped back to centre for a frame before the next card came in. Decouple the outgoing card into its own fly-off overlay (separate transform) so resetting the deck's drag for the incoming card never disturbs it; the next card simply promotes up behind it. Pure JS — intended as the first EAS Update (OTA) payload to the preview build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add expo-updates with the fingerprint runtime-version policy and per-profile channels (development/preview/production), so JS-only client changes — most migration client work — ship over-the-air without an App Store round-trip. Takes effect from the next build (the binary must embed expo-updates). Runbook updated to mark OTA as configured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the production client-release process the sortAt removal exposed: a narrow is gated on the prod client being updated AND adopted (not just a server deploy); under local-first a too-early narrow fails silently. Document EAS Update (OTA) vs store builds as the two release vehicles, the runtime-version gate, the live-client production sequence, and the recovery path (no data loss — dirty rows flush on the new client).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Engineering notes for the local-first data layer, the Convex sync design (ADR), and a zero-downtime schema-migration runbook (widen->migrate->narrow with the worked sortAt removal). Update the widget note and engineering index to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every feature now works offline with no account or network: a local MMKV-backed Zustand store is the source of truth behind a repository seam (src/data), and Convex is gone from the runtime path. Add an optional, account-scoped Convex sync engine (single-user/dogfood mode): an offline outbox of optimistic writes with idempotent upserts keyed by a client UUID and last-write-wins reconciliation, plus the push/pull backend (convex/sync.ts).
Replace the over-engineered sortAt ordering with stable createdAt/completedAt, and drop the sortAt column via a zero-downtime widen->migrate->narrow (@convex-dev/migrations).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously only dark mode was tinted (to tame its brightening); light
mode used the default neutral glass. Derive the glass tint from
palette.background in both modes (~55% alpha, e.g. #FFF6E98C / #15121C8C)
so each bar reads as a quiet, background-coloured pane and the two modes
stay in sync with the palette.
Verified in the simulator in both appearances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "regular" glass material brightens what's behind it — subtle over
the light cream theme, but a distracting bright band over the dark
background. Tint the glass toward the background (#15121C) in dark mode
so it reads as a quiet dark frost; light mode already looked right and
stays untinted. Documented the scheme difference in the liquid-glass note.
Verified in the simulator in both appearances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/engineering/ capturing the non-obvious implementation details
future contributors need before touching these areas:
- home-screen-widget.md — the expo-widgets architecture (serialised body
in a separate WidgetKit runtime), the live Convex→widget bridge, and
the gotchas (no null in the payload, 'use no memo', App Group signing,
@expo/ui rendering quirks).
- liquid-glass.md — the scroll-driven Liquid Glass header/footer, the
opacity-animation workaround, and the React Compiler `.set()` rule for
shared values in JS callbacks.
Linked from the docs index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header and footer glass was always on, so the bars read as a band
even at the scroll boundaries where nothing sits behind them. Drive each
bar's glass from scroll position instead: fully transparent when there's
nothing underneath (the header at the top, the footer at the bottom),
fading in smoothly as cards slide behind it.
Implemented with Reanimated via the expo-glass-effect opacity workaround
— fading a wrapper's opacity directly on a GlassView stops the effect
rendering, so a wrapper view's opacity is animated while glassEffectStyle
toggles to 'none' below a threshold. The header keys off scroll offset,
the footer off distance-from-bottom (content vs viewport height). The
wordmark, badges and action buttons sit above the glass and never dim.
Verified in the simulator across a full ready list: header transparent
at the top and frosting in as you scroll; footer the reverse.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header and footer were opaque cream bars with the list clipped
between them, so cards hit a hard cut-off as they scrolled. Restructure
so the list scrolls full-height behind absolutely-positioned bars, and
back those bars with iOS 26 Liquid Glass (expo-glass-effect) — cards now
frost softly beneath the header and footer for a smooth transition.
Each bar's height is measured (onLayout) and fed back as the scroll
view's top/bottom content insets, so the first and last card sit clear
of the glass at rest. Where Liquid Glass isn't available the bar falls
back to a solid background-coloured bar, keeping the layout identical.
Verified in the simulator with a full ready list: cards frost under both
bars while scrolling; wordmark, badges and action buttons stay crisp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous change put the emoji beside the title everywhere, but the
horizontal layout was only wanted on the medium widget — to fix its
cramped vertical spacing. The in-app view and the roomy large widget
read better with the emoji stacked above the title.
Parameterise emptyCard with a `row` flag: the medium widget passes it
(emoji beside title); the large widget stays vertical. Revert the in-app
EmptyState to its vertical layout.
Verified in the simulator: in-app and large render the emoji above
"All clear"; medium renders it beside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacking the emoji above the title made the short medium widget's
empty card feel cramped — the tall card crowded the header. Put the
emoji beside the title instead (a centred row), so the card is a
compact banner that sits comfortably in the medium widget with room to
breathe. "Nothing ready" keeps its count stacked under the title to the
right of the icon.
Applied to the in-app EmptyState and the medium + large widgets alike
so the card stays consistent across every surface.
Verified in the simulator: in-app, medium and large all render the
emoji beside "All clear", centred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the "Add something to glide through" tagline (needless), and make
the all-clear card render identically in the app and the widgets: a
compact, content-hugging card with a centred 🎉 + "All clear", centred
in the available space rather than filling it. This fixes the medium
widget's All-clear card reading as too tall (it was a full-height card
with sparse, left-aligned content) and aligns it with the in-app
EmptyState.
Switch the widget empty states from SF Symbols to the same emojis the
app uses (🎉 all-clear, 🗂️ nothing-ready) so the two surfaces match.
"Nothing ready" keeps its functional count; only the all-clear tagline
is removed.
Verified in the simulator across in-app, medium and large widgets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completing the last ready task crashed the app with a render error
("Exception in HostFunction") and left the widgets stale. The widget
payload set current to null when nothing was ready, but updateSnapshot
crosses into a native host function that can't convert a null prop
value, so it threw — and because WidgetSync is mounted high in the tree
(inside the Convex provider) the throw took down the whole app.
current is now always a string ('' when nothing is ready), matching the
payload's documented string-only contract; the widget already treats a
falsy current as the empty state, so rendering is unchanged. Also wrap
the updateSnapshot push in try/catch so a peripheral widget-sync failure
can never crash the app again.
Verified in the simulator: completing the last task transitions cleanly
to "All clear" and both widgets update to 0 ready / 1 done.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an iOS home-screen widget (expo-widgets) that mirrors the
Actioning surface outside the app:
- small = a Ready/Done scoreboard (two horizontal coloured badges)
- medium = Glide header + hero card with the current task
- large = mini Actioning view (hero + NEXT UP list + Plan/New links)
The app pushes snapshots from its live getBoard subscription via a
JSON-deduped bridge (src/widget/use-glide-widget-sync.ios.ts), so the
widget updates in real time whenever the app is or has been running.
Tap opens the app (large deep-links to /plan and /new).
Add a session-scoped Done count: a singleton meta table tracks tasks
actioned since the last Plan session (readyCount + done = the ready
batch). Surfaced as a purple Done badge beside the green Ready badge in
the actioning header and widget headers, and as the small widget's
whole purpose. startPlanSession resets it each time Plan opens.
Cap task titles at TITLE_MAX_LENGTH (64) — the binding constraint is
the 2x2 widget — enforced by the capture input and a server-side slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Icon Composer document (assets/Glide.icon): a white glide-arrow on
a brand-purple gradient, built from Liquid Glass primitives. The mark
echoes Glide's swipe-to-ready gesture and reads clearly small/grayscale.
- Wire ios.icon to the new Glide.icon, replacing the Expo placeholder.
- Add an env block to the EAS preview profile so internal-distribution
builds talk to the production Convex deployment (festive-hyena-328).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Planning deck: replace the bouncy spring entrance with a quick ease-out
rise from the first-behind slot, and key the depth cards by slot so they
hold position through advances (no snap) and only fade when the deck
shrinks.
- Actioning hero: matching ease-out entrance, and a non-springy list reflow.
- Actioning list: widen the ScrollView to full width and re-inset its
content so the cards' hard offset shadows render inside the viewport
instead of being clipped at its edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the two-tab shell (Now deck + Signals dashboard) with a single
actioning surface plus on-demand planning and capture:
- Actioning (home): a NOW hero card + NEXT UP list, with a Done action
that completes tasks and a bottom bar to plan or capture.
- Planning (full-screen modal): pass-scoped swipe deck — each open task
is considered once per session; resisting softens it for the next pass.
Adds a directional color wash on drag and button-triggered fly-off.
- Capture (sheet): title-only; resistance is discovered through the loop.
Backend: add createTask + complete mutations, simplify assess and the
starter seed, and relax the now-unused context/minutes/lane fields to
optional. Strip the Signals/lanes domain code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the "Now" decision screen with a bold, playful identity: a
flickable card deck with neo-brutalist hard shadows, a streak counter,
a segmented resistance meter, and spring physics. Swipe right / tap
Ready to clear a task; swipe left / tap Resist to let it soften.
Extract the Resistance Zero loop into a reusable useDecisionEngine hook
and promote the Pop palette to the app's single theme, so the tab bar,
nav chrome, and themed primitives all follow the brand.
Rework the Signals tab to match: a chunky scoreboard of lane stats, a
"still resisting" pile, and a custom springy toggle. Drop the now-unused
GlideMark component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The next-card transition glitched: after a decision the card flung off via the shared dragX, which the fling callback reset to 0 before the deck advanced — so the old card snapped back to centre for a frame before the next card came in. Decouple the outgoing card into its own fly-off overlay (separate transform) so resetting the deck's drag for the incoming card never disturbs it; the next card simply promotes up behind it. Pure JS — intended as the first EAS Update (OTA) payload to the preview build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add expo-updates with the fingerprint runtime-version policy and per-profile channels (development/preview/production), so JS-only client changes — most migration client work — ship over-the-air without an App Store round-trip. Takes effect from the next build (the binary must embed expo-updates). Runbook updated to mark OTA as configured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the production client-release process the sortAt removal exposed: a narrow is gated on the prod client being updated AND adopted (not just a server deploy); under local-first a too-early narrow fails silently. Document EAS Update (OTA) vs store builds as the two release vehicles, the runtime-version gate, the live-client production sequence, and the recovery path (no data loss — dirty rows flush on the new client).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every feature now works offline with no account or network: a local MMKV-backed Zustand store is the source of truth behind a repository seam (src/data), and Convex is gone from the runtime path. Add an optional, account-scoped Convex sync engine (single-user/dogfood mode): an offline outbox of optimistic writes with idempotent upserts keyed by a client UUID and last-write-wins reconciliation, plus the push/pull backend (convex/sync.ts).
Replace the over-engineered sortAt ordering with stable createdAt/completedAt, and drop the sortAt column via a zero-downtime widen->migrate->narrow (@convex-dev/migrations).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously only dark mode was tinted (to tame its brightening); light
mode used the default neutral glass. Derive the glass tint from
palette.background in both modes (~55% alpha, e.g. #FFF6E98C / #15121C8C)
so each bar reads as a quiet, background-coloured pane and the two modes
stay in sync with the palette.
Verified in the simulator in both appearances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "regular" glass material brightens what's behind it — subtle over
the light cream theme, but a distracting bright band over the dark
background. Tint the glass toward the background (#15121C) in dark mode
so it reads as a quiet dark frost; light mode already looked right and
stays untinted. Documented the scheme difference in the liquid-glass note.
Verified in the simulator in both appearances.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/engineering/ capturing the non-obvious implementation details
future contributors need before touching these areas:
- home-screen-widget.md — the expo-widgets architecture (serialised body
in a separate WidgetKit runtime), the live Convex→widget bridge, and
the gotchas (no null in the payload, 'use no memo', App Group signing,
@expo/ui rendering quirks).
- liquid-glass.md — the scroll-driven Liquid Glass header/footer, the
opacity-animation workaround, and the React Compiler `.set()` rule for
shared values in JS callbacks.
Linked from the docs index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header and footer glass was always on, so the bars read as a band
even at the scroll boundaries where nothing sits behind them. Drive each
bar's glass from scroll position instead: fully transparent when there's
nothing underneath (the header at the top, the footer at the bottom),
fading in smoothly as cards slide behind it.
Implemented with Reanimated via the expo-glass-effect opacity workaround
— fading a wrapper's opacity directly on a GlassView stops the effect
rendering, so a wrapper view's opacity is animated while glassEffectStyle
toggles to 'none' below a threshold. The header keys off scroll offset,
the footer off distance-from-bottom (content vs viewport height). The
wordmark, badges and action buttons sit above the glass and never dim.
Verified in the simulator across a full ready list: header transparent
at the top and frosting in as you scroll; footer the reverse.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header and footer were opaque cream bars with the list clipped
between them, so cards hit a hard cut-off as they scrolled. Restructure
so the list scrolls full-height behind absolutely-positioned bars, and
back those bars with iOS 26 Liquid Glass (expo-glass-effect) — cards now
frost softly beneath the header and footer for a smooth transition.
Each bar's height is measured (onLayout) and fed back as the scroll
view's top/bottom content insets, so the first and last card sit clear
of the glass at rest. Where Liquid Glass isn't available the bar falls
back to a solid background-coloured bar, keeping the layout identical.
Verified in the simulator with a full ready list: cards frost under both
bars while scrolling; wordmark, badges and action buttons stay crisp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous change put the emoji beside the title everywhere, but the
horizontal layout was only wanted on the medium widget — to fix its
cramped vertical spacing. The in-app view and the roomy large widget
read better with the emoji stacked above the title.
Parameterise emptyCard with a `row` flag: the medium widget passes it
(emoji beside title); the large widget stays vertical. Revert the in-app
EmptyState to its vertical layout.
Verified in the simulator: in-app and large render the emoji above
"All clear"; medium renders it beside.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacking the emoji above the title made the short medium widget's
empty card feel cramped — the tall card crowded the header. Put the
emoji beside the title instead (a centred row), so the card is a
compact banner that sits comfortably in the medium widget with room to
breathe. "Nothing ready" keeps its count stacked under the title to the
right of the icon.
Applied to the in-app EmptyState and the medium + large widgets alike
so the card stays consistent across every surface.
Verified in the simulator: in-app, medium and large all render the
emoji beside "All clear", centred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the "Add something to glide through" tagline (needless), and make
the all-clear card render identically in the app and the widgets: a
compact, content-hugging card with a centred 🎉 + "All clear", centred
in the available space rather than filling it. This fixes the medium
widget's All-clear card reading as too tall (it was a full-height card
with sparse, left-aligned content) and aligns it with the in-app
EmptyState.
Switch the widget empty states from SF Symbols to the same emojis the
app uses (🎉 all-clear, 🗂️ nothing-ready) so the two surfaces match.
"Nothing ready" keeps its functional count; only the all-clear tagline
is removed.
Verified in the simulator across in-app, medium and large widgets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completing the last ready task crashed the app with a render error
("Exception in HostFunction") and left the widgets stale. The widget
payload set current to null when nothing was ready, but updateSnapshot
crosses into a native host function that can't convert a null prop
value, so it threw — and because WidgetSync is mounted high in the tree
(inside the Convex provider) the throw took down the whole app.
current is now always a string ('' when nothing is ready), matching the
payload's documented string-only contract; the widget already treats a
falsy current as the empty state, so rendering is unchanged. Also wrap
the updateSnapshot push in try/catch so a peripheral widget-sync failure
can never crash the app again.
Verified in the simulator: completing the last task transitions cleanly
to "All clear" and both widgets update to 0 ready / 1 done.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an iOS home-screen widget (expo-widgets) that mirrors the
Actioning surface outside the app:
- small = a Ready/Done scoreboard (two horizontal coloured badges)
- medium = Glide header + hero card with the current task
- large = mini Actioning view (hero + NEXT UP list + Plan/New links)
The app pushes snapshots from its live getBoard subscription via a
JSON-deduped bridge (src/widget/use-glide-widget-sync.ios.ts), so the
widget updates in real time whenever the app is or has been running.
Tap opens the app (large deep-links to /plan and /new).
Add a session-scoped Done count: a singleton meta table tracks tasks
actioned since the last Plan session (readyCount + done = the ready
batch). Surfaced as a purple Done badge beside the green Ready badge in
the actioning header and widget headers, and as the small widget's
whole purpose. startPlanSession resets it each time Plan opens.
Cap task titles at TITLE_MAX_LENGTH (64) — the binding constraint is
the 2x2 widget — enforced by the capture input and a server-side slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New Icon Composer document (assets/Glide.icon): a white glide-arrow on
a brand-purple gradient, built from Liquid Glass primitives. The mark
echoes Glide's swipe-to-ready gesture and reads clearly small/grayscale.
- Wire ios.icon to the new Glide.icon, replacing the Expo placeholder.
- Add an env block to the EAS preview profile so internal-distribution
builds talk to the production Convex deployment (festive-hyena-328).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Planning deck: replace the bouncy spring entrance with a quick ease-out
rise from the first-behind slot, and key the depth cards by slot so they
hold position through advances (no snap) and only fade when the deck
shrinks.
- Actioning hero: matching ease-out entrance, and a non-springy list reflow.
- Actioning list: widen the ScrollView to full width and re-inset its
content so the cards' hard offset shadows render inside the viewport
instead of being clipped at its edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the two-tab shell (Now deck + Signals dashboard) with a single
actioning surface plus on-demand planning and capture:
- Actioning (home): a NOW hero card + NEXT UP list, with a Done action
that completes tasks and a bottom bar to plan or capture.
- Planning (full-screen modal): pass-scoped swipe deck — each open task
is considered once per session; resisting softens it for the next pass.
Adds a directional color wash on drag and button-triggered fly-off.
- Capture (sheet): title-only; resistance is discovered through the loop.
Backend: add createTask + complete mutations, simplify assess and the
starter seed, and relax the now-unused context/minutes/lane fields to
optional. Strip the Signals/lanes domain code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the "Now" decision screen with a bold, playful identity: a
flickable card deck with neo-brutalist hard shadows, a streak counter,
a segmented resistance meter, and spring physics. Swipe right / tap
Ready to clear a task; swipe left / tap Resist to let it soften.
Extract the Resistance Zero loop into a reusable useDecisionEngine hook
and promote the Pop palette to the app's single theme, so the tab bar,
nav chrome, and themed primitives all follow the brand.
Rework the Signals tab to match: a chunky scoreboard of lane stats, a
"still resisting" pile, and a custom springy toggle. Drop the now-unused
GlideMark component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>