@fayz-ai/ui 0.9.0 → 0.10.0
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/README.md +2 -2
- package/dist/analytics/AnalyticsCard.d.ts +10 -0
- package/dist/analytics/AnalyticsCard.d.ts.map +1 -0
- package/dist/analytics/AnalyticsControls.d.ts +18 -0
- package/dist/analytics/AnalyticsControls.d.ts.map +1 -0
- package/dist/analytics/AnalyticsGrid.d.ts +16 -0
- package/dist/analytics/AnalyticsGrid.d.ts.map +1 -0
- package/dist/analytics/context.d.ts +58 -0
- package/dist/analytics/context.d.ts.map +1 -0
- package/dist/analytics/derive.d.ts +81 -0
- package/dist/analytics/derive.d.ts.map +1 -0
- package/dist/analytics/index.d.ts +13 -0
- package/dist/analytics/index.d.ts.map +1 -0
- package/dist/analytics/range.d.ts +22 -0
- package/dist/analytics/range.d.ts.map +1 -0
- package/dist/{chunk-PVIB724L.js → chunk-22EVFMS3.js} +5 -3
- package/dist/chunk-22EVFMS3.js.map +1 -0
- package/dist/{chunk-G6OKZ7WL.js → chunk-6V5MGYXH.js} +13 -9
- package/dist/chunk-6V5MGYXH.js.map +1 -0
- package/dist/{chunk-KV3MHB6N.js → chunk-H63TOSQ7.js} +196 -53
- package/dist/chunk-H63TOSQ7.js.map +1 -0
- package/dist/{chunk-OUPJDGKE.js → chunk-I6CPKAOD.js} +134 -37
- package/dist/chunk-I6CPKAOD.js.map +1 -0
- package/dist/{chunk-YRVE4FOD.js → chunk-OKZ6WQB3.js} +3 -3
- package/dist/chunk-OKZ6WQB3.js.map +1 -0
- package/dist/{chunk-S5KNM4D2.js → chunk-SEIY2F5F.js} +235 -107
- package/dist/chunk-SEIY2F5F.js.map +1 -0
- package/dist/{chunk-75EFDLST.js → chunk-VAKWUUVC.js} +157 -6
- package/dist/chunk-VAKWUUVC.js.map +1 -0
- package/dist/dashboard/OnboardingWidget.d.ts +2 -14
- package/dist/dashboard/OnboardingWidget.d.ts.map +1 -1
- package/dist/dashboard/SidebarOnboarding.d.ts +28 -0
- package/dist/dashboard/SidebarOnboarding.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +2 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +4 -4
- package/dist/dashboard/types.d.ts +18 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/dashboard/useOnboardingProgress.d.ts +28 -0
- package/dist/dashboard/useOnboardingProgress.d.ts.map +1 -0
- package/dist/icons.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +960 -14
- package/dist/index.js.map +1 -1
- package/dist/layout/AppShell.d.ts +14 -1
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/ModulePage.d.ts +11 -2
- package/dist/layout/ModulePage.d.ts.map +1 -1
- package/dist/layout/Sidebar.d.ts +8 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/index.js +4 -4
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/preset.d.ts +3 -0
- package/dist/preset.d.ts.map +1 -0
- package/dist/primitives/data-table.d.ts +19 -4
- package/dist/primitives/data-table.d.ts.map +1 -1
- package/dist/primitives/date-picker.d.ts +4 -2
- package/dist/primitives/date-picker.d.ts.map +1 -1
- package/dist/primitives/filter-chip.d.ts +26 -0
- package/dist/primitives/filter-chip.d.ts.map +1 -0
- package/dist/primitives/header-tabs.d.ts +32 -0
- package/dist/primitives/header-tabs.d.ts.map +1 -0
- package/dist/primitives/hint.d.ts +25 -0
- package/dist/primitives/hint.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +7 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +4 -4
- package/dist/primitives/list-view.d.ts +14 -1
- package/dist/primitives/list-view.d.ts.map +1 -1
- package/dist/primitives/page-header.d.ts +23 -0
- package/dist/primitives/page-header.d.ts.map +1 -0
- package/dist/primitives/stat-tiles.d.ts +30 -0
- package/dist/primitives/stat-tiles.d.ts.map +1 -0
- package/dist/primitives/switch.d.ts +11 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/page-header.store.d.ts +69 -0
- package/dist/stores/page-header.store.d.ts.map +1 -0
- package/package.json +4 -13
- package/dist/chunk-75EFDLST.js.map +0 -1
- package/dist/chunk-G6OKZ7WL.js.map +0 -1
- package/dist/chunk-KV3MHB6N.js.map +0 -1
- package/dist/chunk-OUPJDGKE.js.map +0 -1
- package/dist/chunk-PVIB724L.js.map +0 -1
- package/dist/chunk-S5KNM4D2.js.map +0 -1
- package/dist/chunk-YRVE4FOD.js.map +0 -1
- package/dist/utils/cn.d.ts.map +0 -1
- package/src/dashboard/ChartWidget.tsx +0 -50
- package/src/dashboard/ChartWidgetInner.tsx +0 -83
- package/src/dashboard/DashboardCanvas.tsx +0 -210
- package/src/dashboard/DashboardCustomizeMenu.tsx +0 -53
- package/src/dashboard/DashboardGrid.tsx +0 -35
- package/src/dashboard/DashboardRange.tsx +0 -62
- package/src/dashboard/GoldCard.tsx +0 -38
- package/src/dashboard/KpiCard.tsx +0 -238
- package/src/dashboard/OnboardingWidget.tsx +0 -140
- package/src/dashboard/TableWidget.tsx +0 -51
- package/src/dashboard/define.ts +0 -53
- package/src/dashboard/icon.ts +0 -13
- package/src/dashboard/index.ts +0 -38
- package/src/dashboard/layout.ts +0 -50
- package/src/dashboard/preferences.ts +0 -71
- package/src/dashboard/types.ts +0 -41
- package/src/icons.ts +0 -66
- package/src/index.ts +0 -238
- package/src/layout/AppShell.tsx +0 -987
- package/src/layout/ContentSplit.tsx +0 -153
- package/src/layout/ModulePage.tsx +0 -466
- package/src/layout/PageTransition.tsx +0 -57
- package/src/layout/SaveBar.tsx +0 -247
- package/src/layout/Sidebar.tsx +0 -535
- package/src/layout/Topbar.tsx +0 -462
- package/src/layout/index.ts +0 -39
- package/src/layout/useCrossModuleBack.ts +0 -30
- package/src/primitives/Avatar.tsx +0 -48
- package/src/primitives/SearchCombobox.tsx +0 -216
- package/src/primitives/badge.tsx +0 -68
- package/src/primitives/breadcrumb.tsx +0 -43
- package/src/primitives/button.tsx +0 -60
- package/src/primitives/card.tsx +0 -79
- package/src/primitives/checkbox.tsx +0 -53
- package/src/primitives/confirm-dialog.tsx +0 -125
- package/src/primitives/currency-input.tsx +0 -121
- package/src/primitives/data-table.tsx +0 -160
- package/src/primitives/date-picker.tsx +0 -225
- package/src/primitives/dropdown.tsx +0 -196
- package/src/primitives/index.ts +0 -87
- package/src/primitives/input.tsx +0 -50
- package/src/primitives/list-view.tsx +0 -252
- package/src/primitives/markdown-editor.tsx +0 -75
- package/src/primitives/markdown.tsx +0 -169
- package/src/primitives/modal.tsx +0 -156
- package/src/primitives/phone-link.tsx +0 -138
- package/src/primitives/popover.tsx +0 -30
- package/src/primitives/search-select.tsx +0 -314
- package/src/primitives/segmented-control.tsx +0 -66
- package/src/primitives/select.tsx +0 -157
- package/src/primitives/separator.tsx +0 -29
- package/src/primitives/sheet.tsx +0 -181
- package/src/primitives/skeleton.tsx +0 -46
- package/src/primitives/tabs.tsx +0 -53
- package/src/primitives/time-picker.tsx +0 -148
- package/src/primitives/toast.tsx +0 -46
- package/src/primitives/tooltip.tsx +0 -28
- package/src/stores/index.ts +0 -10
- package/src/stores/layout.store.ts +0 -31
- package/src/stores/theme.store.ts +0 -457
- package/src/styles.css +0 -461
- package/src/tailwind.ts +0 -43
- package/src/theme/index.ts +0 -94
- package/src/theme/preset.ts +0 -209
- package/src/theme/presets.ts +0 -106
- package/src/utils/cn.ts +0 -6
- /package/dist/{utils → lib}/cn.d.ts +0 -0
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { PanelRight, PanelRightClose, PanelRightOpen } from 'lucide-react'
|
|
3
|
-
import { cn } from '../utils/cn'
|
|
4
|
-
import { Button } from '../primitives/button'
|
|
5
|
-
import {
|
|
6
|
-
Sheet, SheetContent, SheetHeader, SheetBody, SheetTitle, SheetDescription,
|
|
7
|
-
} from '../primitives/sheet'
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// ContentSplit — main content with a right-aligned companion panel, mobile
|
|
11
|
-
// first. Below `lg` the aside lives in a right Sheet (opened by a trigger the
|
|
12
|
-
// host places wherever fits — typically SubpageHeader actions, via
|
|
13
|
-
// <ContentSplitTrigger/>). From `lg` up it docks as a sticky right column.
|
|
14
|
-
//
|
|
15
|
-
// Pairs with SubpageHeader/ModulePage: the page keeps its own back-button
|
|
16
|
-
// header; ContentSplit only owns the columns below it. Reuse for any
|
|
17
|
-
// "document + operational panel" page (script + shooting checklist, form +
|
|
18
|
-
// help, detail + activity feed).
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
interface ContentSplitContextValue {
|
|
22
|
-
open: boolean
|
|
23
|
-
setOpen: (open: boolean) => void
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const ContentSplitContext = React.createContext<ContentSplitContextValue | null>(null)
|
|
27
|
-
|
|
28
|
-
export interface ContentSplitProps {
|
|
29
|
-
/** Main column (the document). */
|
|
30
|
-
children: React.ReactNode
|
|
31
|
-
/** Companion panel content (the checklist / help / activity). */
|
|
32
|
-
aside: React.ReactNode
|
|
33
|
-
/** Panel title — Sheet header on mobile, card header on desktop. */
|
|
34
|
-
asideTitle: React.ReactNode
|
|
35
|
-
/** Optional subtitle under the title. */
|
|
36
|
-
asideDescription?: React.ReactNode
|
|
37
|
-
/** Desktop column width (defaults to 20rem). */
|
|
38
|
-
asideWidth?: string
|
|
39
|
-
/** Controlled mobile-sheet state; uncontrolled when omitted. */
|
|
40
|
-
asideOpen?: boolean
|
|
41
|
-
onAsideOpenChange?: (open: boolean) => void
|
|
42
|
-
/** Desktop column can collapse into a slim reopen rail. */
|
|
43
|
-
collapsible?: boolean
|
|
44
|
-
/** Initial desktop collapsed state (e.g. expand only on the relevant day). */
|
|
45
|
-
defaultCollapsed?: boolean
|
|
46
|
-
className?: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function ContentSplit({
|
|
50
|
-
children,
|
|
51
|
-
aside,
|
|
52
|
-
asideTitle,
|
|
53
|
-
asideDescription,
|
|
54
|
-
asideWidth = '20rem',
|
|
55
|
-
asideOpen,
|
|
56
|
-
onAsideOpenChange,
|
|
57
|
-
collapsible = false,
|
|
58
|
-
defaultCollapsed = false,
|
|
59
|
-
className,
|
|
60
|
-
}: ContentSplitProps) {
|
|
61
|
-
const [internalOpen, setInternalOpen] = React.useState(false)
|
|
62
|
-
const open = asideOpen ?? internalOpen
|
|
63
|
-
const setOpen = onAsideOpenChange ?? setInternalOpen
|
|
64
|
-
const [collapsed, setCollapsed] = React.useState(collapsible && defaultCollapsed)
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<ContentSplitContext.Provider value={{ open, setOpen }}>
|
|
68
|
-
<div
|
|
69
|
-
className={cn('lg:grid lg:items-start lg:gap-6', className)}
|
|
70
|
-
style={{ gridTemplateColumns: `minmax(0, 1fr) ${collapsed ? 'auto' : asideWidth}` }}
|
|
71
|
-
>
|
|
72
|
-
<div className="min-w-0">{children}</div>
|
|
73
|
-
|
|
74
|
-
{/* Desktop: docked sticky column (or slim reopen rail when collapsed) */}
|
|
75
|
-
<aside className="hidden lg:block lg:sticky lg:top-4">
|
|
76
|
-
{collapsed ? (
|
|
77
|
-
<Button
|
|
78
|
-
variant="outline"
|
|
79
|
-
size="sm"
|
|
80
|
-
onClick={() => setCollapsed(false)}
|
|
81
|
-
className="h-9 w-9 p-0"
|
|
82
|
-
aria-label={typeof asideTitle === 'string' ? asideTitle : undefined}
|
|
83
|
-
title={typeof asideTitle === 'string' ? asideTitle : undefined}
|
|
84
|
-
>
|
|
85
|
-
<PanelRightOpen className="h-4 w-4" />
|
|
86
|
-
</Button>
|
|
87
|
-
) : (
|
|
88
|
-
<div className="rounded-card border border-border bg-card">
|
|
89
|
-
<div className="flex items-start justify-between gap-2 border-b border-border px-4 py-3">
|
|
90
|
-
<div className="min-w-0">
|
|
91
|
-
<p className="text-sm font-semibold text-foreground">{asideTitle}</p>
|
|
92
|
-
{asideDescription && (
|
|
93
|
-
<p className="mt-0.5 text-xs text-muted-foreground">{asideDescription}</p>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
{collapsible && (
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={() => setCollapsed(true)}
|
|
100
|
-
className="rounded p-1 text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground"
|
|
101
|
-
aria-label="collapse"
|
|
102
|
-
>
|
|
103
|
-
<PanelRightClose className="h-4 w-4" />
|
|
104
|
-
</button>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
<div className="p-4">{aside}</div>
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
110
|
-
</aside>
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
{/* Mobile: right sheet */}
|
|
114
|
-
<div className="lg:hidden">
|
|
115
|
-
<Sheet open={open} onOpenChange={setOpen}>
|
|
116
|
-
<SheetContent width="max-w-sm">
|
|
117
|
-
<SheetHeader>
|
|
118
|
-
<SheetTitle>{asideTitle}</SheetTitle>
|
|
119
|
-
{asideDescription && <SheetDescription>{asideDescription}</SheetDescription>}
|
|
120
|
-
</SheetHeader>
|
|
121
|
-
<SheetBody>{aside}</SheetBody>
|
|
122
|
-
</SheetContent>
|
|
123
|
-
</Sheet>
|
|
124
|
-
</div>
|
|
125
|
-
</ContentSplitContext.Provider>
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface ContentSplitTriggerProps {
|
|
130
|
-
/** Accessible label; also the visible text unless `iconOnly`. */
|
|
131
|
-
label: React.ReactNode
|
|
132
|
-
iconOnly?: boolean
|
|
133
|
-
className?: string
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Opens the mobile aside sheet. Renders nothing from `lg` up (the panel is
|
|
137
|
-
* already docked). Place inside SubpageHeader `actions` or any toolbar. */
|
|
138
|
-
export function ContentSplitTrigger({ label, iconOnly, className }: ContentSplitTriggerProps) {
|
|
139
|
-
const ctx = React.useContext(ContentSplitContext)
|
|
140
|
-
if (!ctx) return null
|
|
141
|
-
return (
|
|
142
|
-
<Button
|
|
143
|
-
variant="outline"
|
|
144
|
-
size="sm"
|
|
145
|
-
onClick={() => ctx.setOpen(true)}
|
|
146
|
-
className={cn('lg:hidden', className)}
|
|
147
|
-
aria-label={typeof label === 'string' ? label : undefined}
|
|
148
|
-
>
|
|
149
|
-
<PanelRight className={cn('h-4 w-4', !iconOnly && 'mr-1.5')} />
|
|
150
|
-
{!iconOnly && label}
|
|
151
|
-
</Button>
|
|
152
|
-
)
|
|
153
|
-
}
|
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import { ChevronDown, ChevronRight, ChevronLeft, Crown } from 'lucide-react'
|
|
4
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
5
|
-
import { cn } from '../utils/cn'
|
|
6
|
-
import { ICON_MAP } from './Topbar'
|
|
7
|
-
import { useModuleHeaderSlot, useModuleHeaderActionsSlot } from './AppShell'
|
|
8
|
-
import { PageTransition } from './PageTransition'
|
|
9
|
-
import { useBackHandler } from './SaveBar'
|
|
10
|
-
import { useCrossModuleBack } from './useCrossModuleBack'
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// PageHeaderActions — portals a page's primary action button(s) into the shell
|
|
14
|
-
// top-bar's right-aligned actions slot. Falls back to a right-aligned inline row
|
|
15
|
-
// when there's no slot (e.g. topbar layout). Any page can use it:
|
|
16
|
-
// <PageHeaderActions><Button>New campaign</Button></PageHeaderActions>
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
export function PageHeaderActions({ children }: { children: React.ReactNode }) {
|
|
20
|
-
const slot = useModuleHeaderActionsSlot()
|
|
21
|
-
if (slot) return createPortal(children, slot)
|
|
22
|
-
return <div className="mb-4 flex items-center justify-end gap-2">{children}</div>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ModuleNavItem {
|
|
26
|
-
id: string
|
|
27
|
-
label: string
|
|
28
|
-
icon?: string
|
|
29
|
-
active?: boolean
|
|
30
|
-
onClick?: () => void
|
|
31
|
-
/** Feature id for the access gate. When set, `useModuleNavAccess` (in
|
|
32
|
-
* @fayz-ai/saas) resolves this link's role (hide) and plan (Crown) state. */
|
|
33
|
-
feature?: string
|
|
34
|
-
/** Freemium discovery: the user's role allows this link but their plan does
|
|
35
|
-
* not entitle `feature`. Renders a small Crown badge (same vocabulary as the
|
|
36
|
-
* Sidebar top-level nav); the click still navigates and the gated CONTENT
|
|
37
|
-
* shows the UpgradePrompt. Typically set by `useModuleNavAccess`. */
|
|
38
|
-
premium?: boolean
|
|
39
|
-
children?: ModuleNavItem[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// PremiumBadge — the small Crown affordance shared by every module-nav renderer
|
|
44
|
-
// (rail, mobile tabs, GHL header). Same vocabulary as the Sidebar top-level nav
|
|
45
|
-
// so a plan-gated sub-module link reads identically wherever it appears.
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
function PremiumBadge({ className }: { className?: string }) {
|
|
49
|
-
const t = useTranslation()
|
|
50
|
-
const tip = t('upgrade.badgeTooltip')
|
|
51
|
-
return (
|
|
52
|
-
<span title={tip} aria-label={tip} className={cn('inline-flex shrink-0', className)}>
|
|
53
|
-
<Crown className="h-3.5 w-3.5 text-amber-500" aria-hidden />
|
|
54
|
-
</span>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface ModulePageProps {
|
|
59
|
-
title: string
|
|
60
|
-
subtitle?: string
|
|
61
|
-
nav: ModuleNavItem[]
|
|
62
|
-
children: React.ReactNode
|
|
63
|
-
className?: string
|
|
64
|
-
/** Optional action element rendered next to the title (e.g. settings gear) */
|
|
65
|
-
headerAction?: React.ReactNode
|
|
66
|
-
/** Show the title/subtitle header. Default: true */
|
|
67
|
-
showHeader?: boolean
|
|
68
|
-
/** Optional content pinned to the bottom of the sidebar (e.g. "Powered by X") */
|
|
69
|
-
sidebarFooter?: React.ReactNode
|
|
70
|
-
/** How the module-internal nav renders. Falls back to the ModuleLayout context
|
|
71
|
-
* (set by the app shell from its layout), then 'rail'. */
|
|
72
|
-
navVariant?: ModuleNavVariant
|
|
73
|
-
/** Active sub-view id — drives the SDK page transition when it changes.
|
|
74
|
-
* Plugins pass their `useModuleNavigation().view`; they no longer animate themselves. */
|
|
75
|
-
viewKey?: string
|
|
76
|
-
/** Navigation direction for the transition (from `useModuleNavigation`). */
|
|
77
|
-
direction?: 'forward' | 'back'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// ModuleLayout context — lets the app shell decide how every module's internal
|
|
82
|
-
// navigation renders ('rail' for topbar products, 'tabs' for sidebar/GHL-style
|
|
83
|
-
// products), so plugins stay agnostic and products feel distinct.
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
export type ModuleNavVariant = 'rail' | 'tabs'
|
|
87
|
-
|
|
88
|
-
const ModuleLayoutContext = React.createContext<ModuleNavVariant>('rail')
|
|
89
|
-
|
|
90
|
-
export function ModuleLayoutProvider({ variant, children }: { variant: ModuleNavVariant; children: React.ReactNode }) {
|
|
91
|
-
return <ModuleLayoutContext.Provider value={variant}>{children}</ModuleLayoutContext.Provider>
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function useModuleLayout(): ModuleNavVariant {
|
|
95
|
-
return React.useContext(ModuleLayoutContext)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function NavItem({ item }: { item: ModuleNavItem }) {
|
|
99
|
-
const [expanded, setExpanded] = React.useState(true)
|
|
100
|
-
const Icon = item.icon ? (ICON_MAP[item.icon] ?? null) : null
|
|
101
|
-
const hasChildren = item.children && item.children.length > 0
|
|
102
|
-
const hasActiveChild = hasChildren && item.children!.some((c) => c.active)
|
|
103
|
-
const isActive = item.active || hasActiveChild
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div>
|
|
107
|
-
<button
|
|
108
|
-
type="button"
|
|
109
|
-
onClick={() => hasChildren ? setExpanded(!expanded) : item.onClick?.()}
|
|
110
|
-
className={cn(
|
|
111
|
-
'flex items-center gap-2 w-full rounded-md px-2.5 py-1.5 text-sm transition-colors',
|
|
112
|
-
isActive
|
|
113
|
-
? 'bg-primary/10 text-primary font-medium'
|
|
114
|
-
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
115
|
-
)}
|
|
116
|
-
>
|
|
117
|
-
{Icon && <Icon className="h-4 w-4 shrink-0" />}
|
|
118
|
-
<span className="flex-1 text-left truncate">{item.label}</span>
|
|
119
|
-
{item.premium && <PremiumBadge className="ml-1" />}
|
|
120
|
-
{hasChildren && (
|
|
121
|
-
expanded
|
|
122
|
-
? <ChevronDown className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
|
123
|
-
: <ChevronRight className="h-3.5 w-3.5 shrink-0 opacity-50" />
|
|
124
|
-
)}
|
|
125
|
-
</button>
|
|
126
|
-
{hasChildren && expanded && (
|
|
127
|
-
<div className="ml-4 mt-0.5 space-y-0.5 border-l pl-2">
|
|
128
|
-
{item.children!.map((child) => (
|
|
129
|
-
<button
|
|
130
|
-
key={child.id}
|
|
131
|
-
type="button"
|
|
132
|
-
onClick={child.onClick}
|
|
133
|
-
className={cn(
|
|
134
|
-
'flex items-center w-full rounded-md px-2.5 py-1 text-xs transition-colors',
|
|
135
|
-
child.active
|
|
136
|
-
? 'text-primary font-medium bg-primary/5'
|
|
137
|
-
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
138
|
-
)}
|
|
139
|
-
>
|
|
140
|
-
<span className="flex-1 text-left truncate">{child.label}</span>
|
|
141
|
-
{child.premium && <PremiumBadge className="ml-1" />}
|
|
142
|
-
</button>
|
|
143
|
-
))}
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Horizontal scrollable tabs for mobile — with expandable children */
|
|
151
|
-
function MobileTabs({ nav }: { nav: ModuleNavItem[] }) {
|
|
152
|
-
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
153
|
-
const [expandedId, setExpandedId] = React.useState<string | null>(null)
|
|
154
|
-
|
|
155
|
-
// Auto-scroll to active tab on mount
|
|
156
|
-
React.useEffect(() => {
|
|
157
|
-
const container = scrollRef.current
|
|
158
|
-
if (!container) return
|
|
159
|
-
const activeEl = container.querySelector('[data-active="true"]') as HTMLElement
|
|
160
|
-
if (activeEl) {
|
|
161
|
-
const left = activeEl.offsetLeft - container.offsetWidth / 2 + activeEl.offsetWidth / 2
|
|
162
|
-
container.scrollTo({ left: Math.max(0, left), behavior: 'smooth' })
|
|
163
|
-
}
|
|
164
|
-
}, [nav])
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div className="md:hidden -mx-1 mb-4 no-print">
|
|
168
|
-
<div ref={scrollRef} className="flex gap-1 px-1 pb-2 overflow-x-auto scrollbar-none">
|
|
169
|
-
{nav.map((item) => {
|
|
170
|
-
const Icon = item.icon ? (ICON_MAP[item.icon] ?? null) : null
|
|
171
|
-
const hasChildren = item.children && item.children.length > 0
|
|
172
|
-
const isActive = item.active || item.children?.some((c) => c.active)
|
|
173
|
-
const isExpanded = expandedId === item.id
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<button
|
|
177
|
-
key={item.id}
|
|
178
|
-
type="button"
|
|
179
|
-
data-active={isActive || undefined}
|
|
180
|
-
onClick={() => {
|
|
181
|
-
if (hasChildren) {
|
|
182
|
-
setExpandedId(isExpanded ? null : item.id)
|
|
183
|
-
} else {
|
|
184
|
-
item.onClick?.()
|
|
185
|
-
setExpandedId(null)
|
|
186
|
-
}
|
|
187
|
-
}}
|
|
188
|
-
className={cn(
|
|
189
|
-
'flex items-center gap-1.5 whitespace-nowrap rounded-full px-3 py-1.5 text-xs font-medium transition-colors shrink-0',
|
|
190
|
-
isActive
|
|
191
|
-
? 'bg-primary text-primary-foreground'
|
|
192
|
-
: 'bg-muted text-muted-foreground hover:text-foreground'
|
|
193
|
-
)}
|
|
194
|
-
>
|
|
195
|
-
{Icon && <Icon className="h-3 w-3" />}
|
|
196
|
-
{item.label}
|
|
197
|
-
{item.premium && <PremiumBadge />}
|
|
198
|
-
{hasChildren && <ChevronDown className={cn('h-2.5 w-2.5 transition-transform', isExpanded && 'rotate-180')} />}
|
|
199
|
-
</button>
|
|
200
|
-
)
|
|
201
|
-
})}
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
{/* Expanded children dropdown */}
|
|
205
|
-
{expandedId && (() => {
|
|
206
|
-
const item = nav.find((n) => n.id === expandedId)
|
|
207
|
-
if (!item?.children) return null
|
|
208
|
-
return (
|
|
209
|
-
<div className="flex gap-1 px-2 pb-2 animate-in fade-in-0 slide-in-from-top-1">
|
|
210
|
-
{item.children.map((child) => (
|
|
211
|
-
<button
|
|
212
|
-
key={child.id}
|
|
213
|
-
type="button"
|
|
214
|
-
onClick={() => { child.onClick?.(); setExpandedId(null) }}
|
|
215
|
-
className={cn(
|
|
216
|
-
'whitespace-nowrap rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors shrink-0',
|
|
217
|
-
child.active
|
|
218
|
-
? 'bg-foreground/10 text-foreground'
|
|
219
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
220
|
-
)}
|
|
221
|
-
>
|
|
222
|
-
{child.label}
|
|
223
|
-
{child.premium && <PremiumBadge />}
|
|
224
|
-
</button>
|
|
225
|
-
))}
|
|
226
|
-
</div>
|
|
227
|
-
)
|
|
228
|
-
})()}
|
|
229
|
-
</div>
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
// SubpageHeader
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
// Back-button style — the SINGLE, app-wide affordance for returning from a
|
|
239
|
-
// detail/subpage. Set once by the product via <BackStyleProvider>, NOT chosen
|
|
240
|
-
// per-plugin (mirrors NavTransitionProvider for page transitions). Plugins just
|
|
241
|
-
// pass `onBack` + `parentLabel` to SubpageHeader and inherit whatever style the
|
|
242
|
-
// app picked:
|
|
243
|
-
// 'link' → subtle "← Back to {parent}" above the title (default)
|
|
244
|
-
// 'breadcrumb' → "{parent} › Title"
|
|
245
|
-
// 'icon' → square chevron button beside the title
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
export type BackButtonStyle = 'link' | 'breadcrumb' | 'icon'
|
|
249
|
-
|
|
250
|
-
const BackStyleContext = React.createContext<BackButtonStyle>('link')
|
|
251
|
-
|
|
252
|
-
export function BackStyleProvider({ style, children }: { style: BackButtonStyle; children: React.ReactNode }) {
|
|
253
|
-
return <BackStyleContext.Provider value={style}>{children}</BackStyleContext.Provider>
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function useBackStyle(): BackButtonStyle {
|
|
257
|
-
return React.useContext(BackStyleContext)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export interface SubpageHeaderProps {
|
|
261
|
-
/** Page title */
|
|
262
|
-
title: string
|
|
263
|
-
/** Short description */
|
|
264
|
-
subtitle?: string
|
|
265
|
-
/** Lucide icon name */
|
|
266
|
-
icon?: string
|
|
267
|
-
/** Back button handler */
|
|
268
|
-
onBack?: () => void
|
|
269
|
-
/** Parent label (e.g. "Invoices") — used by every back style ("Back to Invoices", breadcrumb root, etc.) */
|
|
270
|
-
parentLabel?: string
|
|
271
|
-
/** Actions rendered on the right */
|
|
272
|
-
actions?: React.ReactNode
|
|
273
|
-
/** Override the app-wide back style for this one header. Rarely needed. */
|
|
274
|
-
backStyle?: BackButtonStyle
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function SubpageHeader({ title, subtitle, icon, onBack, parentLabel, actions, backStyle }: SubpageHeaderProps) {
|
|
278
|
-
const t = useTranslation()
|
|
279
|
-
const appStyle = useBackStyle()
|
|
280
|
-
|
|
281
|
-
// Defer to the global navigation history when the user came from another
|
|
282
|
-
// module, so the back link points at the actual previous page. Within the
|
|
283
|
-
// same module the page's own onBack/parentLabel pass through unchanged.
|
|
284
|
-
const { onBack: effectiveOnBack, parentLabel: effectiveParentLabel } = useCrossModuleBack(onBack, parentLabel)
|
|
285
|
-
|
|
286
|
-
// Wire this page's back action into the app-wide Escape key (no per-plugin code).
|
|
287
|
-
useBackHandler(effectiveOnBack)
|
|
288
|
-
const Icon = icon ? (ICON_MAP[icon] ?? null) : null
|
|
289
|
-
// Breadcrumb/link need a parent label; without one we fall back to the icon button.
|
|
290
|
-
const style: BackButtonStyle = !effectiveOnBack ? 'icon' : (backStyle ?? appStyle)
|
|
291
|
-
const effectiveStyle: BackButtonStyle = style !== 'icon' && !effectiveParentLabel ? 'icon' : style
|
|
292
|
-
|
|
293
|
-
const titleBlock = (
|
|
294
|
-
<div className="flex items-center justify-between gap-4">
|
|
295
|
-
<div className="flex items-center gap-3">
|
|
296
|
-
{effectiveStyle === 'icon' && effectiveOnBack && (
|
|
297
|
-
<button
|
|
298
|
-
onClick={effectiveOnBack}
|
|
299
|
-
className="flex h-8 w-8 items-center justify-center rounded-lg border hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors"
|
|
300
|
-
>
|
|
301
|
-
<ChevronLeft className="h-4 w-4" />
|
|
302
|
-
</button>
|
|
303
|
-
)}
|
|
304
|
-
{Icon && (
|
|
305
|
-
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-muted">
|
|
306
|
-
<Icon className="h-5 w-5 text-muted-foreground" />
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
309
|
-
<div>
|
|
310
|
-
<h2 className="text-xl font-bold">{title}</h2>
|
|
311
|
-
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
312
|
-
</div>
|
|
313
|
-
</div>
|
|
314
|
-
{actions && <div className="no-print">{actions}</div>}
|
|
315
|
-
</div>
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
if (effectiveStyle === 'breadcrumb') {
|
|
319
|
-
return (
|
|
320
|
-
<div className="space-y-4 mb-6">
|
|
321
|
-
<div className="flex items-center justify-between gap-4">
|
|
322
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
323
|
-
<button onClick={effectiveOnBack} className="hover:text-foreground transition-colors">{effectiveParentLabel}</button>
|
|
324
|
-
<ChevronRight className="h-3.5 w-3.5" />
|
|
325
|
-
<span className="text-foreground font-medium">{title}</span>
|
|
326
|
-
</div>
|
|
327
|
-
{actions && <div className="no-print">{actions}</div>}
|
|
328
|
-
</div>
|
|
329
|
-
{subtitle && <p className="text-sm text-muted-foreground -mt-2">{subtitle}</p>}
|
|
330
|
-
</div>
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (effectiveStyle === 'link') {
|
|
335
|
-
return (
|
|
336
|
-
<div className="space-y-3 mb-6">
|
|
337
|
-
<button
|
|
338
|
-
onClick={effectiveOnBack}
|
|
339
|
-
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
340
|
-
>
|
|
341
|
-
<ChevronLeft className="h-4 w-4" />
|
|
342
|
-
{t('crud.detail.backTo', { entities: effectiveParentLabel as string })}
|
|
343
|
-
</button>
|
|
344
|
-
{titleBlock}
|
|
345
|
-
</div>
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// icon (default fallback)
|
|
350
|
-
return <div className="space-y-4 mb-6">{titleBlock}</div>
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
// ModuleHeader — GoHighLevel-style module header (title + horizontal sub-nav),
|
|
355
|
-
// portalled into the shell's top-of-page header slot. Active tab's children
|
|
356
|
-
// render as a secondary pill row. Falls back to inline if no slot is present.
|
|
357
|
-
// ---------------------------------------------------------------------------
|
|
358
|
-
|
|
359
|
-
function ModuleHeader({ nav }: { nav: ModuleNavItem[] }) {
|
|
360
|
-
const slot = useModuleHeaderSlot()
|
|
361
|
-
|
|
362
|
-
// Tabs are flat: a parent with children navigates straight to its default
|
|
363
|
-
// "list" view (sub-actions like "New" live as a button inside that list).
|
|
364
|
-
function defaultChild(item: ModuleNavItem) {
|
|
365
|
-
const kids = item.children ?? []
|
|
366
|
-
return kids.find((c) => /list/i.test(c.id) || /^list$/i.test(c.label)) ?? kids[kids.length - 1]
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const content = (
|
|
370
|
-
<div className="flex w-full min-w-0 items-center gap-4">
|
|
371
|
-
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto scrollbar-none">
|
|
372
|
-
{nav.map((item) => {
|
|
373
|
-
const Icon = item.icon ? (ICON_MAP[item.icon] ?? null) : null
|
|
374
|
-
const hasChildren = item.children && item.children.length > 0
|
|
375
|
-
const isActive = item.active || item.children?.some((c) => c.active)
|
|
376
|
-
return (
|
|
377
|
-
<button
|
|
378
|
-
key={item.id}
|
|
379
|
-
type="button"
|
|
380
|
-
onClick={() => (hasChildren ? defaultChild(item)?.onClick?.() : item.onClick?.())}
|
|
381
|
-
className={cn(
|
|
382
|
-
'flex shrink-0 items-center gap-1.5 whitespace-nowrap border-b-2 px-2.5 py-1.5 text-sm font-medium transition-colors',
|
|
383
|
-
isActive
|
|
384
|
-
? 'border-primary text-foreground'
|
|
385
|
-
: 'border-transparent text-muted-foreground hover:text-foreground',
|
|
386
|
-
)}
|
|
387
|
-
>
|
|
388
|
-
{Icon && <Icon className="h-4 w-4" />}
|
|
389
|
-
{item.label}
|
|
390
|
-
{item.premium && <PremiumBadge />}
|
|
391
|
-
</button>
|
|
392
|
-
)
|
|
393
|
-
})}
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
if (slot) return createPortal(content, slot)
|
|
399
|
-
// Fallback: render inline above content when no shell header slot exists.
|
|
400
|
-
return <div className="mb-5 border-b border-border pb-2">{content}</div>
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ---------------------------------------------------------------------------
|
|
404
|
-
// ModulePage
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
|
|
407
|
-
export function ModulePage({ title, subtitle, nav, children, className, headerAction, showHeader = true, sidebarFooter, navVariant, viewKey, direction }: ModulePageProps) {
|
|
408
|
-
const ctxVariant = useModuleLayout()
|
|
409
|
-
const variant = navVariant ?? ctxVariant
|
|
410
|
-
const transitionKey = viewKey ?? 'view'
|
|
411
|
-
|
|
412
|
-
// Tabs variant — the module sub-nav renders in the shell's top-of-page header
|
|
413
|
-
// container and actions in its right slot (GoHighLevel style); the content
|
|
414
|
-
// area holds only the view (the shell owns the page title).
|
|
415
|
-
if (variant === 'tabs') {
|
|
416
|
-
return (
|
|
417
|
-
<>
|
|
418
|
-
<ModuleHeader nav={nav} />
|
|
419
|
-
{/* Header actions belong to the module overview only; internal sub-views
|
|
420
|
-
(showHeader=false) carry their own actions. */}
|
|
421
|
-
{showHeader && headerAction && <PageHeaderActions>{headerAction}</PageHeaderActions>}
|
|
422
|
-
<PageTransition transitionKey={transitionKey} direction={direction} className={cn('min-w-0', className)}>
|
|
423
|
-
{children}
|
|
424
|
-
</PageTransition>
|
|
425
|
-
</>
|
|
426
|
-
)
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Rail variant (default) — left sub-nav column.
|
|
430
|
-
return (
|
|
431
|
-
<div className={cn('flex gap-6 -mt-2', className)}>
|
|
432
|
-
{/* Side navigation — hidden on mobile + print */}
|
|
433
|
-
<div className="w-48 shrink-0 hidden md:block no-print">
|
|
434
|
-
<div className={cn(sidebarFooter && 'sticky top-4 flex flex-col')} style={sidebarFooter ? { height: 'calc(100vh - 8rem)' } : undefined}>
|
|
435
|
-
<div>
|
|
436
|
-
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2.5 mb-2">{title}</p>
|
|
437
|
-
<nav className="space-y-0.5">
|
|
438
|
-
{nav.map((item) => (
|
|
439
|
-
<NavItem key={item.id} item={item} />
|
|
440
|
-
))}
|
|
441
|
-
</nav>
|
|
442
|
-
</div>
|
|
443
|
-
{sidebarFooter && <div className="mt-auto">{sidebarFooter}</div>}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
|
|
447
|
-
{/* Content */}
|
|
448
|
-
<div className="flex-1 min-w-0">
|
|
449
|
-
{showHeader && (
|
|
450
|
-
<div className="mb-6 flex items-start justify-between gap-3">
|
|
451
|
-
<div>
|
|
452
|
-
<h1 className="text-2xl font-bold">{title}</h1>
|
|
453
|
-
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
454
|
-
</div>
|
|
455
|
-
{headerAction}
|
|
456
|
-
</div>
|
|
457
|
-
)}
|
|
458
|
-
{/* Mobile tabs */}
|
|
459
|
-
<MobileTabs nav={nav} />
|
|
460
|
-
<PageTransition transitionKey={transitionKey} direction={direction}>
|
|
461
|
-
{children}
|
|
462
|
-
</PageTransition>
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
)
|
|
466
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { cn } from '../utils/cn'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// PageTransition — the single, SDK-owned animation applied to every nested
|
|
6
|
-
// navigation (route changes in the shell + module-internal view changes).
|
|
7
|
-
// Plugins never implement transitions themselves; they render content and the
|
|
8
|
-
// shell/ModulePage wrap it in <PageTransition>. The style is provided app-wide
|
|
9
|
-
// via NavTransitionProvider so different products/repos can ship a different
|
|
10
|
-
// feel (slide | fade | none | future custom) without touching plugins.
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export type NavTransition = 'slide' | 'fade' | 'none'
|
|
14
|
-
|
|
15
|
-
const NavTransitionContext = React.createContext<NavTransition>('slide')
|
|
16
|
-
|
|
17
|
-
export function NavTransitionProvider({ value, children }: { value: NavTransition; children: React.ReactNode }) {
|
|
18
|
-
return <NavTransitionContext.Provider value={value}>{children}</NavTransitionContext.Provider>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function useNavTransition(): NavTransition {
|
|
22
|
-
return React.useContext(NavTransitionContext)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveTransitionClass(transition: NavTransition, direction?: 'forward' | 'back'): string {
|
|
26
|
-
if (transition === 'none') return ''
|
|
27
|
-
if (transition === 'fade') return 'saas-page-enter'
|
|
28
|
-
// slide (default) — direction-aware, falls back to a plain enter.
|
|
29
|
-
if (direction === 'back') return 'saas-nav-back'
|
|
30
|
-
if (direction === 'forward') return 'saas-nav-forward'
|
|
31
|
-
return 'saas-page-enter'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Wraps content and re-fires the active transition whenever `transitionKey`
|
|
36
|
-
* changes (the key swap forces the node to remount, restarting the CSS
|
|
37
|
-
* animation). Direction is optional and only used by the 'slide' transition.
|
|
38
|
-
*/
|
|
39
|
-
export function PageTransition({
|
|
40
|
-
transitionKey,
|
|
41
|
-
direction,
|
|
42
|
-
className,
|
|
43
|
-
children,
|
|
44
|
-
}: {
|
|
45
|
-
transitionKey: string | number
|
|
46
|
-
direction?: 'forward' | 'back'
|
|
47
|
-
className?: string
|
|
48
|
-
children: React.ReactNode
|
|
49
|
-
}) {
|
|
50
|
-
const transition = useNavTransition()
|
|
51
|
-
const animClass = resolveTransitionClass(transition, direction)
|
|
52
|
-
return (
|
|
53
|
-
<div key={transitionKey} className={cn(animClass, className)}>
|
|
54
|
-
{children}
|
|
55
|
-
</div>
|
|
56
|
-
)
|
|
57
|
-
}
|