@carlonicora/nextjs-jsonapi 6.23.0 → 6.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/{BlockNoteEditor-PPRML5CO.js → BlockNoteEditor-TNX77GCD.js} +28 -25
  2. package/dist/BlockNoteEditor-TNX77GCD.js.map +1 -0
  3. package/dist/{BlockNoteEditor-UO4ZRCUM.mjs → BlockNoteEditor-Z5LDF47Z.mjs} +13 -10
  4. package/dist/{BlockNoteEditor-UO4ZRCUM.mjs.map → BlockNoteEditor-Z5LDF47Z.mjs.map} +1 -1
  5. package/dist/{BlockNoteViewer-K2HBF3IP.js → BlockNoteViewer-6KQJODNF.js} +9 -7
  6. package/dist/BlockNoteViewer-6KQJODNF.js.map +1 -0
  7. package/dist/{BlockNoteViewer-2TJZ6JET.mjs → BlockNoteViewer-TQ452ZYW.mjs} +8 -6
  8. package/dist/{BlockNoteViewer-2TJZ6JET.mjs.map → BlockNoteViewer-TQ452ZYW.mjs.map} +1 -1
  9. package/dist/JsonApiRequest-OKNQZLS6.js +26 -0
  10. package/dist/{JsonApiRequest-KC6DOHKT.js.map → JsonApiRequest-OKNQZLS6.js.map} +1 -1
  11. package/dist/{JsonApiRequest-ADLBJ5RX.mjs → JsonApiRequest-WNPDS356.mjs} +2 -2
  12. package/dist/billing/index.js +504 -517
  13. package/dist/billing/index.js.map +1 -1
  14. package/dist/billing/index.mjs +20 -33
  15. package/dist/billing/index.mjs.map +1 -1
  16. package/dist/{chunk-VNUT2D27.mjs → chunk-2PWYG2WJ.mjs} +2 -2
  17. package/dist/{chunk-W4ML6D2X.mjs → chunk-3U7YJ73T.mjs} +7 -7
  18. package/dist/{chunk-BQ2WAG4K.js → chunk-3VNDFKT7.js} +19 -19
  19. package/dist/{chunk-BQ2WAG4K.js.map → chunk-3VNDFKT7.js.map} +1 -1
  20. package/dist/{chunk-BE6UMM5X.mjs → chunk-47HNJQ7U.mjs} +3 -3
  21. package/dist/chunk-5T2AQMQ2.mjs +90 -0
  22. package/dist/chunk-5T2AQMQ2.mjs.map +1 -0
  23. package/dist/{chunk-A5M3MORY.js → chunk-5WM72MLE.js} +3 -3
  24. package/dist/{chunk-A5M3MORY.js.map → chunk-5WM72MLE.js.map} +1 -1
  25. package/dist/{chunk-S7SPAC6X.mjs → chunk-73NKUBMP.mjs} +3 -3
  26. package/dist/chunk-743M4TIB.mjs +76 -0
  27. package/dist/chunk-743M4TIB.mjs.map +1 -0
  28. package/dist/{chunk-HN7L6RRW.js → chunk-CSBQAPHD.js} +903 -955
  29. package/dist/chunk-CSBQAPHD.js.map +1 -0
  30. package/dist/{chunk-YTDGYI62.mjs → chunk-DWPHHUMV.mjs} +3 -3
  31. package/dist/{chunk-53SD4YRF.js → chunk-FBHUUPRV.js} +5 -5
  32. package/dist/{chunk-53SD4YRF.js.map → chunk-FBHUUPRV.js.map} +1 -1
  33. package/dist/{chunk-T6G2F2BK.mjs → chunk-HJT7TXGG.mjs} +16 -68
  34. package/dist/chunk-HJT7TXGG.mjs.map +1 -0
  35. package/dist/chunk-HROKKUBQ.js +156 -0
  36. package/dist/chunk-HROKKUBQ.js.map +1 -0
  37. package/dist/{chunk-TMV7TL7O.mjs → chunk-JELEQV5T.mjs} +8 -6
  38. package/dist/{chunk-TMV7TL7O.mjs.map → chunk-JELEQV5T.mjs.map} +1 -1
  39. package/dist/{chunk-TNTCLVUR.js → chunk-JGL4P3DS.js} +17 -15
  40. package/dist/chunk-JGL4P3DS.js.map +1 -0
  41. package/dist/{chunk-7OIWOPM5.mjs → chunk-K7KLUYS7.mjs} +4 -3
  42. package/dist/chunk-K7KLUYS7.mjs.map +1 -0
  43. package/dist/{chunk-HP6DT6K3.mjs → chunk-KVYYPKRO.mjs} +311 -241
  44. package/dist/chunk-KVYYPKRO.mjs.map +1 -0
  45. package/dist/{chunk-RCOKQ3R4.js → chunk-LNO2JLU3.js} +2 -2
  46. package/dist/{chunk-RCOKQ3R4.js.map → chunk-LNO2JLU3.js.map} +1 -1
  47. package/dist/{chunk-R5722NLL.js → chunk-M6SNTMK6.js} +1388 -1318
  48. package/dist/chunk-M6SNTMK6.js.map +1 -0
  49. package/dist/{chunk-GMMDXINC.js → chunk-N3NQS7X6.js} +12 -12
  50. package/dist/{chunk-GMMDXINC.js.map → chunk-N3NQS7X6.js.map} +1 -1
  51. package/dist/chunk-NP3AFULU.js +76 -0
  52. package/dist/chunk-NP3AFULU.js.map +1 -0
  53. package/dist/{chunk-SO3YUCL6.js → chunk-OOM54Z5Z.js} +6 -5
  54. package/dist/chunk-OOM54Z5Z.js.map +1 -0
  55. package/dist/{chunk-YSE2Z4RB.js → chunk-PQQFOONG.js} +12 -12
  56. package/dist/{chunk-YSE2Z4RB.js.map → chunk-PQQFOONG.js.map} +1 -1
  57. package/dist/chunk-RW6XZGZG.mjs +156 -0
  58. package/dist/chunk-RW6XZGZG.mjs.map +1 -0
  59. package/dist/chunk-SYMI4IDW.js +90 -0
  60. package/dist/chunk-SYMI4IDW.js.map +1 -0
  61. package/dist/{chunk-N2BFT7KI.mjs → chunk-W7U6O3OE.mjs} +2 -71
  62. package/dist/chunk-W7U6O3OE.mjs.map +1 -0
  63. package/dist/{chunk-YMCL3JXZ.mjs → chunk-YPBIPE2B.mjs} +2 -2
  64. package/dist/{chunk-P7L6OFX5.js → chunk-Z6T2575A.js} +12 -81
  65. package/dist/chunk-Z6T2575A.js.map +1 -0
  66. package/dist/client/index.js +13 -10
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/client/index.mjs +17 -14
  69. package/dist/components/forms/DatePickerPopover.d.ts.map +1 -1
  70. package/dist/components/forms/DateRangeSelector.d.ts.map +1 -1
  71. package/dist/components/forms/EditorSheet.d.ts +6 -1
  72. package/dist/components/forms/EditorSheet.d.ts.map +1 -1
  73. package/dist/components/forms/FormDate.d.ts.map +1 -1
  74. package/dist/components/forms/FormDateTime.d.ts.map +1 -1
  75. package/dist/components/forms/time-cycle.d.ts +14 -0
  76. package/dist/components/forms/time-cycle.d.ts.map +1 -0
  77. package/dist/components/index.js +11 -8
  78. package/dist/components/index.js.map +1 -1
  79. package/dist/components/index.mjs +10 -7
  80. package/dist/components/tables/cells/cell.date.d.ts.map +1 -1
  81. package/dist/components/tables/cells/cell.dateTime.d.ts.map +1 -1
  82. package/dist/contexts/DateLocaleContext.d.ts +56 -0
  83. package/dist/contexts/DateLocaleContext.d.ts.map +1 -0
  84. package/dist/contexts/index.d.ts +1 -0
  85. package/dist/contexts/index.d.ts.map +1 -1
  86. package/dist/contexts/index.js +24 -8
  87. package/dist/contexts/index.js.map +1 -1
  88. package/dist/contexts/index.mjs +23 -7
  89. package/dist/core/index.js +17 -6
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/index.mjs +19 -8
  92. package/dist/date-locale/index.d.ts +2 -0
  93. package/dist/date-locale/index.d.ts.map +1 -0
  94. package/dist/date-locale/index.js +21 -0
  95. package/dist/date-locale/index.js.map +1 -0
  96. package/dist/date-locale/index.mjs +21 -0
  97. package/dist/email-automations/index.js +131 -123
  98. package/dist/email-automations/index.js.map +1 -1
  99. package/dist/email-automations/index.mjs +23 -15
  100. package/dist/email-automations/index.mjs.map +1 -1
  101. package/dist/features/account-deletion/components/AccountDeletionAdminTrigger.d.ts.map +1 -1
  102. package/dist/features/analytics/components/AnalyticsJourneyContainer.d.ts.map +1 -1
  103. package/dist/features/analytics/components/AnalyticsSessionsTable.d.ts.map +1 -1
  104. package/dist/features/analytics/index.js +154 -147
  105. package/dist/features/analytics/index.js.map +1 -1
  106. package/dist/features/analytics/index.mjs +32 -25
  107. package/dist/features/analytics/index.mjs.map +1 -1
  108. package/dist/features/analytics/server.js +11 -9
  109. package/dist/features/analytics/server.js.map +1 -1
  110. package/dist/features/analytics/server.mjs +9 -7
  111. package/dist/features/analytics/server.mjs.map +1 -1
  112. package/dist/features/billing/components/utils/date.d.ts +3 -3
  113. package/dist/features/billing/components/utils/format-options.d.ts +6 -4
  114. package/dist/features/billing/components/utils/format-options.d.ts.map +1 -1
  115. package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts +5 -1
  116. package/dist/features/billing/components/utils/useBillingFormatOptions.d.ts.map +1 -1
  117. package/dist/features/billing/stripe-usage/components/details/UsageSummaryCard.d.ts.map +1 -1
  118. package/dist/features/billing/stripe-usage/components/lists/UsageHistoryTable.d.ts.map +1 -1
  119. package/dist/features/blog/components/admin/BlogTranslationStatus.d.ts.map +1 -1
  120. package/dist/features/blog/components/public/BlogPostMeta.d.ts.map +1 -1
  121. package/dist/features/blog/hooks/useBlogPostTableStructure.d.ts.map +1 -1
  122. package/dist/features/blog/index.js +168 -162
  123. package/dist/features/blog/index.js.map +1 -1
  124. package/dist/features/blog/index.mjs +25 -19
  125. package/dist/features/blog/index.mjs.map +1 -1
  126. package/dist/features/blog/server.js +29 -27
  127. package/dist/features/blog/server.js.map +1 -1
  128. package/dist/features/blog/server.mjs +8 -6
  129. package/dist/features/blog/server.mjs.map +1 -1
  130. package/dist/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.d.ts.map +1 -1
  131. package/dist/features/handbook/components/parts/HandbookIndexStatus.d.ts.map +1 -1
  132. package/dist/features/help/components/HelpArticleBody.d.ts.map +1 -1
  133. package/dist/features/help/index.js +48 -43
  134. package/dist/features/help/index.js.map +1 -1
  135. package/dist/features/help/index.mjs +15 -10
  136. package/dist/features/help/index.mjs.map +1 -1
  137. package/dist/features/notification/components/notifications/Notification.d.ts +2 -1
  138. package/dist/features/notification/components/notifications/Notification.d.ts.map +1 -1
  139. package/dist/features/notification/contexts/NotificationContext.d.ts.map +1 -1
  140. package/dist/features/notification/hooks/useNotificationTableStructure.d.ts.map +1 -1
  141. package/dist/features/oauth/components/OAuthClientCard.d.ts.map +1 -1
  142. package/dist/features/oauth/components/OAuthClientDetail.d.ts.map +1 -1
  143. package/dist/features/tokenusage/index.js +133 -130
  144. package/dist/features/tokenusage/index.js.map +1 -1
  145. package/dist/features/tokenusage/index.mjs +10 -7
  146. package/dist/features/tokenusage/index.mjs.map +1 -1
  147. package/dist/features/tokenusage/lib/formatters.d.ts +6 -3
  148. package/dist/features/tokenusage/lib/formatters.d.ts.map +1 -1
  149. package/dist/index.js +19 -7
  150. package/dist/index.js.map +1 -1
  151. package/dist/index.mjs +23 -11
  152. package/dist/local-transport-AT4TE264.js +15 -0
  153. package/dist/{local-transport-MJPOKPTU.js.map → local-transport-AT4TE264.js.map} +1 -1
  154. package/dist/{local-transport-RVD4FZGA.mjs → local-transport-KWEUZ4GX.mjs} +5 -5
  155. package/dist/offline/index.js +56 -54
  156. package/dist/offline/index.js.map +1 -1
  157. package/dist/offline/index.mjs +23 -21
  158. package/dist/offline/index.mjs.map +1 -1
  159. package/dist/{request-4AWGPIGH.mjs → request-6GAIWWTG.mjs} +2 -2
  160. package/dist/{request-H5FZBZA2.js → request-BWO4Y434.js} +3 -3
  161. package/dist/{request-H5FZBZA2.js.map → request-BWO4Y434.js.map} +1 -1
  162. package/dist/server/ServerSession.d.ts +2 -0
  163. package/dist/server/ServerSession.d.ts.map +1 -1
  164. package/dist/server/index.js +10 -8
  165. package/dist/server/index.js.map +1 -1
  166. package/dist/server/index.mjs +9 -7
  167. package/dist/utils/date-formatter.d.ts +45 -4
  168. package/dist/utils/date-formatter.d.ts.map +1 -1
  169. package/dist/utils/index.d.ts +1 -1
  170. package/dist/utils/index.d.ts.map +1 -1
  171. package/dist/write-through-KNRI5L4V.mjs +15 -0
  172. package/dist/write-through-KNRI5L4V.mjs.map +1 -0
  173. package/dist/write-through-MYHQ6GJU.js +15 -0
  174. package/dist/{write-through-HE2QSOVI.js.map → write-through-MYHQ6GJU.js.map} +1 -1
  175. package/messages/en.json +1 -0
  176. package/messages/it.json +1 -0
  177. package/package.json +6 -1
  178. package/src/components/forms/DatePickerPopover.tsx +5 -3
  179. package/src/components/forms/DateRangeSelector.tsx +9 -7
  180. package/src/components/forms/EditorSheet.tsx +8 -0
  181. package/src/components/forms/FormDate.tsx +5 -3
  182. package/src/components/forms/FormDateTime.tsx +75 -39
  183. package/src/components/forms/__tests__/EditorSheet.spec.tsx +30 -0
  184. package/src/components/forms/__tests__/time-cycle.spec.ts +37 -0
  185. package/src/components/forms/time-cycle.ts +27 -0
  186. package/src/components/tables/cells/cell.date.tsx +3 -5
  187. package/src/components/tables/cells/cell.dateTime.tsx +3 -7
  188. package/src/contexts/DateLocaleContext.tsx +106 -0
  189. package/src/contexts/__tests__/DateLocaleContext.spec.tsx +175 -0
  190. package/src/contexts/index.ts +1 -0
  191. package/src/date-locale/index.ts +12 -0
  192. package/src/features/account-deletion/components/AccountDeletionAdminTrigger.tsx +3 -4
  193. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +8 -8
  194. package/src/features/analytics/components/AnalyticsSessionsTable.tsx +4 -6
  195. package/src/features/auth/components/two-factor/PasskeyList.tsx +3 -3
  196. package/src/features/auth/components/two-factor/TotpAuthenticatorList.tsx +3 -3
  197. package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +14 -6
  198. package/src/features/billing/components/utils/date.ts +5 -5
  199. package/src/features/billing/components/utils/format-options.ts +6 -4
  200. package/src/features/billing/components/utils/useBillingFormatOptions.ts +8 -2
  201. package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +4 -12
  202. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +4 -17
  203. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +3 -2
  204. package/src/features/blog/components/public/BlogPostMeta.tsx +5 -4
  205. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +1 -2
  206. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +2 -3
  207. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +4 -8
  208. package/src/features/email-automation/hooks/useEmailAutomationEnrollmentTableStructure.tsx +11 -5
  209. package/src/features/email-blast/components/containers/EmailBlastContainer.tsx +3 -3
  210. package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +4 -4
  211. package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +4 -3
  212. package/src/features/help/components/HelpArticleBody.tsx +4 -3
  213. package/src/features/notification/components/notifications/Notification.tsx +38 -7
  214. package/src/features/notification/contexts/NotificationContext.tsx +6 -3
  215. package/src/features/notification/hooks/useNotificationTableStructure.tsx +6 -5
  216. package/src/features/oauth/components/OAuthClientCard.tsx +8 -5
  217. package/src/features/oauth/components/OAuthClientDetail.tsx +3 -6
  218. package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +19 -0
  219. package/src/features/tokenusage/lib/formatters.ts +11 -6
  220. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +5 -5
  221. package/src/server/ServerSession.ts +3 -0
  222. package/src/server/__tests__/ServerSession.read.spec.ts +18 -1
  223. package/src/utils/__tests__/date-formatter.spec.ts +134 -10
  224. package/src/utils/date-formatter.ts +200 -44
  225. package/src/utils/index.ts +11 -1
  226. package/dist/BlockNoteEditor-PPRML5CO.js.map +0 -1
  227. package/dist/BlockNoteViewer-K2HBF3IP.js.map +0 -1
  228. package/dist/JsonApiRequest-KC6DOHKT.js +0 -26
  229. package/dist/chunk-7OIWOPM5.mjs.map +0 -1
  230. package/dist/chunk-HN7L6RRW.js.map +0 -1
  231. package/dist/chunk-HP6DT6K3.mjs.map +0 -1
  232. package/dist/chunk-N2BFT7KI.mjs.map +0 -1
  233. package/dist/chunk-P7L6OFX5.js.map +0 -1
  234. package/dist/chunk-R5722NLL.js.map +0 -1
  235. package/dist/chunk-SO3YUCL6.js.map +0 -1
  236. package/dist/chunk-T6G2F2BK.mjs.map +0 -1
  237. package/dist/chunk-TNTCLVUR.js.map +0 -1
  238. package/dist/local-transport-MJPOKPTU.js +0 -15
  239. package/dist/write-through-7I3LA2WN.mjs +0 -15
  240. package/dist/write-through-HE2QSOVI.js +0 -15
  241. /package/dist/{JsonApiRequest-ADLBJ5RX.mjs.map → JsonApiRequest-WNPDS356.mjs.map} +0 -0
  242. /package/dist/{chunk-VNUT2D27.mjs.map → chunk-2PWYG2WJ.mjs.map} +0 -0
  243. /package/dist/{chunk-W4ML6D2X.mjs.map → chunk-3U7YJ73T.mjs.map} +0 -0
  244. /package/dist/{chunk-BE6UMM5X.mjs.map → chunk-47HNJQ7U.mjs.map} +0 -0
  245. /package/dist/{chunk-S7SPAC6X.mjs.map → chunk-73NKUBMP.mjs.map} +0 -0
  246. /package/dist/{chunk-YTDGYI62.mjs.map → chunk-DWPHHUMV.mjs.map} +0 -0
  247. /package/dist/{chunk-YMCL3JXZ.mjs.map → chunk-YPBIPE2B.mjs.map} +0 -0
  248. /package/dist/{local-transport-RVD4FZGA.mjs.map → date-locale/index.mjs.map} +0 -0
  249. /package/dist/{request-4AWGPIGH.mjs.map → local-transport-KWEUZ4GX.mjs.map} +0 -0
  250. /package/dist/{write-through-7I3LA2WN.mjs.map → request-6GAIWWTG.mjs.map} +0 -0
