@flamingo-stack/openframe-frontend-core 0.0.456 → 0.0.457

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 (125) hide show
  1. package/dist/{chunk-JHIT44TA.js → chunk-2ZW2ZPDW.js} +2 -2
  2. package/dist/{chunk-DAEKUR3J.js → chunk-4423DHRX.js} +2 -2
  3. package/dist/{chunk-LMBPJAQ2.js → chunk-5BKBG653.js} +4 -4
  4. package/dist/{chunk-A7FTNFBX.cjs → chunk-5XFJG3TC.cjs} +59 -59
  5. package/dist/{chunk-A7FTNFBX.cjs.map → chunk-5XFJG3TC.cjs.map} +1 -1
  6. package/dist/{chunk-GC63K4VD.cjs → chunk-AB65PWU2.cjs} +62 -62
  7. package/dist/{chunk-GC63K4VD.cjs.map → chunk-AB65PWU2.cjs.map} +1 -1
  8. package/dist/{chunk-2I3GN7WW.js → chunk-AJGZ3YSR.js} +2 -2
  9. package/dist/{chunk-UHVVNVUE.cjs → chunk-BDTQIIJR.cjs} +31 -31
  10. package/dist/{chunk-UHVVNVUE.cjs.map → chunk-BDTQIIJR.cjs.map} +1 -1
  11. package/dist/{chunk-KH5VTGCS.cjs → chunk-C2TDHJBO.cjs} +37 -37
  12. package/dist/{chunk-KH5VTGCS.cjs.map → chunk-C2TDHJBO.cjs.map} +1 -1
  13. package/dist/{chunk-INH6SIP5.cjs → chunk-CCT5VCRP.cjs} +9 -9
  14. package/dist/{chunk-INH6SIP5.cjs.map → chunk-CCT5VCRP.cjs.map} +1 -1
  15. package/dist/{chunk-6K6LAD6H.cjs → chunk-EOVQVVDJ.cjs} +11 -11
  16. package/dist/{chunk-6K6LAD6H.cjs.map → chunk-EOVQVVDJ.cjs.map} +1 -1
  17. package/dist/{chunk-YMGOTPY7.cjs → chunk-FM5DTJPA.cjs} +1519 -1257
  18. package/dist/chunk-FM5DTJPA.cjs.map +1 -0
  19. package/dist/{chunk-LAFCXJVT.js → chunk-GJ7BN4MI.js} +4 -4
  20. package/dist/{chunk-ZQHGSR3H.js → chunk-I7M6KCCS.js} +4 -4
  21. package/dist/{chunk-VZNJ3RYI.cjs → chunk-KQ36EHE4.cjs} +15 -15
  22. package/dist/{chunk-VZNJ3RYI.cjs.map → chunk-KQ36EHE4.cjs.map} +1 -1
  23. package/dist/{chunk-DU6R7XNU.js → chunk-KXIIUE75.js} +2 -2
  24. package/dist/{chunk-UEKCD6WV.js → chunk-NSL5MNS4.js} +5103 -4841
  25. package/dist/chunk-NSL5MNS4.js.map +1 -0
  26. package/dist/{chunk-U27TXTAK.js → chunk-O7F7RZFN.js} +4 -4
  27. package/dist/{chunk-YLTY4Z4B.js → chunk-R5IQJXA3.js} +2 -2
  28. package/dist/{chunk-F3IBBAKV.js → chunk-RSW42GTZ.js} +2 -2
  29. package/dist/{chunk-GQZGCMRF.cjs → chunk-SQ3CP2IV.cjs} +7 -7
  30. package/dist/{chunk-GQZGCMRF.cjs.map → chunk-SQ3CP2IV.cjs.map} +1 -1
  31. package/dist/{chunk-DYBATZJP.cjs → chunk-U4APSAAR.cjs} +5 -5
  32. package/dist/{chunk-DYBATZJP.cjs.map → chunk-U4APSAAR.cjs.map} +1 -1
  33. package/dist/{chunk-UXKSOJR7.js → chunk-WXDEMTLD.js} +2 -2
  34. package/dist/{chunk-F2CCUPKC.cjs → chunk-YCIVMKRS.cjs} +26 -26
  35. package/dist/{chunk-F2CCUPKC.cjs.map → chunk-YCIVMKRS.cjs.map} +1 -1
  36. package/dist/{chunk-TXS2AMFV.cjs → chunk-YHB4MNUX.cjs} +26 -26
  37. package/dist/{chunk-TXS2AMFV.cjs.map → chunk-YHB4MNUX.cjs.map} +1 -1
  38. package/dist/components/case-studies/index.cjs +8 -8
  39. package/dist/components/case-studies/index.js +2 -2
  40. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  41. package/dist/components/chat/chat-sidebar.d.ts.map +1 -1
  42. package/dist/components/chat/chat-ticket-list.d.ts.map +1 -1
  43. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  44. package/dist/components/chat/index.cjs +2 -2
  45. package/dist/components/chat/index.js +1 -1
  46. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  47. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  48. package/dist/components/contact/index.cjs +3 -3
  49. package/dist/components/contact/index.js +2 -2
  50. package/dist/components/docs/index.cjs +5 -5
  51. package/dist/components/docs/index.js +4 -4
  52. package/dist/components/embeds/index.cjs +3 -3
  53. package/dist/components/embeds/index.js +2 -2
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/index.cjs +2 -2
  57. package/dist/components/features/index.js +1 -1
  58. package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
  59. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
  60. package/dist/components/help-center-pages/index.cjs +20 -20
  61. package/dist/components/help-center-pages/index.js +11 -11
  62. package/dist/components/index.cjs +135 -129
  63. package/dist/components/index.cjs.map +1 -1
  64. package/dist/components/index.js +15 -9
  65. package/dist/components/index.js.map +1 -1
  66. package/dist/components/layout/page-container.d.ts.map +1 -1
  67. package/dist/components/navigation/app-layout-drawer.d.ts +1 -1
  68. package/dist/components/navigation/index.cjs +2 -2
  69. package/dist/components/navigation/index.js +1 -1
  70. package/dist/components/onboarding-guides/index.cjs +5 -5
  71. package/dist/components/onboarding-guides/index.js +4 -4
  72. package/dist/components/related-content/index.cjs +3 -3
  73. package/dist/components/related-content/index.js +2 -2
  74. package/dist/components/tickets/index.cjs +5 -5
  75. package/dist/components/tickets/index.js +4 -4
  76. package/dist/components/ui/drawer.d.ts +1 -1
  77. package/dist/components/ui/drawer.d.ts.map +1 -1
  78. package/dist/components/ui/index.cjs +8 -2
  79. package/dist/components/ui/index.cjs.map +1 -1
  80. package/dist/components/ui/index.d.ts +1 -0
  81. package/dist/components/ui/index.d.ts.map +1 -1
  82. package/dist/components/ui/index.js +7 -1
  83. package/dist/components/ui/marquee-wall.d.ts.map +1 -1
  84. package/dist/components/ui/menubar.d.ts +3 -6
  85. package/dist/components/ui/menubar.d.ts.map +1 -1
  86. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  87. package/dist/components/ui/modal.d.ts.map +1 -1
  88. package/dist/components/ui/overlay-scroll-area.d.ts +59 -0
  89. package/dist/components/ui/overlay-scroll-area.d.ts.map +1 -0
  90. package/dist/index.cjs +8 -2
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.js +7 -1
  93. package/package.json +3 -1
  94. package/src/components/chat/chat-message-list.tsx +7 -6
  95. package/src/components/chat/chat-sidebar.tsx +9 -4
  96. package/src/components/chat/chat-ticket-list.tsx +8 -2
  97. package/src/components/chat/guide-welcome.tsx +6 -4
  98. package/src/components/chat/mingo-chat-history.tsx +6 -4
  99. package/src/components/chat/mingo-welcome.tsx +6 -4
  100. package/src/components/features/notifications/notification-drawer.tsx +6 -4
  101. package/src/components/features/time-tracker/time-tracker-panel.tsx +5 -3
  102. package/src/components/layout/page-container.tsx +13 -5
  103. package/src/components/ui/drawer.tsx +8 -3
  104. package/src/components/ui/index.ts +1 -0
  105. package/src/components/ui/marquee-wall.tsx +28 -4
  106. package/src/components/ui/modal-v2.tsx +6 -2
  107. package/src/components/ui/modal.tsx +6 -2
  108. package/src/components/ui/overlay-scroll-area.tsx +219 -0
  109. package/src/stories/OverlayScrollArea.stories.tsx +52 -0
  110. package/src/styles/index.css +2 -0
  111. package/src/styles/overlay-scrollbars-theme.css +19 -0
  112. package/src/styles/vendor-overlayscrollbars.css +673 -0
  113. package/dist/chunk-UEKCD6WV.js.map +0 -1
  114. package/dist/chunk-YMGOTPY7.cjs.map +0 -1
  115. /package/dist/{chunk-JHIT44TA.js.map → chunk-2ZW2ZPDW.js.map} +0 -0
  116. /package/dist/{chunk-DAEKUR3J.js.map → chunk-4423DHRX.js.map} +0 -0
  117. /package/dist/{chunk-LMBPJAQ2.js.map → chunk-5BKBG653.js.map} +0 -0
  118. /package/dist/{chunk-2I3GN7WW.js.map → chunk-AJGZ3YSR.js.map} +0 -0
  119. /package/dist/{chunk-LAFCXJVT.js.map → chunk-GJ7BN4MI.js.map} +0 -0
  120. /package/dist/{chunk-ZQHGSR3H.js.map → chunk-I7M6KCCS.js.map} +0 -0
  121. /package/dist/{chunk-DU6R7XNU.js.map → chunk-KXIIUE75.js.map} +0 -0
  122. /package/dist/{chunk-U27TXTAK.js.map → chunk-O7F7RZFN.js.map} +0 -0
  123. /package/dist/{chunk-YLTY4Z4B.js.map → chunk-R5IQJXA3.js.map} +0 -0
  124. /package/dist/{chunk-F3IBBAKV.js.map → chunk-RSW42GTZ.js.map} +0 -0
  125. /package/dist/{chunk-UXKSOJR7.js.map → chunk-WXDEMTLD.js.map} +0 -0
