@flamingo-stack/openframe-frontend-core 0.0.310 → 0.0.311

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 (86) hide show
  1. package/dist/{chunk-PLFQJ5E7.cjs → chunk-2EILUSCY.cjs} +37 -37
  2. package/dist/{chunk-PLFQJ5E7.cjs.map → chunk-2EILUSCY.cjs.map} +1 -1
  3. package/dist/{chunk-E7FIV5LH.js → chunk-3JT6LRGB.js} +2 -2
  4. package/dist/{chunk-6RMANFX7.cjs → chunk-BZA3APSK.cjs} +30 -30
  5. package/dist/{chunk-6RMANFX7.cjs.map → chunk-BZA3APSK.cjs.map} +1 -1
  6. package/dist/{chunk-AIHM4TT7.cjs → chunk-CKNJYFLB.cjs} +5 -5
  7. package/dist/{chunk-AIHM4TT7.cjs.map → chunk-CKNJYFLB.cjs.map} +1 -1
  8. package/dist/{chunk-46KRPHHL.js → chunk-DH2R5ZMA.js} +2 -2
  9. package/dist/{chunk-QPTJOLAP.js → chunk-DXOGAFXQ.js} +2 -2
  10. package/dist/{chunk-4RHOLPFU.cjs → chunk-GAKX3IB4.cjs} +14 -14
  11. package/dist/{chunk-4RHOLPFU.cjs.map → chunk-GAKX3IB4.cjs.map} +1 -1
  12. package/dist/{chunk-5OFBD6EQ.js → chunk-HLQQN2H2.js} +2 -2
  13. package/dist/{chunk-6TRTIHGW.cjs → chunk-KNUY2CAL.cjs} +104 -87
  14. package/dist/chunk-KNUY2CAL.cjs.map +1 -0
  15. package/dist/{chunk-WSEK6W4B.js → chunk-KYQGJD2Q.js} +2 -2
  16. package/dist/{chunk-2YYAKVL7.cjs → chunk-L4ND4ZAB.cjs} +12 -12
  17. package/dist/{chunk-2YYAKVL7.cjs.map → chunk-L4ND4ZAB.cjs.map} +1 -1
  18. package/dist/{chunk-ER4CMF47.js → chunk-NS2WLRTI.js} +50 -33
  19. package/dist/chunk-NS2WLRTI.js.map +1 -0
  20. package/dist/{chunk-CKFHYXSJ.cjs → chunk-P5QS2625.cjs} +7 -7
  21. package/dist/{chunk-CKFHYXSJ.cjs.map → chunk-P5QS2625.cjs.map} +1 -1
  22. package/dist/{chunk-6SBJVDH3.js → chunk-PYKMJPWX.js} +4 -4
  23. package/dist/{chunk-N3YPIZBH.js → chunk-RUJCGYDL.js} +2 -2
  24. package/dist/{chunk-4RI7S6ZD.cjs → chunk-WLW35D5O.cjs} +9 -9
  25. package/dist/{chunk-4RI7S6ZD.cjs.map → chunk-WLW35D5O.cjs.map} +1 -1
  26. package/dist/{chunk-J3YKVLQ5.cjs → chunk-YVTTTQKF.cjs} +26 -26
  27. package/dist/{chunk-J3YKVLQ5.cjs.map → chunk-YVTTTQKF.cjs.map} +1 -1
  28. package/dist/{chunk-QWMYOUGP.js → chunk-ZRUQRXCP.js} +2 -2
  29. package/dist/components/case-studies/index.cjs +8 -8
  30. package/dist/components/case-studies/index.js +2 -2
  31. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  32. package/dist/components/chat/index.cjs +2 -2
  33. package/dist/components/chat/index.js +1 -1
  34. package/dist/components/chat/types/entities/investor-update.d.ts.map +1 -1
  35. package/dist/components/contact/index.cjs +3 -3
  36. package/dist/components/contact/index.js +2 -2
  37. package/dist/components/docs/index.cjs +5 -5
  38. package/dist/components/docs/index.js +4 -4
  39. package/dist/components/embeds/index.cjs +3 -3
  40. package/dist/components/embeds/index.js +2 -2
  41. package/dist/components/faq/index.cjs +3 -3
  42. package/dist/components/faq/index.js +2 -2
  43. package/dist/components/features/index.cjs +2 -2
  44. package/dist/components/features/index.js +1 -1
  45. package/dist/components/index.cjs +172 -172
  46. package/dist/components/index.js +8 -8
  47. package/dist/components/logs-list.d.ts.map +1 -1
  48. package/dist/components/navigation/index.cjs +2 -2
  49. package/dist/components/navigation/index.js +1 -1
  50. package/dist/components/onboarding-guides/index.cjs +23 -23
  51. package/dist/components/onboarding-guides/index.js +3 -3
  52. package/dist/components/related-content/index.cjs +3 -3
  53. package/dist/components/related-content/index.js +2 -2
  54. package/dist/components/tickets/index.cjs +60 -60
  55. package/dist/components/tickets/index.js +3 -3
  56. package/dist/components/ui/device-card.d.ts.map +1 -1
  57. package/dist/components/ui/index.cjs +2 -2
  58. package/dist/components/ui/index.js +1 -1
  59. package/dist/index.cjs +2 -2
  60. package/dist/index.js +1 -1
  61. package/dist/utils/date-utils.d.ts.map +1 -1
  62. package/dist/utils/format.d.ts +12 -3
  63. package/dist/utils/format.d.ts.map +1 -1
  64. package/dist/utils/index.cjs +26 -14
  65. package/dist/utils/index.cjs.map +1 -1
  66. package/dist/utils/index.js +26 -14
  67. package/dist/utils/index.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/components/chat/entity-cards/blog-card.tsx +2 -2
  70. package/src/components/chat/entity-cards/dispatch.tsx +1 -1
  71. package/src/components/chat/entity-cards/program-card.tsx +17 -4
  72. package/src/components/chat/types/entities/investor-update.ts +2 -0
  73. package/src/components/logs-list.tsx +8 -6
  74. package/src/components/ui/device-card.tsx +8 -6
  75. package/src/utils/date-utils.ts +27 -14
  76. package/src/utils/format.ts +27 -8
  77. package/dist/chunk-6TRTIHGW.cjs.map +0 -1
  78. package/dist/chunk-ER4CMF47.js.map +0 -1
  79. /package/dist/{chunk-E7FIV5LH.js.map → chunk-3JT6LRGB.js.map} +0 -0
  80. /package/dist/{chunk-46KRPHHL.js.map → chunk-DH2R5ZMA.js.map} +0 -0
  81. /package/dist/{chunk-QPTJOLAP.js.map → chunk-DXOGAFXQ.js.map} +0 -0
  82. /package/dist/{chunk-5OFBD6EQ.js.map → chunk-HLQQN2H2.js.map} +0 -0
  83. /package/dist/{chunk-WSEK6W4B.js.map → chunk-KYQGJD2Q.js.map} +0 -0
  84. /package/dist/{chunk-6SBJVDH3.js.map → chunk-PYKMJPWX.js.map} +0 -0
  85. /package/dist/{chunk-N3YPIZBH.js.map → chunk-RUJCGYDL.js.map} +0 -0
  86. /package/dist/{chunk-QWMYOUGP.js.map → chunk-ZRUQRXCP.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.310",
3
+ "version": "0.0.311",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -129,7 +129,7 @@ export function BlogCard({
129
129
 
130
130
  if (size === 'sm') {
131
131
  const dateStr = post.published_at
132
- ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
132
+ ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })
133
133
  : ''
134
134
  const firstCategory = post.categories?.find((c) => c && c.name)?.name
135
135
  return (
@@ -179,7 +179,7 @@ export function BlogCard({
179
179
 
180
180
  // Default: full vertical card.
181
181
  const dateStr = post.published_at
182
- ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
182
+ ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })
183
183
  : ''
184
184
  return (
185
185
  <article
@@ -893,7 +893,7 @@ function ProgramChatCard({
893
893
  ) {
894
894
  typeMeta = item.location_name
895
895
  } else if (configKey === 'webinar' && item?.start_at) {
896
- const time = formatTimeWithTimezone(item.start_at, null)
896
+ const time = formatTimeWithTimezone(item.start_at, item.timezone ?? null)
897
897
  const dur = formatDurationFromRange(item.start_at, item.end_at)
898
898
  typeMeta = dur ? `${time} · ${dur}` : time
899
899
  }
@@ -45,6 +45,19 @@ import { useEntityCardPlaceholder } from './use-entity-card-placeholder'
45
45
 
46
46
  type CardSize = 'default' | 'sm'
47
47
 
48
+ /**
49
+ * Format a Date with date-fns pinned to UTC. `date-fns` `format()` reads the
50
+ * runtime's LOCAL wall-clock, so the same instant renders differently on the
51
+ * server (Vercel = UTC) and the client (visitor tz) → React #418 hydration
52
+ * mismatch. Shifting by the local offset before formatting emits the UTC
53
+ * wall-clock on every machine, so server and client agree. Mirrors the helper
54
+ * in the hub's `program-header.tsx` (kept local — the lib has no date-fns-tz
55
+ * dep) and the repo-wide "pin program dates to UTC" convention.
56
+ */
57
+ function formatUtc(date: Date, fmt: string): string {
58
+ return format(new Date(date.getTime() + date.getTimezoneOffset() * 60_000), fmt)
59
+ }
60
+
48
61
  export function ProgramCardSkeleton({ size = 'default' }: { size?: CardSize }) {
49
62
  if (size === 'sm') {
50
63
  return (
@@ -226,7 +239,7 @@ export function ProgramCard<T extends BaseProgramItem>({
226
239
 
227
240
  if (size === 'sm') {
228
241
  const itemDate = (() => {
229
- try { return format(new Date(item.date), 'MMM d, yyyy') } catch { return '' }
242
+ try { return formatUtc(new Date(item.date), 'MMM d, yyyy') } catch { return '' }
230
243
  })()
231
244
  const compactCover = coverImage || placeholderUrl || null
232
245
  let typeMeta: string | null = null
@@ -238,7 +251,7 @@ export function ProgramCard<T extends BaseProgramItem>({
238
251
  if (typeof loc === 'string' && loc.trim().length > 0) typeMeta = loc
239
252
  } else if (config.type === 'webinar' && 'start_at' in item) {
240
253
  const w = item as any
241
- const time = formatTimeWithTimezone(w.start_at, null)
254
+ const time = formatTimeWithTimezone(w.start_at, w.timezone ?? null)
242
255
  const dur = formatDurationFromRange(w.start_at, w.end_at)
243
256
  typeMeta = dur ? `${time} · ${dur}` : time
244
257
  }
@@ -293,7 +306,7 @@ export function ProgramCard<T extends BaseProgramItem>({
293
306
  }
294
307
 
295
308
  const itemDate = new Date(item.date)
296
- const dateFormat = format(itemDate, 'EEEE d MMMM')
309
+ const dateFormat = formatUtc(itemDate, 'EEEE d MMMM')
297
310
 
298
311
  const defaultRenderMeta = () => {
299
312
  if (config.type === 'podcast' && 'duration_seconds' in item && !isScheduled) {
@@ -320,7 +333,7 @@ export function ProgramCard<T extends BaseProgramItem>({
320
333
  <>
321
334
  <Video className="w-4 h-4 text-ods-text-secondary" />
322
335
  <span className="font-['DM_Sans'] text-ods-text-secondary">
323
- {formatTimeWithTimezone(webinarItem.start_at, null)}
336
+ {formatTimeWithTimezone(webinarItem.start_at, webinarItem.timezone ?? null)}
324
337
  {duration && ` · ${duration}`}
325
338
  </span>
326
339
  {webinarItem.timezone && (
@@ -93,6 +93,8 @@ export function formatInvestorUpdatePeriod(
93
93
  new Date(d).toLocaleDateString('en-US', {
94
94
  month: options?.monthFormat || 'short',
95
95
  year: 'numeric',
96
+ // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).
97
+ timeZone: 'UTC',
96
98
  });
97
99
  const s = start ? fmt(start) : '?';
98
100
  const e = end ? fmt(end) : '?';
@@ -7,12 +7,14 @@ import { ToolIcon } from './tool-icon'
7
7
  const formatTimestamp = (timestamp: string | Date): string => {
8
8
  const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
9
9
 
10
- const year = date.getFullYear()
11
- const month = String(date.getMonth() + 1).padStart(2, '0')
12
- const day = String(date.getDate()).padStart(2, '0')
13
- const hours = String(date.getHours()).padStart(2, '0')
14
- const minutes = String(date.getMinutes()).padStart(2, '0')
15
-
10
+ // UTC getters so the timestamp is identical on server (UTC) and client
11
+ // (local) otherwise React #418 hydration mismatch.
12
+ const year = date.getUTCFullYear()
13
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0')
14
+ const day = String(date.getUTCDate()).padStart(2, '0')
15
+ const hours = String(date.getUTCHours()).padStart(2, '0')
16
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0')
17
+
16
18
  return `${year}/${month}/${day},${hours}:${minutes}`
17
19
  }
18
20
 
@@ -66,12 +66,14 @@ export function DeviceCard({
66
66
  if (!lastSeen) return null
67
67
 
68
68
  const date = typeof lastSeen === 'string' ? new Date(lastSeen) : lastSeen
69
- const year = date.getFullYear()
70
- const month = String(date.getMonth() + 1).padStart(2, '0')
71
- const day = String(date.getDate()).padStart(2, '0')
72
- const hours = String(date.getHours()).padStart(2, '0')
73
- const minutes = String(date.getMinutes()).padStart(2, '0')
74
-
69
+ // UTC getters so "last seen" is identical on server (UTC) and client
70
+ // (local) otherwise React #418 hydration mismatch.
71
+ const year = date.getUTCFullYear()
72
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0')
73
+ const day = String(date.getUTCDate()).padStart(2, '0')
74
+ const hours = String(date.getUTCHours()).padStart(2, '0')
75
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0')
76
+
75
77
  return `${year}/${month}/${day}, ${hours}:${minutes}`
76
78
  }
77
79
 
@@ -17,9 +17,11 @@ export function formatTicketRelativeTime(iso: string): string {
17
17
  if (diffMin < 60) return `${diffMin} min ago`
18
18
  const diffHours = Math.floor(diffMin / 60)
19
19
  if (diffHours < 24) return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`
20
- const mm = String(date.getMonth() + 1).padStart(2, '0')
21
- const dd = String(date.getDate()).padStart(2, '0')
22
- const yyyy = date.getFullYear()
20
+ // UTC getters so the MM/DD/YYYY tail is identical on server (UTC) and client
21
+ // (local) otherwise React #418 hydration mismatch.
22
+ const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
23
+ const dd = String(date.getUTCDate()).padStart(2, '0')
24
+ const yyyy = date.getUTCFullYear()
23
25
  return `${mm}/${dd}/${yyyy}`
24
26
  }
25
27
 
@@ -29,11 +31,13 @@ export function formatTicketRelativeTime(iso: string): string {
29
31
  export function formatTicketFullTimestamp(iso: string): string {
30
32
  const date = new Date(iso)
31
33
  if (Number.isNaN(date.getTime())) return ''
32
- const mm = String(date.getMonth() + 1).padStart(2, '0')
33
- const dd = String(date.getDate()).padStart(2, '0')
34
- const yyyy = date.getFullYear()
35
- let hours = date.getHours()
36
- const minutes = String(date.getMinutes()).padStart(2, '0')
34
+ // UTC getters so the tooltip timestamp is identical on server (UTC) and
35
+ // client (local) — otherwise React #418 hydration mismatch.
36
+ const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
37
+ const dd = String(date.getUTCDate()).padStart(2, '0')
38
+ const yyyy = date.getUTCFullYear()
39
+ let hours = date.getUTCHours()
40
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0')
37
41
  const ampm = hours >= 12 ? 'PM' : 'AM'
38
42
  hours = hours % 12 || 12
39
43
  return `${mm}/${dd}/${yyyy}, ${hours}:${minutes} ${ampm}`
@@ -88,11 +92,14 @@ export function formatRelativeTime(timestamp: string | Date): string {
88
92
  return `${weeks}w ago`;
89
93
  }
90
94
 
91
- // Older than 30 days - show formatted date
92
- return targetTime.toLocaleDateString('en-US', {
93
- month: 'short',
95
+ // Older than 30 days - show formatted date, pinned to UTC so SSR (UTC) and
96
+ // the client agree (React #418). Year comparison also uses UTC for the same
97
+ // reason (avoids a server/client split right at a year boundary).
98
+ return targetTime.toLocaleDateString('en-US', {
99
+ month: 'short',
94
100
  day: 'numeric',
95
- year: targetTime.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
101
+ year: targetTime.getUTCFullYear() !== now.getUTCFullYear() ? 'numeric' : undefined,
102
+ timeZone: 'UTC',
96
103
  });
97
104
  }
98
105
 
@@ -118,9 +125,12 @@ export function formatAbsoluteDate(
118
125
  year: 'numeric',
119
126
  month: 'short',
120
127
  day: 'numeric',
128
+ // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).
129
+ // Caller can override via `options`.
130
+ timeZone: 'UTC',
121
131
  ...options
122
132
  };
123
-
133
+
124
134
  return targetTime.toLocaleDateString('en-US', defaultOptions);
125
135
  }
126
136
 
@@ -148,9 +158,12 @@ export function formatDateTime(
148
158
  day: 'numeric',
149
159
  hour: 'numeric',
150
160
  minute: '2-digit',
161
+ // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).
162
+ // Caller can override via `options`.
163
+ timeZone: 'UTC',
151
164
  ...options
152
165
  };
153
-
166
+
154
167
  return targetTime.toLocaleDateString('en-US', defaultOptions);
155
168
  }
156
169
 
@@ -24,7 +24,10 @@ export function formatDate(
24
24
  return "Invalid Date"
25
25
  }
26
26
 
27
- return dateObj.toLocaleDateString("en-US", options)
27
+ // Pin to UTC by default so SSR (Vercel = UTC) and the client agree (React
28
+ // #418 hydration mismatch). A caller can still override by passing its own
29
+ // `timeZone` in `options`.
30
+ return dateObj.toLocaleDateString("en-US", { timeZone: "UTC", ...options })
28
31
  }
29
32
 
30
33
  /**
@@ -223,9 +226,18 @@ export function formatDurationCompact(seconds: number | null | undefined): strin
223
226
  }
224
227
 
225
228
  /**
226
- * Format time with optional timezone
227
- * Used for webinar and event times
228
- * Returns: "10:30 AM EST" or "10:30 AM"
229
+ * Format a webinar/event start time as the wall-clock in its OWN timezone.
230
+ *
231
+ * `date` is an instant (UTC timestamp / ISO string). `timezone` is the event's
232
+ * IANA zone (e.g. `'America/New_York'`). Rendering in that explicit zone makes
233
+ * the server (Vercel = UTC) and the visitor's browser emit the SAME text —
234
+ * fixing the React #418 hydration mismatch — AND shows the true event time
235
+ * (4:00 PM EDT, not the 8:00 PM UTC a plain UTC pin would show, nor the
236
+ * viewer-local time the old unpinned call produced). Falls back to UTC when no
237
+ * zone is given, and tolerates a non-IANA label (legacy data) without throwing.
238
+ * The zone LABEL is rendered separately by callers, so it is never appended here.
239
+ *
240
+ * Returns: "4:00 PM"
229
241
  */
230
242
  export function formatTimeWithTimezone(
231
243
  date: Date | string | null | undefined,
@@ -234,13 +246,20 @@ export function formatTimeWithTimezone(
234
246
  if (!date) return '';
235
247
 
236
248
  const dateObj = typeof date === 'string' ? new Date(date) : date;
237
- const timeStr = dateObj.toLocaleTimeString('en-US', {
249
+ const opts: Intl.DateTimeFormatOptions = {
238
250
  hour: 'numeric',
239
251
  minute: '2-digit',
240
252
  hour12: true,
241
- });
242
-
243
- return timezone ? `${timeStr} ${timezone}` : timeStr;
253
+ timeZone: timezone || 'UTC',
254
+ };
255
+ try {
256
+ return dateObj.toLocaleTimeString('en-US', opts);
257
+ } catch {
258
+ // Non-IANA `timezone` (e.g. a bare "EST" label) makes Intl throw a
259
+ // RangeError. Fall back to a UTC-pinned render so we stay deterministic
260
+ // (still no #418) instead of crashing.
261
+ return dateObj.toLocaleTimeString('en-US', { ...opts, timeZone: 'UTC' });
262
+ }
244
263
  }
245
264
 
246
265
  /**