@@ -11,12 +11,53 @@ type FormatOption = "date" | "time" | "dateTime" | "timeSince" | "default";
11
11
  * instant in time and the UTC shift is correct.
12
12
  */
13
13
  export declare const formatLocalDate: (d: Date) => string;
14
+ export type DateFormatStyle = "date" | "dateMedium" | "dateLong" | "dateTime" | "dateTimeMedium" | "dateTimeLong" | "dateTimeSeconds" | "time" | "weekdayDateTime" | "weekdayDay" | "monthYear" | "monthYearLong" | "month" | "monthLong" | "dayMonth" | "weekday" | "weekdayLong";
14
15
  /**
15
- * The locale `formatDate` should use for an app locale. English keeps "en-GB",
16
- * the formatter's historical default, so English output never changes; any
17
- * other app locale ("it") is used as is.
16
+ * The option set behind each named style. No style pins `hour12` / `hourCycle`:
17
+ * the clock (12 or 24 hours) follows the locale.
18
18
  */
19
+ export declare const DATE_STYLE_OPTIONS: Record<DateFormatStyle, Intl.DateTimeFormatOptions>;
20
+ /** Public pages never show an all-numeric date: the month is spelled out. */
21
+ export declare const PUBLIC_STYLE_MAP: Partial<Record<DateFormatStyle, DateFormatStyle>>;
22
+ /**
23
+ * The locale every real-world date is formatted in.
24
+ *
25
+ * The first `Accept-Language` entry (quality order) whose language equals the
26
+ * app language's and that `Intl` supports; otherwise the app language. A
27
+ * malformed entry (`*`, an unparseable or zero quality, an unknown tag) is
28
+ * skipped, never thrown on.
29
+ */
30
+ export declare function resolveDateLocale(params: {
31
+ appLocale: string;
32
+ acceptLanguage?: string;
33
+ }): string;
34
+ /**
35
+ * Formats a date in a named style. Returns "" for `undefined` or a value that
36
+ * does not parse. `isPublic` swaps the all-numeric styles for month-as-word.
37
+ */
38
+ export declare function formatDateStyle(params: {
39
+ date: Date | string | undefined;
40
+ style: DateFormatStyle;
41
+ locale: string;
42
+ timeZone?: string;
43
+ isPublic?: boolean;
44
+ }): string;
45
+ /** "3 hours ago" / "3 ore fa", always numeric ("1 day ago", never "yesterday"). */
46
+ export declare function formatRelativeDate(params: {
47
+ date: Date | string;
48
+ locale: string;
49
+ now?: Date;
50
+ }): string;
51
+ /** True when the locale's clock is 12-hour (hour cycle h11 / h12). */
52
+ export declare function usesTwelveHourClock(locale: string): boolean;
53
+ /** The locale to format dates in for an app locale: the app locale itself, English when missing. */
19
54
  export declare const dateFormatLocale: (appLocale?: string) => string;
