@flamingo-stack/openframe-frontend-core 0.0.400 → 0.0.401-snapshot.20260710165213

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 (112) hide show
  1. package/dist/{chunk-ABRDOCVN.cjs → chunk-32K5CRL2.cjs} +7 -7
  2. package/dist/{chunk-ABRDOCVN.cjs.map → chunk-32K5CRL2.cjs.map} +1 -1
  3. package/dist/{chunk-4HAVKWTJ.js → chunk-3TCCW7B4.js} +2 -2
  4. package/dist/{chunk-I4XG5PJA.js → chunk-6FIVVZO2.js} +2 -2
  5. package/dist/{chunk-56XCRZ7I.js → chunk-6VAJDGZK.js} +2 -2
  6. package/dist/{chunk-KIC4ZEZ7.cjs → chunk-A4RDD2PS.cjs} +62 -62
  7. package/dist/{chunk-KIC4ZEZ7.cjs.map → chunk-A4RDD2PS.cjs.map} +1 -1
  8. package/dist/{chunk-MQRTT3DO.js → chunk-COGPHD7D.js} +4 -4
  9. package/dist/{chunk-EFTKJFEF.cjs → chunk-CWLKGOU4.cjs} +5 -5
  10. package/dist/{chunk-EFTKJFEF.cjs.map → chunk-CWLKGOU4.cjs.map} +1 -1
  11. package/dist/{chunk-VTSEL7CJ.cjs → chunk-D3HPV6YE.cjs} +59 -59
  12. package/dist/{chunk-VTSEL7CJ.cjs.map → chunk-D3HPV6YE.cjs.map} +1 -1
  13. package/dist/{chunk-MFRP2UDK.cjs → chunk-DYV47UA6.cjs} +26 -26
  14. package/dist/{chunk-MFRP2UDK.cjs.map → chunk-DYV47UA6.cjs.map} +1 -1
  15. package/dist/{chunk-ILWSJCWD.cjs → chunk-EDOLCPBS.cjs} +26 -26
  16. package/dist/{chunk-ILWSJCWD.cjs.map → chunk-EDOLCPBS.cjs.map} +1 -1
  17. package/dist/{chunk-UFP45JLE.js → chunk-F63VMX4S.js} +2 -2
  18. package/dist/{chunk-6XQJSN7G.cjs → chunk-HU5HWEQ5.cjs} +15 -15
  19. package/dist/{chunk-6XQJSN7G.cjs.map → chunk-HU5HWEQ5.cjs.map} +1 -1
  20. package/dist/{chunk-RSFHAFZN.cjs → chunk-JXSWDXUA.cjs} +37 -37
  21. package/dist/{chunk-RSFHAFZN.cjs.map → chunk-JXSWDXUA.cjs.map} +1 -1
  22. package/dist/{chunk-FHEIG7QO.js → chunk-LAFLYVZ4.js} +2 -2
  23. package/dist/{chunk-J6IQQJJQ.cjs → chunk-MEU2JEXL.cjs} +1167 -1172
  24. package/dist/chunk-MEU2JEXL.cjs.map +1 -0
  25. package/dist/{chunk-463X3AW2.js → chunk-NHFQ2LCC.js} +2 -2
  26. package/dist/{chunk-RYPCNUXA.js → chunk-O5QU5KAC.js} +2 -2
  27. package/dist/{chunk-556BG7SX.cjs → chunk-ONAWSVFK.cjs} +31 -31
  28. package/dist/{chunk-556BG7SX.cjs.map → chunk-ONAWSVFK.cjs.map} +1 -1
  29. package/dist/{chunk-OPD5V57R.cjs → chunk-OPCGDJU7.cjs} +12 -12
  30. package/dist/{chunk-OPD5V57R.cjs.map → chunk-OPCGDJU7.cjs.map} +1 -1
  31. package/dist/{chunk-UYBNOHTC.js → chunk-RZIFF62P.js} +4709 -4714
  32. package/dist/chunk-RZIFF62P.js.map +1 -0
  33. package/dist/{chunk-LAL63W5J.js → chunk-VMJTNXOH.js} +4 -4
  34. package/dist/{chunk-6VR7BJF6.js → chunk-WM72XFOD.js} +4 -4
  35. package/dist/{chunk-QGQ76SSR.cjs → chunk-YIJOSOSA.cjs} +9 -9
  36. package/dist/{chunk-QGQ76SSR.cjs.map → chunk-YIJOSOSA.cjs.map} +1 -1
  37. package/dist/{chunk-PD5MXNTJ.js → chunk-YPNI74ZP.js} +4 -4
  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-ticket-list.d.ts.map +1 -1
  41. package/dist/components/chat/guide-welcome.d.ts.map +1 -1
  42. package/dist/components/chat/index.cjs +2 -2
  43. package/dist/components/chat/index.js +1 -1
  44. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -1
  45. package/dist/components/chat/mingo-welcome.d.ts.map +1 -1
  46. package/dist/components/contact/index.cjs +3 -3
  47. package/dist/components/contact/index.js +2 -2
  48. package/dist/components/docs/index.cjs +5 -5
  49. package/dist/components/docs/index.js +4 -4
  50. package/dist/components/embeds/index.cjs +3 -3
  51. package/dist/components/embeds/index.js +2 -2
  52. package/dist/components/faq/index.cjs +3 -3
  53. package/dist/components/faq/index.js +2 -2
  54. package/dist/components/features/index.cjs +2 -2
  55. package/dist/components/features/index.js +1 -1
  56. package/dist/components/help-center-pages/index.cjs +20 -20
  57. package/dist/components/help-center-pages/index.js +11 -11
  58. package/dist/components/index.cjs +126 -120
  59. package/dist/components/index.cjs.map +1 -1
  60. package/dist/components/index.js +15 -9
  61. package/dist/components/index.js.map +1 -1
  62. package/dist/components/navigation/index.cjs +2 -2
  63. package/dist/components/navigation/index.js +1 -1
  64. package/dist/components/onboarding-guides/index.cjs +5 -5
  65. package/dist/components/onboarding-guides/index.js +4 -4
  66. package/dist/components/related-content/index.cjs +3 -3
  67. package/dist/components/related-content/index.js +2 -2
  68. package/dist/components/tickets/index.cjs +5 -5
  69. package/dist/components/tickets/index.js +4 -4
  70. package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
  71. package/dist/components/ui/data-table/types.d.ts +6 -0
  72. package/dist/components/ui/data-table/types.d.ts.map +1 -1
  73. package/dist/components/ui/date-picker.d.ts +41 -4
  74. package/dist/components/ui/date-picker.d.ts.map +1 -1
  75. package/dist/components/ui/filter-modal.d.ts +20 -1
  76. package/dist/components/ui/filter-modal.d.ts.map +1 -1
  77. package/dist/components/ui/index.cjs +8 -2
  78. package/dist/components/ui/index.cjs.map +1 -1
  79. package/dist/components/ui/index.d.ts +1 -0
  80. package/dist/components/ui/index.d.ts.map +1 -1
  81. package/dist/components/ui/index.js +7 -1
  82. package/dist/components/ui/scroll-fade.d.ts +33 -0
  83. package/dist/components/ui/scroll-fade.d.ts.map +1 -0
  84. package/dist/index.cjs +8 -2
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +7 -1
  87. package/package.json +1 -1
  88. package/src/components/chat/chat-ticket-list.tsx +8 -36
  89. package/src/components/chat/guide-welcome.tsx +5 -43
  90. package/src/components/chat/mingo-chat-history.tsx +6 -40
  91. package/src/components/chat/mingo-welcome.tsx +7 -47
  92. package/src/components/ui/data-table/data-table-header.tsx +6 -3
  93. package/src/components/ui/data-table/types.ts +6 -0
  94. package/src/components/ui/date-picker.tsx +149 -41
  95. package/src/components/ui/filter-modal.tsx +63 -2
  96. package/src/components/ui/index.ts +1 -0
  97. package/src/components/ui/scroll-fade.tsx +90 -0
  98. package/src/stories/DatePicker.stories.tsx +4 -2
  99. package/src/stories/FilterModal.stories.tsx +33 -0
  100. package/dist/chunk-J6IQQJJQ.cjs.map +0 -1
  101. package/dist/chunk-UYBNOHTC.js.map +0 -1
  102. /package/dist/{chunk-4HAVKWTJ.js.map → chunk-3TCCW7B4.js.map} +0 -0
  103. /package/dist/{chunk-I4XG5PJA.js.map → chunk-6FIVVZO2.js.map} +0 -0
  104. /package/dist/{chunk-56XCRZ7I.js.map → chunk-6VAJDGZK.js.map} +0 -0
  105. /package/dist/{chunk-MQRTT3DO.js.map → chunk-COGPHD7D.js.map} +0 -0
  106. /package/dist/{chunk-UFP45JLE.js.map → chunk-F63VMX4S.js.map} +0 -0
  107. /package/dist/{chunk-FHEIG7QO.js.map → chunk-LAFLYVZ4.js.map} +0 -0
  108. /package/dist/{chunk-463X3AW2.js.map → chunk-NHFQ2LCC.js.map} +0 -0
  109. /package/dist/{chunk-RYPCNUXA.js.map → chunk-O5QU5KAC.js.map} +0 -0
  110. /package/dist/{chunk-LAL63W5J.js.map → chunk-VMJTNXOH.js.map} +0 -0
  111. /package/dist/{chunk-6VR7BJF6.js.map → chunk-WM72XFOD.js.map} +0 -0
  112. /package/dist/{chunk-PD5MXNTJ.js.map → chunk-YPNI74ZP.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
- const scrollRef = React.useRef<HTMLDivElement>(null)
19
- const [fadeTop, setFadeTop] = React.useState(false)
20
- const [fadeBottom, setFadeBottom] = React.useState(false)
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. The
72
- token flips with `data-theme` (light #fafafa, dark #161616),
73
- unlike the previous alpha mask. Card-coloured fade (#ffffff) was
74
- invisible in light theme against the items' own bg. Same token as
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 = React.useRef<HTMLDivElement>(null)
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
- {/* Top scroll-fade — visible only when content is hidden above. */}
208
- <div
209
- aria-hidden
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 (same pattern as MingoWelcome's greeting region).
361
- const scrollRef = React.useRef<HTMLDivElement>(null)
362
- const [fade, setFade] = React.useState({ top: false, bottom: false })
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
- <div
449
- aria-hidden
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: show a 48px gradient at the top/bottom edge of the
235
- // scroll region only while content is actually hidden in that direction.
236
- const scrollRef = React.useRef<HTMLDivElement>(null)
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
- {/* Top scroll-fade — visible only when content is hidden above. */}
369
- <div
370
- aria-hidden
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
- return Boolean(header.column.columnDef.meta?.filter)
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 && !hasFilter) return null
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
- !(hasFilter && !isLgUp) && getHideClasses(meta?.hideAt),
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
- setDraftRange({ from: triggerDate, to: undefined });
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
- * Selections are drafted internally and only committed via `onApply`.
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
- <Button
1092
- type="button"
1093
- variant="outline"
1094
- size="icon"
1095
- disabled={disabled}
1096
- aria-label={ariaLabel}
1097
- className={className}
1098
- leftIcon={<Calendar className="size-6" />}
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 direction selector */}
1116
- <Select
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
- fluid
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"