@flamingo-stack/openframe-frontend-core 0.0.216 → 0.0.217

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 (103) hide show
  1. package/dist/{chunk-SMCG2CCC.cjs → chunk-6DCKL73F.cjs} +24 -24
  2. package/dist/{chunk-SMCG2CCC.cjs.map → chunk-6DCKL73F.cjs.map} +1 -1
  3. package/dist/{chunk-QTKU6ULP.js → chunk-BVFRD34B.js} +2 -2
  4. package/dist/{chunk-CDLYRFDE.js → chunk-ENBGG2K2.js} +3767 -3610
  5. package/dist/chunk-ENBGG2K2.js.map +1 -0
  6. package/dist/{chunk-K4DFAVSO.cjs → chunk-G2HHSZ3S.cjs} +9 -9
  7. package/dist/{chunk-K4DFAVSO.cjs.map → chunk-G2HHSZ3S.cjs.map} +1 -1
  8. package/dist/{chunk-2V4SACHE.js → chunk-L6IBKPVM.js} +2 -2
  9. package/dist/{chunk-572WQWIX.cjs → chunk-MVQ3OODK.cjs} +9 -9
  10. package/dist/{chunk-572WQWIX.cjs.map → chunk-MVQ3OODK.cjs.map} +1 -1
  11. package/dist/{chunk-GVNQAGXB.js → chunk-N5IKPYRL.js} +3 -81
  12. package/dist/chunk-N5IKPYRL.js.map +1 -0
  13. package/dist/{chunk-VC3ND5RB.js → chunk-SWZUZYWR.js} +2 -2
  14. package/dist/{chunk-IH76P5R6.cjs → chunk-TYIBMDUZ.cjs} +8 -86
  15. package/dist/chunk-TYIBMDUZ.cjs.map +1 -0
  16. package/dist/{chunk-ZGTDUPTW.cjs → chunk-YWDC5BXM.cjs} +382 -225
  17. package/dist/chunk-YWDC5BXM.cjs.map +1 -0
  18. package/dist/components/chat/chat-attachment-bar.d.ts +13 -2
  19. package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
  20. package/dist/components/chat/chat-input.d.ts.map +1 -1
  21. package/dist/components/chat/chat-message-row.d.ts +25 -0
  22. package/dist/components/chat/chat-message-row.d.ts.map +1 -0
  23. package/dist/components/chat/index.cjs +6 -2
  24. package/dist/components/chat/index.cjs.map +1 -1
  25. package/dist/components/chat/index.d.ts +1 -0
  26. package/dist/components/chat/index.d.ts.map +1 -1
  27. package/dist/components/chat/index.js +5 -1
  28. package/dist/components/chat/types/component.types.d.ts +8 -1
  29. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  30. package/dist/components/contact/index.cjs +3 -3
  31. package/dist/components/contact/index.js +2 -2
  32. package/dist/components/features/index.cjs +2 -2
  33. package/dist/components/features/index.js +1 -1
  34. package/dist/components/form.d.ts +1 -1
  35. package/dist/components/form.d.ts.map +1 -1
  36. package/dist/components/index.cjs +56 -52
  37. package/dist/components/index.cjs.map +1 -1
  38. package/dist/components/index.js +9 -5
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/components/navigation/index.cjs +2 -2
  41. package/dist/components/navigation/index.js +1 -1
  42. package/dist/components/onboarding-guides/index.cjs +18 -18
  43. package/dist/components/onboarding-guides/index.js +3 -3
  44. package/dist/components/shared/dev-section/dev-card-row.d.ts +5 -45
  45. package/dist/components/shared/dev-section/dev-card-row.d.ts.map +1 -1
  46. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
  47. package/dist/components/tickets/help-center-card.d.ts.map +1 -1
  48. package/dist/components/tickets/help-center-list.d.ts.map +1 -1
  49. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts +9 -1
  50. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
  51. package/dist/components/tickets/hooks/use-tickets-list.d.ts +7 -0
  52. package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
  53. package/dist/components/tickets/index.cjs +294 -255
  54. package/dist/components/tickets/index.cjs.map +1 -1
  55. package/dist/components/tickets/index.d.ts +1 -0
  56. package/dist/components/tickets/index.d.ts.map +1 -1
  57. package/dist/components/tickets/index.js +360 -321
  58. package/dist/components/tickets/index.js.map +1 -1
  59. package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
  60. package/dist/components/tickets/ticket-reply-composer.d.ts +33 -0
  61. package/dist/components/tickets/ticket-reply-composer.d.ts.map +1 -0
  62. package/dist/components/tickets/types.d.ts +13 -0
  63. package/dist/components/tickets/types.d.ts.map +1 -1
  64. package/dist/components/ui/index.cjs +6 -2
  65. package/dist/components/ui/index.cjs.map +1 -1
  66. package/dist/components/ui/index.js +5 -1
  67. package/dist/components/ui/ticket-attachments-list.d.ts +5 -1
  68. package/dist/components/ui/ticket-attachments-list.d.ts.map +1 -1
  69. package/dist/index.cjs +6 -2
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.js +5 -1
  72. package/dist/utils/index.cjs +59 -4
  73. package/dist/utils/index.cjs.map +1 -1
  74. package/dist/utils/index.js +59 -4
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/utils/scroll-into-view.d.ts +43 -48
  77. package/dist/utils/scroll-into-view.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/chat/chat-attachment-bar.tsx +58 -22
  80. package/src/components/chat/chat-input.tsx +68 -29
  81. package/src/components/chat/chat-message-row.tsx +124 -0
  82. package/src/components/chat/index.ts +1 -0
  83. package/src/components/chat/types/component.types.ts +8 -1
  84. package/src/components/shared/dev-section/dev-card-row.tsx +5 -183
  85. package/src/components/shared/legal-document/use-legal-docs.ts +5 -1
  86. package/src/components/tickets/help-center-card.tsx +26 -29
  87. package/src/components/tickets/help-center-list.tsx +57 -10
  88. package/src/components/tickets/hooks/use-ticket-engagements.ts +17 -1
  89. package/src/components/tickets/hooks/use-tickets-list.ts +13 -0
  90. package/src/components/tickets/index.ts +4 -0
  91. package/src/components/tickets/ticket-detail-drawer.tsx +144 -200
  92. package/src/components/tickets/ticket-reply-composer.tsx +195 -0
  93. package/src/components/tickets/types.ts +14 -0
  94. package/src/components/ui/ticket-attachments-list.tsx +26 -8
  95. package/src/styles/app-globals.css +13 -0
  96. package/src/utils/scroll-into-view.ts +127 -53
  97. package/dist/chunk-CDLYRFDE.js.map +0 -1
  98. package/dist/chunk-GVNQAGXB.js.map +0 -1
  99. package/dist/chunk-IH76P5R6.cjs.map +0 -1
  100. package/dist/chunk-ZGTDUPTW.cjs.map +0 -1
  101. /package/dist/{chunk-QTKU6ULP.js.map → chunk-BVFRD34B.js.map} +0 -0
  102. /package/dist/{chunk-2V4SACHE.js.map → chunk-L6IBKPVM.js.map} +0 -0
  103. /package/dist/{chunk-VC3ND5RB.js.map → chunk-SWZUZYWR.js.map} +0 -0