20
- export declare const formatDate: (eventDate: Date, formatOption: FormatOption, locale?: string) => string;
55
+ /**
56
+ * @deprecated Use `formatDateStyle` (or `useDateFormatter().format`) and
57
+ * `formatRelativeDate` (or `useDateFormatter().relative`). Kept for other nja
58
+ * apps: same default locale and the same "date" / "time" / "dateTime" output.
59
+ * Pass `now` from render code; the `new Date()` fallback is for legacy callers.
60
+ */
61
+ export declare const formatDate: (eventDate: Date, formatOption: FormatOption, locale?: string, now?: Date) => string;
21
62
  export type { FormatOption };
22
63
  //# sourceMappingURL=date-formatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"date-formatter.d.ts","sourceRoot":"","sources":["../../src/utils/date-formatter.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,GAAG,IAAI,KAAG,MAKzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,MACgB,CAAC;AAEvE,eAAO,MAAM,UAAU,GAAI,WAAW,IAAI,EAAE,cAAc,YAAY,EAAE,SAAQ,MAAgB,KAAG,MAsDlG,CAAC;AAEF,YAAY,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"date-formatter.d.ts","sourceRoot":"","sources":["../../src/utils/date-formatter.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,CAAC;AAE3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,GAAG,IAAI,KAAG,MAKzC,CAAC;AAEF,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,YAAY,GACZ,UAAU,GACV,UAAU,GACV,gBAAgB,GAChB,cAAc,GACd,iBAAiB,GACjB,MAAM,GACN,iBAAiB,GACjB,YAAY,GACZ,WAAW,GACX,eAAe,GACf,OAAO,GACP,WAAW,GACX,UAAU,GACV,SAAS,GACT,aAAa,CAAC;AAIlB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,qBAAqB,CAkBlF,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAI9E,CAAC;AAyCF;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAqChG;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,MAAM,CAKT;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CAAE,GAAG,MAAM,CAgBtG;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED,oGAAoG;AACpG,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,MAA2B,CAAC;AAKlF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GACrB,WAAW,IAAI,EACf,cAAc,YAAY,EAC1B,SAAQ,MAAgB,EACxB,MAAM,IAAI,KACT,MAqBF,CAAC;AAEF,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -2,7 +2,7 @@ export type { ClassValue } from "clsx";