@@ -3,6 +3,7 @@
3
3
  import { useRef, useEffect, useLayoutEffect, useImperativeHandle, forwardRef } from "react"
4
4
  import { useStickToBottom } from "use-stick-to-bottom"
5
5
  import { cn } from "../../utils/cn"
6
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
6
7
  import { ChatMessageEnhanced } from "./chat-message-enhanced"
7
8
  import { ChatMessageListSkeleton } from "./chat-message-skeleton"
8
9
  import { DotsLoaderIcon } from "../icons-v2-generated"
@@ -529,10 +530,11 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
529
530
 
530
531
  return (
531
532
  <div className="relative flex-1 min-h-0 flex flex-col">
532
- <div
533
- ref={setScrollRef}
534
- className={cn(
535
- "flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1",
533
+ <OverlayScrollArea
534
+ viewportRef={setScrollRef}
535
+ className="flex-1 min-h-0"
536
+ contentClassName={cn(
537
+ "flex flex-col overflow-x-hidden",
536
538
  // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):
537
539
  // reaching the top/bottom of the thread must NOT chain the wheel/
538
540
  // touch scroll to the page behind the chat — e.g. the company-hub
@@ -542,7 +544,6 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
542
544
  // the surrounding page keeps scrolling normally when the pointer is
543
545
  // over the (non-interactive) thread.
544
546
  overscrollContain && "overscroll-contain",
545
- "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
546
547
  className,
547
548
  )}
548
549
  {...props}
@@ -597,7 +598,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
597
598
  )
598
599
  })}
