@flamingo-stack/openframe-frontend-core 0.0.400 → 0.0.401
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-4HAVKWTJ.js → chunk-2B5FIJE5.js} +2 -2
- package/dist/{chunk-MFRP2UDK.cjs → chunk-3GXZPZHR.cjs} +26 -26
- package/dist/{chunk-MFRP2UDK.cjs.map → chunk-3GXZPZHR.cjs.map} +1 -1
- package/dist/{chunk-6VR7BJF6.js → chunk-5P5WMTVW.js} +4 -4
- package/dist/{chunk-556BG7SX.cjs → chunk-7YKHVAUU.cjs} +31 -31
- package/dist/{chunk-556BG7SX.cjs.map → chunk-7YKHVAUU.cjs.map} +1 -1
- package/dist/{chunk-PD5MXNTJ.js → chunk-B3IDZ6JJ.js} +4 -4
- package/dist/{chunk-RSFHAFZN.cjs → chunk-BI23LOMC.cjs} +37 -37
- package/dist/{chunk-RSFHAFZN.cjs.map → chunk-BI23LOMC.cjs.map} +1 -1
- package/dist/{chunk-VTSEL7CJ.cjs → chunk-CG7LIPSA.cjs} +59 -59
- package/dist/{chunk-VTSEL7CJ.cjs.map → chunk-CG7LIPSA.cjs.map} +1 -1
- package/dist/{chunk-UYBNOHTC.js → chunk-EDR55RQJ.js} +4707 -4714
- package/dist/chunk-EDR55RQJ.js.map +1 -0
- package/dist/{chunk-J6IQQJJQ.cjs → chunk-F2LZ67KM.cjs} +1165 -1172
- package/dist/chunk-F2LZ67KM.cjs.map +1 -0
- package/dist/{chunk-I4XG5PJA.js → chunk-FLDYBM25.js} +2 -2
- package/dist/{chunk-ABRDOCVN.cjs → chunk-FRLLKYMO.cjs} +7 -7
- package/dist/{chunk-ABRDOCVN.cjs.map → chunk-FRLLKYMO.cjs.map} +1 -1
- package/dist/{chunk-LAL63W5J.js → chunk-MZPEN27A.js} +4 -4
- package/dist/{chunk-OPD5V57R.cjs → chunk-PLB6Z3E4.cjs} +12 -12
- package/dist/{chunk-OPD5V57R.cjs.map → chunk-PLB6Z3E4.cjs.map} +1 -1
- package/dist/{chunk-FHEIG7QO.js → chunk-PMR74LEE.js} +2 -2
- package/dist/{chunk-6XQJSN7G.cjs → chunk-Q4HI3DGY.cjs} +15 -15
- package/dist/{chunk-6XQJSN7G.cjs.map → chunk-Q4HI3DGY.cjs.map} +1 -1
- package/dist/{chunk-ILWSJCWD.cjs → chunk-QDERWRT4.cjs} +26 -26
- package/dist/{chunk-ILWSJCWD.cjs.map → chunk-QDERWRT4.cjs.map} +1 -1
- package/dist/{chunk-EFTKJFEF.cjs → chunk-QOOWYEPL.cjs} +5 -5
- package/dist/{chunk-EFTKJFEF.cjs.map → chunk-QOOWYEPL.cjs.map} +1 -1
- package/dist/{chunk-463X3AW2.js → chunk-RWKVTABY.js} +2 -2
- package/dist/{chunk-KIC4ZEZ7.cjs → chunk-TGWHUEPD.cjs} +62 -62
- package/dist/{chunk-KIC4ZEZ7.cjs.map → chunk-TGWHUEPD.cjs.map} +1 -1
- package/dist/{chunk-UFP45JLE.js → chunk-TZDROKLI.js} +2 -2
- package/dist/{chunk-RYPCNUXA.js → chunk-V3NY65ET.js} +2 -2
- package/dist/{chunk-MQRTT3DO.js → chunk-V4YFWSB2.js} +4 -4
- package/dist/{chunk-QGQ76SSR.cjs → chunk-VIEWUAFE.cjs} +9 -9
- package/dist/{chunk-QGQ76SSR.cjs.map → chunk-VIEWUAFE.cjs.map} +1 -1
- package/dist/{chunk-56XCRZ7I.js → chunk-YON2QYWK.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- 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/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +126 -120
- 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/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/data-table/data-table-header.d.ts.map +1 -1
- package/dist/components/ui/data-table/types.d.ts +6 -0
- package/dist/components/ui/data-table/types.d.ts.map +1 -1
- package/dist/components/ui/date-picker.d.ts +41 -4
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/filter-modal.d.ts +16 -1
- package/dist/components/ui/filter-modal.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/scroll-fade.d.ts +33 -0
- package/dist/components/ui/scroll-fade.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 +1 -1
- package/src/components/chat/chat-ticket-list.tsx +8 -36
- package/src/components/chat/guide-welcome.tsx +5 -43
- package/src/components/chat/mingo-chat-history.tsx +6 -40
- package/src/components/chat/mingo-welcome.tsx +7 -47
- package/src/components/ui/data-table/data-table-header.tsx +6 -3
- package/src/components/ui/data-table/types.ts +6 -0
- package/src/components/ui/date-picker.tsx +149 -41
- package/src/components/ui/filter-modal.tsx +57 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/scroll-fade.tsx +90 -0
- package/src/stories/DatePicker.stories.tsx +4 -2
- package/src/stories/FilterModal.stories.tsx +33 -0
- package/dist/chunk-J6IQQJJQ.cjs.map +0 -1
- package/dist/chunk-UYBNOHTC.js.map +0 -1
- /package/dist/{chunk-4HAVKWTJ.js.map → chunk-2B5FIJE5.js.map} +0 -0
- /package/dist/{chunk-6VR7BJF6.js.map → chunk-5P5WMTVW.js.map} +0 -0
- /package/dist/{chunk-PD5MXNTJ.js.map → chunk-B3IDZ6JJ.js.map} +0 -0
- /package/dist/{chunk-I4XG5PJA.js.map → chunk-FLDYBM25.js.map} +0 -0
- /package/dist/{chunk-LAL63W5J.js.map → chunk-MZPEN27A.js.map} +0 -0
- /package/dist/{chunk-FHEIG7QO.js.map → chunk-PMR74LEE.js.map} +0 -0
- /package/dist/{chunk-463X3AW2.js.map → chunk-RWKVTABY.js.map} +0 -0
- /package/dist/{chunk-UFP45JLE.js.map → chunk-TZDROKLI.js.map} +0 -0
- /package/dist/{chunk-RYPCNUXA.js.map → chunk-V3NY65ET.js.map} +0 -0
- /package/dist/{chunk-MQRTT3DO.js.map → chunk-V4YFWSB2.js.map} +0 -0
- /package/dist/{chunk-56XCRZ7I.js.map → chunk-YON2QYWK.js.map} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../../utils/cn'
|
|
5
|
+
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
5
6
|
import { ChatTicketItem, type ChatTicketItemData } from './entity-cards/chat-ticket-item'
|
|
6
7
|
|
|
7
8
|
export interface ChatTicketListProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -15,21 +16,9 @@ export interface ChatTicketListProps extends React.HTMLAttributes<HTMLDivElement
|
|
|
15
16
|
|
|
16
17
|
const ChatTicketList = React.forwardRef<HTMLDivElement, ChatTicketListProps>(
|
|
17
18
|
({ className, tickets, onTicketClick, isLoading = false, skeletonCount = 5, ...props }, ref) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const updateFade = React.useCallback(() => {
|
|
23
|
-
const el = scrollRef.current
|
|
24
|
-
if (!el) return
|
|
25
|
-
setFadeTop(el.scrollTop > 0)
|
|
26
|
-
setFadeBottom(el.scrollHeight - el.scrollTop - el.clientHeight > 1)
|
|
27
|
-
}, [])
|
|
28
|
-
|
|
29
|
-
const ticketCount = tickets.length
|
|
30
|
-
React.useLayoutEffect(() => {
|
|
31
|
-
updateFade()
|
|
32
|
-
}, [ticketCount, updateFade])
|
|
19
|
+
// Shared scroll-shadow tracking (ui/scroll-fade) — re-measures on resize
|
|
20
|
+
// and content changes, so no manual ticket-count effect is needed.
|
|
21
|
+
const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>()
|
|
33
22
|
|
|
34
23
|
if (isLoading) {
|
|
35
24
|
return (
|
|
@@ -68,27 +57,10 @@ const ChatTicketList = React.forwardRef<HTMLDivElement, ChatTicketListProps>(
|
|
|
68
57
|
</div>
|
|
69
58
|
|
|
70
59
|
{/* Scroll-fade overlays — tinted with the page background so edge
|
|
71
|
-
tickets fade into the surface behind the list in BOTH themes
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
guide-welcome. */}
|
|
76
|
-
<div
|
|
77
|
-
aria-hidden
|
|
78
|
-
className={cn(
|
|
79
|
-
'pointer-events-none absolute inset-x-0 top-0 h-16 transition-opacity duration-150',
|
|
80
|
-
fadeTop ? 'opacity-100' : 'opacity-0',
|
|
81
|
-
)}
|
|
82
|
-
style={{ background: 'linear-gradient(0deg, transparent 0%, var(--color-bg) 100%)' }}
|
|
83
|
-
/>
|
|
84
|
-
<div
|
|
85
|
-
aria-hidden
|
|
86
|
-
className={cn(
|
|
87
|
-
'pointer-events-none absolute inset-x-0 bottom-0 h-16 transition-opacity duration-150',
|
|
88
|
-
fadeBottom ? 'opacity-100' : 'opacity-0',
|
|
89
|
-
)}
|
|
90
|
-
style={{ background: 'linear-gradient(180deg, transparent 0%, var(--color-bg) 100%)' }}
|
|
91
|
-
/>
|
|
60
|
+
tickets fade into the surface behind the list in BOTH themes
|
|
61
|
+
(`--color-bg` flips with `data-theme`). Shared ui/scroll-fade. */}
|
|
62
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} />
|
|
63
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} />
|
|
92
64
|
</div>
|
|
93
65
|
</div>
|
|
94
66
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../../utils/cn'
|
|
5
5
|
import { MingoIcon } from '../icons'
|
|
6
|
+
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
6
7
|
import { Skeleton } from '../ui/skeleton'
|
|
7
8
|
import { ChatQuickActionRow } from './chat-quick-action-row'
|
|
8
9
|
import { EntityIcon } from '../icon-display'
|
|
@@ -100,25 +101,7 @@ export function GuideWelcome({
|
|
|
100
101
|
// Scroll-fade affordances: a 48px gradient at the top/bottom edge of the
|
|
101
102
|
// scroll region, shown only while content is actually hidden in that
|
|
102
103
|
// direction. (Same behaviour as MingoWelcome.)
|
|
103
|
-
const scrollRef =
|
|
104
|
-
const [scrollFade, setScrollFade] = React.useState({ top: false, bottom: false })
|
|
105
|
-
const updateScrollFade = React.useCallback(() => {
|
|
106
|
-
const el = scrollRef.current
|
|
107
|
-
if (!el) return
|
|
108
|
-
const top = el.scrollTop > 1
|
|
109
|
-
const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
|
|
110
|
-
setScrollFade((prev) =>
|
|
111
|
-
prev.top === top && prev.bottom === bottom ? prev : { top, bottom },
|
|
112
|
-
)
|
|
113
|
-
}, [])
|
|
114
|
-
React.useEffect(() => {
|
|
115
|
-
updateScrollFade()
|
|
116
|
-
const el = scrollRef.current
|
|
117
|
-
if (!el || typeof ResizeObserver === 'undefined') return
|
|
118
|
-
const ro = new ResizeObserver(updateScrollFade)
|
|
119
|
-
ro.observe(el)
|
|
120
|
-
return () => ro.disconnect()
|
|
121
|
-
}, [updateScrollFade])
|
|
104
|
+
const { scrollRef, fadeTop, fadeBottom, update: updateScrollFade } = useScrollFade<HTMLDivElement>()
|
|
122
105
|
|
|
123
106
|
// Map to the shared `ChatQuickActionRow` chip shape. In `wrap` mode every chip
|
|
124
107
|
// renders (no overflow), and `onHoverStart`/`onHoverEnd` drive the composer
|
|
@@ -204,30 +187,9 @@ export function GuideWelcome({
|
|
|
204
187
|
{children}
|
|
205
188
|
</div>
|
|
206
189
|
|
|
207
|
-
{/*
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
className={cn(
|
|
211
|
-
'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
|
|
212
|
-
scrollFade.top ? 'opacity-100' : 'opacity-0',
|
|
213
|
-
)}
|
|
214
|
-
style={{
|
|
215
|
-
background:
|
|
216
|
-
'linear-gradient(0deg, transparent 0%, var(--color-bg) 100%)',
|
|
217
|
-
}}
|
|
218
|
-
/>
|
|
219
|
-
{/* Bottom scroll-fade — visible only when content is hidden below. */}
|
|
220
|
-
<div
|
|
221
|
-
aria-hidden
|
|
222
|
-
className={cn(
|
|
223
|
-
'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
|
|
224
|
-
scrollFade.bottom ? 'opacity-100' : 'opacity-0',
|
|
225
|
-
)}
|
|
226
|
-
style={{
|
|
227
|
-
background:
|
|
228
|
-
'linear-gradient(180deg, transparent 0%, var(--color-bg) 100%)',
|
|
229
|
-
}}
|
|
230
|
-
/>
|
|
190
|
+
{/* Edge scroll-fades — visible only when content is hidden beyond them. */}
|
|
191
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} className="h-12" />
|
|
192
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} className="h-12" />
|
|
231
193
|
</div>
|
|
232
194
|
|
|
233
195
|
{/* Pinned quick-action chips above the composer — the shared
|
|
@@ -6,6 +6,7 @@ import { useDebounce } from '../../hooks/ui/use-debounce'
|
|
|
6
6
|
import { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'
|
|
7
7
|
import { Button } from '../ui/button'
|
|
8
8
|
import { Input } from '../ui/input'
|
|
9
|
+
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
9
10
|
import { Ellipsis01Icon, SearchIcon } from '../icons-v2-generated'
|
|
10
11
|
import type { DialogItem } from './types/component.types'
|
|
11
12
|
|
|
@@ -357,24 +358,9 @@ export function MingoChatHistory({
|
|
|
357
358
|
const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])
|
|
358
359
|
const noSearchResults = groups.length === 0 && !!searchQuery?.trim()
|
|
359
360
|
|
|
360
|
-
// Scroll-fade affordances
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
const updateFade = React.useCallback(() => {
|
|
364
|
-
const el = scrollRef.current
|
|
365
|
-
if (!el) return
|
|
366
|
-
const top = el.scrollTop > 1
|
|
367
|
-
const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
|
|
368
|
-
setFade((p) => (p.top === top && p.bottom === bottom ? p : { top, bottom }))
|
|
369
|
-
}, [])
|
|
370
|
-
React.useEffect(() => {
|
|
371
|
-
updateFade()
|
|
372
|
-
const el = scrollRef.current
|
|
373
|
-
if (!el || typeof ResizeObserver === 'undefined') return
|
|
374
|
-
const ro = new ResizeObserver(updateFade)
|
|
375
|
-
ro.observe(el)
|
|
376
|
-
return () => ro.disconnect()
|
|
377
|
-
}, [updateFade, dialogs])
|
|
361
|
+
// Scroll-fade affordances — shared ui/scroll-fade (re-measures on resize
|
|
362
|
+
// and content mutations, so no manual `dialogs` dependency is needed).
|
|
363
|
+
const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>()
|
|
378
364
|
|
|
379
365
|
// Infinite scroll — load the next page when the sentinel enters the
|
|
380
366
|
// scroll viewport.
|
|
@@ -445,28 +431,8 @@ export function MingoChatHistory({
|
|
|
445
431
|
</div>
|
|
446
432
|
|
|
447
433
|
{/* Scroll-fade — only while content is hidden in that direction. */}
|
|
448
|
-
<
|
|
449
|
-
|
|
450
|
-
className={cn(
|
|
451
|
-
'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
|
|
452
|
-
fade.top ? 'opacity-100' : 'opacity-0',
|
|
453
|
-
)}
|
|
454
|
-
style={{
|
|
455
|
-
background:
|
|
456
|
-
'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
457
|
-
}}
|
|
458
|
-
/>
|
|
459
|
-
<div
|
|
460
|
-
aria-hidden
|
|
461
|
-
className={cn(
|
|
462
|
-
'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
|
|
463
|
-
fade.bottom ? 'opacity-100' : 'opacity-0',
|
|
464
|
-
)}
|
|
465
|
-
style={{
|
|
466
|
-
background:
|
|
467
|
-
'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
468
|
-
}}
|
|
469
|
-
/>
|
|
434
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} color="var(--color-bg-card)" className="h-12" />
|
|
435
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} color="var(--color-bg-card)" className="h-12" />
|
|
470
436
|
</div>
|
|
471
437
|
</div>
|
|
472
438
|
)
|
|
@@ -7,6 +7,7 @@ import { MingoChatHistorySkeleton } from './mingo-chat-history'
|
|
|
7
7
|
import { ChatQuickActionRow } from './chat-quick-action-row'
|
|
8
8
|
import { QuickActionChipButton } from './quick-action-chip'
|
|
9
9
|
import { Button } from '../ui/button'
|
|
10
|
+
import { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'
|
|
10
11
|
import { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'
|
|
11
12
|
import {
|
|
12
13
|
Rocket01Icon,
|
|
@@ -231,29 +232,9 @@ export function MingoWelcome({
|
|
|
231
232
|
}
|
|
232
233
|
}, [promoStorage, promoStorageKey])
|
|
233
234
|
|
|
234
|
-
// Scroll-fade affordances
|
|
235
|
-
//
|
|
236
|
-
const scrollRef =
|
|
237
|
-
const [scrollFade, setScrollFade] = React.useState({ top: false, bottom: false })
|
|
238
|
-
const updateScrollFade = React.useCallback(() => {
|
|
239
|
-
const el = scrollRef.current
|
|
240
|
-
if (!el) return
|
|
241
|
-
const top = el.scrollTop > 1
|
|
242
|
-
const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
|
|
243
|
-
setScrollFade((prev) =>
|
|
244
|
-
prev.top === top && prev.bottom === bottom ? prev : { top, bottom },
|
|
245
|
-
)
|
|
246
|
-
}, [])
|
|
247
|
-
React.useEffect(() => {
|
|
248
|
-
updateScrollFade()
|
|
249
|
-
const el = scrollRef.current
|
|
250
|
-
if (!el || typeof ResizeObserver === 'undefined') return
|
|
251
|
-
// Recompute when the panel resizes — a taller panel may remove the need
|
|
252
|
-
// to scroll entirely, a shorter one introduces it.
|
|
253
|
-
const ro = new ResizeObserver(updateScrollFade)
|
|
254
|
-
ro.observe(el)
|
|
255
|
-
return () => ro.disconnect()
|
|
256
|
-
}, [updateScrollFade])
|
|
235
|
+
// Scroll-fade affordances — shared ui/scroll-fade (48px edge gradients shown
|
|
236
|
+
// only while content is hidden in that direction).
|
|
237
|
+
const { scrollRef, fadeTop, fadeBottom, update: updateScrollFade } = useScrollFade<HTMLDivElement>()
|
|
257
238
|
|
|
258
239
|
const cellCount = featureCards.length
|
|
259
240
|
// Last row's first index — used to drop the bottom divider on the final row.
|
|
@@ -365,30 +346,9 @@ export function MingoWelcome({
|
|
|
365
346
|
)}
|
|
366
347
|
</div>
|
|
367
348
|
|
|
368
|
-
{/*
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
className={cn(
|
|
372
|
-
'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
|
|
373
|
-
scrollFade.top ? 'opacity-100' : 'opacity-0',
|
|
374
|
-
)}
|
|
375
|
-
style={{
|
|
376
|
-
background:
|
|
377
|
-
'linear-gradient(0deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
378
|
-
}}
|
|
379
|
-
/>
|
|
380
|
-
{/* Bottom scroll-fade — visible only when content is hidden below. */}
|
|
381
|
-
<div
|
|
382
|
-
aria-hidden
|
|
383
|
-
className={cn(
|
|
384
|
-
'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
|
|
385
|
-
scrollFade.bottom ? 'opacity-100' : 'opacity-0',
|
|
386
|
-
)}
|
|
387
|
-
style={{
|
|
388
|
-
background:
|
|
389
|
-
'linear-gradient(180deg, transparent 0%, var(--color-bg-card) 100%)',
|
|
390
|
-
}}
|
|
391
|
-
/>
|
|
349
|
+
{/* Edge scroll-fades — visible only when content is hidden beyond them. */}
|
|
350
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} color="var(--color-bg-card)" className="h-12" />
|
|
351
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} color="var(--color-bg-card)" className="h-12" />
|
|
392
352
|
</div>
|
|
393
353
|
</>
|
|
394
354
|
))}
|
|
@@ -69,7 +69,8 @@ export function DataTableHeader({
|
|
|
69
69
|
const hasVisibleHeaderCell = headerGroup.headers.some(header => {
|
|
70
70
|
if (header.isPlaceholder) return false
|
|
71
71
|
if (isLgUp) return true
|
|
72
|
-
|
|
72
|
+
const meta = header.column.columnDef.meta
|
|
73
|
+
return Boolean(meta?.filter) || meta?.alwaysShowHeader === true
|
|
73
74
|
})
|
|
74
75
|
|
|
75
76
|
return (
|
|
@@ -122,6 +123,8 @@ function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
|
|
|
122
123
|
const column = header.column
|
|
123
124
|
const meta = column.columnDef.meta
|
|
124
125
|
const hasFilter = Boolean(meta?.filter)
|
|
126
|
+
// Tablet visibility: filterable columns and explicit opt-ins stay rendered.
|
|
127
|
+
const keepOnTablet = hasFilter || meta?.alwaysShowHeader === true
|
|
125
128
|
const align = meta?.align ?? 'left'
|
|
126
129
|
// Sort is opt-in via `meta.sortable`. Direction is fully consumer-driven via
|
|
127
130
|
// the `sort` prop; we do not consult TanStack's sort APIs here.
|
|
@@ -130,7 +133,7 @@ function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
|
|
|
130
133
|
sort?.id === column.id ? (sort.desc ? 'desc' : 'asc') : false
|
|
131
134
|
|
|
132
135
|
// Mobile (md, below lg): hide non-filter columns so only filters are accessible.
|
|
133
|
-
if (!isLgUp && !
|
|
136
|
+
if (!isLgUp && !keepOnTablet) return null
|
|
134
137
|
|
|
135
138
|
return (
|
|
136
139
|
<div
|
|
@@ -139,7 +142,7 @@ function HeaderCell({ header, isLgUp, sort, onSortChange }: HeaderCellProps) {
|
|
|
139
142
|
isLgUp && (meta?.width || 'flex-1 min-w-0'),
|
|
140
143
|
meta?.headerClassName,
|
|
141
144
|
// Don't apply hide classes if column is filterable on tablet (keep filter accessible)
|
|
142
|
-
!(
|
|
145
|
+
!(keepOnTablet && !isLgUp) && getHideClasses(meta?.hideAt),
|
|
143
146
|
)}
|
|
144
147
|
>
|
|
145
148
|
{hasFilter ? (
|
|
@@ -16,6 +16,12 @@ declare module '@tanstack/react-table' {
|
|
|
16
16
|
align?: 'left' | 'center' | 'right'
|
|
17
17
|
/** Hide column at/below these Tailwind breakpoints. */
|
|
18
18
|
hideAt?: TailwindBreakpoint | TailwindBreakpoint[]
|
|
19
|
+
/**
|
|
20
|
+
* Keep this header visible on tablet (md, below lg) even without
|
|
21
|
+
* `meta.filter` — for custom headers hosting their own filter UI
|
|
22
|
+
* (e.g. the date filter calendar trigger).
|
|
23
|
+
*/
|
|
24
|
+
alwaysShowHeader?: boolean
|
|
19
25
|
/** If present, header renders a filter dropdown with these options. */
|
|
20
26
|
filter?: {
|
|
21
27
|
options: DataTableFilterOption[]
|
|
@@ -150,6 +150,14 @@ function DatePickerCalendar({
|
|
|
150
150
|
);
|
|
151
151
|
const [hoveredDate, setHoveredDate] = React.useState<Date | undefined>(undefined);
|
|
152
152
|
|
|
153
|
+
// Keep the internal range in sync when the consumer changes `selected`
|
|
154
|
+
// externally (e.g. DateFilterMenu Reset while the popover stays open).
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (mode === "range") {
|
|
157
|
+
setDraftRange(selected as DateRange | undefined);
|
|
158
|
+
}
|
|
159
|
+
}, [mode, selected]);
|
|
160
|
+
|
|
153
161
|
const rangeSelected = draftRange;
|
|
154
162
|
const hasCompleteRange =
|
|
155
163
|
mode === "range" &&
|
|
@@ -168,7 +176,11 @@ function DatePickerCalendar({
|
|
|
168
176
|
if (!triggerDate) return;
|
|
169
177
|
|
|
170
178
|
if (!draftRange?.from || draftRange.to) {
|
|
171
|
-
|
|
179
|
+
// First click starts the range — propagate it so consumers can already
|
|
180
|
+
// act on a single-day selection (e.g. DateFilterMenu Apply/Reset).
|
|
181
|
+
const started: DateRange = { from: triggerDate, to: undefined };
|
|
182
|
+
setDraftRange(started);
|
|
183
|
+
onSelect(started);
|
|
172
184
|
return;
|
|
173
185
|
}
|
|
174
186
|
// Second click closes the range, ordering the two ends.
|
|
@@ -981,6 +993,89 @@ export function DatePickerInputSimple({
|
|
|
981
993
|
);
|
|
982
994
|
}
|
|
983
995
|
|
|
996
|
+
// ============================================================================
|
|
997
|
+
// DateFilterPanel Component (sort select + fluid calendar)
|
|
998
|
+
// ============================================================================
|
|
999
|
+
|
|
1000
|
+
export interface DateFilterPanelProps {
|
|
1001
|
+
/** Selection mode for the calendar. Defaults to "range". */
|
|
1002
|
+
mode?: DatePickerMode;
|
|
1003
|
+
/** Current sort direction shown in the select. */
|
|
1004
|
+
sort: SortDirection;
|
|
1005
|
+
onSortChange: (sort: SortDirection) => void;
|
|
1006
|
+
/** Current calendar selection. */
|
|
1007
|
+
selected: Date | DateRange | undefined;
|
|
1008
|
+
onSelect: (value: Date | DateRange | undefined) => void;
|
|
1009
|
+
/** Minimum selectable date. */
|
|
1010
|
+
fromDate?: Date;
|
|
1011
|
+
/** Maximum selectable date. */
|
|
1012
|
+
toDate?: Date;
|
|
1013
|
+
/** Locale for the calendar. */
|
|
1014
|
+
locale?: DayPickerProps["locale"];
|
|
1015
|
+
/** Label for the ascending sort option. */
|
|
1016
|
+
ascLabel?: string;
|
|
1017
|
+
/** Label for the descending sort option. */
|
|
1018
|
+
descLabel?: string;
|
|
1019
|
+
className?: string;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* DateFilterPanel — the controlled sort-direction select + fluid calendar
|
|
1024
|
+
* block shared by DateFilterMenu (popover) and FilterModal (mobile "Sort and
|
|
1025
|
+
* Filter"). Owns no state: the consumer drafts and commits the values.
|
|
1026
|
+
*/
|
|
1027
|
+
export function DateFilterPanel({
|
|
1028
|
+
mode = "range",
|
|
1029
|
+
sort,
|
|
1030
|
+
onSortChange,
|
|
1031
|
+
selected,
|
|
1032
|
+
onSelect,
|
|
1033
|
+
fromDate,
|
|
1034
|
+
toDate,
|
|
1035
|
+
locale,
|
|
1036
|
+
ascLabel = "Sort by Ascending",
|
|
1037
|
+
descLabel = "Sort by Descending",
|
|
1038
|
+
className,
|
|
1039
|
+
}: DateFilterPanelProps) {
|
|
1040
|
+
return (
|
|
1041
|
+
<div className={cn("flex flex-col gap-4", className)}>
|
|
1042
|
+
{/* Sort direction selector */}
|
|
1043
|
+
<Select
|
|
1044
|
+
value={sort}
|
|
1045
|
+
onValueChange={(value) => onSortChange(value as SortDirection)}
|
|
1046
|
+
>
|
|
1047
|
+
<SelectTrigger className="gap-2" aria-label="Sort direction">
|
|
1048
|
+
{/* Wrapper is a <div> (not <span>) so SelectTrigger's
|
|
1049
|
+
`[&>span]:line-clamp-1` rule doesn't force it to a vertical
|
|
1050
|
+
-webkit-box and stack the icon/label into a column. */}
|
|
1051
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
1052
|
+
<ArrowUpDown className="size-6 shrink-0 text-ods-text-secondary" />
|
|
1053
|
+
<span className="truncate">
|
|
1054
|
+
<SelectValue />
|
|
1055
|
+
</span>
|
|
1056
|
+
</div>
|
|
1057
|
+
</SelectTrigger>
|
|
1058
|
+
<SelectContent>
|
|
1059
|
+
<SelectItem value="asc">{ascLabel}</SelectItem>
|
|
1060
|
+
<SelectItem value="desc">{descLabel}</SelectItem>
|
|
1061
|
+
</SelectContent>
|
|
1062
|
+
</Select>
|
|
1063
|
+
|
|
1064
|
+
{/* Calendar */}
|
|
1065
|
+
<DatePickerCalendar
|
|
1066
|
+
mode={mode}
|
|
1067
|
+
selected={selected}
|
|
1068
|
+
onSelect={onSelect}
|
|
1069
|
+
numberOfMonths={1}
|
|
1070
|
+
fromDate={fromDate}
|
|
1071
|
+
toDate={toDate}
|
|
1072
|
+
locale={locale}
|
|
1073
|
+
fluid
|
|
1074
|
+
/>
|
|
1075
|
+
</div>
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
984
1079
|
// ============================================================================
|
|
985
1080
|
// DateFilterMenu Component (sort + calendar filter popover from Figma)
|
|
986
1081
|
// ============================================================================
|
|
@@ -999,14 +1094,21 @@ export interface DateFilterMenuProps {
|
|
|
999
1094
|
mode?: DatePickerMode;
|
|
1000
1095
|
/** Current (applied) sort direction. Defaults to "desc". */
|
|
1001
1096
|
sort?: SortDirection;
|
|
1097
|
+
/** Baseline sort direction — Reset restores it, and a draft that differs
|
|
1098
|
+
* from it counts as an active change (shows Reset). Defaults to "desc". */
|
|
1099
|
+
defaultSort?: SortDirection;
|
|
1002
1100
|
/** Current (applied) single date — used when mode === "single". */
|
|
1003
1101
|
date?: Date;
|
|
1004
1102
|
/** Current (applied) range — used when mode === "range". */
|
|
1005
1103
|
range?: DateRange;
|
|
1006
|
-
/** Fired when the user presses Apply with the drafted selection.
|
|
1104
|
+
/** Fired when the user presses Apply with the drafted selection. Also fired
|
|
1105
|
+
* by Reset with a cleared selection so the consumer refetches unfiltered data. */
|
|
1007
1106
|
onApply?: (result: DateFilterResult) => void;
|
|
1008
1107
|
/** Fired when the menu closes (Close button, outside click, Esc). */
|
|
1009
1108
|
onClose?: () => void;
|
|
1109
|
+
/** Custom trigger element (rendered via Radix `asChild` — must accept a ref,
|
|
1110
|
+
* e.g. a native button). Defaults to the outline calendar icon Button. */
|
|
1111
|
+
trigger?: React.ReactNode;
|
|
1010
1112
|
/** Disable the trigger. */
|
|
1011
1113
|
disabled?: boolean;
|
|
1012
1114
|
/** Minimum selectable date. */
|
|
@@ -1029,16 +1131,21 @@ export interface DateFilterMenuProps {
|
|
|
1029
1131
|
|
|
1030
1132
|
/**
|
|
1031
1133
|
* DateFilterMenu — a calendar-icon-triggered popover combining a sort-direction
|
|
1032
|
-
* selector and a date / date-range calendar, with Close and Apply actions.
|
|
1033
|
-
*
|
|
1134
|
+
* selector and a date / date-range calendar, with Close/Reset and Apply actions.
|
|
1135
|
+
* The sort and date selection are drafted internally and committed via
|
|
1136
|
+
* `onApply`. While a date is selected, Close is replaced by Reset, which
|
|
1137
|
+
* clears and commits the empty selection (fires `onApply`) so the consumer
|
|
1138
|
+
* drops the filter.
|
|
1034
1139
|
*/
|
|
1035
1140
|
export function DateFilterMenu({
|
|
1036
1141
|
mode = "range",
|
|
1037
1142
|
sort = "desc",
|
|
1143
|
+
defaultSort = "desc",
|
|
1038
1144
|
date,
|
|
1039
1145
|
range,
|
|
1040
1146
|
onApply,
|
|
1041
1147
|
onClose,
|
|
1148
|
+
trigger,
|
|
1042
1149
|
disabled = false,
|
|
1043
1150
|
fromDate,
|
|
1044
1151
|
toDate,
|
|
@@ -1085,18 +1192,39 @@ export function DateFilterMenu({
|
|
|
1085
1192
|
setOpen(false);
|
|
1086
1193
|
};
|
|
1087
1194
|
|
|
1195
|
+
// Anything to reset? A calendar selection or a non-default sort — drives Close vs Reset.
|
|
1196
|
+
const hasSelection =
|
|
1197
|
+
mode === "single"
|
|
1198
|
+
? Boolean(draftSelected)
|
|
1199
|
+
: Boolean((draftSelected as DateRange | undefined)?.from);
|
|
1200
|
+
const hasChanges = hasSelection || draftSort !== defaultSort;
|
|
1201
|
+
|
|
1202
|
+
// Reset restores the defaults and commits them so the consumer drops the
|
|
1203
|
+
// filter and refetches; the menu stays open with the button back to Close.
|
|
1204
|
+
const handleReset = () => {
|
|
1205
|
+
setDraftSort(defaultSort);
|
|
1206
|
+
setDraftSelected(undefined);
|
|
1207
|
+
onApply?.(
|
|
1208
|
+
mode === "single"
|
|
1209
|
+
? { sort: defaultSort, date: undefined }
|
|
1210
|
+
: { sort: defaultSort, range: undefined }
|
|
1211
|
+
);
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1088
1214
|
return (
|
|
1089
1215
|
<Popover.Root open={open} onOpenChange={handleOpenChange}>
|
|
1090
1216
|
<Popover.Trigger asChild>
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1217
|
+
{trigger ?? (
|
|
1218
|
+
<Button
|
|
1219
|
+
type="button"
|
|
1220
|
+
variant="outline"
|
|
1221
|
+
size="icon"
|
|
1222
|
+
disabled={disabled}
|
|
1223
|
+
aria-label={ariaLabel}
|
|
1224
|
+
className={className}
|
|
1225
|
+
leftIcon={<Calendar className="size-6" />}
|
|
1226
|
+
/>
|
|
1227
|
+
)}
|
|
1100
1228
|
</Popover.Trigger>
|
|
1101
1229
|
|
|
1102
1230
|
<Popover.Portal>
|
|
@@ -1112,38 +1240,18 @@ export function DateFilterMenu({
|
|
|
1112
1240
|
sideOffset={8}
|
|
1113
1241
|
align={align}
|
|
1114
1242
|
>
|
|
1115
|
-
{/* Sort
|
|
1116
|
-
<
|
|
1117
|
-
value={draftSort}
|
|
1118
|
-
onValueChange={(value) => setDraftSort(value as SortDirection)}
|
|
1119
|
-
>
|
|
1120
|
-
<SelectTrigger className="gap-2" aria-label="Sort direction">
|
|
1121
|
-
{/* Wrapper is a <div> (not <span>) so SelectTrigger's
|
|
1122
|
-
`[&>span]:line-clamp-1` rule doesn't force it to a vertical
|
|
1123
|
-
-webkit-box and stack the icon/label into a column. */}
|
|
1124
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
1125
|
-
<ArrowUpDown className="size-6 shrink-0 text-ods-text-secondary" />
|
|
1126
|
-
<span className="truncate">
|
|
1127
|
-
<SelectValue />
|
|
1128
|
-
</span>
|
|
1129
|
-
</div>
|
|
1130
|
-
</SelectTrigger>
|
|
1131
|
-
<SelectContent>
|
|
1132
|
-
<SelectItem value="asc">{ascLabel}</SelectItem>
|
|
1133
|
-
<SelectItem value="desc">{descLabel}</SelectItem>
|
|
1134
|
-
</SelectContent>
|
|
1135
|
-
</Select>
|
|
1136
|
-
|
|
1137
|
-
{/* Calendar */}
|
|
1138
|
-
<DatePickerCalendar
|
|
1243
|
+
{/* Sort + calendar (shared panel) */}
|
|
1244
|
+
<DateFilterPanel
|
|
1139
1245
|
mode={mode}
|
|
1246
|
+
sort={draftSort}
|
|
1247
|
+
onSortChange={setDraftSort}
|
|
1140
1248
|
selected={draftSelected}
|
|
1141
1249
|
onSelect={setDraftSelected}
|
|
1142
|
-
numberOfMonths={1}
|
|
1143
1250
|
fromDate={fromDate}
|
|
1144
1251
|
toDate={toDate}
|
|
1145
1252
|
locale={locale}
|
|
1146
|
-
|
|
1253
|
+
ascLabel={ascLabel}
|
|
1254
|
+
descLabel={descLabel}
|
|
1147
1255
|
/>
|
|
1148
1256
|
|
|
1149
1257
|
{/* Actions */}
|
|
@@ -1152,9 +1260,9 @@ export function DateFilterMenu({
|
|
|
1152
1260
|
type="button"
|
|
1153
1261
|
variant="outline"
|
|
1154
1262
|
fullWidth
|
|
1155
|
-
onClick={handleClose}
|
|
1263
|
+
onClick={hasChanges ? handleReset : handleClose}
|
|
1156
1264
|
>
|
|
1157
|
-
Close
|
|
1265
|
+
{hasChanges ? "Reset" : "Close"}
|
|
1158
1266
|
</Button>
|
|
1159
1267
|
<Button
|
|
1160
1268
|
type="button"
|