@open-mercato/ui 0.6.2-develop.3372.1.de9a169973 → 0.6.2-develop.3380.1.bb14509041
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/ai/AiIcon.js +1 -1
- package/dist/ai/AiIcon.js.map +2 -2
- package/dist/backend/AppShell.js +223 -153
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +101 -79
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/notifications/NotificationCountBadge.js +1 -1
- package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +67 -50
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/backend/notifications/NotificationPanel.js +108 -85
- package/dist/backend/notifications/NotificationPanel.js.map +2 -2
- package/dist/primitives/breadcrumb.js +161 -0
- package/dist/primitives/breadcrumb.js.map +7 -0
- package/dist/primitives/sheet.js +114 -0
- package/dist/primitives/sheet.js.map +7 -0
- package/package.json +3 -3
- package/src/ai/AiIcon.tsx +1 -1
- package/src/backend/AppShell.tsx +151 -49
- package/src/backend/ProfileDropdown.tsx +115 -88
- package/src/backend/__tests__/AppShell.test.tsx +8 -0
- package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
- package/src/backend/notifications/NotificationItem.tsx +97 -104
- package/src/backend/notifications/NotificationPanel.tsx +126 -102
- package/src/primitives/__tests__/breadcrumb.test.tsx +298 -0
- package/src/primitives/__tests__/sheet.test.tsx +152 -0
- package/src/primitives/breadcrumb.tsx +185 -0
- package/src/primitives/sheet.tsx +165 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
Sheet,
|
|
9
|
+
SheetClose,
|
|
10
|
+
SheetContent,
|
|
11
|
+
SheetDescription,
|
|
12
|
+
SheetFooter,
|
|
13
|
+
SheetHeader,
|
|
14
|
+
SheetTitle,
|
|
15
|
+
SheetTrigger,
|
|
16
|
+
} from '../sheet'
|
|
17
|
+
|
|
18
|
+
function renderControlled(
|
|
19
|
+
initial = false,
|
|
20
|
+
contentProps: Partial<React.ComponentProps<typeof SheetContent>> = {},
|
|
21
|
+
) {
|
|
22
|
+
function Harness() {
|
|
23
|
+
const [open, setOpen] = React.useState(initial)
|
|
24
|
+
return (
|
|
25
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
26
|
+
<SheetTrigger asChild>
|
|
27
|
+
<button type="button">Open</button>
|
|
28
|
+
</SheetTrigger>
|
|
29
|
+
<SheetContent {...contentProps}>
|
|
30
|
+
<SheetHeader>
|
|
31
|
+
<SheetTitle>Notifications</SheetTitle>
|
|
32
|
+
<SheetDescription>Recent activity from your team.</SheetDescription>
|
|
33
|
+
</SheetHeader>
|
|
34
|
+
<div data-testid="sheet-body">body</div>
|
|
35
|
+
<SheetFooter>
|
|
36
|
+
<SheetClose asChild>
|
|
37
|
+
<button type="button" data-testid="explicit-close">
|
|
38
|
+
Close
|
|
39
|
+
</button>
|
|
40
|
+
</SheetClose>
|
|
41
|
+
</SheetFooter>
|
|
42
|
+
</SheetContent>
|
|
43
|
+
</Sheet>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return render(<Harness />)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('Sheet primitive', () => {
|
|
50
|
+
it('does not render content when closed', () => {
|
|
51
|
+
renderControlled(false)
|
|
52
|
+
expect(screen.queryByText('Notifications')).not.toBeInTheDocument()
|
|
53
|
+
expect(screen.queryByTestId('sheet-body')).not.toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('opens when the trigger is clicked', () => {
|
|
57
|
+
renderControlled(false)
|
|
58
|
+
fireEvent.click(screen.getByText('Open'))
|
|
59
|
+
expect(screen.getByText('Notifications')).toBeInTheDocument()
|
|
60
|
+
expect(screen.getByTestId('sheet-body')).toBeInTheDocument()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('renders content immediately when controlled open=true', () => {
|
|
64
|
+
renderControlled(true)
|
|
65
|
+
expect(screen.getByText('Notifications')).toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('renders a built-in close X with the default closeLabel', () => {
|
|
69
|
+
renderControlled(true)
|
|
70
|
+
expect(screen.getByLabelText('Close')).toBeInTheDocument()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('respects a custom closeLabel', () => {
|
|
74
|
+
renderControlled(true, { closeLabel: 'Close notifications' })
|
|
75
|
+
expect(screen.getByLabelText('Close notifications')).toBeInTheDocument()
|
|
76
|
+
expect(screen.queryByLabelText('Close')).not.toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('omits the built-in close when hideClose is set', () => {
|
|
80
|
+
renderControlled(true, { hideClose: true, closeLabel: 'Close notifications' })
|
|
81
|
+
expect(screen.queryByLabelText('Close notifications')).not.toBeInTheDocument()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('closes when the built-in close X is clicked', () => {
|
|
85
|
+
renderControlled(true)
|
|
86
|
+
fireEvent.click(screen.getByLabelText('Close'))
|
|
87
|
+
expect(screen.queryByText('Notifications')).not.toBeInTheDocument()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('closes when an explicit SheetClose child is clicked', () => {
|
|
91
|
+
renderControlled(true)
|
|
92
|
+
fireEvent.click(screen.getByTestId('explicit-close'))
|
|
93
|
+
expect(screen.queryByText('Notifications')).not.toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('renders content with dialog role', () => {
|
|
97
|
+
renderControlled(true)
|
|
98
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('renders side="right" by default with slide-in-from-right animation class', () => {
|
|
102
|
+
renderControlled(true)
|
|
103
|
+
const dialog = screen.getByRole('dialog')
|
|
104
|
+
expect(dialog.className).toContain('right-0')
|
|
105
|
+
expect(dialog.className).toContain('data-[state=open]:slide-in-from-right')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('renders side="left" with corresponding slide direction', () => {
|
|
109
|
+
renderControlled(true, { side: 'left' })
|
|
110
|
+
const dialog = screen.getByRole('dialog')
|
|
111
|
+
expect(dialog.className).toContain('left-0')
|
|
112
|
+
expect(dialog.className).toContain('data-[state=open]:slide-in-from-left')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('side="left" uses separate top/bottom utilities (not invalid combined inset-y two-value) so the panel honors --topbar-height', () => {
|
|
116
|
+
// Regression: a previous implementation used `inset-y-[var(--topbar-height,0px)_0]`
|
|
117
|
+
// which compiles to `top: var(--topbar-height,0px) 0; bottom: var(--topbar-height,0px) 0;`
|
|
118
|
+
// — both rules invalid (top/bottom accept ONE length) and silently dropped by the
|
|
119
|
+
// browser. Assert the resolved class string carries two separate inset utilities.
|
|
120
|
+
renderControlled(true, { side: 'left' })
|
|
121
|
+
const dialog = screen.getByRole('dialog')
|
|
122
|
+
expect(dialog.className).toContain('top-[var(--topbar-height,0px)]')
|
|
123
|
+
expect(dialog.className).toContain('bottom-0')
|
|
124
|
+
expect(dialog.className).not.toMatch(/inset-y-\[[^\]]+_[^\]]+\]/)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('renders side="top" with corresponding slide direction', () => {
|
|
128
|
+
renderControlled(true, { side: 'top' })
|
|
129
|
+
const dialog = screen.getByRole('dialog')
|
|
130
|
+
expect(dialog.className).toContain('top-0')
|
|
131
|
+
expect(dialog.className).toContain('data-[state=open]:slide-in-from-top')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('renders side="bottom" with corresponding slide direction', () => {
|
|
135
|
+
renderControlled(true, { side: 'bottom' })
|
|
136
|
+
const dialog = screen.getByRole('dialog')
|
|
137
|
+
expect(dialog.className).toContain('bottom-0')
|
|
138
|
+
expect(dialog.className).toContain('data-[state=open]:slide-in-from-bottom')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('content references --topbar-height CSS variable so it sits below the AppShell sticky topbar', () => {
|
|
142
|
+
renderControlled(true)
|
|
143
|
+
const dialog = screen.getByRole('dialog')
|
|
144
|
+
expect(dialog.className).toContain('top-[var(--topbar-height,0px)]')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('merges custom className onto the content node', () => {
|
|
148
|
+
renderControlled(true, { className: 'custom-test-class' })
|
|
149
|
+
const dialog = screen.getByRole('dialog')
|
|
150
|
+
expect(dialog.className).toContain('custom-test-class')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
5
|
+
import { ChevronRight, MoreHorizontal } from 'lucide-react'
|
|
6
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
|
+
|
|
8
|
+
export type BreadcrumbDivider = 'slash' | 'arrow' | 'dot'
|
|
9
|
+
|
|
10
|
+
type BreadcrumbContextValue = {
|
|
11
|
+
divider: BreadcrumbDivider
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const BreadcrumbContext = React.createContext<BreadcrumbContextValue>({
|
|
15
|
+
divider: 'slash',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export type BreadcrumbProps = React.ComponentProps<'nav'> & {
|
|
19
|
+
divider?: BreadcrumbDivider
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Breadcrumb({
|
|
23
|
+
divider = 'slash',
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
...props
|
|
27
|
+
}: BreadcrumbProps) {
|
|
28
|
+
const contextValue = React.useMemo<BreadcrumbContextValue>(
|
|
29
|
+
() => ({ divider }),
|
|
30
|
+
[divider],
|
|
31
|
+
)
|
|
32
|
+
return (
|
|
33
|
+
<BreadcrumbContext.Provider value={contextValue}>
|
|
34
|
+
<nav
|
|
35
|
+
aria-label="Breadcrumb"
|
|
36
|
+
data-slot="breadcrumb"
|
|
37
|
+
data-divider={divider}
|
|
38
|
+
className={cn('min-w-0', className)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</nav>
|
|
43
|
+
</BreadcrumbContext.Provider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type BreadcrumbListProps = React.ComponentProps<'ol'>
|
|
48
|
+
|
|
49
|
+
export function BreadcrumbList({ className, children, ...props }: BreadcrumbListProps) {
|
|
50
|
+
if (React.Children.count(children) === 0) return null
|
|
51
|
+
return (
|
|
52
|
+
<ol
|
|
53
|
+
data-slot="breadcrumb-list"
|
|
54
|
+
className={cn(
|
|
55
|
+
'flex items-center gap-1.5 text-sm font-medium leading-5 tracking-tight min-w-0',
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</ol>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type BreadcrumbItemProps = React.ComponentProps<'li'>
|
|
66
|
+
|
|
67
|
+
export function BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) {
|
|
68
|
+
return (
|
|
69
|
+
<li
|
|
70
|
+
data-slot="breadcrumb-item"
|
|
71
|
+
className={cn('inline-flex items-center gap-1.5 min-w-0', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type BreadcrumbLinkProps = React.ComponentProps<'a'> & {
|
|
78
|
+
asChild?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function BreadcrumbLink({
|
|
82
|
+
asChild = false,
|
|
83
|
+
className,
|
|
84
|
+
children,
|
|
85
|
+
...props
|
|
86
|
+
}: BreadcrumbLinkProps) {
|
|
87
|
+
const Comp = asChild ? Slot : 'a'
|
|
88
|
+
return (
|
|
89
|
+
<Comp
|
|
90
|
+
data-slot="breadcrumb-link"
|
|
91
|
+
className={cn(
|
|
92
|
+
"inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:text-foreground rounded-sm focus-visible:shadow-focus [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</Comp>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type BreadcrumbPageProps = React.ComponentProps<'span'>
|
|
103
|
+
|
|
104
|
+
export function BreadcrumbPage({ className, ...props }: BreadcrumbPageProps) {
|
|
105
|
+
return (
|
|
106
|
+
<span
|
|
107
|
+
aria-current="page"
|
|
108
|
+
data-slot="breadcrumb-page"
|
|
109
|
+
className={cn(
|
|
110
|
+
"inline-block text-foreground truncate min-w-0 max-w-[45vw] md:max-w-[60vw] [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0 [&_svg]:inline-block [&_svg]:align-middle",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type BreadcrumbStaticProps = React.ComponentProps<'span'>
|
|
119
|
+
|
|
120
|
+
export function BreadcrumbStatic({ className, ...props }: BreadcrumbStaticProps) {
|
|
121
|
+
return (
|
|
122
|
+
<span
|
|
123
|
+
data-slot="breadcrumb-static"
|
|
124
|
+
className={cn(
|
|
125
|
+
"inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground select-none [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0",
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type BreadcrumbSeparatorProps = React.ComponentProps<'li'> & {
|
|
134
|
+
divider?: BreadcrumbDivider
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function BreadcrumbSeparator({
|
|
138
|
+
divider: dividerProp,
|
|
139
|
+
className,
|
|
140
|
+
children,
|
|
141
|
+
...props
|
|
142
|
+
}: BreadcrumbSeparatorProps) {
|
|
143
|
+
const { divider: contextDivider } = React.useContext(BreadcrumbContext)
|
|
144
|
+
const divider = dividerProp ?? contextDivider
|
|
145
|
+
return (
|
|
146
|
+
<li
|
|
147
|
+
role="presentation"
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
data-slot="breadcrumb-separator"
|
|
150
|
+
data-divider={divider}
|
|
151
|
+
className={cn(
|
|
152
|
+
"inline-flex items-center text-text-disabled select-none [&_svg:not([class*='size-'])]:size-5",
|
|
153
|
+
className,
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
{children ?? <DefaultSeparator divider={divider} />}
|
|
158
|
+
</li>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function DefaultSeparator({ divider }: { divider: BreadcrumbDivider }) {
|
|
163
|
+
if (divider === 'arrow') return <ChevronRight aria-hidden="true" />
|
|
164
|
+
if (divider === 'dot') return <span aria-hidden="true">·</span>
|
|
165
|
+
return <span aria-hidden="true">/</span>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type BreadcrumbEllipsisProps = React.ComponentProps<'span'>
|
|
169
|
+
|
|
170
|
+
export function BreadcrumbEllipsis({ className, children, ...props }: BreadcrumbEllipsisProps) {
|
|
171
|
+
return (
|
|
172
|
+
<span
|
|
173
|
+
role="presentation"
|
|
174
|
+
data-slot="breadcrumb-ellipsis"
|
|
175
|
+
className={cn(
|
|
176
|
+
'inline-flex size-5 items-center justify-center text-muted-foreground',
|
|
177
|
+
className,
|
|
178
|
+
)}
|
|
179
|
+
{...props}
|
|
180
|
+
>
|
|
181
|
+
{children ?? <MoreHorizontal aria-hidden="true" className="size-4" />}
|
|
182
|
+
<span className="sr-only">More</span>
|
|
183
|
+
</span>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
5
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
6
|
+
import { X } from 'lucide-react'
|
|
7
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
8
|
+
|
|
9
|
+
const Sheet = DialogPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const SheetTrigger = DialogPrimitive.Trigger
|
|
12
|
+
|
|
13
|
+
const SheetClose = DialogPrimitive.Close
|
|
14
|
+
|
|
15
|
+
const SheetPortal = DialogPrimitive.Portal
|
|
16
|
+
|
|
17
|
+
const SheetOverlay = React.forwardRef<
|
|
18
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
19
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
20
|
+
>(({ className, ...props }, ref) => (
|
|
21
|
+
<DialogPrimitive.Overlay
|
|
22
|
+
className={cn(
|
|
23
|
+
'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',
|
|
24
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
25
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
ref={ref}
|
|
30
|
+
/>
|
|
31
|
+
))
|
|
32
|
+
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
33
|
+
|
|
34
|
+
const sheetVariants = cva(
|
|
35
|
+
cn(
|
|
36
|
+
'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',
|
|
37
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
38
|
+
'data-[state=closed]:duration-200 data-[state=open]:duration-300',
|
|
39
|
+
),
|
|
40
|
+
{
|
|
41
|
+
variants: {
|
|
42
|
+
side: {
|
|
43
|
+
top:
|
|
44
|
+
'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
|
45
|
+
bottom:
|
|
46
|
+
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
|
47
|
+
left:
|
|
48
|
+
'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',
|
|
49
|
+
right:
|
|
50
|
+
'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
defaultVariants: {
|
|
54
|
+
side: 'right',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
|
|
60
|
+
VariantProps<typeof sheetVariants> & {
|
|
61
|
+
/** Hide the built-in close (X) button — useful when the consumer renders its own close affordance. */
|
|
62
|
+
hideClose?: boolean
|
|
63
|
+
/** Override the aria-label for the built-in close button. */
|
|
64
|
+
closeLabel?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SheetContent — side-anchored Radix Dialog wrapper.
|
|
69
|
+
*
|
|
70
|
+
* Topbar integration: the `left` and `right` variants anchor at
|
|
71
|
+
* `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above
|
|
72
|
+
* the panel. Consumers that render inside `AppShell` get the correct offset
|
|
73
|
+
* because the shell sets `--topbar-height` on the outer container. Surfaces
|
|
74
|
+
* outside the shell (portal modals, marketing pages, standalone embeds) keep
|
|
75
|
+
* the default `0px` fallback and align to the viewport top — that is the
|
|
76
|
+
* correct behavior when there is no topbar to clear.
|
|
77
|
+
*
|
|
78
|
+
* To opt into the offset from your own layout, set the CSS variable on any
|
|
79
|
+
* ancestor of `<SheetContent>`:
|
|
80
|
+
*
|
|
81
|
+
* ```css
|
|
82
|
+
* .my-shell { --topbar-height: 60px; }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
const SheetContent = React.forwardRef<
|
|
87
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
88
|
+
SheetContentProps
|
|
89
|
+
>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (
|
|
90
|
+
<SheetPortal>
|
|
91
|
+
<SheetOverlay />
|
|
92
|
+
<DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
|
93
|
+
{children}
|
|
94
|
+
{!hideClose ? (
|
|
95
|
+
<DialogPrimitive.Close
|
|
96
|
+
className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus"
|
|
97
|
+
aria-label={closeLabel}
|
|
98
|
+
>
|
|
99
|
+
<X className="size-4" aria-hidden="true" />
|
|
100
|
+
</DialogPrimitive.Close>
|
|
101
|
+
) : null}
|
|
102
|
+
</DialogPrimitive.Content>
|
|
103
|
+
</SheetPortal>
|
|
104
|
+
))
|
|
105
|
+
SheetContent.displayName = DialogPrimitive.Content.displayName
|
|
106
|
+
|
|
107
|
+
const SheetHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
108
|
+
({ className, ...props }, ref) => (
|
|
109
|
+
<div
|
|
110
|
+
ref={ref}
|
|
111
|
+
className={cn('flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left', className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
SheetHeader.displayName = 'SheetHeader'
|
|
117
|
+
|
|
118
|
+
const SheetFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
119
|
+
({ className, ...props }, ref) => (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cn('flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end', className)}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
SheetFooter.displayName = 'SheetFooter'
|
|
128
|
+
|
|
129
|
+
const SheetTitle = React.forwardRef<
|
|
130
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
131
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
132
|
+
>(({ className, ...props }, ref) => (
|
|
133
|
+
<DialogPrimitive.Title
|
|
134
|
+
ref={ref}
|
|
135
|
+
className={cn('text-base font-semibold text-foreground', className)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
))
|
|
139
|
+
SheetTitle.displayName = DialogPrimitive.Title.displayName
|
|
140
|
+
|
|
141
|
+
const SheetDescription = React.forwardRef<
|
|
142
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
143
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
144
|
+
>(({ className, ...props }, ref) => (
|
|
145
|
+
<DialogPrimitive.Description
|
|
146
|
+
ref={ref}
|
|
147
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
))
|
|
151
|
+
SheetDescription.displayName = DialogPrimitive.Description.displayName
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
Sheet,
|
|
155
|
+
SheetClose,
|
|
156
|
+
SheetContent,
|
|
157
|
+
SheetDescription,
|
|
158
|
+
SheetFooter,
|
|
159
|
+
SheetHeader,
|
|
160
|
+
SheetOverlay,
|
|
161
|
+
SheetPortal,
|
|
162
|
+
SheetTitle,
|
|
163
|
+
SheetTrigger,
|
|
164
|
+
sheetVariants,
|
|
165
|
+
}
|