# Home-screen widget

Glide has an iOS home-screen widget built with **`expo-widgets`**, living in
`src/widget/`. It mirrors the Action surface outside the app and updates in real
time whenever the app is (or has recently been) running.

## Files

- `glide-widget.tsx` — the widget UI. `createWidget('GlideWidget', fn)`, where `fn`
  carries the `'widget'` directive. Renders native SwiftUI via `@expo/ui/swift-ui`
  + `/modifiers`. One definition renders all three families (small / medium /
  large); branch on `environment.widgetFamily`.
- `widget-payload.ts` — `toWidgetPayload(board)`, a pure function from the local
  board (`useBoard`) to the flat, serialisable payload the widget renders.
- `use-glide-widget-sync.ios.ts` (+ a no-op `.ts` base) — reads the local board via
  `useBoard` and pushes `GlideWidget.updateSnapshot(payload)` on change
  (JSON-deduped). Mounted as `<WidgetSync/>` in `src/app/_layout.tsx`. This is the
  "real-time" channel — no APNs; it works while the app is or has been running. See
  `docs/engineering/local-first.md` for the data layer it reads from.
- `app.json` → `expo-widgets` plugin config: widget `name` (`GlideWidget`, must
  match the `createWidget` id), `bundleIdentifier`, `groupIdentifier` (App Group),
  and `supportedFamilies`.

## How the UI reaches the device

The babel `widgets-plugin` **serialises the `'widget'` function body to a source
string at build time**. The app ships that string to native via `createWidget`,
and it runs in a **separate WidgetKit JS runtime** where the `@expo/ui` primitives
are injected as globals. The app pushes data with `updateSnapshot(props)`;
WidgetKit persists the last snapshot and renders it.

Consequence: **editing the widget UI is JS-only** — just reload Metro, no native
rebuild. (`ios/` is generated from `app.json` via CNG; the widget target
regenerates on prebuild / EAS.)

## Gotchas (each one cost real debugging time)

- **The widget body must be fully self-contained.** It may only reference its
  `props` / `environment`, plain JS built-ins, and the injected `@expo/ui`
  globals. No module-level imports, constants, or helpers referenced at runtime —
  define helper components **inside** the function.
- **React Compiler breaks it.** `experiments.reactCompiler` injects a `_c` memo
  cache into the inner helpers → runtime `Can't find variable: _c`. The `'widget'`
  directive only opts out the outer function, so the file starts with a
  module-level **`'use no memo'`** directive.
- **The payload must be string/number-only — never `null`/`undefined`.**
  `updateSnapshot` crosses into a native host function that can't convert a `null`
  prop value; it throws `Exception in HostFunction`, and because `<WidgetSync/>`
  is mounted high in the tree that throw is a **full-app render crash**. The
  "current task" field is therefore `''` (not `null`) when nothing is ready, and
  the push in `use-glide-widget-sync.ios.ts` is wrapped in try/catch so a
  peripheral widget failure can never crash the app.
- **App Group signing on the simulator.** App ↔ widget data sharing needs the App
  Group entitlement (`group.com.h14h.glide`) on **both** targets. Simulator builds
  with no dev team drop it at signing → widget error "No layout found for
  `group.com.h14h.glide::GlideWidget`". This affects **EAS `--profile development`
  (`ios.simulator`) builds too**, not just local Xcode ones — check with
  `codesign -d --entitlements :- <app>` and `…/PlugIns/*.appex` (both come back with
  an empty `<dict/>`). Sim workaround:
  1. Re-sign ad-hoc, inside-out: every nested framework/dylib in `<app>/Frameworks`
     (`codesign --force --sign -`), then the appex, then the app — the latter two
     with `--entitlements` carrying the App Group.
  2. **Reinstall** (`xcrun simctl install <udid> <app>`). Re-signing in place is not
     enough: `installd` only provisions the shared App Group container
     (`Containers/Shared/AppGroup/…`) on install. Verify with
     `xcrun simctl get_app_container <udid> com.h14h.glide group.com.h14h.glide`.
  3. Relaunch so the app pushes a fresh snapshot into the now-shared container.

  Production / store EAS builds sign correctly and need none of this.
- **`@expo/ui/swift-ui` rendering quirks** (the app's "Pop" look — thick rounded
  border + hard offset shadow — is rebuilt from shapes):
  - SwiftUI `.shadow` shadows the **text too**. For a box-shadow, put a shadowed
    `RoundedRectangle` *behind* the content in a `ZStack`.
  - Don't use `capsule` shapes for bordered pills — layered capsule fills render a
    deviant concentric double outline. Use `RoundedRectangle` with a near-pill
    radius.
  - `ZStack` shape backgrounds are greedy (no intrinsic size) → add
    `fixedSize({ horizontal: false, vertical: true })` so a card hugs its content.
  - Children must be **flat keyed arrays** (no fragments; native flattens only one
    array level).

## Task title length

Task titles are capped at **`TITLE_MAX_LENGTH = 64`** (`src/domain/glide.ts`,
enforced in the local store `src/data/task-store.ts`). The binding constraint is the
2×2 small widget; if you change the cap, re-stress-test that size.

## macOS

Deferred (a native Mac target may come later). The widget UI and payload are kept
platform-agnostic to ease that.