@@ -16,10 +16,15 @@ export interface TicketAttachment {
16
16
  export interface TicketAttachmentsListProps {
17
17
  attachments: TicketAttachment[]
18
18
  className?: string
19
+ /** `compact` shrinks padding / icon / text / download-button for
20
+ * in-message rendering (the ticket conversation feed). Default keeps the
21
+ * roomier full-row layout for any other surface. */
22
+ size?: 'default' | 'compact'
19
23
  }
20
24
 
21
- export function TicketAttachmentsList({ attachments, className }: TicketAttachmentsListProps) {
25
+ export function TicketAttachmentsList({ attachments, className, size = 'default' }: TicketAttachmentsListProps) {
22
26
  if (attachments.length === 0) return null
27
+ const compact = size === 'compact'
23
28
 
24
29
  return (
25
30
  <div className={cn("rounded-[6px] border border-ods-border overflow-hidden", className)}>
@@ -27,7 +32,8 @@ export function TicketAttachmentsList({ attachments, className }: TicketAttachme
27
32
  <div
28
33
  key={attachment.id}
29
34
  className={cn(
30
- "flex items-center gap-4 px-4 py-3 bg-ods-card",
35
+ "flex items-center bg-ods-card",
36
+ compact ? "gap-2 px-2 py-1.5" : "gap-4 px-4 py-3",
31
37
  index < attachments.length - 1 && "border-b border-ods-border",
32
38
  )}
33
39
  >
@@ -35,17 +41,29 @@ export function TicketAttachmentsList({ attachments, className }: TicketAttachme
35
41
  <SquareAvatar
36
42
  src={attachment.thumbnailSrc}
37
43
  alt={attachment.fileName}
38
- size="md"
44
+ size={compact ? "sm" : "md"}
39
45
  className="shrink-0"
40
46
  />
41
47
  ) : (
42
- <div className="shrink-0 size-10 flex items-center justify-center rounded-[6px] bg-ods-card border border-ods-border">
43
- <FileIcon className="size-6 text-ods-text-secondary" />
48
+ <div
49
+ className={cn(
50
+ "shrink-0 flex items-center justify-center rounded-[6px] bg-ods-card border border-ods-border",
51
+ compact ? "size-8" : "size-10",
52
+ )}
53
+ >
54
+ <FileIcon className={cn("text-ods-text-secondary", compact ? "size-4" : "size-6")} />
44
55
  </div>
45
56
  )}
46
57
  <div className="flex-1 min-w-0 overflow-hidden">
47
- <p className="text-h4 text-ods-text-primary truncate" title={attachment.fileName}>{attachment.fileName}</p>
48
- <p className="text-h6 text-ods-text-secondary">{attachment.fileSize}</p>
58
+ <p
59
+ className={cn("text-ods-text-primary truncate", compact ? "text-h5" : "text-h4")}
60
+ title={attachment.fileName}
61
+ >
62
+ {attachment.fileName}
63
+ </p>
64
+ {attachment.fileSize && (
65
+ <p className="text-h6 text-ods-text-secondary">{attachment.fileSize}</p>
66
+ )}
49
67
  </div>
50
68
  {attachment.onDownload && (
51
69
  <button
@@ -54,7 +72,7 @@ export function TicketAttachmentsList({ attachments, className }: TicketAttachme
54
72
  className="shrink-0 text-ods-text-secondary hover:text-ods-text-primary transition-colors"
55
73
  aria-label={`Download ${attachment.fileName}`}
56
74
  >
57
- <Download className="size-6" />
75
+ <Download className={compact ? "size-4" : "size-6"} />
58
76
  </button>
59
77
  )}
60
78
  </div>
@@ -39,11 +39,24 @@ img {
39
39
 
40
40
  html {
41
41
  background-color: var(--color-bg);
42
+ /* Defense-in-depth for programmatic smooth scrolls (see
43
+ `utils/scroll-into-view.ts`). Browser SCROLL ANCHORING issues a synchronous
44
+ scrollTop correction when content is inserted/removed (e.g. a ticket drawer
45
+ expanding) to keep the anchored element stable — and per CSSOM-View that
46
+ correction ABORTS any in-flight scroll. That cancelled our smooth
47
+ scroll-to-card on every open after the first (anchoring is suppressed only
48
+ at scrollY=0, so the first open looked smooth and every repeat jumped).
49
+ Disabling anchoring on the document scroller removes the correction
50
+ entirely. Put on html/body (the page scroller), NOT a descendant — a child
51
+ can't re-enable it for an ancestor scroll container. The self-driven rAF
52
+ tween in scroll-into-view.ts is the primary fix; this is belt-and-braces. */
53
+ overflow-anchor: none;
42
54
  }
43
55
 
44
56
  body {
45
57
  background-color: var(--color-bg);
46
58
  color: var(--color-text-primary);
59
+ overflow-anchor: none;
47
60
  }
48
61
 
49
62
  /* Light mode specific styles (used in alternatives-list.tsx) */
@@ -1,74 +1,148 @@
1
1
  /**
2
- * `scrollElementIntoView` — canonical "smooth scroll element to top
3
- * of viewport, account for sticky chrome, optionally adjust for
4
- * known layout shifts" helper.
2
+ * `scrollElementIntoView` — canonical "scroll an element to the top of the
3
+ * viewport, account for sticky chrome, survive layout shifts" helper.
5
4
  *
6
- * Before this util existed, ~3 different call sites in the lib + hub
7
- * had the same 5-line snippet copy-pasted with subtle differences:
5
+ * One shared implementation so every caller (the ticket drawer expand, the
6
+ * hub's `useUnifiedNav` / `use-nav-link` hash scroll, doc-tree, delivery
7
+ * `?focus=`, sticky-section-nav, …) inherits the SAME cancellation-proof
8
+ * motion.
8
9
  *
9
- * - `useUnifiedNav` same-URL re-scroll branch (hub)
10
- * - `<HelpCenterCard>` click-to-expand (lib, with cross-row
11
- * layout-shift adjustment)
12
- * - Future ticket-row / docs anchor scrolls
10
+ * WHY A SELF-DRIVEN rAF TWEEN INSTEAD OF `window.scrollTo({behavior:'smooth'})`:
11
+ * the native smooth scroll is CANCELLABLE, and in real pages it gets cancelled
12
+ * constantly:
13
13
  *
14
- * The canonical pattern is `window.scrollTo({top, behavior:'smooth'})`
15
- * with a pre-computed pixel target NOT `element.scrollIntoView()`.
16
- * Pre-computing the target lets the browser run a clean uninterrupted
17
- * smooth animation to a fixed pixel value. `scrollIntoView` re-targets
18
- * continuously as the page layout shifts during the animation, which
19
- * causes visible jitter when content above the target is also moving
20
- * (a sibling collapsing, an async image loading, …).
14
+ * - Browser SCROLL ANCHORING: when content is inserted/removed above or
15
+ * around the target (a collapsible drawer expanding, an async image
16
+ * loading, a list re-rendering) the browser issues a synchronous scrollTop
17
+ * correction to keep the anchored element stable. Per CSSOM-View "perform a
18
+ * scroll" step 1 ("abort any ongoing smooth scroll"), that correction
19
+ * ABORTS an in-flight native smooth scroll so it lands as an instant jump.
20
+ * Anchoring is suppressed when the scroll offset is 0, which is exactly why
21
+ * a native smooth scroll appears to work the FIRST time (page at top) and
22
+ * jumps on every repeat (page already scrolled). This was a multi-day
23
+ * "smooth only works once" bug on the /tickets drawer.
24
+ * - A second programmatic scroll on the same frame, or a `focus()` without
25
+ * `{preventScroll:true}`, cancels it the same way.
21
26
  *
22
- * The `adjustTargetY` callback is the escape hatch for cases where
23
- * the consumer KNOWS about an upcoming layout shift and can compute
24
- * the correct FINAL target before the animation starts. Example: in
25
- * `HelpCenterCard`, clicking row B while row A above is currently
26
- * expanded A's drawer collapses simultaneously with B's expansion,
27
- * shifting B's tile up by A's drawer height. The consumer passes
28
- * `adjustTargetY: raw => raw - getAboveDrawerHeight()` and the
29
- * browser smooth-scrolls to the post-collapse position directly.
27
+ * A tween that re-asserts the position with INSTANT writes every frame is
28
+ * immune: there is no "ongoing native smooth scroll" for anchoring/focus to
29
+ * abort, and any correction that lands between our frames is overwritten on the
30
+ * next frame. We also RECOMPUTE the target each frame, so an element whose
31
+ * final position is still settling (drawer still expanding, images loading)
32
+ * is tracked to its resting place instead of animating to a stale pixel.
33
+ *
34
+ * Honors `prefers-reduced-motion` (jumps instantly) and cancels on genuine user
35
+ * scroll intent (wheel / touch) so we never fight the user.
30
36
  */
31
37
 
32
38
  export interface ScrollElementIntoViewOptions {
33
- /** Pixels to subtract from the target element's `top` so it lands
34
- * BELOW sticky chrome. Defaults to 0. Pass `96` (matches
35
- * `scroll-mt-24`) for the standard hub header offset. */
39
+ /** Pixels to subtract from the target element's `top` so it lands BELOW
40
+ * sticky chrome. Defaults to 0. Pass `96` for the standard hub header. */
36
41
  headerOffset?: number
37
- /** Scroll animation style. Defaults to `'smooth'`. Use `'instant'`
38
- * for imperative jumps where animation would feel laggy (deep
39
- * link land, programmatic focus moves). */
42
+ /** `'smooth'` (default) runs the self-driven tween; `'instant'` / `'auto'`
43
+ * jump in one synchronous write (deep-link land, programmatic focus moves). */
40
44
  behavior?: ScrollBehavior
41
- /** Optional adjustment applied to the computed pixel target. The
42
- * callback receives the "raw" Y (`element.top + scrollY -
43
- * headerOffset`) and returns the FINAL pixel target. Use this
44
- * when the caller knows about a layout shift that will happen
45
- * between the call and the animation completing — the browser's
46
- * smooth-scroll commits to a single pixel value, so providing the
47
- * post-shift target up front lands the element correctly even as
48
- * content above it moves. */
45
+ /** Optional adjustment applied to the computed pixel target each frame. The
46
+ * callback receives the "raw" Y (`element.top + scrollY - headerOffset`) and
47
+ * returns the FINAL target. Use when the caller knows about a layout shift
48
+ * (e.g. a sibling drawer collapsing) the geometry can't yet reflect. */
49
49
  adjustTargetY?: (rawTargetY: number) => number
50
+ /** Tween duration in ms (smooth only). Default 320. */
51
+ durationMs?: number
52
+ }
53
+
54
+ /** Module-level handle to the in-flight tween so a new call (or a user
55
+ * gesture) cancels the previous one — only ever one page-scroll animation at
56
+ * a time. */
57
+ let activeRaf = 0
58
+ let teardownActive: (() => void) | null = null
59
+
60
+ function cancelActiveScroll(): void {
61
+ if (activeRaf) {
62
+ cancelAnimationFrame(activeRaf)
63
+ activeRaf = 0
64
+ }
65
+ if (teardownActive) {
66
+ teardownActive()
67
+ teardownActive = null
68
+ }
50
69
  }
51
70
 
71
+ const easeOutCubic = (t: number): number => 1 - Math.pow(1 - t, 3)
72
+
52
73
  /**
53
- * Scroll the page so `target` lands at the top of the viewport,
54
- * accounting for sticky chrome via `headerOffset`. Returns void; the
55
- * scroll runs async via the browser's smooth-scroll engine.
56
- *
57
- * Accepts:
58
- * - `HTMLElement` — direct reference (most common from `useRef`).
59
- * - `null` / `undefined` — no-op so callers can pass refs without
60
- * defensive branching.
61
- *
62
- * SSR-safe: short-circuits when `window` is undefined.
74
+ * Scroll the page so `target` lands at the top of the viewport (below sticky
75
+ * chrome via `headerOffset`). SSR-safe; `null`/`undefined` target is a no-op so
76
+ * callers can pass refs without defensive branching.
63
77
  */
64
78
  export function scrollElementIntoView(
65
79
  target: HTMLElement | null | undefined,
66
80
  options: ScrollElementIntoViewOptions = {},
67
81
  ): void {
68
82
  if (typeof window === 'undefined' || !target) return
69
- const { headerOffset = 0, behavior = 'smooth', adjustTargetY } = options
70
- const rawTargetY =
71
- target.getBoundingClientRect().top + window.scrollY - headerOffset
72
- const finalY = adjustTargetY ? adjustTargetY(rawTargetY) : rawTargetY
73
- window.scrollTo({ top: Math.max(0, finalY), behavior })
83
+ const { headerOffset = 0, behavior = 'smooth', adjustTargetY, durationMs = 320 } = options
84
+
85
+ // Target is recomputed every frame: the row's absolute position can move as
86
+ // the page reflows (a sibling drawer collapsing) and the reachable max grows
87
+ // as the just-opened drawer expands. Clamp to the LIVE max each frame.
88
+ const computeTarget = (): number => {
89
+ const raw = target.getBoundingClientRect().top + window.scrollY - headerOffset
90
+ const adjusted = adjustTargetY ? adjustTargetY(raw) : raw
91
+ const maxScroll = Math.max(
92
+ 0,
93
+ document.documentElement.scrollHeight - window.innerHeight,
94
+ )
95
+ return Math.min(Math.max(0, adjusted), maxScroll)
96
+ }
97
+
98
+ // Any prior animation loses — one page scroll at a time.
99
+ cancelActiveScroll()
100
+
101
+ const prefersReduced =
102
+ typeof window.matchMedia === 'function' &&
103
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
104
+
105
+ // Instant paths: a single synchronous write. No tween, no anchoring race.
106
+ if (behavior === 'instant' || behavior === 'auto' || prefersReduced) {
107
+ window.scrollTo(0, computeTarget())
108
+ return
109
+ }
110
+
111
+ // Smooth: self-driven tween with instant per-frame writes (anchoring-proof).
112
+ let startY: number | null = null
113
+ let startTime = 0
114
+
115
+ // Bail the moment the user takes over with a real scroll gesture — we must
116
+ // never fight them. (Not keydown: the ticket composer auto-focuses on open,
117
+ // and typing there should not abort the scroll.)
118
+ const onUserGesture = () => cancelActiveScroll()
119
+ window.addEventListener('wheel', onUserGesture, { passive: true })
120
+ window.addEventListener('touchmove', onUserGesture, { passive: true })
121
+ teardownActive = () => {
122
+ window.removeEventListener('wheel', onUserGesture)
123
+ window.removeEventListener('touchmove', onUserGesture)
124
+ }
125
+
126
+ const step = (now: number) => {
127
+ if (startY === null) {
128
+ startY = window.scrollY
129
+ startTime = now
130
+ }
131
+ const targetY = computeTarget()
132
+ const t = Math.min(1, (now - startTime) / durationMs)
133
+ const y = startY + (targetY - startY) * easeOutCubic(t)
134
+ window.scrollTo(0, y)
135
+ if (t < 1) {
136
+ activeRaf = requestAnimationFrame(step)
137
+ } else {
138
+ // Final exact write in case easing left a sub-pixel gap, then teardown.
139
+ window.scrollTo(0, computeTarget())
140
+ activeRaf = 0
141
+ if (teardownActive) {
142
+ teardownActive()
143
+ teardownActive = null
144
+ }
145
+ }
146
+ }
147
+ activeRaf = requestAnimationFrame(step)
74
148
  }