@@ -22,21 +22,42 @@ backend and frontend.
2222 value as naive local time.
2323- Reject or treat as a bug any timestamp arriving without offset/zone info.
2424
25+ ## Client timezone
26+
27+ The frontend renders and inputs in the ** client timezone** : the browser zone by
28+ default, replaced by the user's saved override (Settings page, persisted in
29+ ` localStorage ` ).
30+
31+ - Read it with ` useClientTimezone() ` , which always resolves to a zone and also
32+ reports ` isOverridden ` .
33+ - ` BROWSER_TIMEZONE ` is the provider's fallback, not a value call sites read on
34+ their own, and ` Intl.DateTimeFormat().resolvedOptions().timeZone ` never
35+ belongs in a component.
36+ - The override is a display and input preference only. It relabels instants, it
37+ never rewrites stored ones.
38+
2539## Frontend rendering
2640
27- - Render in the ** browser timezone** by default .
41+ - Render in the ** client timezone** .
2842- Optionally offer a switch to render in the entity's associated timezone
29- (account zone for transactions). Note the consequence: with browser-default
30- rendering, a transaction's calendar day and statement bucket are
31- viewer-dependent until the switch is used.
43+ (account zone for transactions). Note the consequence: with client-zone
44+ rendering, a transaction's calendar day and statement bucket follow the
45+ viewer's preference until the switch is used.
3246
3347## Frontend create / edit
3448
35- - The picker always reflects ** browser local time** . The user inputs the instant
36- as they experience it.
37- - When the associated entity's zone differs from the browser zone, preview the
38- same instant re-expressed in that zone (e.g. ` toZonedISOString ` ) so the user
39- sees which day/statement it lands in for the account.
49+ - The picker speaks the ** client timezone** , which is the user's own clock, not
50+ necessarily the machine's. Picking Aug 7 12:00 AM with an ` Asia/Taipei `
51+ override means Taipei midnight even when the browser sits in SF.
52+ - Every wall-clock read and write goes through the Temporal helpers in
53+ ` lib/utils.ts ` (` formatTimePart ` , ` zonedCalendarDate ` , ` withDate ` ,
54+ ` withTime ` ), which all take an explicit ` timeZone ` . Native ` Date ` getters
55+ (` getHours ` , ` getFullYear ` ) are hardcoded to the browser zone, so they must
56+ not drive the picker.
57+ - When the associated entity's zone differs from the client zone, preview the
58+ same instant re-expressed in that zone (` toZonedISOString ` , rendered by
59+ ` ZonedTimePicker ` ) so the user sees which day/statement it lands in for the
60+ account.
4061- ** Re-expressing in the associated zone is a relabel of the same instant, never
4162 a reschedule.** 2:00 PM SF = 5:00 PM NYC = 22:00 UTC are one instant with three
4263 labels. Sending ` 22:00Z ` , ` -07:00 ` , or ` -04:00 ` all persist the identical row.
@@ -45,9 +66,15 @@ backend and frontend.
4566
4667## Worked example
4768
48- A client in SF creates a transaction on an NYC account:
69+ A user whose client timezone resolves to ` America/Los_Angeles ` creates a
70+ transaction on an NYC account:
4971
50- 1 . Picker shows SF local time . User picks 2:00 PM.
72+ 1 . Picker shows the client zone . User picks 2:00 PM.
51732 . FE previews the account zone: 5:00 PM America/New_York (same instant).
52743 . FE sends that instant (offset-aware ISO in the account zone).
53754 . Postgres stores 22:00 UTC. Reads recover the zone from the account to render.
76+
77+ If that same user overrides their timezone to ` Asia/Taipei ` , step 1 changes
78+ meaning: 2:00 PM now names Taipei 2:00 PM (06:00 UTC), and the preview line and
79+ the stored row follow. The override changes which instant the input names. It
80+ does not touch instants already stored.
0 commit comments