252 Commits

Author SHA1 Message Date
b650a94bb8 Fix birthday DatePicker stale closure in Settings profile save
The onBlur handler captured the stale dateOfBirth value from before
onChange updated state, causing the equality guard to silently abort
the save. Fixed by saving inline in onChange with the fresh value
and removing onBlur/onKeyDown from the DatePicker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 02:05:49 +08:00
47645ec115 Merge feature/user-connections into main
User connections system: search by umbral name, send/accept/reject/cancel
requests, bidirectional Person records on accept, per-connection sharing
overrides, in-app notification centre with toast popups, ntfy push
integration. Includes QA fixes, pentest hardening, and contact sync fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:36:02 +08:00
c8805ee4c4 Fix registration enumeration + contact detail panel name sync
M-01: Unify registration error messages to prevent username/email
enumeration — both now return the same generic message.

Bug fix: Contact detail panel header and initials now use shared_fields
for umbral contacts instead of stale Person record first_name/last_name.
The table list already did this via sf() helper; the detail panel header
and getPersonInitialsName were bypassing it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:31:10 +08:00
d22600ac19 Fix remaining bare notification type string literal in cancel endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:58:09 +08:00
20692632f2 Address QA warnings W-02 through W-07
W-02: Purge accepted connection requests after 90 days (rejected/cancelled stay at 30)
W-04: Rename shadowed `type` parameter to `notification_type` with alias
W-05: Extract notification type string literals to constants in connection service
W-06: Match notification list polling interval to unread count (15s when visible)
W-07: Add filter_to_shareable defence-in-depth gate on resolve_shared_profile output
W-03: Verified false positive — no double person lookup exists in accept flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:55:11 +08:00
3fe344c3a0 Fix QA review findings: per-card responding state, preserve data on detach
C-01: ConnectionRequestCard now uses local isResponding state instead of
shared hook boolean, so accepting one card doesn't disable all others.

C-03: detach_umbral_contact no longer wipes person data (name, email,
phone, etc.) when a connection is severed. The person becomes a standard
contact with all data preserved, preventing data loss for pre-existing
contacts that were linked to connections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:40:24 +08:00
aeb30afbce Fix nginx rate limit blocking accept: exact match for send request location
location /api/connections/request was a prefix match, unintentionally
rate-limiting /api/connections/requests/incoming, /outgoing, and
/requests/{id}/respond at 3r/m. This caused the accept button on toast
notifications to get 429'd when clicked immediately — the incoming
requests poll + outgoing poll had already exhausted the rate limit.