2
2
  export { cn } from "./cn";
3
3
  export { composeRefs, useComposedRefs } from "./compose-refs";
4
4
  export { useIsMobile, MOBILE_BREAKPOINT, VIEWPORT_COOKIE_NAME } from "./use-mobile";
5
- export { dateFormatLocale, formatDate, formatLocalDate, type FormatOption } from "./date-formatter";
5
+ export { dateFormatLocale, formatDate, formatDateStyle, formatLocalDate, formatRelativeDate, resolveDateLocale, usesTwelveHourClock, type DateFormatStyle, type FormatOption, } from "./date-formatter";
6
6
  export { exists } from "./exists";
7
7
  export { getTableComponents, getTableOptions, TableOptions } from "./table-options";
8
8
  export { entityObjectSchema, userObjectSchema, type EntityObject, type UserObject } from "./schemas";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAErG,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,8BAA8B,GACpC,MAAM,sBAAsB,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qCAAqC,CAAC;AACpD,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGpF,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAErG,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,8BAA8B,GACpC,MAAM,sBAAsB,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qCAAqC,CAAC;AACpD,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,15 @@
1
+ import {
2
+ writeThrough
3
+ } from "./chunk-3U7YJ73T.mjs";
4
+ import "./chunk-DWPHHUMV.mjs";
5
+ import "./chunk-4RPGFQBA.mjs";
6
+ import "./chunk-47HNJQ7U.mjs";
7
+ import "./chunk-73NKUBMP.mjs";
8
+ import "./chunk-4ZJIUTIV.mjs";
9
+ import "./chunk-YPBIPE2B.mjs";
10
+ import "./chunk-AUXK7QSA.mjs";
11
+ import "./chunk-PAWJFY3S.mjs";
12
+ export {
13
+ writeThrough
14
+ };
15
+ //# sourceMappingURL=write-through-KNRI5L4V.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,15 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+ var _chunkPQQFOONGjs = require('./chunk-PQQFOONG.js');
4
+ require('./chunk-FBHUUPRV.js');
5
+ require('./chunk-6RCZ64BQ.js');
6
+ require('./chunk-3VNDFKT7.js');
7
+ require('./chunk-5WM72MLE.js');
8
+ require('./chunk-WUNSG3IN.js');
9
+ require('./chunk-LNO2JLU3.js');
10
+ require('./chunk-LXKSUWAV.js');
11
+ require('./chunk-7QVYU63E.js');
12
+
13
+
14
+ exports.writeThrough = _chunkPQQFOONGjs.writeThrough;
15
+ //# sourceMappingURL=write-through-MYHQ6GJU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-HE2QSOVI.js"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B;AACE;AACF,qDAAC","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-HE2QSOVI.js"}
1
+ {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-MYHQ6GJU.js"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B;AACE;AACF,qDAAC","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-MYHQ6GJU.js"}
package/messages/en.json CHANGED
@@ -1102,6 +1102,7 @@
1102
1102
  },