599
600
  </div>
600
- </div>
601
+ </OverlayScrollArea>
601
602
 
602
603
  {/* Footer-pinned streaming loader — outside the scroller so it
603
604
  doesn't jitter as the streaming message grows. Color is set
@@ -1,6 +1,7 @@
1
1
  import { forwardRef, useRef, useEffect } from "react"
2
2
  import { cn } from "../../utils/cn"
3
3
  import { Button } from "../ui/button"
4
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
4
5
  import { ChatPlusIcon, ChatsIcon } from "../icons-v2-generated"
5
6
  import { Chevron02RightIcon } from "../icons-v2-generated"
6
7
  import { ChatSidebarSkeleton, DialogListItemSkeleton } from "./chat-sidebar-skeleton"
@@ -154,12 +155,16 @@ const ChatSidebar = forwardRef<HTMLDivElement, ChatSidebarProps>(
154
155
  </div>
155
156
  ) : children ? (
156
157
  /* Custom children content */
157
- <div className="flex-1 overflow-y-auto overscroll-contain">
158
+ <OverlayScrollArea className="flex-1 min-h-0" contentClassName="overscroll-contain">
158
159
  {children}
159
- </div>
160
+ </OverlayScrollArea>
160
161
  ) : (
161
162
  /* Dialogs List */
162
- <div ref={scrollContainerRef} className="flex-1 overflow-y-auto overscroll-contain min-h-0">
163
+ <OverlayScrollArea
164
+ viewportRef={scrollContainerRef}
165
+ className="flex-1 min-h-0"
166
+ contentClassName="overscroll-contain"
167
+ >
163
168
  <div className="flex flex-col">
164
169
  {dialogs.map((dialog) => (
165
170
  <DialogListItem
@@ -183,7 +188,7 @@ const ChatSidebar = forwardRef<HTMLDivElement, ChatSidebarProps>(
183
188
  </div>
184
189
  )}
185
190
  </div>
186
- </div>
191
+ </OverlayScrollArea>
187
192
  )}
188
193
  </div>
189
194
  </div>
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
6
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
6
7
  import { ChatTicketItem, type ChatTicketItemData } from './entity-cards/chat-ticket-item'
7
8
 
8
9
  export interface ChatTicketListProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -46,7 +47,12 @@ const ChatTicketList = React.forwardRef<HTMLDivElement, ChatTicketListProps>(
46
47
  !fadeBottom && "border-b rounded-b-md",
47
48
  )}
