Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

97 lines
4.4 KiB
Markdown

# App UI Shell
This page collects app-facing shell rules: status bar space, navigation bar foreground, keyboard resize, pointer events, and URL-driven UI state. The complete app contract remains in [module-contract.md](module-contract.md).
## Status and Navigation Bars
Every page's outermost element must reserve status bar space:
```tsx
<main className="pt-10 ..." data-status-bar-foreground="dark">
...
</main>
```
Use:
| Attribute | Values | Purpose |
|---|---|---|
| `data-status-bar-foreground` | `dark` / `light` | Sets status bar icon/text color for that page. |
| `data-navigation-bar-foreground` | `dark` / `light` | Sets bottom gesture bar color when it differs from the status bar. |
The OS no longer relies on DOM color auto-detection fallback. Declare the foreground when a page differs from the manifest default.
### Foreground fallback chain
The system chrome resolves its foreground color from declarative signals only — it never samples the page's background pixels.
**Status bar**:
1. The current page's `data-status-bar-foreground` (if set).
2. The app manifest's `theme.colors.statusBarForeground` (if set).
3. Default dark.
**Bottom gesture (navigation) bar**:
1. The current page's `data-navigation-bar-foreground` (if set).
2. The app manifest's `theme.colors.navigationBarForeground` (if set).
3. The current page's `data-status-bar-foreground` (status-bar fallback).
4. The app manifest's `theme.colors.statusBarForeground`.
5. Default dark.
Place declarative attributes on the **outermost page container**. Don't render a status-bar background of your own — the OS draws transparent chrome over your page, and `pt-10` is the only spacing convention.
## Keyboard Resize
The OS implements Android-like `adjustResize`: when the keyboard is visible, the active Activity container shrinks by keyboard height. App pages should use flex layouts so content reflows naturally.
Rules:
| Do | Avoid |
|---|---|
| Use `flex` layout and `flex-shrink-0` for chat/input bars. | `position: fixed; bottom: keyboardHeight`. |
| Add `data-keep-keyboard="true"` on keyboard-attached controls that should not blur input. | Manual keyboard-height positioning. |
| Add `data-hide-on-keyboard` for bottom tabs that should disappear while typing. | Letting tab bars get pushed above the keyboard. |
CSS zoom from `designViewportWidth` changes coordinate spaces; fixed bottom offsets are especially prone to drift.
## Pointer Events
Continuous interactions — drag, swipe, slider, bottom sheet, drawer, drag-to-reorder, page-turning — must use a single Pointer Event source:
```tsx
onPointerDown
onPointerMove
onPointerUp
onPointerCancel
```
Use `setPointerCapture` when dragging should continue after the pointer leaves the element.
Hard rules:
- **Do not** maintain parallel `touch*` + `mouse*` handlers on the same element. They fire from different event paths and create double-trigger bugs.
- **Do not** rely on `touchmove` for the drag body with a `click` "fallback" for mouse. Pointer events already unify both.
- The bench's `bindTap` / `bindLongPress` / `bindDoubleTap` / `bindBack` hooks **do not** replace a custom drag implementation — they're for discrete taps. Slider thumbs, reader pagination handles, and draggable overlays must implement their own pointer handlers.
- The `mousedown` handlers used to `preventDefault` and stop focus theft on inputs are acceptable; they don't drive a drag state machine.
> DevTools "touch device emulation" simulates touch then synthesizes mouse/click events, producing ghost double-fires that can mask Pointer-event correctness bugs. Use it for layout/scale checks only; validate continuous interactions against a real mouse and a non-emulated touch surface.
## URL-Driven UI State
Discrete UI state belongs in the route/history stack:
| UI state | Preferred modeling |
|---|---|
| Main tabs | Separate pathname routes such as `/`, `/contacts`, `/me`; switch with `replace`. |
| Dialogs / drawers | Push search params or dialog routes; close with back. |
| Finite substates | `uiStates` in `navigation.declaration.ts`. |
Avoid `useState` for modal visibility when Back should close the modal. App Back only understands URL/history state.
## Related Docs
- Keyboard and IME details → [../os/services/input-keyboard.md](../os/services/input-keyboard.md)
- Navigation declaration grammar → [../navigation/declaration.md](../navigation/declaration.md)
- App contract → [module-contract.md](module-contract.md)