Changed to exact match (= /api/connections/request) so only the send
endpoint is rate-limited, not the entire /requests/* tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:31:28 +08:00
b16bca919f Fix send request 500: revert to naive datetime.now() per project contract
The QA fix incorrectly changed datetime.now() to datetime.now(timezone.utc),
but the DB uses TIMESTAMP WITHOUT TIME ZONE (naive). Passing a tz-aware
datetime to a naive column causes asyncpg DataError. Also removes the
temporary diagnostic wrapper and builds response before commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:14:39 +08:00
ea491a4b89 Add temporary diagnostic wrapper to send_connection_request
Exposes actual exception type and message in 500 response detail
to identify the unhandled error. Will be removed after diagnosis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:54:38 +08:00
87d232cbcd Fix send request 500: build response before commit to avoid MissingGreenlet
After db.commit(), all ORM objects are expired. Accessing their attributes
in _build_request_response triggered lazy loads which fail in async
SQLAlchemy with MissingGreenlet. Move response construction before commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:48:25 +08:00
416f616457 Allow dots in umbral name validation (matches username regex)
Username validation allows dots ([a-z0-9_.\-]+) but the connection
search and umbral name validators used [a-zA-Z0-9_-] which rejected
dots. This caused a 422 on any search for users with dots in their
username (e.g. rca.account01), silently showing "User not found".

Fixed regex in both connection.py schema and auth.py ProfileUpdate
to include dots: [a-zA-Z0-9_.-]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:30:27 +08:00
9bcf5ace5d Fix search cold-cache gate, 429 handling, and datetime.now() violations
ConnectionSearch.tsx:
- Add loading guard for useSettings() — prevents cold cache from showing
  "enable connections" gate when settings haven't loaded yet
- Add 429 rate limit handling in search catch block — shows user-friendly
  message instead of silently showing "User not found"
- Import axios for isAxiosError type guard

connections.py:
- Fix 3x datetime.now() → datetime.now(timezone.utc) per hard rule
  (lines 187, 378, 565)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:02:52 +08:00
360a14b87b Restore refetchIntervalInBackground for unread count polling
The W-03 QA fix (removing refetchIntervalInBackground) broke toast
notifications when the receiver tab is hidden (e.g. user switches to
sender's tab). Without background polling, unread count never updates,
NotificationToaster never detects new notifications, and no toast fires.

Restored with explanatory comment documenting the dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:30:16 +08:00
4e2d48c50b Fix QA review findings: detach cleanup, sf() fallthrough, polling, commit guard
- W-01: Wrap accept flow db.commit() in IntegrityError handler (409)
- W-03: Remove refetchIntervalInBackground from unread count polling
- W-04: detach_umbral_contact now clears all shared fields on Person
- W-05: sf() callers no longer fall through via ?? to stale local data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:12:31 +08:00
053c2ae85e Mark notification as read when accepting via toast
Toast accept/reject now calls markRead on the corresponding notification
so it clears from unread in the notifications tab. Uses markReadRef to
avoid stale closure in Sonner toast callbacks. Covers both success and
409 (already resolved) paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:32:54 +08:00
1e736eb333 Fix connection accept regression: revert disabled gates, add 409 handling
Previous fix (2139ea8) caused regression: staleTime:0 nuked cache on
every mount, isLoadingIncoming disabled buttons during cold-cache fetch.

Changes:
- Remove staleTime:0 (keep refetchOnMount:'always' for background refresh)
- Revert button gate to pendingRequestIds.has() only (no is_read gate)
- Remove isLoadingIncoming from disabled prop and spinner condition
- Add 409-as-success handling to ConnectionRequestCard (People tab)
- Use axios.isAxiosError() instead of err:any in all 3 catch blocks
- Add markRead() call to 409 branch so notifications clear properly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:23:47 +08:00
2139ea8077 Fix connection accept: stale cache, hidden button, and false 409 error
- incomingQuery: staleTime:0 + refetchOnMount:'always' so pending
  requests are always fresh when components mount (was inheriting
  5-min global staleTime, causing empty pendingRequestIds on nav)
- NotificationsPage: show Accept button while incoming data loads
  (was hidden during async gap); disable with spinner until ready
- Both toast and page: treat 409 as success ("already accepted")
  instead of showing error (fixes race when both fire respond)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:37:21 +08:00
2fb41e0cf4 Fix toast accept stale closure + harden backend error responses
Toast accept button captured a stale `respond` reference from the
Sonner closure. Use respondRef pattern so clicks always dispatch
through the current mutation. Backend respond endpoint now catches
unhandled exceptions and returns proper JSON with detail field
instead of plain-text 500s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:54:28 +08:00
6b59d61bf3 Fix connection mutation delays: make all onSuccess fire-and-forget
sendRequestMutation and cancelMutation were awaiting query invalidation
in onSuccess, which blocked mutateAsync until 3+ network refetches
completed (~1s+ delay). This caused noticeable lag on send/link/cancel
operations. Now all mutations use sync fire-and-forget invalidation,
matching respondMutation's pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:22:51 +08:00
dff36f30c8 Fix toast accept button: instant feedback + double-click guard
Toast buttons are static Sonner elements that can't bind React state,
so clicks had no visual feedback and allowed duplicate mutations.
Now: dismiss custom toast immediately, show loading toast, and block
concurrent clicks via a ref-based Set guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 19:38:29 +08:00
60281caa64 Unify toast accept path with notification center via useConnections
Toast now uses the same respond() from useConnections hook instead
of raw api.put, making both accept surfaces share identical code.
Also made respondMutation.onSuccess fire-and-forget to prevent
invalidation errors from surfacing as mutation failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:30:35 +08:00
5828bbf8e2 Fix toast accept showing false error when invalidations fail
Separate API error handling from query invalidation in
NotificationToaster's handleConnectionRespond so a failed refetch
doesn't surface as "Failed to respond" after a successful accept.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:15:23 +08:00
f854987f53 Fix ~60s delay before accept buttons work on new requests
Root cause: ['connections', 'incoming'] query has no polling, so
pendingRequestIds stays empty until manually refetched. Accept buttons
on NotificationsPage are gated by this set.

- NotificationsPage: detect connection_request notifications not in
  pendingRequestIds and eagerly invalidate incoming requests query
- NotificationToaster: invalidate ['connections', 'incoming'] when
  connection_request notifications arrive, so accept buttons are
  ready on all surfaces immediately

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:02:50 +08:00
14a77f0f11 Fix stale UI after accept: await invalidations, dismiss toasts
- Await all query invalidations in respondMutation/cancelMutation
  onSuccess so UI has fresh data before mutation promise resolves
- Use deterministic toast IDs (connection-request-{id}) for Sonner
  toasts so they can be dismissed from any accept surface
- Dismiss stale connection toasts in respondMutation.onSuccess
- Fix handleCancel setting resolved before API call completes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:43:22 +08:00
b554ba7151 Fix QA: IntegrityError handling, dict mutation, birthday sync, None guard
- C-01: Wrap accept flow flush/commit in IntegrityError handling (409)
- C-02: Use separate remote_timestamps dict instead of pop() on shared profile
- W-01: Add birthday sync in Link conversion path (existing person → umbral)
- W-02: Add None guard on max(updated_at) comparison in get_person

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:05:46 +08:00
568a78e64b Show connected user's latest update time on umbral contacts
Override updated_at on PersonResponse with max(Person, User, Settings)
so the panel reflects when the connected user last changed their profile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:44:54 +08:00
73cef1df55 Add umbral name header, preferred name field, and link button for contacts
- Inject umbral_name into shared_fields for umbral contacts (always visible)
- Show @umbralname subtitle in detail panel header
- Add preferred_name to panel fields with synced label for umbral contacts
- Add Link button on standard contacts to tie to umbral user via connection request
- Migration 046: person_id FK on connection_requests with index
- Validate person_id ownership on send, re-validate + convert on accept

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:37:01 +08:00
4513227338 Fix share name toggle revert and stale table data for umbral contacts
Bug 1: _to_settings_response() was missing share_first_name and
share_last_name — the response always returned false (Pydantic default),
causing the frontend to sync toggles back to off after save.

Bug 2: Table column renderers read from stale Person record fields.
Added sf() helper that overlays shared_fields for umbral contacts,
applied to name, phone, email, role, and birthday columns. The table
now shows live shared profile data matching the detail panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:07:45 +08:00
33aac72639 Add delete-with-sever and unlink actions for umbral contacts
Delete person now severs the bidirectional connection when the person
is an umbral contact — removes both UserConnection rows and detaches
the counterpart's Person record. Fixes "Already connected" error
when trying to reconnect after deleting an umbral contact.

New PUT /people/{id}/unlink endpoint converts an umbral contact to a
standard contact (detaches linked fields) while also severing the
bidirectional connection, keeping the Person in the contact list.

Frontend: EntityDetailPanel gains extraActions prop. PeoplePage renders
an "Unlink" button in the panel footer for umbral contacts. Delete
mutation now also invalidates connections query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:50:31 +08:00
75fc3e3485 Fix notification background polling, add first/last name sharing
Notifications: enable refetchIntervalInBackground on unread count
query so notifications appear in background tabs without requiring
a tab switch to trigger refetchOnWindowFocus.

Name sharing: add share_first_name and share_last_name to the full
sharing pipeline — migration 045, Settings model/schema, SHAREABLE_FIELDS,
resolve_shared_profile, create_person_from_connection (now populates
first_name + last_name + computed display name), SharingOverrideUpdate,
frontend types and SettingsPage toggles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:34:13 +08:00
820ff46efa Fix QA W-01/W-05/W-06/W-08: cancel requests, detach umbral, notifications
W-08: Add CHECK constraint on notifications.type (migration 044) with
defensive pre-check and matching __table_args__ on model.

W-05: Auto-detach umbral contact before Person delete — nulls out
connection's person_id so the connection survives deletion.

W-01: Add PUT /requests/{id}/cancel endpoint with atomic UPDATE,
silent notification cleanup, and audit logging. Frontend: direction-aware
ConnectionRequestCard, cancel mutation, pending requests section on
PeoplePage with incoming/outgoing subsections.

W-06: Convert useNotifications to context provider pattern — single
subscription shared via NotificationProvider in AppLayout. Adds
refreshNotifications convenience function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 07:17:31 +08:00
0e94b6e1f7 Fix QA review findings: race condition, detached session, validation
- C-01: Wrap connection request flush in IntegrityError handler for
  TOCTOU race on partial unique index
- W-02: Extract ntfy config into plain dict before commit to avoid
  DetachedInstanceError in background tasks
- W-04: Add integer range validation (1–2147483647) on notification IDs
- W-07: Add typed response models for respond_to_request endpoint
- W-09: Document resolved_at requirement for future cancel endpoint
- S-02: Use Literal type for ConnectionRequestResponse.status
- S-04: Check ntfy master switch in extract_ntfy_config
- S-05: Move date import to module level in connection service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 06:36:14 +08:00
e27beb7736 Fix toast notifications, require accept_connections for senders
- Rewrite NotificationToaster with max-ID watermark for reliable
  new-notification detection and faster unread count polling (15s)
- Block connection search and requests when sender has
  accept_connections disabled (backend + frontend gate)
- Remove duplicate sender_settings fetch in send_connection_request
- Show actionable error messages in toast respond failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 06:21:43 +08:00
03fd8dba97 Fix notification UX, admin panel error handling, and data bugs
Notification fixes:
- Add NotificationToaster component with real-time toast notifications
  for new incoming notifications (30s polling, 15s stale time)
- Connection request toasts show inline Accept/Reject buttons
- Add inline Accept/Reject buttons to connection_request notifications
  in NotificationsPage (prevents bricked requests after navigation)
- Don't mark connection_request as read or navigate away when pending
- Auto-refetch notification list when unread count increases

Admin panel fixes:
- Add error state UI to UserDetailSection and ConfigPage (previously
  silently returned null/empty on API errors)
- Fix get_user response missing must_change_password and locked_until
- Fix create_user response missing preferred_name and date_of_birth
- Add defensive limit(1) on settings query to prevent MultipleResultsFound
- Guard _target_username_col JSONB cast with CASE to prevent crash on
  non-JSON audit detail values
- Add connection audit action types to ConfigPage filter dropdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:55:14 +08:00
03abbbf8a7 Restrict umbral name to single word (no spaces)
Explicit space check with clear error message on both backend
validator and frontend client-side validation. The existing regex
already disallows spaces but the dedicated check gives a better UX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:03:22 +08:00
6130d09ae8 Make umbral name editable in user settings
- Add umbral_name to ProfileUpdate schema with regex validation
- Add uniqueness check in PUT /auth/profile handler
- Replace disabled input with editable save-on-blur field in Social card
- Client-side validation (3-50 chars, alphanumeric/hyphens/underscores)
- Inline error display for validation failures and taken names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:00:33 +08:00
8e226fee0f Set umbral_name=username on all user creation paths
Admin create, first-user setup, and registration endpoints were
missing umbral_name assignment, causing NOT NULL constraint failures
when creating new users after migration 039.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 04:56:22 +08:00
337b50c7ce Fix QA review findings: source_id, N+1 queries, event bubbling, type mismatches
Critical fixes:
- C-01: Add receiver_umbral_name/receiver_preferred_name to frontend ConnectionRequest type
- C-02: Flush connection request before notification to populate source_id
- C-03: Add umbral_name to ProfileResponse/UserProfile, use in Settings Social card
- C-04: Remove dead code in sharing-overrides endpoint, merge instead of replace

Warning fixes:
- W-01/W-02: Batch-fetch settings in incoming/outgoing/list connection endpoints (N+1 fix)
- W-04: Add _purge_resolved_requests job for rejected/cancelled requests (30-day retention)
- W-10: Add e.stopPropagation() to notification mark-read and delete buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 02:29:04 +08:00
3d22568b9c Add user connections, notification centre, and people integration
Implements the full User Connections & Notification Centre feature:

Phase 1 - Database: migrations 039-043 adding umbral_name to users,
profile/social fields to settings, notifications table, connection
request/user_connection tables, and linked_user_id to people.

Phase 2 - Notifications: backend CRUD router + service + 90-day purge,
frontend NotificationsPage with All/Unread filter, bell icon in sidebar
with unread badge polling every 60s.

Phase 3 - Settings: profile fields (phone, mobile, address, company,
job_title), social card with accept_connections toggle and per-field
sharing defaults, umbral name display with CopyableField.

Phase 4 - Connections: timing-safe user search, send/accept/reject flow
with atomic status updates, bidirectional UserConnection + Person records,
in-app + ntfy notifications, per-receiver pending cap, nginx rate limiting.

Phase 5 - People integration: batch-loaded shared profiles (N+1 prevention),
Ghost icon for umbral contacts, Umbral filter pill, split Add Person button,
shared field indicators (synced labels + Lock icons), disabled form inputs
for synced fields on umbral contacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 02:10:16 +08:00
2a21809066 Merge feature/registration-profile-fields into main
- Registration profile fields (preferred name, email, DOB)
- Custom DatePicker component replacing all native date inputs
- Default date/time fields to today/now on create forms
- Pentest hardening: Cache-Control, SSRF save-time validation,
  Permissions-Policy, nginx header inheritance fix, 0.0.0.0/8 block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:52:26 +08:00
0e0da4bd14 Fix nginx header inheritance regression and add 0.0.0.0/8 to SSRF blocklist
NEW-1: add_header in location /api block suppressed server-level security
headers (HSTS, CSP, X-Frame-Options, etc). Duplicate all security headers
into the /api block explicitly per nginx inheritance rules.

NEW-2: Add 0.0.0.0/8 to _BLOCKED_NETWORKS — on Linux 0.0.0.0 connects
to localhost, bypassing the existing loopback check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:41:16 +08:00
20d0c2ff57 Fix pentest findings: Cache-Control, SSRF save-time validation, Permissions-Policy
L-01: Add Cache-Control: no-store to all /api/ responses via nginx
L-02: Validate ntfy_server_url against blocked networks at save time
I-03: Add Permissions-Policy header to restrict unused browser APIs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:52:28 +08:00
b04854a488 Default date/time fields to today/now on create forms
Todo, reminder, project, and task forms now pre-fill date/time
fields with today's date and current time when creating new items.
Edit mode still uses stored values. DOB fields excluded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:08:55 +08:00
0c6ea1ccff Fix QA review findings: server-side DOB validation, naive date max prop
- W-01: Add date_of_birth validators to RegisterRequest and ProfileUpdate
  (reject future dates and years before 1900)
- W-05: Replace .toISOString().slice() with local date formatting for
  DatePicker max prop on registration form

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:59:54 +08:00
6cd648f3a8 Replace native date/time inputs with DatePicker across calendar and todo forms
- EventForm + EventDetailPanel: native <Input type=date|datetime-local> → DatePicker with dynamic mode via all_day toggle
- TodoForm + TodoDetailPanel: merge date + time into single datetime DatePicker, remove separate time input, move recurrence select into 2-col grid beside date picker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:43:14 +08:00
e20c04ac4f Fix DatePicker popup flashing at top-left in Chromium
Latent bug: useEffect runs after paint, so the popup rendered at
{top:0, left:0} before repositioning. Switched to useLayoutEffect
which runs synchronously before paint, ensuring correct position
on first frame. Both Chromium and Firefox unaffected by the change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:26:49 +08:00
63b3a3a073 Fix Firefox DatePicker popup positioning at top-left
When Firefox input variant falls through to button variant, the
positioning logic, close handler, and click-outside handler still
checked variant==='input' and used wrapperRef (which is unattached).
Introduced usesNativeInput flag (input variant + not Firefox) so all
three handlers correctly use triggerRef for Firefox fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:56:05 +08:00
e7979afba3 Firefox DatePicker input variant falls through to button variant
Instead of type=text with raw ISO strings, Firefox users now get
the same button-style picker used on the registration screen.
Chromium keeps native date/datetime-local for segmented editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:49:03 +08:00
8e922a1f1c Use type=text for DatePicker in Firefox to eliminate double icon
Firefox has no CSS pseudo-element to hide its native date picker
calendar icon (Mozilla bug 1830890, open P3). Firefox's date input
doesn't provide Chrome's segmented editing anyway — it renders as
a plain text field with an appended icon.

Fix: detect Firefox via user agent at module load, render type=text
with ISO format placeholder. Chromium keeps native date/datetime-local
for segmented editing UX. min/max omitted for Firefox (only valid on
native date inputs). Custom popup handles all date selection in both.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 04:01:38 +08:00
db2ec156e4 Fix Firefox double calendar icon with opaque cover button
@-moz-document url-prefix() was dead since Firefox 61 and
-moz-appearance: textfield has no effect on date inputs.
Firefox has no CSS pseudo-element for the date picker icon.

Fix: custom Calendar button resized to a full-height w-9 panel
with bg-background + rounded-r-md that completely occludes
Firefox's native icon underneath. Chromium still uses
::-webkit-calendar-picker-indicator to remove its native icon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 03:53:36 +08:00