@flamingo-stack/openframe-frontend-core 0.0.399 → 0.0.400-snapshot.20260710144905
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-XBCAY6NE.js → chunk-2B5FIJE5.js} +3 -3
- package/dist/{chunk-JBLBFJ4N.cjs → chunk-3GXZPZHR.cjs} +32 -32
- package/dist/{chunk-JBLBFJ4N.cjs.map → chunk-3GXZPZHR.cjs.map} +1 -1
- package/dist/{chunk-BSJO5VFV.js → chunk-5P5WMTVW.js} +5 -5
- package/dist/{chunk-G5NDKTX6.cjs → chunk-7YKHVAUU.cjs} +40 -40
- package/dist/{chunk-G5NDKTX6.cjs.map → chunk-7YKHVAUU.cjs.map} +1 -1
- package/dist/{chunk-CRRH7RJX.js → chunk-B3IDZ6JJ.js} +5 -5
- package/dist/{chunk-ZDT2MBKE.cjs → chunk-BI23LOMC.cjs} +40 -40
- package/dist/{chunk-ZDT2MBKE.cjs.map → chunk-BI23LOMC.cjs.map} +1 -1
- package/dist/{chunk-NCW22SII.cjs → chunk-CG7LIPSA.cjs} +73 -73
- package/dist/{chunk-NCW22SII.cjs.map → chunk-CG7LIPSA.cjs.map} +1 -1
- package/dist/{chunk-ZSZRJWY3.js → chunk-DH6HTYOV.js} +153 -134
- package/dist/chunk-DH6HTYOV.js.map +1 -0
- package/dist/{chunk-2XVOYDRF.js → chunk-EDR55RQJ.js} +4763 -4735
- package/dist/chunk-EDR55RQJ.js.map +1 -0
- package/dist/{chunk-7OKNKZBW.cjs → chunk-F2LZ67KM.cjs} +1250 -1222
- package/dist/chunk-F2LZ67KM.cjs.map +1 -0
- package/dist/{chunk-Z5ERSGP2.js → chunk-FLDYBM25.js} +2 -2
- package/dist/{chunk-QNYNXSYQ.cjs → chunk-FRLLKYMO.cjs} +7 -7
- package/dist/{chunk-QNYNXSYQ.cjs.map → chunk-FRLLKYMO.cjs.map} +1 -1
- package/dist/{chunk-JWBRDNYH.cjs → chunk-J4XR6YYZ.cjs} +23 -4
- package/dist/chunk-J4XR6YYZ.cjs.map +1 -0
- package/dist/{chunk-4KWIQEZT.js → chunk-MTBFE3JF.js} +2 -2
- package/dist/{chunk-VVKWNVKE.js → chunk-MZPEN27A.js} +6 -6
- package/dist/{chunk-RIC4TCPD.cjs → chunk-PLB6Z3E4.cjs} +19 -19
- package/dist/{chunk-RIC4TCPD.cjs.map → chunk-PLB6Z3E4.cjs.map} +1 -1
- package/dist/{chunk-G3A7QHCX.js → chunk-PMR74LEE.js} +2 -2
- package/dist/{chunk-WBLRUO3F.cjs → chunk-Q3AO2QWK.cjs} +3 -3
- package/dist/{chunk-WBLRUO3F.cjs.map → chunk-Q3AO2QWK.cjs.map} +1 -1
- package/dist/{chunk-VOHOA3WV.cjs → chunk-Q4HI3DGY.cjs} +15 -15
- package/dist/{chunk-VOHOA3WV.cjs.map → chunk-Q4HI3DGY.cjs.map} +1 -1
- package/dist/{chunk-D5IHYUBY.cjs → chunk-QDERWRT4.cjs} +29 -29
- package/dist/{chunk-D5IHYUBY.cjs.map → chunk-QDERWRT4.cjs.map} +1 -1
- package/dist/{chunk-YWCCKB4J.cjs → chunk-QOOWYEPL.cjs} +5 -5
- package/dist/{chunk-YWCCKB4J.cjs.map → chunk-QOOWYEPL.cjs.map} +1 -1
- package/dist/{chunk-4K5XJ5EJ.js → chunk-RWKVTABY.js} +3 -3
- package/dist/{chunk-O5NA3ULU.cjs → chunk-TGWHUEPD.cjs} +73 -73
- package/dist/{chunk-O5NA3ULU.cjs.map → chunk-TGWHUEPD.cjs.map} +1 -1
- package/dist/{chunk-FXWURWDU.js → chunk-TZDROKLI.js} +3 -3
- package/dist/{chunk-4H2ICJYB.js → chunk-V3NY65ET.js} +4 -4
- package/dist/{chunk-A6KGHQRF.js → chunk-V4YFWSB2.js} +6 -6
- package/dist/{chunk-4PEC5KU6.cjs → chunk-VIEWUAFE.cjs} +12 -12
- package/dist/{chunk-4PEC5KU6.cjs.map → chunk-VIEWUAFE.cjs.map} +1 -1
- package/dist/{chunk-2L6ALD2Q.js → chunk-YON2QYWK.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- 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 +3 -3
- package/dist/components/chat/index.js +2 -2
- 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 +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
- package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
- package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
- package/dist/components/features/auth/login-form.d.ts.map +1 -1
- package/dist/components/features/auth/openframe-sso-login-form.d.ts.map +1 -1
- package/dist/components/features/auth/openframe-sso-signup-form.d.ts.map +1 -1
- package/dist/components/features/auth/password-reset-form.d.ts.map +1 -1
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +131 -125
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +17 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- 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 +9 -3
- 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 +8 -2
- package/dist/components/ui/scroll-fade.d.ts +33 -0
- package/dist/components/ui/scroll-fade.d.ts.map +1 -0
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-deferred-error.d.ts +16 -0
- package/dist/hooks/ui/use-deferred-error.d.ts.map +1 -0
- package/dist/index.cjs +11 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -2
- 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/features/auth/auth-benefits-panel.tsx +1 -1
- package/src/components/features/auth/complete-account-form.tsx +15 -4
- package/src/components/features/auth/create-organization-form.tsx +14 -5
- package/src/components/features/auth/login-form.tsx +6 -1
- package/src/components/features/auth/openframe-sso-login-form.tsx +9 -2
- package/src/components/features/auth/openframe-sso-signup-form.tsx +18 -5
- package/src/components/features/auth/password-reset-form.tsx +9 -2
- 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/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-deferred-error.ts +38 -0
- package/src/stories/DatePicker.stories.tsx +4 -2
- package/src/stories/FilterModal.stories.tsx +33 -0
- package/src/stories/Input.stories.tsx +1 -1
- package/src/stories/auth/CreateOrganization.stories.tsx +2 -1
- package/dist/chunk-2XVOYDRF.js.map +0 -1
- package/dist/chunk-7OKNKZBW.cjs.map +0 -1
- package/dist/chunk-JWBRDNYH.cjs.map +0 -1
- package/dist/chunk-ZSZRJWY3.js.map +0 -1
- /package/dist/{chunk-XBCAY6NE.js.map → chunk-2B5FIJE5.js.map} +0 -0
- /package/dist/{chunk-BSJO5VFV.js.map → chunk-5P5WMTVW.js.map} +0 -0
- /package/dist/{chunk-CRRH7RJX.js.map → chunk-B3IDZ6JJ.js.map} +0 -0
- /package/dist/{chunk-Z5ERSGP2.js.map → chunk-FLDYBM25.js.map} +0 -0
- /package/dist/{chunk-4KWIQEZT.js.map → chunk-MTBFE3JF.js.map} +0 -0
- /package/dist/{chunk-VVKWNVKE.js.map → chunk-MZPEN27A.js.map} +0 -0
- /package/dist/{chunk-G3A7QHCX.js.map → chunk-PMR74LEE.js.map} +0 -0
- /package/dist/{chunk-4K5XJ5EJ.js.map → chunk-RWKVTABY.js.map} +0 -0
- /package/dist/{chunk-FXWURWDU.js.map → chunk-TZDROKLI.js.map} +0 -0
- /package/dist/{chunk-4H2ICJYB.js.map → chunk-V3NY65ET.js.map} +0 -0
- /package/dist/{chunk-A6KGHQRF.js.map → chunk-V4YFWSB2.js.map} +0 -0
- /package/dist/{chunk-2L6ALD2Q.js.map → chunk-YON2QYWK.js.map} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type * as React from 'react'
|
|
4
4
|
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
|
|
5
6
|
import { Button } from '../../ui/button'
|
|
6
7
|
import { Input } from '../../ui/input'
|
|
7
8
|
import { PasswordInput } from '../../ui/password-input'
|
|
@@ -72,6 +73,13 @@ export function OpenFrameSsoSignUpForm({
|
|
|
72
73
|
}: OpenFrameSsoSignUpFormProps) {
|
|
73
74
|
const fieldsDisabled = disabled || loading
|
|
74
75
|
|
|
76
|
+
// Validation messages are deferred while the user is typing (shown on blur or after a pause).
|
|
77
|
+
const emailErr = useDeferredError(errors?.email, email)
|
|
78
|
+
const firstNameErr = useDeferredError(errors?.firstName, firstName)
|
|
79
|
+
const lastNameErr = useDeferredError(errors?.lastName, lastName)
|
|
80
|
+
const passwordErr = useDeferredError(errors?.password, password)
|
|
81
|
+
const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword)
|
|
82
|
+
|
|
75
83
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
76
84
|
if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
|
|
77
85
|
onSubmit()
|
|
@@ -96,8 +104,9 @@ export function OpenFrameSsoSignUpForm({
|
|
|
96
104
|
label={emailLabel}
|
|
97
105
|
placeholder="username@mail.com"
|
|
98
106
|
value={email}
|
|
99
|
-
error={
|
|
107
|
+
error={emailErr.error}
|
|
100
108
|
disabled={fieldsDisabled || emailReadOnly}
|
|
109
|
+
onBlur={emailErr.onBlur}
|
|
101
110
|
onChange={(event) => onEmailChange(event.target.value)}
|
|
102
111
|
onKeyDown={handleKeyDown}
|
|
103
112
|
/>
|
|
@@ -107,8 +116,9 @@ export function OpenFrameSsoSignUpForm({
|
|
|
107
116
|
label="First Name"
|
|
108
117
|
placeholder="Enter First Name"
|
|
109
118
|
value={firstName}
|
|
110
|
-
error={
|
|
119
|
+
error={firstNameErr.error}
|
|
111
120
|
disabled={fieldsDisabled}
|
|
121
|
+
onBlur={firstNameErr.onBlur}
|
|
112
122
|
onChange={(event) => onFirstNameChange(event.target.value)}
|
|
113
123
|
onKeyDown={handleKeyDown}
|
|
114
124
|
/>
|
|
@@ -116,8 +126,9 @@ export function OpenFrameSsoSignUpForm({
|
|
|
116
126
|
label="Last Name"
|
|
117
127
|
placeholder="Enter Last Name"
|
|
118
128
|
value={lastName}
|
|
119
|
-
error={
|
|
129
|
+
error={lastNameErr.error}
|
|
120
130
|
disabled={fieldsDisabled}
|
|
131
|
+
onBlur={lastNameErr.onBlur}
|
|
121
132
|
onChange={(event) => onLastNameChange(event.target.value)}
|
|
122
133
|
onKeyDown={handleKeyDown}
|
|
123
134
|
/>
|
|
@@ -125,8 +136,9 @@ export function OpenFrameSsoSignUpForm({
|
|
|
125
136
|
label="Password"
|
|
126
137
|
placeholder="Enter Password"
|
|
127
138
|
value={password}
|
|
128
|
-
error={
|
|
139
|
+
error={passwordErr.error}
|
|
129
140
|
disabled={fieldsDisabled}
|
|
141
|
+
onBlur={passwordErr.onBlur}
|
|
130
142
|
onChange={(event) => onPasswordChange(event.target.value)}
|
|
131
143
|
onKeyDown={handleKeyDown}
|
|
132
144
|
/>
|
|
@@ -134,8 +146,9 @@ export function OpenFrameSsoSignUpForm({
|
|
|
134
146
|
label="Confirm Password"
|
|
135
147
|
placeholder="Confirm Password"
|
|
136
148
|
value={confirmPassword}
|
|
137
|
-
error={
|
|
149
|
+
error={confirmErr.error}
|
|
138
150
|
disabled={fieldsDisabled}
|
|
151
|
+
onBlur={confirmErr.onBlur}
|
|
139
152
|
onChange={(event) => onConfirmPasswordChange(event.target.value)}
|
|
140
153
|
onKeyDown={handleKeyDown}
|
|
141
154
|
/>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type * as React from 'react'
|
|
4
4
|
import { cn } from '../../../utils/cn'
|
|
5
|
+
import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
|
|
5
6
|
import { Button } from '../../ui/button'
|
|
6
7
|
import { Input } from '../../ui/input'
|
|
7
8
|
import { BackToLoginLink } from './back-to-login-link'
|
|
@@ -66,6 +67,10 @@ export function PasswordResetForm({
|
|
|
66
67
|
}: PasswordResetFormProps) {
|
|
67
68
|
const fieldsDisabled = disabled || loading
|
|
68
69
|
|
|
70
|
+
// Validation messages are deferred while the user is typing (shown on blur or after a pause).
|
|
71
|
+
const passwordErr = useDeferredError(errors?.password, password)
|
|
72
|
+
const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword)
|
|
73
|
+
|
|
69
74
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
70
75
|
if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
|
|
71
76
|
onSubmit()
|
|
@@ -91,8 +96,9 @@ export function PasswordResetForm({
|
|
|
91
96
|
label={newPasswordLabel}
|
|
92
97
|
placeholder={newPasswordPlaceholder}
|
|
93
98
|
value={password}
|
|
94
|
-
error={
|
|
99
|
+
error={passwordErr.error}
|
|
95
100
|
disabled={fieldsDisabled}
|
|
101
|
+
onBlur={passwordErr.onBlur}
|
|
96
102
|
onChange={(event) => onPasswordChange(event.target.value)}
|
|
97
103
|
onKeyDown={handleKeyDown}
|
|
98
104
|
/>
|
|
@@ -103,8 +109,9 @@ export function PasswordResetForm({
|
|
|
103
109
|
label={confirmPasswordLabel}
|
|
104
110
|
placeholder={confirmPasswordPlaceholder}
|
|
105
111
|
value={confirmPassword}
|
|
106
|
-
error={
|
|
112
|
+
error={confirmErr.error}
|
|
107
113
|
disabled={fieldsDisabled}
|
|
114
|
+
onBlur={confirmErr.onBlur}
|
|
108
115
|
onChange={(event) => onConfirmPasswordChange(event.target.value)}
|
|
109
116
|
onKeyDown={handleKeyDown}
|
|
110
117
|
/>
|
|
@@ -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"
|
|
@@ -6,7 +6,9 @@ import { cn } from '../../utils/cn'
|
|
|
6
6
|
import { Filter02Icon } from '../icons-v2-generated/sort-and-filter/filter-02-icon'
|
|
7
7
|
import { Button } from './button'
|
|
8
8
|
import { FilterCheckboxItem } from './filter-checkbox-item'
|
|
9
|
+
import { DateFilterPanel, type DateFilterResult, type DateRange } from './date-picker'
|
|
9
10
|
import { ModalV2, ModalV2Content, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2'
|
|
11
|
+
import { ScrollFadeOverlay, useScrollFade } from './scroll-fade'
|
|
10
12
|
import { Skeleton } from './skeleton'
|
|
11
13
|
import { SortColumnItem, type SortConfig, type SortDirection } from './sort-column-item'
|
|
12
14
|
import { TagKeyValueFilter, type TagKeyConfig } from './tag-key-value-filter'
|
|
@@ -51,6 +53,20 @@ export interface FilterModalProps {
|
|
|
51
53
|
tagFilterTitle?: string
|
|
52
54
|
/** Show skeleton loading state */
|
|
53
55
|
isLoading?: boolean
|
|
56
|
+
/** Date sort + range filter rendered as the last section. Drafted with the
|
|
57
|
+
* other filters and committed via `onChange` on Apply (defaults on Reset). */
|
|
58
|
+
dateFilter?: {
|
|
59
|
+
/** Section title. Defaults to 'Date'. */
|
|
60
|
+
title?: string
|
|
61
|
+
/** Applied sort direction. Defaults to `defaultSort`. */
|
|
62
|
+
sort?: SortDirection
|
|
63
|
+
/** Baseline sort direction restored by Reset. Defaults to 'desc'. */
|
|
64
|
+
defaultSort?: SortDirection
|
|
65
|
+
/** Applied date range. */
|
|
66
|
+
range?: DateRange
|
|
67
|
+
/** Fired on Apply with the drafted values, and on Reset with the defaults. */
|
|
68
|
+
onChange: (result: DateFilterResult) => void
|
|
69
|
+
}
|
|
54
70
|
/** Text shown in empty state title */
|
|
55
71
|
emptyStateTitle?: string
|
|
56
72
|
/** Text shown in empty state description */
|
|
@@ -74,6 +90,7 @@ export function FilterModal({
|
|
|
74
90
|
selectedTags,
|
|
75
91
|
onTagsChange,
|
|
76
92
|
tagFilterTitle,
|
|
93
|
+
dateFilter,
|
|
77
94
|
isLoading = false,
|
|
78
95
|
emptyStateTitle = 'No filters available',
|
|
79
96
|
emptyStateDescription = 'There are no filter options to display at the moment',
|
|
@@ -83,10 +100,20 @@ export function FilterModal({
|
|
|
83
100
|
})
|
|
84
101
|
const [pendingTags, setPendingTags] = useState<string[]>(selectedTags ?? [])
|
|
85
102
|
|
|
103
|
+
const dateDefaultSort: SortDirection = dateFilter?.defaultSort ?? 'desc'
|
|
104
|
+
const [draftDateSort, setDraftDateSort] = useState<SortDirection>(dateFilter?.sort ?? dateDefaultSort)
|
|
105
|
+
const [draftDateRange, setDraftDateRange] = useState<DateRange | undefined>(dateFilter?.range)
|
|
106
|
+
|
|
107
|
+
// Scroll shadows: bottom fade by default while content continues below,
|
|
108
|
+
// top fade once scrolled (same pattern as the chat lists).
|
|
109
|
+
const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>()
|
|
110
|
+
|
|
86
111
|
useEffect(() => {
|
|
87
112
|
if (isOpen) {
|
|
88
113
|
setSelectedFilters({ ...currentFilters })
|
|
89
114
|
setPendingTags(selectedTags ?? [])
|
|
115
|
+
setDraftDateSort(dateFilter?.sort ?? dateDefaultSort)
|
|
116
|
+
setDraftDateRange(dateFilter?.range)
|
|
90
117
|
}
|
|
91
118
|
}, [isOpen, JSON.stringify(currentFilters), JSON.stringify(selectedTags)])
|
|
92
119
|
|
|
@@ -109,12 +136,14 @@ export function FilterModal({
|
|
|
109
136
|
const handleReset = () => {
|
|
110
137
|
onFilterChange({})
|
|
111
138
|
onTagsChange?.([])
|
|
139
|
+
dateFilter?.onChange({ sort: dateDefaultSort, range: undefined })
|
|
112
140
|
onClose()
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
const handleApply = () => {
|
|
116
144
|
onFilterChange(selectedFilters)
|
|
117
145
|
onTagsChange?.(pendingTags)
|
|
146
|
+
dateFilter?.onChange({ sort: draftDateSort, range: draftDateRange })
|
|
118
147
|
onClose()
|
|
119
148
|
}
|
|
120
149
|
|
|
@@ -125,7 +154,7 @@ export function FilterModal({
|
|
|
125
154
|
const hasSort = !!sortConfig && sortConfig.columns.length > 0
|
|
126
155
|
const hasFilterGroups = filterGroups.length > 0
|
|
127
156
|
const hasTagFilter = !!tagFilterKeys && tagFilterKeys.length > 0 && !!onTagsChange
|
|
128
|
-
const isEmpty = !isLoading && !hasSort && !hasFilterGroups && !hasTagFilter
|
|
157
|
+
const isEmpty = !isLoading && !hasSort && !hasFilterGroups && !hasTagFilter && !dateFilter
|
|
129
158
|
|
|
130
159
|
return (
|
|
131
160
|
<ModalV2
|
|
@@ -143,7 +172,12 @@ export function FilterModal({
|
|
|
143
172
|
!isEmpty && 'md:bg-ods-bg md:border md:border-ods-border md:rounded-md md:px-3',
|
|
144
173
|
)}
|
|
145
174
|
>
|
|
146
|
-
<div className="flex-1 min-h-0
|
|
175
|
+
<div className="relative flex-1 min-h-0 flex flex-col">
|
|
176
|
+
<div
|
|
177
|
+
ref={scrollRef}
|
|
178
|
+
onScroll={updateFade}
|
|
179
|
+
className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-4 md:py-3"
|
|
180
|
+
>
|
|
147
181
|
{isLoading ? (
|
|
148
182
|
<>
|
|
149
183
|
{[0, 1].map((group) => (
|
|
@@ -229,10 +263,31 @@ export function FilterModal({
|
|
|
229
263
|
onTagsChange={setPendingTags}
|
|
230
264
|
keysTitle={tagFilterTitle}
|
|
231
265
|
/>
|
|
266
|
+
)}
|
|
267
|
+
{/* Date sort + range filter */}
|
|
268
|
+
{dateFilter && (
|
|
269
|
+
<div className="flex flex-col gap-1">
|
|
270
|
+
<span className="text-h5 text-ods-text-secondary">
|
|
271
|
+
{dateFilter.title ?? 'Date'}
|
|
272
|
+
</span>
|
|
273
|
+
<DateFilterPanel
|
|
274
|
+
mode="range"
|
|
275
|
+
sort={draftDateSort}
|
|
276
|
+
onSortChange={setDraftDateSort}
|
|
277
|
+
selected={draftDateRange}
|
|
278
|
+
onSelect={(value) => setDraftDateRange(value as DateRange | undefined)}
|
|
279
|
+
className="gap-[var(--spacing-system-xxs)]"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
232
282
|
)}
|
|
233
283
|
</>
|
|
234
284
|
)}
|
|
235
285
|
</div>
|
|
286
|
+
|
|
287
|
+
{/* Scroll shadows over the filter list edges */}
|
|
288
|
+
<ScrollFadeOverlay edge="top" visible={fadeTop} />
|
|
289
|
+
<ScrollFadeOverlay edge="bottom" visible={fadeBottom} />
|
|
290
|
+
</div>
|
|
236
291
|
</ModalV2Content>
|
|
237
292
|
|
|
238
293
|
<ModalV2Footer>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tracks whether a scrollable element is scrolled away from its top/bottom
|
|
8
|
+
* edge, so edge fades ("scroll shadows") can be shown only where content
|
|
9
|
+
* continues. Attach `scrollRef` to the scrollable element and `update` to its
|
|
10
|
+
* `onScroll`; content/size changes are re-measured via ResizeObserver.
|
|
11
|
+
*
|
|
12
|
+
* Canonical fade pattern shared by chat lists and FilterModal.
|
|
13
|
+
*/
|
|
14
|
+
export function useScrollFade<T extends HTMLElement = HTMLDivElement>() {
|
|
15
|
+
const scrollRef = React.useRef<T | null>(null)
|
|
16
|
+
const [fade, setFade] = React.useState({ top: false, bottom: false })
|
|
17
|
+
|
|
18
|
+
const update = React.useCallback(() => {
|
|
19
|
+
const el = scrollRef.current
|
|
20
|
+
if (!el) return
|
|
21
|
+
setFade((prev) => {
|
|
22
|
+
const top = el.scrollTop > 0
|
|
23
|
+
const bottom = el.scrollHeight - el.scrollTop - el.clientHeight > 1
|
|
24
|
+
return prev.top === top && prev.bottom === bottom ? prev : { top, bottom }
|
|
25
|
+
})
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
React.useLayoutEffect(() => {
|
|
29
|
+
update()
|
|
30
|
+
const el = scrollRef.current
|
|
31
|
+
if (!el) return
|
|
32
|
+
const cleanups: Array<() => void> = []
|
|
33
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
34
|
+
const resizeObserver = new ResizeObserver(update)
|
|
35
|
+
resizeObserver.observe(el)
|
|
36
|
+
cleanups.push(() => resizeObserver.disconnect())
|
|
37
|
+
}
|
|
38
|
+
// ResizeObserver only sees the container box; content growth (appended
|
|
39
|
+
// list items) changes scrollHeight without resizing it — watch mutations.
|
|
40
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
41
|
+
const mutationObserver = new MutationObserver(update)
|
|
42
|
+
mutationObserver.observe(el, { childList: true, subtree: true, characterData: true })
|
|
43
|
+
cleanups.push(() => mutationObserver.disconnect())
|
|
44
|
+
}
|
|
45
|
+
return () => {
|
|
46
|
+
for (const cleanup of cleanups) cleanup()
|
|
47
|
+
}
|
|
48
|
+
}, [update])
|
|
49
|
+
|
|
50
|
+
return { scrollRef, fadeTop: fade.top, fadeBottom: fade.bottom, update }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ScrollFadeOverlayProps {
|
|
54
|
+
/** Which edge of the scroll container the fade sits on. */
|
|
55
|
+
edge: 'top' | 'bottom'
|
|
56
|
+
/** Whether the fade is currently shown (content continues past this edge). */
|
|
57
|
+
visible: boolean
|
|
58
|
+
/**
|
|
59
|
+
* CSS color the content fades into — should match the surface behind the
|
|
60
|
+
* list. Defaults to the page background token.
|
|
61
|
+
*/
|
|
62
|
+
color?: string
|
|
63
|
+
className?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Edge overlay that fades scrollable content into the surface behind it.
|
|
68
|
+
* Render inside a `relative` wrapper around the scrollable element.
|
|
69
|
+
*/
|
|
70
|
+
export function ScrollFadeOverlay({
|
|
71
|
+
edge,
|
|
72
|
+
visible,
|
|
73
|
+
color = 'var(--color-bg)',
|
|
74
|
+
className,
|
|
75
|
+
}: ScrollFadeOverlayProps) {
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
aria-hidden
|
|
79
|
+
className={cn(
|
|
80
|
+
'pointer-events-none absolute inset-x-0 h-16 transition-opacity duration-150',
|
|
81
|
+
edge === 'top' ? 'top-0' : 'bottom-0',
|
|
82
|
+
visible ? 'opacity-100' : 'opacity-0',
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
style={{
|
|
86
|
+
background: `linear-gradient(${edge === 'top' ? '0deg' : '180deg'}, transparent 0%, ${color} 100%)`,
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
package/src/hooks/ui/index.ts
CHANGED