48
49
  >
49
- <div ref={scrollRef} className="overflow-y-auto overscroll-contain h-full" onScroll={updateFade}>
50
+ <OverlayScrollArea
51
+ viewportRef={scrollRef}
52
+ className="h-full"
53
+ contentClassName="overscroll-contain"
54
+ onScroll={updateFade}
55
+ >
50
56
  {tickets.map((ticket) => (
51
57
  <ChatTicketItem
52
58
  key={ticket.id}
@@ -54,7 +60,7 @@ const ChatTicketList = React.forwardRef<HTMLDivElement, ChatTicketListProps>(
54
60
  onClick={onTicketClick}
55
61
  />
56
62
  ))}
57
- </div>
63
+ </OverlayScrollArea>
58
64
 
59
65
  {/* Scroll-fade overlays — tinted with the page background so edge
60
66
  tickets fade into the surface behind the list in BOTH themes
@@ -4,6 +4,7 @@ import * as React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import { MingoIcon } from '../icons'
6
6
  import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
7
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
7
8
  import { Skeleton } from '../ui/skeleton'
8
9
  import { QuickActionWall } from './quick-action-wall'
9
10
  import { EntityIcon } from '../icon-display'
@@ -141,10 +142,11 @@ export function GuideWelcome({
141
142
  {/* Greeting + slash-command list share one scroll region; `relative` so
142
143
  the edge fades can overlay it. */}
143
144
  <div className="relative flex flex-1 min-h-0 flex-col">
144
- <div
145
- ref={scrollRef}
145
+ <OverlayScrollArea
146
+ viewportRef={scrollRef}
146
147
  onScroll={updateScrollFade}
147
- className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain"
148
+ className="flex-1 min-h-0"
149
+ contentClassName="flex min-h-full flex-col gap-[var(--spacing-system-m)] overscroll-contain"
148
150
  >
149
151
  {/* Greeting grows to fill (`flex-1`) so the slash-command list stays
150
152
  anchored below it — but its content is anchored at the TOP of the
@@ -187,7 +189,7 @@ export function GuideWelcome({
187
189
  </div>
188
190
 
189
191
  {children}
190
- </div>
192
+ </OverlayScrollArea>
191
193
 
192
194
  {/* Edge scroll-fades — visible only when content is hidden beyond them. */}
193
195
  <ScrollFadeOverlay edge="top" visible={fadeTop} className="h-12" />
@@ -5,6 +5,7 @@ import { cn } from '../../utils/cn'
5
5
  import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
6
6
  import { Button } from '../ui/button'
7
7
  import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
8
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
8
9
  import { Ellipsis01Icon, SearchXmarkIcon } from '../icons-v2-generated'
9
10
  import { ChatListEmptyState } from './chat-list-empty-state'
10
11
  import type { DialogItem } from './types/component.types'
@@ -338,10 +339,11 @@ export function MingoChatHistory({
338
339
  {/* Scroll region — the search INPUT now lives in the panel header
339
340
  (Figma 116:51217), so the list is just the grouped rows + fades. */}
340
341
  <div className="relative flex flex-1 min-h-0 flex-col">
341
- <div
342
- ref={scrollRef}
342
+ <OverlayScrollArea
343
+ viewportRef={scrollRef}
343
344
  onScroll={updateFade}
344
- className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain"
345
+ className="flex-1 min-h-0"
346
+ contentClassName="flex flex-col gap-[var(--spacing-system-m)] overscroll-contain"
345
347
  >
346
348
  {noSearchResults ? (
347
349
  // No search matches — same centred glyph + title + caption layout as
@@ -374,7 +376,7 @@ export function MingoChatHistory({
374
376
  ))
375
377
  )}
376
378
  {hasMore ? <div ref={sentinelRef} className="h-px shrink-0" /> : null}
377
- </div>
379
+ </OverlayScrollArea>
378
380
 
379
381
  {/* Scroll-fade — only while content is hidden in that direction. */}
380
382
  <ScrollFadeOverlay edge="top" visible={fadeTop} color="var(--color-bg-card)" className="h-12" />
@@ -8,6 +8,7 @@ import { QuickActionChipButton } from './quick-action-chip'
8
8
  import { QuickActionWall } from './quick-action-wall'
9
9
  import { Button } from '../ui/button'
10
10
  import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
11
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
11
12
  import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
12
13
  import {
13
14
  CompassIcon,
@@ -239,10 +240,11 @@ export function MingoWelcome({
239
240
  input. The wrapper is `relative` so the scroll-fade gradients can
240
241
  overlay the top/bottom edges. */}
241
242
  <div className="relative flex flex-1 min-h-0 flex-col">
242
- <div
243
- ref={scrollRef}
243
+ <OverlayScrollArea
244
+ viewportRef={scrollRef}
244
245
  onScroll={updateScrollFade}
245
- className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain"
246
+ className="flex-1 min-h-0"
247
+ contentClassName="flex min-h-full flex-col gap-[var(--spacing-system-m)] overscroll-contain"
246
248
  >
247
249
  {/* Greeting — grows to fill (`flex-1`) so it centres vertically,
248
250
  keeping the grid anchored at the bottom of the scroll area. Default
@@ -261,7 +263,7 @@ export function MingoWelcome({
261
263
  <p className="text-h4 text-ods-text-secondary">{subtitle}</p>
262
264
  </div>
263
265
  </div>
264
- </div>
266
+ </OverlayScrollArea>
265
267
 
266
268
  {/* Edge scroll-fades — visible only when content is hidden beyond them.
267
269
  Fade into the panel's dark `ods-bg` surface (the default color), matching
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useRef } from 'react'
4
+ import { OverlayScrollArea } from '../../ui/overlay-scroll-area'
4
5
  import { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon'
5
6
  import { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated'
6
7
  import { useAppLayoutDrawerContainer } from '../../navigation/app-layout-context'
@@ -189,9 +190,10 @@ function DrawerScrollList({
189
190
 
190
191
  const isEmpty = unreadNotifications.length === 0
191
192
  return (
192
- <div
193
- ref={scrollRef}
194
- className="flex flex-1 flex-col gap-[var(--spacing-system-xs)] overflow-y-auto px-[var(--spacing-system-m)]"
193
+ <OverlayScrollArea
194
+ viewportRef={scrollRef}
195
+ className="flex-1 min-h-0"
196
+ contentClassName="flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)]"
195
197
  >
196
198
  {isEmpty && !isLoadingMore ? (
197
199
  <EmptyState />
@@ -214,7 +216,7 @@ function DrawerScrollList({
214
216
  {loadMore && hasMore && <div ref={sentinelRef} className="h-1" aria-hidden="true" />}
215
217
  </>
216
218
  )}
217
- </div>
219
+ </OverlayScrollArea>
218
220
  )
219
221
  }
220
222
 
@@ -21,6 +21,7 @@ import {
21
21
  XmarkIcon,
22
22
  } from '../../icons-v2-generated'
23
23
  import { useTrackerClock } from './use-tracker-clock'
24
+ import { OverlayScrollArea } from '../../ui/overlay-scroll-area'
24
25
  import type { TimeTrackerData, TimeTrackerEntry, TimeTrackerStatus } from './types'
25
26
 
26
27
  interface CustomerAutocompleteOption extends AutocompleteOption {
@@ -149,11 +150,12 @@ export function TimeTrackerPanel({
149
150
  const visibleEntries = lastEntries.slice(0, 3)
150
151
 
151
152
  return (
152
- <div
153
+ <OverlayScrollArea
153
154
  className={cn(
154
- 'relative flex max-h-[calc(100vh-6rem)] w-full flex-col gap-[var(--spacing-system-l)] overflow-y-auto rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]',
155
+ 'max-h-[calc(100vh-6rem)] w-full rounded-md border border-ods-border bg-ods-card',
155
156
  className,
156
157
  )}
158
+ contentClassName="relative flex flex-col gap-[var(--spacing-system-l)] p-[var(--spacing-system-m)]"
157
159
  >
158
160
  <Button
159
161
  variant="transparent"
@@ -314,7 +316,7 @@ export function TimeTrackerPanel({
314
316
  <Button variant="outline" fullWidth onClick={onClose} className="md:hidden">
315
317
  Close
316
318
  </Button>
317
- </div>
319
+ </OverlayScrollArea>
318
320
  )
319
321
  }
320
322
 
@@ -4,6 +4,7 @@ import React from 'react'
4
4
  import { cn } from '../../utils/cn'
5
5
  import type { ActionsMenuGroup } from '../ui/actions-menu'
6
6
  import { PageActions, type PageActionButton } from '../ui/page-actions'
7
+ import { OverlayScrollArea } from '../ui/overlay-scroll-area'
7
8
  import { BackButton } from './back-button'
8
9
 
9
10
  // Legacy interface for backward compatibility (layout version)
@@ -393,7 +394,7 @@ function renderAdvancedPageContainer({
393
394
 
394
395
  switch (variant) {
395
396
  case 'detail':
396
- return cn('flex-1 overflow-auto', mobilePadding, contentClassName)
397
+ return cn('flex-1 min-h-0', mobilePadding, contentClassName)
397
398
  case 'list':
398
399
  return cn('flex flex-col gap-4 md:gap-6', mobilePadding, contentClassName)
399
400
  case 'form':
@@ -407,10 +408,17 @@ function renderAdvancedPageContainer({
407
408
  return (
408
409
  <div className={getContainerClasses()}>
409
410
  {renderHeader()}
410
-
411
- <div className={getContentClasses()}>
412
- {children}
413
- </div>
411
+
412
+ {variant === 'detail' ? (
413
+ // The only scrolling variant — standardized overlay scrollbar.
414
+ <OverlayScrollArea className={getContentClasses()}>
415
+ {children}
416
+ </OverlayScrollArea>
417
+ ) : (
418
+ <div className={getContentClasses()}>
419
+ {children}
420
+ </div>
421
+ )}
414
422
  </div>
415
423
  )
416
424
  }
@@ -7,6 +7,7 @@ import { X } from "lucide-react"
7
7
 
8
8
  import { cn } from "../../utils/cn"
9
9
  import { useHeaderHeight } from "../../hooks/ui/use-header-height"
10
+ import { OverlayScrollArea } from "./overlay-scroll-area"
10
11
 
11
12
  /** Unified overlay backdrop — dimmed, no blur. Single source of truth for
12
13
  * every full-screen backdrop (Drawer, AppLayoutDrawer, MobileBurgerMenu,
@@ -530,12 +531,16 @@ DrawerDescription.displayName = "DrawerDescription"
530
531
 
531
532
  const DrawerBody = ({
532
533
  className,
534
+ children,
533
535
  ...props
534
536
  }: React.HTMLAttributes<HTMLDivElement>) => (
535
- <div
536
- className={cn("flex flex-1 flex-col gap-4 overflow-y-auto", className)}
537
+ <OverlayScrollArea
538
+ className={cn("flex-1 min-h-0", className)}
539
+ contentClassName="flex flex-col gap-4"
537
540
  {...props}
538
- />
541
+ >
542
+ {children}
543
+ </OverlayScrollArea>
539
544
  )
540
545
  DrawerBody.displayName = "DrawerBody"
541
546
 
@@ -177,3 +177,4 @@ export * from './simple-markdown-renderer'
177
177
  export { RichMarkdownRenderer, type RichMarkdownRendererProps } from './rich-markdown-renderer'
178
178
  export * from './filter-pill-row'
179
179
 
180
+ export * from './overlay-scroll-area'
@@ -551,13 +551,24 @@ export function MarqueeWall({
551
551
  // tap's action. A new press is unambiguously a new intent.
552
552
  suppressClickRef.current = false
553
553
  dragStartRef.current = { coord: axis === 'y' ? e.clientY : e.clientX, pos: posRef.current }
554
- try { containerRef.current?.setPointerCapture(e.pointerId) } catch { /* capture is best-effort */ }
554
+ // NOTE deliberately NO setPointerCapture here capture is taken LAZILY in
555
+ // `onDragPointerMove`, once the press is proven to be a drag. Capturing on
556
+ // pointerdown retargets the compatibility mouse events (mouseup, and hence
557
+ // `click`, which fires at the nearest common ancestor of down/up) from the
558
+ // pressed element to this container — so a plain tap on a quick-action chip
559
+ // never reaches the chip's own onClick and the whole wall reads as dead.
555
560
  }, [marqueeMounted, axis, posRef])
556
561
 
557
562
  const onDragPointerMove = React.useCallback((e: React.PointerEvent) => {
558
563
  if (!draggingRef.current) return
559
564
  const delta = (axis === 'y' ? e.clientY : e.clientX) - dragStartRef.current.coord
560
- if (Math.abs(delta) > 3) dragMovedRef.current = true
565
+ if (Math.abs(delta) > 3 && !dragMovedRef.current) {
566
+ dragMovedRef.current = true
567
+ // Past the threshold this IS a drag: capture so the gesture keeps
568
+ // tracking once the pointer leaves the wall. Retargeting click is now
569
+ // WANTED — the trailing synthetic click is suppressed below anyway.
570
+ try { containerRef.current?.setPointerCapture(e.pointerId) } catch { /* best-effort */ }
571
+ }
561
572
  // Track follows the pointer. `applyPos` maps pos→translate as `-pos`
562
573
  // (forward) / `pos-wrap` (reverse), so a rightward/downward drag (delta > 0)
563
574
  // must DECREASE pos on a forward wall and INCREASE it on a reverse one.
@@ -574,9 +585,22 @@ export function MarqueeWall({
574
585
  // doesn't fire the chip's action.
575
586
  if (dragMovedRef.current) suppressClickRef.current = true
576
587
  dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS
577
- try { containerRef.current?.releasePointerCapture(e.pointerId) } catch { /* best-effort */ }
588
+ // Only ever captured past the drag threshold (see `onDragPointerMove`).
589
+ if (dragMovedRef.current) {
590
+ try { containerRef.current?.releasePointerCapture(e.pointerId) } catch { /* best-effort */ }
591
+ }
578
592
  }, [])
579
593
 
594
+ // Pre-threshold presses hold NO pointer capture, so a pointer that leaves the
595
+ // wall before becoming a drag would never deliver its `pointerup` here and
596
+ // `draggingRef` would stay stuck true — freezing the marquee forever. End the
597
+ // gesture on leave; a captured (real) drag keeps running, capture guarantees
598
+ // its own pointerup.
599
+ const onDragPointerLeave = React.useCallback((e: React.PointerEvent) => {
600
+ onPointerLeave(e)
601
+ if (draggingRef.current && !dragMovedRef.current) draggingRef.current = false
602
+ }, [onPointerLeave])
603
+
580
604
  const onDragClickCapture = React.useCallback((e: React.MouseEvent) => {
581
605
  if (suppressClickRef.current) {
582
606
  suppressClickRef.current = false
@@ -645,7 +669,7 @@ export function MarqueeWall({
645
669
  style={dragScroll && marqueeMounted ? { touchAction: axis === 'y' ? 'pan-x' : 'pan-y' } : undefined}
646
670
  data-marquee-track={trackId}
647
671
  onPointerEnter={onPointerEnter}
648
- onPointerLeave={onPointerLeave}
672
+ onPointerLeave={dragScroll ? onDragPointerLeave : onPointerLeave}
649
673
  onPointerDown={dragScroll ? onDragPointerDown : undefined}
650
674
  onPointerMove={dragScroll ? onDragPointerMove : undefined}
651
675
  onPointerUp={dragScroll ? onDragPointerUp : undefined}
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react"
5
5
  import { usePreventScroll } from "@react-aria/overlays"
6
6
  import { XmarkIcon } from "../icons-v2-generated"
7
7
  import { cn } from "../../utils/cn"
8
+ import { OverlayScrollArea } from "./overlay-scroll-area"
8
9
 
9
10
  // Duration of the open/close animation in ms — keep in sync with the
10
11
  // `duration-200` utilities applied to the backdrop and panel below.
@@ -131,9 +132,12 @@ Modal.displayName = "ModalV2"
131
132
 
132
133
  const ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(
133
134
  ({ children, className }, ref) => (
134
- <div ref={ref} className={cn("flex-1 min-h-0 overflow-y-auto", className)}>
135
+ <OverlayScrollArea
136
+ viewportRef={ref}
137
+ className={cn("flex-1 min-h-0", className)}
138
+ >
135
139
  {children}
136
- </div>
140
+ </OverlayScrollArea>
137
141
  )
138
142
  )
139
143
  ModalContent.displayName = "ModalV2Content"
@@ -4,6 +4,7 @@ import * as React from "react"
4
4
  import { useEffect } from "react"
5
5
  import { usePreventScroll } from "@react-aria/overlays"
6
6
  import { cn } from "../../utils/cn"
7
+ import { OverlayScrollArea } from "./overlay-scroll-area"
7
8
 
8
9
  interface ModalProps {
9
10
  isOpen: boolean
@@ -82,9 +83,12 @@ Modal.displayName = "Modal"
82
83
  /** @deprecated Use ModalV2Content from './modal-v2' instead. */
83
84
  const ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(
84
85
  ({ children, className }, ref) => (
85
- <div ref={ref} className={cn("overflow-y-auto min-h-0 flex-1", className)}>
86
+ <OverlayScrollArea
87
+ viewportRef={ref}
88
+ className={cn("min-h-0 flex-1", className)}
89
+ >
86
90
  {children}
87
- </div>
91
+ </OverlayScrollArea>
88
92
  )
89
93
  )
90
94
  ModalContent.displayName = "ModalContent"