1103
1103
  "hide_password": "Hide Password",
1104
1104
  "home": "Home",
1105
+ "dayPeriod": "AM/PM",
1105
1106
  "hours": "Hours",
1106
1107
  "loading": "Loading...",
1107
1108
  "minutes": "Minutes",
package/messages/it.json CHANGED
@@ -1102,6 +1102,7 @@
1102
1102
  },
1103
1103
  "hide_password": "Nascondi password",
1104
1104
  "home": "Home",
1105
+ "dayPeriod": "AM/PM",
1105
1106
  "hours": "Ore",
1106
1107
  "loading": "Caricamento...",
1107
1108
  "minutes": "Minuti",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "6.23.0",
3
+ "version": "6.25.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -53,6 +53,11 @@
53
53
  "import": "./dist/contexts/index.mjs",
54
54
  "require": "./dist/contexts/index.js"
55
55
  },
56
+ "./date-locale": {
57
+ "types": "./dist/date-locale/index.d.ts",
58
+ "import": "./dist/date-locale/index.mjs",
59
+ "require": "./dist/date-locale/index.js"
60
+ },
56
61
  "./contexts/styles.css": "./dist/contexts/index.css",
57
62
  "./blocknote/server": {
58
63
  "types": "./dist/components/editors/server-entry.d.ts",
@@ -3,7 +3,8 @@
3
3
  import { isValid, parse } from "date-fns";
4
4
  import { Calendar as CalendarIcon, CircleXIcon } from "lucide-react";
5
5
  import { ReactElement, useMemo, useState } from "react";
6
- import { useI18nDateFnsLocale, useI18nLocale } from "../../i18n";
6
+ import { useDateLocale } from "../../contexts/DateLocaleContext";
7
+ import { useI18nDateFnsLocale } from "../../i18n";
7
8
  import {
8
9
  Calendar,
9
10
  Input,
@@ -48,8 +49,9 @@ export const DatePickerPopover = ({
48
49
  align = "start",
49
50
  className,
50
51
  }: DatePickerPopoverProps) => {
51
- const locale = useI18nLocale();
52
+ const { locale } = useDateLocale();
52
53
  const dateFnsLocale = useI18nDateFnsLocale();
54
+ const weekStartsOn = (dateFnsLocale?.options?.weekStartsOn ?? 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
53
55
  const [isOpen, setIsOpen] = useState(false);
54
56
  const [displayMonth, setDisplayMonth] = useState<Date>(() => value || new Date());
55
57
 
@@ -239,7 +241,7 @@ export const DatePickerPopover = ({
239
241
  onSelect={handleCalendarSelect}
240
242
  disabled={(date) => (minDate && date < minDate ? true : false)}
241
243
  locale={dateFnsLocale}
242
- weekStartsOn={1}
244
+ weekStartsOn={weekStartsOn}
243
245
  month={displayMonth}
244
246
  onMonthChange={setDisplayMonth}
245
247
  />
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
- import { format } from "date-fns";
4
3
  import { CalendarIcon } from "lucide-react";
5
4
  import { useTranslations } from "next-intl";
6
5
  import { useEffect, useState } from "react";
7
6
  import { DateRange } from "react-day-picker";
7
+ import { useDateFormatter } from "../../contexts/DateLocaleContext";
8
8
  import { useI18nDateFnsLocale } from "../../i18n";
9
9
  import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "../../shadcnui";
10
10
  import { cn } from "../../utils";
@@ -50,7 +50,9 @@ type DateRangeSelectorProps = {
50
50
 
51
51
  export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviousMonth }: DateRangeSelectorProps) {
52
52
  const t = useTranslations();
53
+ const { format } = useDateFormatter();
53
54
  const dateFnsLocale = useI18nDateFnsLocale();
55
+ const weekStartsOn = (dateFnsLocale?.options?.weekStartsOn ?? 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
54
56
  const [date, setDate] = useState<DateRange | undefined>(
55
57
  avoidSettingDates
56
58
  ? undefined
@@ -100,11 +102,10 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
100
102
  {date?.from ? (
101
103
  date.to ? (
102
104
  <>
103
- {format(date.from, "d MMM yyyy", { locale: dateFnsLocale })} -{" "}
104
- {format(date.to, "d MMM yyyy", { locale: dateFnsLocale })}
105
+ {format(date.from, "dateMedium")} - {format(date.to, "dateMedium")}
105
106
  </>
106
107
  ) : (
107
- format(date.from, "d MMM yyyy", { locale: dateFnsLocale })
108
+ format(date.from, "dateMedium")
108
109
  )
109
110
  ) : (
110
111
  <span>{t("ui.labels.pick_a_date")}</span>
@@ -130,11 +131,10 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
130
131
  {date?.from ? (
131
132
  date.to ? (
132
133
  <>
133
- {format(date.from, "d MMM yyyy", { locale: dateFnsLocale })} -{" "}
134
- {format(date.to, "d MMM yyyy", { locale: dateFnsLocale })}
134
+ {format(date.from, "dateMedium")} - {format(date.to, "dateMedium")}
135
135
  </>
136
136
  ) : (
137
- format(date.from, "d MMM yyyy", { locale: dateFnsLocale })
137
+ format(date.from, "dateMedium")
138
138
  )
139
139
  ) : (
140
140
  <span>{t("ui.labels.pick_a_date")}</span>
@@ -151,6 +151,8 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
151
151
  selected={date}
152
152
  onSelect={handleSelect}
153
153
  numberOfMonths={2}
154
+ locale={dateFnsLocale}
155
+ weekStartsOn={weekStartsOn}
154
156
  />
155
157
  <Button
156
158
  variant="secondary"
@@ -93,6 +93,11 @@ export type EditorSheetProps<T extends FieldValues> = {
93
93
  * sheet stays open with the form unchanged. Use it for a last confirmation
94
94
  * (e.g. "this information will be deleted") that must not close the sheet. */
95
95
  onBeforeSubmit?: (values: T) => Promise<boolean> | boolean;
96
+ /** Optional. Called when `onSubmit` throws. Return `true` when the caller
97
+ * showed its own message for that error: the generic "could not save" toast
98
+ * is then skipped. Either way the sheet stays open with the user's input.
99
+ * Omitted, every error shows the generic toast (the default). */
100
+ onSubmitError?: (error: unknown) => boolean;
96
101
  onReset: () => T;
97
102
  isFormDirty?: () => boolean;
98
103
 
@@ -150,6 +155,7 @@ export function EditorSheet<T extends FieldValues>({
150
155
  form,
151
156
  onSubmit,
152
157
  onBeforeSubmit,
158
+ onSubmitError,
153
159
  onReset,
154
160
  isFormDirty: isFormDirtyProp,
155
161
  entityType,
@@ -250,6 +256,7 @@ export function EditorSheet<T extends FieldValues>({
250
256
  }
251
257
  }
252
258
  } catch (error) {
259
+ if (onSubmitError?.(error)) return;
253
260
  errorToast({
254
261
  title: isEdit ? t("generic.errors.update") : t("generic.errors.create"),
255
262
  error,
@@ -259,6 +266,7 @@ export function EditorSheet<T extends FieldValues>({
259
266
  [
260
267
  onSubmit,
261
268
  onBeforeSubmit,
269
+ onSubmitError,
262
270
  setOpen,
263
271
  onSuccess,
264
272
  onSaved,
@@ -3,7 +3,8 @@
3
3
  import { isValid, parse } from "date-fns";
4
4
  import { Calendar as CalendarIcon, CircleXIcon } from "lucide-react";
5
5
  import { useMemo, useState } from "react";
6
- import { useI18nDateFnsLocale, useI18nLocale, useI18nTranslations } from "../../i18n";
6
+ import { useDateLocale } from "../../contexts/DateLocaleContext";
7
+ import { useI18nDateFnsLocale, useI18nTranslations } from "../../i18n";
7
8
  import {
8
9
  Button,
9
10
  Calendar,
@@ -42,8 +43,9 @@ export function FormDate({
42
43
  futureYears?: number;
43
44
  }) {
44
45
  const t = useI18nTranslations();
45
- const locale = useI18nLocale();
46
+ const { locale } = useDateLocale();
46
47
  const dateFnsLocale = useI18nDateFnsLocale();
48
+ const weekStartsOn = (dateFnsLocale?.options?.weekStartsOn ?? 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
47
49
  const [open, setOpen] = useState<boolean>(false);
48
50
  const [displayMonth, setDisplayMonth] = useState<Date>(() => {
49
51
  const currentValue = form.getValues(id);
@@ -162,7 +164,7 @@ export function FormDate({
162
164
  }}
163
165
  disabled={(date) => (minDate && date < minDate ? true : false)}
164
166
  locale={dateFnsLocale}
165
- weekStartsOn={1}
167
+ weekStartsOn={weekStartsOn}
166
168
  month={displayMonth}
167
169
  onMonthChange={setDisplayMonth}
168
170
  startMonth={new Date(1900, 0)}
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { Calendar as CalendarIcon, CircleXIcon } from "lucide-react";
4
4
  import { useMemo, useState } from "react";
5
- import { useI18nDateFnsLocale, useI18nLocale, useI18nTranslations } from "../../i18n";
5
+ import { useDateFormatter } from "../../contexts/DateLocaleContext";
6
+ import { useI18nDateFnsLocale, useI18nTranslations } from "../../i18n";
6
7
  import {
7
8
  Button,
8
9
  Calendar,
@@ -18,6 +19,7 @@ import {
18
19
  } from "../../shadcnui";
19
20
  import { cn } from "../../utils";
20
21
  import { FormFieldWrapper } from "./FormFieldWrapper";
22
+ import { DayPeriod, dayPeriodLabels, from24Hour, to24Hour } from "./time-cycle";
21
23
 
22
24
  // Unwrapped rounding: setMinutes(60) carries into the hour, so 10:58 becomes 11:00
23
25
  function snapToFiveMinutes(date: Date): Date {
@@ -57,30 +59,20 @@ export function FormDateTime({
57
59
  }) {
58
60
  const [open, setOpen] = useState<boolean>(false);
59
61
  const t = useI18nTranslations();
60
- const locale = useI18nLocale();
62
+ const { format, locale, twelveHourClock } = useDateFormatter();
61
63
  const dateFnsLocale = useI18nDateFnsLocale();
64
+ const weekStartsOn = (dateFnsLocale?.options?.weekStartsOn ?? 1) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
62
65
 
63
66
  const [displayMonth, setDisplayMonth] = useState<Date>(() => {
64
67
  const currentValue = form.getValues(id);
65
68
  return currentValue || defaultMonth || reference?.date || new Date();
66
69
  });
67
70
 
68
- // Locale-aware date-time formatter
69
- const dateTimeFormatter = useMemo(
70
- () =>
71
- new Intl.DateTimeFormat(locale, {
72
- year: "numeric",
73
- month: "long",
74
- day: "numeric",
75
- hour: "2-digit",
76
- minute: "2-digit",
77
- hour12: false,
78
- }),
79
- [locale],
80
- );
71
+ // Format date-time for display; the clock (12/24-hour) follows the resolved locale
72
+ const formatDateTime = (date: Date): string => format(date, "dateTimeLong");
81
73
 
82
- // Format date-time for display
83
- const formatDateTime = (date: Date): string => dateTimeFormatter.format(date);
74
+ // AM/PM labels from Intl, only needed on a 12-hour locale
75
+ const periodLabels = useMemo(() => dayPeriodLabels(locale), [locale]);
84
76
 
85
77
  // An empty field starts its time selects at the reference "now" when one is given
86
78
  // Snapped as an instant (not by wrapping minutes) so 10:58 starts at 11:00
@@ -100,13 +92,27 @@ export function FormDateTime({
100
92
  marked: "[&>button]:underline [&>button]:decoration-2 [&>button]:underline-offset-4",
101
93
  };
102
94
 
103
- const hoursOptions = Array.from({ length: 24 }, (_, i) => {
104
- const hour = i;
105
- return {
106
- value: hour,
107
- label: hour.toString().padStart(2, "0"),
108
- };
109
- });
95
+ // 12-hour locales list 1–12 (plus an AM/PM select); 24-hour locales list 0–23
96
+ const hoursOptions = twelveHourClock
97
+ ? Array.from({ length: 12 }, (_, i) => {
98
+ const hour = i + 1;
99
+ return {
100
+ value: hour,
101
+ label: hour.toString(),
102
+ };
103
+ })
104
+ : Array.from({ length: 24 }, (_, i) => {
105
+ const hour = i;
106
+ return {
107
+ value: hour,
108
+ label: hour.toString().padStart(2, "0"),
109
+ };
110
+ });
111
+
112
+ const periodOptions: { value: DayPeriod; label: string }[] = [
113
+ { value: "am", label: periodLabels.am },
114
+ { value: "pm", label: periodLabels.pm },
115
+ ];
110
116
 
111
117
  const minutesOptions = Array.from({ length: 12 }, (_, i) => {
112
118
  const minute = i * 5;
@@ -118,6 +124,7 @@ export function FormDateTime({
118
124
 
119
125
  const hoursItems = Object.fromEntries(hoursOptions.map((option) => [String(option.value), option.label]));
120
126
  const minutesItems = Object.fromEntries(minutesOptions.map((option) => [String(option.value), option.label]));
127
+ const periodItems = Object.fromEntries(periodOptions.map((option) => [option.value, option.label]));
121
128
 
122
129
  function roundToNearestFiveMinutes(minutes: number): number {
123
130
  return (Math.round(minutes / 5) * 5) % 60;
@@ -133,6 +140,11 @@ export function FormDateTime({
133
140
  setSelectedMinutes(referenceDate.getMinutes());
134
141
  };
135
142
 
143
+ // The time the selects show: the field's own time, else the pending selection (0–23 / 0–55)
144
+ const currentHours = (value: Date | undefined): number => (value ? new Date(value).getHours() : selectedHours);
145
+ const currentMinutes = (value: Date | undefined): number =>
146
+ value ? roundToNearestFiveMinutes(new Date(value).getMinutes()) : selectedMinutes;
147
+
136
148
  const handleTimeChange = (hours: number, minutes: number) => {
137
149
  const currentDate = form.getValues(id);
138
150
  if (currentDate) {
@@ -205,7 +217,7 @@ export function FormDateTime({
205
217
  }}
206
218
  disabled={(date) => (minDate && date < minDate ? true : false)}
207
219
  locale={dateFnsLocale}
208
- weekStartsOn={1}
220
+ weekStartsOn={weekStartsOn}
209
221
  startMonth={new Date(1900, 0)}
210
222
  endMonth={new Date(new Date().getFullYear() + 10, 11)}
211
223
  modifiers={dayModifiers}
@@ -228,20 +240,20 @@ export function FormDateTime({
228
240
  <Label htmlFor="hours-select">{t(`common.hours`)}</Label>
229
241
  <Select
230
242
  items={hoursItems}
231
- value={String(field.value ? new Date(field.value).getHours() : selectedHours)}
243
+ value={String(
244
+ twelveHourClock ? from24Hour(currentHours(field.value)).hour12 : currentHours(field.value),
245
+ )}
232
246
  onValueChange={(value) => {
233
247
  if (!value) return;
234
- const hours = parseInt(value);
248
+ const picked = parseInt(value);
249
+ const hours = twelveHourClock
250
+ ? to24Hour(picked, from24Hour(currentHours(field.value)).period)
251
+ : picked;
235
252
  setSelectedHours(hours);
236
- handleTimeChange(
237
- hours,
238
- field.value
239
- ? roundToNearestFiveMinutes(new Date(field.value).getMinutes())
240
- : selectedMinutes,
241
- );
253
+ handleTimeChange(hours, currentMinutes(field.value));
242
254
  }}
243
255
  >
244
- <SelectTrigger id="hours-select" className="w-[70px]">
256
+ <SelectTrigger id="hours-select" data-testid="form-datetime-hours" className="w-[70px]">
245
257
  <SelectValue />
246
258
  </SelectTrigger>
247
259
  <SelectContent>
@@ -258,17 +270,15 @@ export function FormDateTime({
258
270
  <Label htmlFor="minutes-select">{t(`common.minutes`)}</Label>
259
271
  <Select
260
272
  items={minutesItems}
261
- value={String(
262
- field.value ? roundToNearestFiveMinutes(new Date(field.value).getMinutes()) : selectedMinutes,
263
- )}
273
+ value={String(currentMinutes(field.value))}
264
274
  onValueChange={(value) => {
265
275
  if (!value) return;
266
276
  const minutes = parseInt(value);
267
277
  setSelectedMinutes(minutes);
268
- handleTimeChange(field.value ? new Date(field.value).getHours() : selectedHours, minutes);
278
+ handleTimeChange(currentHours(field.value), minutes);
269
279
  }}
270
280
  >
271
- <SelectTrigger id="minutes-select" className="w-[70px]">
281
+ <SelectTrigger id="minutes-select" data-testid="form-datetime-minutes" className="w-[70px]">
272
282
  <SelectValue />
273
283
  </SelectTrigger>
274
284
  <SelectContent>
@@ -280,6 +290,32 @@ export function FormDateTime({
280
290
  </SelectContent>
281
291
  </Select>
282
292
  </div>
293
+ {twelveHourClock && (
294
+ <div className="flex flex-col space-y-2">
295
+ <Label htmlFor="period-select">{t(`common.dayPeriod`)}</Label>
296
+ <Select
297
+ items={periodItems}
298
+ value={from24Hour(currentHours(field.value)).period}
299
+ onValueChange={(value) => {
300
+ if (!value) return;
301
+ const hours = to24Hour(from24Hour(currentHours(field.value)).hour12, value as DayPeriod);
302
+ setSelectedHours(hours);
303
+ handleTimeChange(hours, currentMinutes(field.value));
304
+ }}
305
+ >
306
+ <SelectTrigger id="period-select" data-testid="form-datetime-period" className="w-[70px]">
307
+ <SelectValue />
308
+ </SelectTrigger>
309
+ <SelectContent>
310
+ {periodOptions.map((option) => (
311
+ <SelectItem key={option.value} value={option.value}>
312
+ {option.label}
313
+ </SelectItem>
314
+ ))}
315
+ </SelectContent>
316
+ </Select>
317
+ </div>
318
+ )}
283
319
  </div>
284
320
  <div className="mt-2 flex flex-row gap-x-2">
285
321
  {allowEmpty !== false && (
@@ -248,6 +248,36 @@ describe("EditorSheet", () => {
248
248
  error,
249
249
  });
250
250
  });
251
+
252
+ it("skips the generic error toast when onSubmitError handles the error", async () => {
253
+ const { errorToast: mockErrorToast } = await import("../../errors/errorToast");
254
+ vi.mocked(mockErrorToast).mockClear();
255
+ const error = new Error("402:TRIAL_START_FAILED");
256
+ const onSubmit = vi.fn().mockRejectedValue(error);
257
+ const onSubmitError = vi.fn().mockReturnValue(true);
258
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onSubmitError={onSubmitError} />);
259
+
260
+ await userEvent.type(screen.getByTestId("name-input"), "Test Name");
261
+ await userEvent.click(screen.getByTestId("modal-button-create"));
262
+
263
+ expect(onSubmitError).toHaveBeenCalledWith(error);
264
+ expect(mockErrorToast).not.toHaveBeenCalled();
265
+ expect(screen.getByTestId("name-input")).toHaveValue("Test Name");
266
+ });
267
+
268
+ it("still shows the generic error toast when onSubmitError does not handle the error", async () => {
269
+ const { errorToast: mockErrorToast } = await import("../../errors/errorToast");
270
+ vi.mocked(mockErrorToast).mockClear();
271
+ const error = new Error("Network error");
272
+ const onSubmit = vi.fn().mockRejectedValue(error);
273
+ const onSubmitError = vi.fn().mockReturnValue(false);
274
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onSubmitError={onSubmitError} />);
275
+
276
+ await userEvent.type(screen.getByTestId("name-input"), "Test Name");
277
+ await userEvent.click(screen.getByTestId("modal-button-create"));
278
+
279
+ expect(mockErrorToast).toHaveBeenCalledWith({ title: "generic.errors.create", error });
280
+ });
251
281
  });
252
282
 
253
283
  describe("onBeforeSubmit", () => {
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { dayPeriodLabels, from24Hour, to24Hour } from "../time-cycle";
3
+
4
+ describe("to24Hour / from24Hour round-trip", () => {
5
+ it("maps 12-hour values around midnight and noon", () => {
6
+ expect(to24Hour(12, "am")).toBe(0);
7
+ expect(to24Hour(12, "pm")).toBe(12);
8
+ expect(to24Hour(11, "pm")).toBe(23);
9
+ expect(to24Hour(1, "am")).toBe(1);
10
+ });
11
+
12
+ it("maps 24-hour values to 12-hour values", () => {
13
+ expect(from24Hour(0)).toEqual({ hour12: 12, period: "am" });
14
+ expect(from24Hour(13)).toEqual({ hour12: 1, period: "pm" });
15
+ expect(from24Hour(12)).toEqual({ hour12: 12, period: "pm" });
16
+ });
17
+
18
+ it("round-trips every hour of the day", () => {
19
+ for (let hour = 0; hour < 24; hour++) {
20
+ const { hour12, period } = from24Hour(hour);
21
+ expect(to24Hour(hour12, period)).toBe(hour);
22
+ }
23
+ });
24
+ });
25
+
26
+ describe("dayPeriodLabels", () => {
27
+ it("reads the en-US labels from Intl", () => {
28
+ expect(dayPeriodLabels("en-US")).toEqual({ am: "AM", pm: "PM" });
29
+ });
30
+
31
+ it("returns two non-empty, different labels for it", () => {
32
+ const labels = dayPeriodLabels("it");
33
+ expect(labels.am.length).toBeGreaterThan(0);
34
+ expect(labels.pm.length).toBeGreaterThan(0);
35
+ expect(labels.am).not.toBe(labels.pm);
36
+ });
37
+ });
@@ -0,0 +1,27 @@
1
+ export type DayPeriod = "am" | "pm";
2
+
3
+ /** 12-hour clock value + period → 0–23 (12 am → 0, 12 pm → 12). */
4
+ export function to24Hour(hour12: number, period: DayPeriod): number {
5
+ const base = hour12 % 12;
6
+ return period === "pm" ? base + 12 : base;
7
+ }
8
+
9
+ /** 0–23 → 12-hour clock value + period (0 → 12 am, 12 → 12 pm). */
10
+ export function from24Hour(hour24: number): { hour12: number; period: DayPeriod } {
11
+ const period: DayPeriod = hour24 >= 12 ? "pm" : "am";
12
+ const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
13
+ return { hour12, period };
14
+ }
15
+
16
+ function dayPeriodOf(formatter: Intl.DateTimeFormat, date: Date): string {
17
+ return formatter.formatToParts(date).find((part) => part.type === "dayPeriod")?.value ?? "";
18
+ }
19
+
20
+ /** The locale's own AM/PM labels, read from Intl (never hard-coded). */
21
+ export function dayPeriodLabels(locale: string): { am: string; pm: string } {
22
+ const formatter = new Intl.DateTimeFormat(locale, { hour: "numeric", hourCycle: "h12" });
23
+ return {
24
+ am: dayPeriodOf(formatter, new Date(2000, 0, 1, 1, 0)),
25
+ pm: dayPeriodOf(formatter, new Date(2000, 0, 1, 13, 0)),
26
+ };
27
+ }
@@ -1,11 +1,9 @@
1
1
  import { ColumnDef } from "@tanstack/react-table";
2
- import { useI18nLocale } from "../../../i18n";
2
+ import { useDateFormatter } from "../../../contexts/DateLocaleContext";
3
3
 
4
4
  function DateCell({ date }: { date: Date }) {
5
- const locale = useI18nLocale();
6
- return (
7
- <span className="text-muted-foreground text-xs">{date.toLocaleDateString(locale, { dateStyle: "medium" })}</span>
8
- );
5
+ const { format } = useDateFormatter();
6
+ return <span className="text-muted-foreground text-xs">{format(date, "dateMedium")}</span>;
9
7
  }
10
8
 
11
9
  export const cellDate = (params: { name: string; title: string }): ColumnDef<any> => {