@flamingo-stack/openframe-frontend-core 0.0.458-snapshot.20260721084902 → 0.0.458
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.
- package/dist/{chunk-T2EBDVMZ.cjs → chunk-5USSSRAL.cjs} +26 -26
- package/dist/{chunk-T2EBDVMZ.cjs.map → chunk-5USSSRAL.cjs.map} +1 -1
- package/dist/{chunk-JWXOZRZ6.js → chunk-6B27O3ZB.js} +4 -4
- package/dist/{chunk-DMU3BSPR.cjs → chunk-7IRPMNRL.cjs} +59 -59
- package/dist/{chunk-DMU3BSPR.cjs.map → chunk-7IRPMNRL.cjs.map} +1 -1
- package/dist/{chunk-IG4QVZSP.cjs → chunk-DQ3EHM6G.cjs} +9 -9
- package/dist/{chunk-IG4QVZSP.cjs.map → chunk-DQ3EHM6G.cjs.map} +1 -1
- package/dist/{chunk-XVHNAMAL.cjs → chunk-FTXDIDNX.cjs} +15 -15
- package/dist/{chunk-XVHNAMAL.cjs.map → chunk-FTXDIDNX.cjs.map} +1 -1
- package/dist/{chunk-MRKRE54X.js → chunk-H7ETLG65.js} +4 -4
- package/dist/{chunk-Q74TSDZY.js → chunk-HWTUVLYB.js} +2 -2
- package/dist/{chunk-LDVQIOS7.js → chunk-IDKDOQZL.js} +2 -2
- package/dist/{chunk-WVC627C7.cjs → chunk-KIIOBK7G.cjs} +26 -26
- package/dist/{chunk-WVC627C7.cjs.map → chunk-KIIOBK7G.cjs.map} +1 -1
- package/dist/{chunk-AS5B343N.js → chunk-KQGFKG6Y.js} +2 -2
- package/dist/{chunk-O5E7ZWHK.cjs → chunk-L676XVP4.cjs} +7 -7
- package/dist/{chunk-O5E7ZWHK.cjs.map → chunk-L676XVP4.cjs.map} +1 -1
- package/dist/{chunk-OFPHMNRL.cjs → chunk-LDAOPWXZ.cjs} +11 -11
- package/dist/{chunk-OFPHMNRL.cjs.map → chunk-LDAOPWXZ.cjs.map} +1 -1
- package/dist/{chunk-YFPZLP63.cjs → chunk-MKLP2SOZ.cjs} +1376 -1196
- package/dist/chunk-MKLP2SOZ.cjs.map +1 -0
- package/dist/{chunk-EFGFAVOI.js → chunk-QL7ODLJA.js} +4 -4
- package/dist/{chunk-KW33TBQX.js → chunk-R5QYMUKN.js} +2 -2
- package/dist/{chunk-TP5MVWQ2.js → chunk-TNLFAJPA.js} +4901 -4721
- package/dist/chunk-TNLFAJPA.js.map +1 -0
- package/dist/{chunk-RTQ57HC5.js → chunk-U2YDR5DT.js} +2 -2
- package/dist/{chunk-A2RF7ZG6.cjs → chunk-VTQSB6QB.cjs} +5 -5
- package/dist/{chunk-A2RF7ZG6.cjs.map → chunk-VTQSB6QB.cjs.map} +1 -1
- package/dist/{chunk-CBNHSXTR.js → chunk-WGJPTFRH.js} +2 -2
- package/dist/{chunk-X5TAAOE7.cjs → chunk-XUNJILJT.cjs} +62 -62
- package/dist/{chunk-X5TAAOE7.cjs.map → chunk-XUNJILJT.cjs.map} +1 -1
- package/dist/{chunk-GSKLTMIY.js → chunk-YQBPIUZU.js} +2 -2
- package/dist/{chunk-6XYHSDCL.cjs → chunk-YV2YA3QH.cjs} +31 -31
- package/dist/{chunk-6XYHSDCL.cjs.map → chunk-YV2YA3QH.cjs.map} +1 -1
- package/dist/{chunk-OJ5ROJSS.cjs → chunk-ZDJ4ZUQ4.cjs} +37 -37
- package/dist/{chunk-OJ5ROJSS.cjs.map → chunk-ZDJ4ZUQ4.cjs.map} +1 -1
- package/dist/{chunk-JGSBLQLE.js → chunk-ZHQQRYMB.js} +4 -4
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chart.d.ts +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/chat-sidebar.d.ts.map +1 -1
- package/dist/components/chat/chat-ticket-list.d.ts.map +1 -1
- package/dist/components/chat/guide-welcome.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
- package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +135 -129
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +15 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-container.d.ts.map +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/drawer.d.ts +1 -1
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +8 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +7 -1
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/modal.d.ts.map +1 -1
- package/dist/components/ui/overlay-scroll-area.d.ts +59 -0
- package/dist/components/ui/overlay-scroll-area.d.ts.map +1 -0
- package/dist/index.cjs +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -1
- package/package.json +3 -1
- package/src/components/chat/chat-message-list.tsx +7 -6
- package/src/components/chat/chat-sidebar.tsx +9 -4
- package/src/components/chat/chat-ticket-list.tsx +8 -2
- package/src/components/chat/guide-welcome.tsx +6 -4
- package/src/components/chat/mingo-chat-history.tsx +6 -4
- package/src/components/chat/mingo-welcome.tsx +6 -4
- package/src/components/features/notifications/notification-drawer.tsx +8 -4
- package/src/components/features/time-tracker/time-tracker-panel.tsx +11 -3
- package/src/components/layout/page-container.tsx +17 -5
- package/src/components/ui/drawer.tsx +8 -3
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/modal-v2.tsx +12 -2
- package/src/components/ui/modal.tsx +9 -2
- package/src/components/ui/overlay-scroll-area.tsx +219 -0
- package/src/stories/OverlayScrollArea.stories.tsx +52 -0
- package/src/styles/index.css +2 -0
- package/src/styles/overlay-scrollbars-theme.css +19 -0
- package/src/styles/vendor-overlayscrollbars.css +673 -0
- package/dist/chunk-TP5MVWQ2.js.map +0 -1
- package/dist/chunk-YFPZLP63.cjs.map +0 -1
- /package/dist/{chunk-JWXOZRZ6.js.map → chunk-6B27O3ZB.js.map} +0 -0
- /package/dist/{chunk-MRKRE54X.js.map → chunk-H7ETLG65.js.map} +0 -0
- /package/dist/{chunk-Q74TSDZY.js.map → chunk-HWTUVLYB.js.map} +0 -0
- /package/dist/{chunk-LDVQIOS7.js.map → chunk-IDKDOQZL.js.map} +0 -0
- /package/dist/{chunk-AS5B343N.js.map → chunk-KQGFKG6Y.js.map} +0 -0
- /package/dist/{chunk-EFGFAVOI.js.map → chunk-QL7ODLJA.js.map} +0 -0
- /package/dist/{chunk-KW33TBQX.js.map → chunk-R5QYMUKN.js.map} +0 -0
- /package/dist/{chunk-RTQ57HC5.js.map → chunk-U2YDR5DT.js.map} +0 -0
- /package/dist/{chunk-CBNHSXTR.js.map → chunk-WGJPTFRH.js.map} +0 -0
- /package/dist/{chunk-GSKLTMIY.js.map → chunk-YQBPIUZU.js.map} +0 -0
- /package/dist/{chunk-JGSBLQLE.js.map → chunk-ZHQQRYMB.js.map} +0 -0
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
145
|
-
|
|
145
|
+
<OverlayScrollArea
|
|
146
|
+
viewportRef={scrollRef}
|
|
146
147
|
onScroll={updateScrollFade}
|
|
147
|
-
className="flex
|
|
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
|
-
</
|
|
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
|
-
<
|
|
342
|
-
|
|
342
|
+
<OverlayScrollArea
|
|
343
|
+
viewportRef={scrollRef}
|
|
343
344
|
onScroll={updateFade}
|
|
344
|
-
className="flex
|
|
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
|
-
</
|
|
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
|
-
<
|
|
243
|
-
|
|
243
|
+
<OverlayScrollArea
|
|
244
|
+
viewportRef={scrollRef}
|
|
244
245
|
onScroll={updateScrollFade}
|
|
245
|
-
className="flex
|
|
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
|
-
</
|
|
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,12 @@ function DrawerScrollList({
|
|
|
189
190
|
|
|
190
191
|
const isEmpty = unreadNotifications.length === 0
|
|
191
192
|
return (
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
<OverlayScrollArea
|
|
194
|
+
viewportRef={scrollRef}
|
|
195
|
+
// Horizontal padding lives on the host: OverlayScrollbars zeroes viewport
|
|
196
|
+
// padding, so `px-*` on `contentClassName` would be silently dropped.
|
|
197
|
+
className="flex-1 min-h-0 px-[var(--spacing-system-m)]"
|
|
198
|
+
contentClassName="flex flex-col gap-[var(--spacing-system-xs)]"
|
|
195
199
|
>
|
|
196
200
|
{isEmpty && !isLoadingMore ? (
|
|
197
201
|
<EmptyState />
|
|
@@ -214,7 +218,7 @@ function DrawerScrollList({
|
|
|
214
218
|
{loadMore && hasMore && <div ref={sentinelRef} className="h-1" aria-hidden="true" />}
|
|
215
219
|
</>
|
|
216
220
|
)}
|
|
217
|
-
</
|
|
221
|
+
</OverlayScrollArea>
|
|
218
222
|
)
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -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,18 @@ export function TimeTrackerPanel({
|
|
|
149
150
|
const visibleEntries = lastEntries.slice(0, 3)
|
|
150
151
|
|
|
151
152
|
return (
|
|
152
|
-
<
|
|
153
|
+
<OverlayScrollArea
|
|
153
154
|
className={cn(
|
|
154
|
-
|
|
155
|
+
// Padding lives on the host, NOT `contentClassName`: OverlayScrollbars
|
|
156
|
+
// forces `padding: 0` on the viewport, so padding on the scroller is
|
|
157
|
+
// silently dropped (see OverlayScrollArea docs).
|
|
158
|
+
'max-h-[calc(100vh-6rem)] w-full rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]',
|
|
155
159
|
className,
|
|
156
160
|
)}
|
|
161
|
+
// No `relative` here so the absolute close button anchors to the padded
|
|
162
|
+
// host (OverlayScrollArea's host is always relative) and keeps its inset
|
|
163
|
+
// from the card edge instead of from the padding box.
|
|
164
|
+
contentClassName="flex flex-col gap-[var(--spacing-system-l)]"
|
|
157
165
|
>
|
|
158
166
|
<Button
|
|
159
167
|
variant="transparent"
|
|
@@ -314,7 +322,7 @@ export function TimeTrackerPanel({
|
|
|
314
322
|
<Button variant="outline" fullWidth onClick={onClose} className="md:hidden">
|
|
315
323
|
Close
|
|
316
324
|
</Button>
|
|
317
|
-
</
|
|
325
|
+
</OverlayScrollArea>
|
|
318
326
|
)
|
|
319
327
|
}
|
|
320
328
|
|
|
@@ -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
|
|
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,21 @@ function renderAdvancedPageContainer({
|
|
|
407
408
|
return (
|
|
408
409
|
<div className={getContainerClasses()}>
|
|
409
410
|
{renderHeader()}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
|
|
412
|
+
{variant === 'detail' ? (
|
|
413
|
+
// The only scrolling variant — standardized overlay scrollbar.
|
|
414
|
+
// `contentClassName` is forwarded to BOTH layers: padding stays effective
|
|
415
|
+
// on the host, while layout (`flex flex-col gap-*`) reaches the scroller,
|
|
416
|
+
// the direct parent of the children (OverlayScrollbars zeroes viewport
|
|
417
|
+
// padding, so padding on the scroller alone would be dropped).
|
|
418
|
+
<OverlayScrollArea className={getContentClasses()} contentClassName={contentClassName}>
|
|
419
|
+
{children}
|
|
420
|
+
</OverlayScrollArea>
|
|
421
|
+
) : (
|
|
422
|
+
<div className={getContentClasses()}>
|
|
423
|
+
{children}
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
414
426
|
</div>
|
|
415
427
|
)
|
|
416
428
|
}
|
|
@@ -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
|
-
<
|
|
536
|
-
className={cn("flex
|
|
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
|
|
|
@@ -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,18 @@ Modal.displayName = "ModalV2"
|
|
|
131
132
|
|
|
132
133
|
const ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(
|
|
133
134
|
({ children, className }, ref) => (
|
|
134
|
-
|
|
135
|
+
// `className` goes to BOTH layers on purpose: historically it styled the
|
|
136
|
+
// single scrolling div that directly parented the children, so it may carry
|
|
137
|
+
// layout (`flex flex-col gap-*` — must reach the scroller/`contentClassName`,
|
|
138
|
+
// the direct parent of the children) OR padding (must stay on the host, since
|
|
139
|
+
// OverlayScrollbars zeroes viewport padding). Each class lands where it works.
|
|
140
|
+
<OverlayScrollArea
|
|
141
|
+
viewportRef={ref}
|
|
142
|
+
className={cn("flex-1 min-h-0", className)}
|
|
143
|
+
contentClassName={className}
|
|
144
|
+
>
|
|
135
145
|
{children}
|
|
136
|
-
</
|
|
146
|
+
</OverlayScrollArea>
|
|
137
147
|
)
|
|
138
148
|
)
|
|
139
149
|
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,15 @@ 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
|
-
|
|
86
|
+
// See ModalV2Content: `className` may carry layout (must reach the scroller)
|
|
87
|
+
// or padding (must stay on the host), so it goes to both layers.
|
|
88
|
+
<OverlayScrollArea
|
|
89
|
+
viewportRef={ref}
|
|
90
|
+
className={cn("min-h-0 flex-1", className)}
|
|
91
|
+
contentClassName={className}
|
|
92
|
+
>
|
|
86
93
|
{children}
|
|
87
|
-
</
|
|
94
|
+
</OverlayScrollArea>
|
|
88
95
|
)
|
|
89
96
|
)
|
|
90
97
|
ModalContent.displayName = "ModalContent"
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { OverlayScrollbars, type PartialOptions } from 'overlayscrollbars'
|
|
5
|
+
import { cn } from '../../utils/cn'
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Standardized macOS-like overlay scrollbars on every platform.
|
|
9
|
+
//
|
|
10
|
+
// OverlayScrollbars hides the native scrollbar and draws a styleable overlay
|
|
11
|
+
// one on top while keeping NATIVE scrolling (wheel, touchpad momentum,
|
|
12
|
+
// keyboard, drag-the-thumb). With `autoHide` the bar shows only while
|
|
13
|
+
// scrolling and fades out when idle — the macOS behavior, now identical on
|
|
14
|
+
// Windows/Linux and for mouse users.
|
|
15
|
+
//
|
|
16
|
+
// Touch devices keep the native scrollbars: mobile bars are already overlay
|
|
17
|
+
// and auto-hiding, so the custom layer is initialized only on fine-pointer
|
|
18
|
+
// devices (desktop / tablet with a mouse).
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/** Overlay scrollbars are desktop-only; touch keeps native scrolling. */
|
|
22
|
+
const FINE_POINTER_QUERY = '(hover: hover) and (pointer: fine)'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `matchMedia` for the fine-pointer query, or `null` where it doesn't exist —
|
|
26
|
+
* jsdom (every consumer's component tests) and non-browser render paths. The
|
|
27
|
+
* whole overlay layer is progressive enhancement, so "can't detect" degrades
|
|
28
|
+
* to the native scrollbar instead of throwing.
|
|
29
|
+
*/
|
|
30
|
+
const matchFinePointer = (): MediaQueryList | null =>
|
|
31
|
+
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
32
|
+
? window.matchMedia(FINE_POINTER_QUERY)
|
|
33
|
+
: null
|
|
34
|
+
|
|
35
|
+
/** True once mounted on a device with a mouse/trackpad-grade pointer. */
|
|
36
|
+
export function useFinePointer(): boolean {
|
|
37
|
+
const [fine, setFine] = React.useState(false)
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
const mq = matchFinePointer()
|
|
40
|
+
if (!mq) return
|
|
41
|
+
setFine(mq.matches)
|
|
42
|
+
const onChange = (e: MediaQueryListEvent) => setFine(e.matches)
|
|
43
|
+
mq.addEventListener('change', onChange)
|
|
44
|
+
return () => mq.removeEventListener('change', onChange)
|
|
45
|
+
}, [])
|
|
46
|
+
return fine
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Shared ODS defaults: thin grey thumb, transparent track, show on scroll. */
|
|
50
|
+
const ODS_SCROLLBAR_OPTIONS: PartialOptions = {
|
|
51
|
+
scrollbars: {
|
|
52
|
+
theme: 'os-theme-ods',
|
|
53
|
+
autoHide: 'scroll',
|
|
54
|
+
autoHideDelay: 800,
|
|
55
|
+
// Keep the bar visible until the first scroll so keyboard/AT users still
|
|
56
|
+
// get the affordance on freshly opened views.
|
|
57
|
+
autoHideSuspend: true,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const setRef = <T,>(ref: React.Ref<T> | undefined, value: T | null) => {
|
|
62
|
+
if (!ref) return
|
|
63
|
+
if (typeof ref === 'function') ref(value)
|
|
64
|
+
else (ref as React.MutableRefObject<T | null>).current = value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface OverlayScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
68
|
+
/** Sizing/box classes of the scroll container (`flex-1 min-h-0`, `max-h-…`,
|
|
69
|
+
* borders, background) — AND paddings. Do NOT include `overflow-*` here.
|
|
70
|
+
*
|
|
71
|
+
* PADDING BELONGS HERE, NOT ON `contentClassName`: OverlayScrollbars owns
|
|
72
|
+
* the viewport's box and writes `padding: 0` inline on it, so a padding
|
|
73
|
+
* class on the viewport is silently dropped (measured — this is why the
|
|
74
|
+
* wrapper components keep passing their consumer `className` to the host). */
|
|
75
|
+
className?: string
|
|
76
|
+
/** Layout classes for the scrolling element itself (`flex flex-col gap-…`).
|
|
77
|
+
* Children are its direct children, so `gap`/`flex` applies exactly as it
|
|
78
|
+
* did on the old single `overflow-auto` div. Paddings do NOT work here —
|
|
79
|
+
* see `className`. */
|
|
80
|
+
contentClassName?: string
|
|
81
|
+
/** Receives the actual scrolling element — use for scrollTop/scrollHeight
|
|
82
|
+
* logic and as IntersectionObserver root. Set from the first commit, valid
|
|
83
|
+
* on both the overlay and the native fallback path. */
|
|
84
|
+
viewportRef?: React.Ref<HTMLDivElement>
|
|
85
|
+
/** Extra OverlayScrollbars options merged over the ODS defaults. */
|
|
86
|
+
options?: PartialOptions
|
|
87
|
+
children?: React.ReactNode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Replacement for an `overflow-auto` scroll container with the standardized
|
|
92
|
+
* ODS overlay scrollbar. Split the old scroller's classes: sizing goes to
|
|
93
|
+
* `className`, content layout to `contentClassName`. The inner scroller div
|
|
94
|
+
* is stable across pointer modes — refs and `onScroll` (via the standard
|
|
95
|
+
* HTML props) keep working like on the plain div it replaces.
|
|
96
|
+
*
|
|
97
|
+
* On touch devices (no fine pointer) the overlay layer is never initialized
|
|
98
|
+
* and the inner div scrolls with its native (already overlay) scrollbar.
|
|
99
|
+
*/
|
|
100
|
+
export function OverlayScrollArea({
|
|
101
|
+
className,
|
|
102
|
+
contentClassName,
|
|
103
|
+
viewportRef,
|
|
104
|
+
options,
|
|
105
|
+
children,
|
|
106
|
+
...props
|
|
107
|
+
}: OverlayScrollAreaProps) {
|
|
108
|
+
const hostRef = React.useRef<HTMLDivElement>(null)
|
|
109
|
+
const scrollerRef = React.useRef<HTMLDivElement>(null)
|
|
110
|
+
|
|
111
|
+
// Serialize to keep the effect dep stable for inline `options` objects.
|
|
112
|
+
const optionsJson = JSON.stringify(options ?? null)
|
|
113
|
+
|
|
114
|
+
// Layout effect so the viewport is upgraded before consumers' own layout
|
|
115
|
+
// effects (scroll-fade hooks, observers) measure it.
|
|
116
|
+
React.useLayoutEffect(() => {
|
|
117
|
+
const host = hostRef.current
|
|
118
|
+
const viewport = scrollerRef.current
|
|
119
|
+
if (!host || !viewport) return
|
|
120
|
+
const extra = (JSON.parse(optionsJson) ?? {}) as PartialOptions
|
|
121
|
+
const mq = matchFinePointer()
|
|
122
|
+
if (!mq) return
|
|
123
|
+
let instance: ReturnType<typeof OverlayScrollbars> | null = null
|
|
124
|
+
|
|
125
|
+
const sync = () => {
|
|
126
|
+
if (mq.matches && !instance) {
|
|
127
|
+
// Our own div is passed as the viewport: children stay inside it and
|
|
128
|
+
// it remains the scrolling element; the library only suppresses its
|
|
129
|
+
// native scrollbar and appends the overlay handles to the host.
|
|
130
|
+
instance = OverlayScrollbars(
|
|
131
|
+
{ target: host, elements: { viewport } },
|
|
132
|
+
// `scrollbars` merges one level deep: a caller overriding a single
|
|
133
|
+
// key (e.g. `autoHide`) must not drop the ODS theme and the other
|
|
134
|
+
// defaults with it.
|
|
135
|
+
{
|
|
136
|
+
...ODS_SCROLLBAR_OPTIONS,
|
|
137
|
+
...extra,
|
|
138
|
+
scrollbars: { ...ODS_SCROLLBAR_OPTIONS.scrollbars, ...extra.scrollbars },
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
} else if (!mq.matches && instance) {
|
|
142
|
+
instance.destroy()
|
|
143
|
+
instance = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sync()
|
|
148
|
+
mq.addEventListener('change', sync)
|
|
149
|
+
return () => {
|
|
150
|
+
mq.removeEventListener('change', sync)
|
|
151
|
+
instance?.destroy()
|
|
152
|
+
}
|
|
153
|
+
}, [optionsJson])
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div ref={hostRef} className={cn('relative', className)}>
|
|
157
|
+
<div
|
|
158
|
+
ref={(el) => {
|
|
159
|
+
scrollerRef.current = el
|
|
160
|
+
setRef(viewportRef, el)
|
|
161
|
+
}}
|
|
162
|
+
className={cn('h-full w-full overflow-auto', contentClassName)}
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface GlobalOverlayScrollbarsProps {
|
|
172
|
+
/** Extra OverlayScrollbars options merged over the ODS defaults. */
|
|
173
|
+
options?: PartialOptions
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Applies the same overlay scrollbar to the PAGE scroller (`<body>`).
|
|
178
|
+
* Render once near the app root (client side). No-op on touch devices.
|
|
179
|
+
*
|
|
180
|
+
* ONLY mount this on APP-SHELL pages whose body barely scrolls — the real
|
|
181
|
+
* scrolling happens in inner `OverlayScrollArea` containers (openframe-frontend,
|
|
182
|
+
* openframe-chat). Do NOT mount it on long scroll-driven documents: the thumb is
|
|
183
|
+
* repositioned from JS on every scroll frame, so on a page whose main thread is
|
|
184
|
+
* already busy with reveal-on-scroll sections, tickers and marquees the scroll
|
|
185
|
+
* visibly stutters. Measured on multi-platform-hub; raising the update debounce
|
|
186
|
+
* (`update.debounce.mutation`/`resize`) did NOT help, which rules out the
|
|
187
|
+
* observer re-measurement and points at the per-frame cost itself — i.e. it is
|
|
188
|
+
* inherent to any JS-drawn scrollbar, not to this library. Such pages keep the
|
|
189
|
+
* native page scrollbar, styled by the ODS `scrollbar-width`/`scrollbar-color`
|
|
190
|
+
* globals; their inner containers can still use `OverlayScrollArea`.
|
|
191
|
+
*/
|
|
192
|
+
export function GlobalOverlayScrollbars({ options }: GlobalOverlayScrollbarsProps = {}) {
|
|
193
|
+
const fine = useFinePointer()
|
|
194
|
+
// Serialize to keep the effect dep stable for inline `options` objects.
|
|
195
|
+
const optionsJson = JSON.stringify(options ?? null)
|
|
196
|
+
|
|
197
|
+
React.useEffect(() => {
|
|
198
|
+
if (!fine) return
|
|
199
|
+
const extra = (JSON.parse(optionsJson) ?? {}) as PartialOptions
|
|
200
|
+
// Required by OverlayScrollbars for body initialization — suppresses the
|
|
201
|
+
// native scrollbars on both scroll roots before the overlay takes over.
|
|
202
|
+
document.documentElement.setAttribute('data-overlayscrollbars-initialize', '')
|
|
203
|
+
document.body.setAttribute('data-overlayscrollbars-initialize', '')
|
|
204
|
+
const instance = OverlayScrollbars(document.body, {
|
|
205
|
+
...ODS_SCROLLBAR_OPTIONS,
|
|
206
|
+
...extra,
|
|
207
|
+
// Same one-level-deep merge as OverlayScrollArea: a caller overriding a
|
|
208
|
+
// single key must not drop the ODS theme (or a tuning preset's siblings).
|
|
209
|
+
scrollbars: { ...ODS_SCROLLBAR_OPTIONS.scrollbars, ...extra.scrollbars },
|
|
210
|
+
})
|
|
211
|
+
return () => {
|
|
212
|
+
instance.destroy()
|
|
213
|
+
document.documentElement.removeAttribute('data-overlayscrollbars-initialize')
|
|
214
|
+
document.body.removeAttribute('data-overlayscrollbars-initialize')
|
|
215
|
+
}
|
|
216
|
+
}, [fine, optionsJson])
|
|
217
|
+
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import { OverlayScrollArea } from '../components/ui/overlay-scroll-area'
|
|
3
|
+
|
|
4
|
+
// POC: standardized macOS-like overlay scrollbar (OverlayScrollbars) — the
|
|
5
|
+
// bar appears while scrolling / hovering and fades out when idle, on every
|
|
6
|
+
// platform. Native scrolling (wheel, touchpad momentum, thumb drag) is kept.
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof OverlayScrollArea> = {
|
|
9
|
+
title: 'UI/OverlayScrollArea',
|
|
10
|
+
component: OverlayScrollArea,
|
|
11
|
+
parameters: { layout: 'centered' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof OverlayScrollArea>
|
|
16
|
+
|
|
17
|
+
const rows = (count: number) =>
|
|
18
|
+
Array.from({ length: count }, (_, i) => (
|
|
19
|
+
<div key={i} className="border-b border-ods-border px-[var(--spacing-system-m)] py-[var(--spacing-system-s)] text-ods-text-primary">
|
|
20
|
+
Row {i + 1} — scrollable content
|
|
21
|
+
</div>
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
export const Vertical: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<OverlayScrollArea className="h-72 w-96 rounded-lg border border-ods-border bg-ods-card">
|
|
27
|
+
{rows(40)}
|
|
28
|
+
</OverlayScrollArea>
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Horizontal: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<OverlayScrollArea className="h-24 w-96 rounded-lg border border-ods-border bg-ods-card">
|
|
35
|
+
<div className="flex w-max gap-[var(--spacing-system-s)] p-[var(--spacing-system-m)]">
|
|
36
|
+
{Array.from({ length: 30 }, (_, i) => (
|
|
37
|
+
<div key={i} className="flex h-12 w-28 shrink-0 items-center justify-center rounded-md bg-ods-bg text-ods-text-secondary">
|
|
38
|
+
Chip {i + 1}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
</OverlayScrollArea>
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Both: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<OverlayScrollArea className="h-72 w-96 rounded-lg border border-ods-border bg-ods-card">
|
|
49
|
+
<div className="w-[800px]">{rows(40)}</div>
|
|
50
|
+
</OverlayScrollArea>
|
|
51
|
+
),
|
|
52
|
+
}
|
package/src/styles/index.css
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* ODS theme for OverlayScrollbars (see components/ui/overlay-scroll-area.tsx).
|
|
2
|
+
Thin grey draggable thumb on a transparent track — matches the global
|
|
3
|
+
elevator scrollbar styling, but with macOS-like auto-hide on all platforms. */
|
|
4
|
+
.os-theme-ods {
|
|
5
|
+
--os-size: 8px;
|
|
6
|
+
--os-padding-perpendicular: 2px;
|
|
7
|
+
--os-padding-axis: 2px;
|
|
8
|
+
--os-track-bg: transparent;
|
|
9
|
+
--os-track-bg-hover: transparent;
|
|
10
|
+
--os-track-bg-active: transparent;
|
|
11
|
+
--os-handle-bg: var(--ods-system-greys-grey);
|
|
12
|
+
--os-handle-bg-hover: var(--ods-system-greys-grey-hover);
|
|
13
|
+
--os-handle-bg-active: var(--ods-system-greys-grey-action);
|
|
14
|
+
--os-handle-border-radius: 4px;
|
|
15
|
+
--os-handle-perpendicular-size: 100%;
|
|
16
|
+
--os-handle-perpendicular-size-hover: 100%;
|
|
17
|
+
--os-handle-perpendicular-size-active: 100%;
|
|
18
|
+
--os-handle-min-size: 33px;
|
|
19
|
+
}
|