yiekheng 68d3de5ee2 feat(reminders): user-supplied name; auto-derived as fallback
Reminders pick up a real, user-controlled name instead of being
auto-named from the first message body. Auto-derive stays as the
fallback so empty inputs still produce something useful.

Resolution policy (single source of truth in lib/reminder-name.ts)
------------------------------------------------------------------
1. User-supplied name, trimmed, clamped to 60 chars.
2. First text-bearing message part — text body or media caption,
   trimmed, clamped to 60.
3. Literal "Reminder" (only if every part is media-without-caption
   and no name was given).

Wizard
------
- New "Name" input above the message stack on step 2 (Compose).
  Optional (label says so), maxLength 60, placeholder gives an
  example. Blank flows through the URL as an absent param.
- The name parameter passes through every subsequent step
  (when, groups, review) via the existing URL-state pattern.
- Review step gains a "Name" row at the very top showing what the
  resolver will produce. If the user left it blank, the row shows
  the auto-derived value plus a muted "(auto from message)" tag so
  they know what's happening.

Edit forms
----------
- `EditMessageForm` gains the same Name input at the top —
  consistent with the wizard's compose step.
- `EditAccountForm` / `EditWhenForm` / `EditGroupsForm` accept the
  current `name` and forward it unchanged on save. Otherwise saving
  any of those sections would re-auto-derive the name from the
  message body, silently overriding what the operator typed.

Server action
-------------
- Both `createReminderAction` and `updateReminderAction` accept an
  optional `name` field on the schema. The body collapses through
  the new `resolveReminderName` helper, replacing the inline
  `firstLabel ?? "Reminder"` slice.

Tests (+17 new in lib/reminder-name.test.ts)
--------------------------------------------
- User priority: user name wins over message body even when both
  are present; trimming.
- Auto-derive: first text part, first non-empty after skipping
  empties, media caption when present, trims around the value.
- Fallback: null/undefined/empty stack, every-part-empty, every
  part media-without-caption.
- Clamping: user-supplied long names truncate at 60; auto-derived
  long names truncate at 60; short names pass through.
- The 60-char ceiling matches what the wizard's <Input maxLength>
  enforces and what the DB column allows.

Existing tests updated to pass the new required prop (`initialName`
on EditMessageForm, `name` on EditAccountForm/EditGroupsForm SSR
fixtures, plus a couple in no-render-warnings.test.tsx).

Total: 298 web + 31 shared + 26 bot = 355 passing (was 338).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:43:22 +08:00
..