@flamingo-stack/openframe-frontend-core 0.0.217 → 0.0.218
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-L6IBKPVM.js → chunk-EKBM4FHK.js} +2 -2
- package/dist/{chunk-SWZUZYWR.js → chunk-EWA2NFUR.js} +2 -2
- package/dist/{chunk-TYIBMDUZ.cjs → chunk-FZZBCRID.cjs} +7 -7
- package/dist/{chunk-TYIBMDUZ.cjs.map → chunk-FZZBCRID.cjs.map} +1 -1
- package/dist/{chunk-G2HHSZ3S.cjs → chunk-GE64T3JT.cjs} +9 -9
- package/dist/{chunk-G2HHSZ3S.cjs.map → chunk-GE64T3JT.cjs.map} +1 -1
- package/dist/{chunk-YWDC5BXM.cjs → chunk-L5RSJE2I.cjs} +1940 -915
- package/dist/chunk-L5RSJE2I.cjs.map +1 -0
- package/dist/{chunk-BVFRD34B.js → chunk-OHOUSDAY.js} +2 -2
- package/dist/{chunk-MVQ3OODK.cjs → chunk-S4SVD5JI.cjs} +9 -9
- package/dist/{chunk-MVQ3OODK.cjs.map → chunk-S4SVD5JI.cjs.map} +1 -1
- package/dist/{chunk-N5IKPYRL.js → chunk-SWIR5EB2.js} +2 -2
- package/dist/{chunk-6DCKL73F.cjs → chunk-TCJ5B2ZD.cjs} +24 -24
- package/dist/{chunk-6DCKL73F.cjs.map → chunk-TCJ5B2ZD.cjs.map} +1 -1
- package/dist/{chunk-ENBGG2K2.js → chunk-V5JY5RSY.js} +2954 -1929
- package/dist/chunk-V5JY5RSY.js.map +1 -0
- package/dist/components/chat/embeddable-chat.d.ts +13 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +104 -10
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-slash-commands.d.ts +6 -0
- package/dist/components/chat/hooks/use-slash-commands.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +81 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/index.cjs +73 -51
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +26 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts +7 -0
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/app-layout-drawer.d.ts +65 -0
- package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -0
- package/dist/components/navigation/app-layout.d.ts +9 -1
- package/dist/components/navigation/app-layout.d.ts.map +1 -1
- package/dist/components/navigation/header-mingo-button.d.ts +21 -0
- package/dist/components/navigation/header-mingo-button.d.ts.map +1 -0
- package/dist/components/navigation/index.cjs +24 -2
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +5 -1
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +23 -1
- package/dist/components/onboarding-guides/index.cjs +18 -18
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +80 -66
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +20 -6
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/index.cjs +2 -2
- package/dist/components/ui/index.js +1 -1
- package/dist/index.cjs +26 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +25 -1
- package/dist/utils/embed-authed-fetch.d.ts +80 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +70 -5
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +70 -6
- package/dist/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/chat/embeddable-chat.tsx +154 -37
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +601 -23
- package/src/components/chat/hooks/use-slash-commands.ts +10 -1
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +45 -0
- package/src/components/chat/hooks/use-unified-chat.ts +59 -0
- package/src/components/chat/types/unified-chat-state.types.ts +116 -0
- package/src/components/navigation/app-header.tsx +23 -0
- package/src/components/navigation/app-layout-drawer.tsx +620 -0
- package/src/components/navigation/app-layout.tsx +65 -26
- package/src/components/navigation/header-mingo-button.tsx +58 -0
- package/src/components/navigation/index.ts +17 -1
- package/src/components/tickets/hooks/use-ticket-engagements.ts +24 -4
- package/src/stories/AppLayoutDrawer.stories.tsx +228 -0
- package/src/utils/.embed-authed-fetch.md +7 -0
- package/src/utils/__tests__/embed-authed-fetch.test.ts +103 -1
- package/src/utils/embed-authed-fetch.ts +247 -7
- package/src/utils/index.ts +5 -1
- package/dist/chunk-ENBGG2K2.js.map +0 -1
- package/dist/chunk-YWDC5BXM.cjs.map +0 -1
- /package/dist/{chunk-L6IBKPVM.js.map → chunk-EKBM4FHK.js.map} +0 -0
- /package/dist/{chunk-SWZUZYWR.js.map → chunk-EWA2NFUR.js.map} +0 -0
- /package/dist/{chunk-BVFRD34B.js.map → chunk-OHOUSDAY.js.map} +0 -0
- /package/dist/{chunk-N5IKPYRL.js.map → chunk-SWIR5EB2.js.map} +0 -0
|
@@ -0,0 +1,620 @@
|
|
|
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
|
+
|
|
7
|
+
import { cn } from "../../utils/cn"
|
|
8
|
+
import {
|
|
9
|
+
DrawerBody,
|
|
10
|
+
DrawerDescription,
|
|
11
|
+
DrawerFooter,
|
|
12
|
+
DrawerHeader,
|
|
13
|
+
DrawerTitle,
|
|
14
|
+
type DrawerSide,
|
|
15
|
+
} from "../ui/drawer"
|
|
16
|
+
import { useAppLayoutDrawerContainer } from "./app-layout"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* AppLayoutDrawer is a Drawer variant that renders **inside** AppLayout's main
|
|
20
|
+
* content area instead of as a viewport-level overlay. Header and sidebar stay
|
|
21
|
+
* visible and interactive while it is open.
|
|
22
|
+
*
|
|
23
|
+
* Implementation differs from the standard Drawer in three ways:
|
|
24
|
+
* 1. `DialogPrimitive.Portal` targets the AppLayout container (provided via
|
|
25
|
+
* React Context) rather than `document.body`.
|
|
26
|
+
* 2. Positioning is `absolute` (clipped to the container) instead of `fixed`.
|
|
27
|
+
* 3. The Dialog is non-modal (`modal={false}`) — outside content is not
|
|
28
|
+
* inert, so header/sidebar interactions still work while the drawer is open.
|
|
29
|
+
*
|
|
30
|
+
* Caveat of non-modal mode: Radix's built-in `DialogPrimitive.Overlay` returns
|
|
31
|
+
* null (see @radix-ui/react-dialog source). We render our own overlay div as a
|
|
32
|
+
* sibling of `DialogPrimitive.Content` in the portal so it (a) provides the
|
|
33
|
+
* standard dim backdrop and (b) catches pointer events over the main area —
|
|
34
|
+
* otherwise clicks pass through to underlying buttons, causing a close-and-
|
|
35
|
+
* reopen flicker when the click target is also a trigger.
|
|
36
|
+
*
|
|
37
|
+
* Everything else (visual chrome, slide animation, resize handle, sub-components)
|
|
38
|
+
* matches the standard Drawer. Sub-components (header/title/body/footer) are
|
|
39
|
+
* re-exported aliases of the originals so styling stays in lockstep.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const DrawerOpenContext = React.createContext<boolean>(false)
|
|
43
|
+
|
|
44
|
+
interface AppLayoutDrawerRootProps
|
|
45
|
+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {}
|
|
46
|
+
|
|
47
|
+
const AppLayoutDrawerRoot = ({
|
|
48
|
+
open: openProp,
|
|
49
|
+
defaultOpen,
|
|
50
|
+
onOpenChange,
|
|
51
|
+
modal = false,
|
|
52
|
+
children,
|
|
53
|
+
...rest
|
|
54
|
+
}: AppLayoutDrawerRootProps) => {
|
|
55
|
+
// Shadow Dialog.Root's open state so descendants (specifically the overlay
|
|
56
|
+
// rendered inside Content) can drive their own `data-state` for animations.
|
|
57
|
+
// Radix doesn't expose its internal open state via a public context.
|
|
58
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false)
|
|
59
|
+
const isControlled = openProp !== undefined
|
|
60
|
+
const open = isControlled ? (openProp as boolean) : internalOpen
|
|
61
|
+
|
|
62
|
+
const handleOpenChange = React.useCallback(
|
|
63
|
+
(next: boolean) => {
|
|
64
|
+
if (!isControlled) setInternalOpen(next)
|
|
65
|
+
onOpenChange?.(next)
|
|
66
|
+
},
|
|
67
|
+
[isControlled, onOpenChange],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<DrawerOpenContext.Provider value={open}>
|
|
72
|
+
<DialogPrimitive.Root
|
|
73
|
+
{...rest}
|
|
74
|
+
open={open}
|
|
75
|
+
onOpenChange={handleOpenChange}
|
|
76
|
+
modal={modal}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</DialogPrimitive.Root>
|
|
80
|
+
</DrawerOpenContext.Provider>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
AppLayoutDrawerRoot.displayName = "AppLayoutDrawer"
|
|
84
|
+
|
|
85
|
+
const AppLayoutDrawerTrigger = DialogPrimitive.Trigger
|
|
86
|
+
const AppLayoutDrawerClose = DialogPrimitive.Close
|
|
87
|
+
|
|
88
|
+
const appLayoutDrawerVariants = cva(
|
|
89
|
+
"absolute z-[45] flex outline-none focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
|
|
90
|
+
{
|
|
91
|
+
variants: {
|
|
92
|
+
side: {
|
|
93
|
+
right:
|
|
94
|
+
"inset-y-0 right-0 items-center data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
95
|
+
left:
|
|
96
|
+
"inset-y-0 left-0 items-center data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
|
|
97
|
+
top:
|
|
98
|
+
"inset-x-0 top-0 justify-center data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
99
|
+
bottom:
|
|
100
|
+
"inset-x-0 bottom-0 justify-center data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
101
|
+
},
|
|
102
|
+
flush: {
|
|
103
|
+
false: "",
|
|
104
|
+
true: "",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
compoundVariants: [
|
|
108
|
+
{ side: "right", flush: false, class: "pr-4 py-4" },
|
|
109
|
+
{ side: "left", flush: false, class: "pl-4 py-4" },
|
|
110
|
+
{ side: "top", flush: false, class: "pt-4 px-4" },
|
|
111
|
+
{ side: "bottom", flush: false, class: "pb-4 px-4" },
|
|
112
|
+
// flush=true: keep wrapper padding on desktop (uniform 16px gap so the
|
|
113
|
+
// panel floats), drop on mobile for full-bleed — matches base Drawer.
|
|
114
|
+
{ side: "right", flush: true, class: "md:pr-4 md:py-4" },
|
|
115
|
+
{ side: "left", flush: true, class: "md:pl-4 md:py-4" },
|
|
116
|
+
{ side: "top", flush: true, class: "md:pt-4 md:px-4" },
|
|
117
|
+
{ side: "bottom", flush: true, class: "md:pb-4 md:px-4" },
|
|
118
|
+
],
|
|
119
|
+
defaultVariants: {
|
|
120
|
+
side: "right",
|
|
121
|
+
flush: false,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const appLayoutDrawerPanelVariants = cva(
|
|
127
|
+
"relative flex flex-col overflow-hidden bg-ods-card outline-none focus:outline-none focus-visible:outline-none",
|
|
128
|
+
{
|
|
129
|
+
variants: {
|
|
130
|
+
side: {
|
|
131
|
+
right: "h-full",
|
|
132
|
+
left: "h-full",
|
|
133
|
+
top: "w-full",
|
|
134
|
+
bottom: "w-full",
|
|
135
|
+
},
|
|
136
|
+
flush: {
|
|
137
|
+
false: "gap-4 rounded-md border border-ods-border p-4",
|
|
138
|
+
true: "",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
compoundVariants: [
|
|
142
|
+
{ side: "right", flush: true, class: "md:rounded-md md:border md:border-ods-border" },
|
|
143
|
+
{ side: "left", flush: true, class: "md:rounded-md md:border md:border-ods-border" },
|
|
144
|
+
{ side: "top", flush: true, class: "md:rounded-md md:border md:border-ods-border" },
|
|
145
|
+
{ side: "bottom", flush: true, class: "md:rounded-md md:border md:border-ods-border" },
|
|
146
|
+
],
|
|
147
|
+
defaultVariants: {
|
|
148
|
+
side: "right",
|
|
149
|
+
flush: false,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const HORIZONTAL_SIDES: ReadonlySet<DrawerSide> = new Set(["left", "right"])
|
|
155
|
+
|
|
156
|
+
function clamp(value: number, min: number, max: number): number {
|
|
157
|
+
if (max < min) return min
|
|
158
|
+
return Math.min(max, Math.max(min, value))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface UseContainedResizableSizeArgs {
|
|
162
|
+
enabled: boolean
|
|
163
|
+
isHorizontal: boolean
|
|
164
|
+
minSize: number
|
|
165
|
+
maxSize: number
|
|
166
|
+
defaultSize: number
|
|
167
|
+
storageKey?: string
|
|
168
|
+
container: HTMLElement | null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function useContainedResizableSize({
|
|
172
|
+
enabled,
|
|
173
|
+
isHorizontal,
|
|
174
|
+
minSize,
|
|
175
|
+
maxSize,
|
|
176
|
+
defaultSize,
|
|
177
|
+
storageKey,
|
|
178
|
+
container,
|
|
179
|
+
}: UseContainedResizableSizeArgs) {
|
|
180
|
+
const [available, setAvailable] = React.useState(0)
|
|
181
|
+
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (!enabled || !container) return
|
|
184
|
+
const update = () => {
|
|
185
|
+
setAvailable(isHorizontal ? container.clientWidth : container.clientHeight)
|
|
186
|
+
}
|
|
187
|
+
update()
|
|
188
|
+
const ro = new ResizeObserver(update)
|
|
189
|
+
ro.observe(container)
|
|
190
|
+
return () => ro.disconnect()
|
|
191
|
+
}, [enabled, container, isHorizontal])
|
|
192
|
+
|
|
193
|
+
const clampToContainer = React.useCallback(
|
|
194
|
+
(value: number) => {
|
|
195
|
+
// Reserve 32px (16px outside-edge padding from the wrapper + 16px
|
|
196
|
+
// matching gap on the inside edge) so the panel sits symmetrically
|
|
197
|
+
// inside the container. This also keeps the resize grip on-screen at
|
|
198
|
+
// maximum extent.
|
|
199
|
+
const effectiveMax = available > 0 ? Math.min(maxSize, available - 32) : maxSize
|
|
200
|
+
return clamp(value, minSize, Math.max(minSize, effectiveMax))
|
|
201
|
+
},
|
|
202
|
+
[available, minSize, maxSize],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const readInitial = React.useCallback(() => {
|
|
206
|
+
if (!enabled) return defaultSize
|
|
207
|
+
if (typeof window === "undefined") return defaultSize
|
|
208
|
+
if (!storageKey) return defaultSize
|
|
209
|
+
try {
|
|
210
|
+
const raw = window.localStorage.getItem(storageKey)
|
|
211
|
+
if (!raw) return defaultSize
|
|
212
|
+
const parsed = parseFloat(raw)
|
|
213
|
+
if (!Number.isFinite(parsed)) return defaultSize
|
|
214
|
+
return parsed
|
|
215
|
+
} catch {
|
|
216
|
+
return defaultSize
|
|
217
|
+
}
|
|
218
|
+
}, [enabled, storageKey, defaultSize])
|
|
219
|
+
|
|
220
|
+
const [size, setSizeRaw] = React.useState<number>(readInitial)
|
|
221
|
+
|
|
222
|
+
const setSize = React.useCallback(
|
|
223
|
+
(next: number) => setSizeRaw(clampToContainer(next)),
|
|
224
|
+
[clampToContainer],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
React.useEffect(() => {
|
|
228
|
+
if (!enabled || !storageKey || typeof window === "undefined") return
|
|
229
|
+
try {
|
|
230
|
+
window.localStorage.setItem(storageKey, String(Math.round(size)))
|
|
231
|
+
} catch {
|
|
232
|
+
// ignore quota / disabled-storage
|
|
233
|
+
}
|
|
234
|
+
}, [enabled, size, storageKey])
|
|
235
|
+
|
|
236
|
+
// Re-clamp the stored size whenever the container resizes so a previously
|
|
237
|
+
// saved size never overflows after the user shrinks the viewport.
|
|
238
|
+
React.useEffect(() => {
|
|
239
|
+
if (!enabled) return
|
|
240
|
+
setSizeRaw((prev) => clampToContainer(prev))
|
|
241
|
+
}, [enabled, clampToContainer])
|
|
242
|
+
|
|
243
|
+
return { size, setSize }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface AppLayoutDrawerResizeHandleProps {
|
|
247
|
+
side: DrawerSide
|
|
248
|
+
size: number
|
|
249
|
+
minSize: number
|
|
250
|
+
maxSize: number
|
|
251
|
+
onSize: (next: number) => void
|
|
252
|
+
ariaLabel?: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function AppLayoutDrawerResizeHandle({
|
|
256
|
+
side,
|
|
257
|
+
size,
|
|
258
|
+
minSize,
|
|
259
|
+
maxSize,
|
|
260
|
+
onSize,
|
|
261
|
+
ariaLabel,
|
|
262
|
+
}: AppLayoutDrawerResizeHandleProps) {
|
|
263
|
+
const isHorizontal = HORIZONTAL_SIDES.has(side)
|
|
264
|
+
const startRef = React.useRef<{ x: number; y: number; size: number } | null>(null)
|
|
265
|
+
|
|
266
|
+
const direction = side === "right" || side === "bottom" ? -1 : 1
|
|
267
|
+
|
|
268
|
+
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
269
|
+
if (e.button !== 0 && e.pointerType === "mouse") return
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
startRef.current = { x: e.clientX, y: e.clientY, size }
|
|
272
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
273
|
+
document.body.style.cursor = isHorizontal ? "col-resize" : "row-resize"
|
|
274
|
+
document.body.style.userSelect = "none"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
278
|
+
const start = startRef.current
|
|
279
|
+
if (!start) return
|
|
280
|
+
const delta = isHorizontal ? e.clientX - start.x : e.clientY - start.y
|
|
281
|
+
onSize(start.size + delta * direction)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const endDrag = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
285
|
+
if (!startRef.current) return
|
|
286
|
+
startRef.current = null
|
|
287
|
+
try {
|
|
288
|
+
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
289
|
+
} catch {
|
|
290
|
+
// ignore — pointer may already be released
|
|
291
|
+
}
|
|
292
|
+
document.body.style.cursor = ""
|
|
293
|
+
document.body.style.userSelect = ""
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
297
|
+
const step = e.shiftKey ? 40 : 16
|
|
298
|
+
if (isHorizontal) {
|
|
299
|
+
if (e.key === "ArrowLeft") {
|
|
300
|
+
e.preventDefault()
|
|
301
|
+
onSize(size + step * (side === "right" ? 1 : -1))
|
|
302
|
+
} else if (e.key === "ArrowRight") {
|
|
303
|
+
e.preventDefault()
|
|
304
|
+
onSize(size + step * (side === "right" ? -1 : 1))
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
if (e.key === "ArrowUp") {
|
|
308
|
+
e.preventDefault()
|
|
309
|
+
onSize(size + step * (side === "bottom" ? 1 : -1))
|
|
310
|
+
} else if (e.key === "ArrowDown") {
|
|
311
|
+
e.preventDefault()
|
|
312
|
+
onSize(size + step * (side === "bottom" ? -1 : 1))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (e.key === "Home") {
|
|
316
|
+
e.preventDefault()
|
|
317
|
+
onSize(minSize)
|
|
318
|
+
} else if (e.key === "End") {
|
|
319
|
+
e.preventDefault()
|
|
320
|
+
onSize(maxSize)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const trackPosition =
|
|
325
|
+
side === "right"
|
|
326
|
+
? "right-full top-4 bottom-4 w-3 items-center justify-end pr-1"
|
|
327
|
+
: side === "left"
|
|
328
|
+
? "left-full top-4 bottom-4 w-3 items-center justify-start pl-1"
|
|
329
|
+
: side === "bottom"
|
|
330
|
+
? "bottom-full left-4 right-4 h-3 justify-center items-end pb-1"
|
|
331
|
+
: "top-full left-4 right-4 h-3 justify-center items-start pt-1"
|
|
332
|
+
|
|
333
|
+
const cursorClass = isHorizontal ? "cursor-col-resize" : "cursor-row-resize"
|
|
334
|
+
const gripClass = isHorizontal ? "h-10 w-1" : "w-10 h-1"
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div
|
|
338
|
+
role="separator"
|
|
339
|
+
tabIndex={0}
|
|
340
|
+
aria-orientation={isHorizontal ? "vertical" : "horizontal"}
|
|
341
|
+
aria-valuenow={Math.round(size)}
|
|
342
|
+
aria-valuemin={minSize}
|
|
343
|
+
aria-valuemax={maxSize}
|
|
344
|
+
aria-label={
|
|
345
|
+
ariaLabel ?? (isHorizontal ? "Resize panel width" : "Resize panel height")
|
|
346
|
+
}
|
|
347
|
+
onPointerDown={handlePointerDown}
|
|
348
|
+
onPointerMove={handlePointerMove}
|
|
349
|
+
onPointerUp={endDrag}
|
|
350
|
+
onPointerCancel={endDrag}
|
|
351
|
+
onKeyDown={handleKeyDown}
|
|
352
|
+
className={cn(
|
|
353
|
+
"group absolute z-20 flex select-none touch-none",
|
|
354
|
+
"outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0",
|
|
355
|
+
trackPosition,
|
|
356
|
+
cursorClass,
|
|
357
|
+
)}
|
|
358
|
+
>
|
|
359
|
+
<div
|
|
360
|
+
aria-hidden
|
|
361
|
+
className={cn(
|
|
362
|
+
"rounded-full bg-ods-border",
|
|
363
|
+
gripClass,
|
|
364
|
+
)}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface AppLayoutDrawerContentProps
|
|
371
|
+
extends Omit<
|
|
372
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
|
373
|
+
"style"
|
|
374
|
+
>,
|
|
375
|
+
VariantProps<typeof appLayoutDrawerVariants> {
|
|
376
|
+
flush?: boolean
|
|
377
|
+
resizable?: boolean
|
|
378
|
+
minSize?: number
|
|
379
|
+
maxSize?: number
|
|
380
|
+
defaultSize?: number
|
|
381
|
+
storageKey?: string
|
|
382
|
+
/** Pixel breakpoint below which `resizable` is disabled and inline size is
|
|
383
|
+
* not applied (so the panel can render full-area on mobile). */
|
|
384
|
+
mobileBreakpoint?: number
|
|
385
|
+
overlayClassName?: string
|
|
386
|
+
resizeAriaLabel?: string
|
|
387
|
+
style?: React.CSSProperties
|
|
388
|
+
panelStyle?: React.CSSProperties
|
|
389
|
+
panelClassName?: string
|
|
390
|
+
/** Override the portal container. Defaults to AppLayout's main-area container
|
|
391
|
+
* (from context). Pass `null` to opt out of portaling. */
|
|
392
|
+
container?: HTMLElement | null
|
|
393
|
+
/** When `true` (default), clicks on the dim overlay over the main content
|
|
394
|
+
* area close the drawer. Clicks on AppLayout chrome (header, sidebar) NEVER
|
|
395
|
+
* close the drawer regardless of this flag — chrome interactions shouldn't
|
|
396
|
+
* accidentally dismiss a persistent panel. Pass `false` to make the drawer
|
|
397
|
+
* fully persistent (X button or Escape only). Consumer-provided
|
|
398
|
+
* `onInteractOutside` still runs first and can preventDefault to override. */
|
|
399
|
+
dismissOnInteractOutside?: boolean
|
|
400
|
+
/** Debug helper — when `true`, on each open the component snapshots the
|
|
401
|
+
* scroll metrics (scrollLeft / scrollWidth / clientWidth / overflow-x) of
|
|
402
|
+
* every ancestor of the portal container, at mount, after RAF, +150ms,
|
|
403
|
+
* +350ms, and on close. Use to diagnose layout-shift on open: look for an
|
|
404
|
+
* ancestor that gains `scrollLeft > 0` or has `overflow-x: visible` while
|
|
405
|
+
* `scrollWidth > clientWidth`. Remove the prop once diagnosed. */
|
|
406
|
+
debugLayoutShift?: boolean
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const AppLayoutDrawerContent = React.forwardRef<
|
|
410
|
+
React.ComponentRef<typeof DialogPrimitive.Content>,
|
|
411
|
+
AppLayoutDrawerContentProps
|
|
412
|
+
>(
|
|
413
|
+
(
|
|
414
|
+
{
|
|
415
|
+
side = "right",
|
|
416
|
+
flush = false,
|
|
417
|
+
resizable = false,
|
|
418
|
+
minSize = 320,
|
|
419
|
+
// No upper cap by default — the AppLayout container width is the natural
|
|
420
|
+
// limit. Consumers can still pass an explicit `maxSize` to clamp tighter.
|
|
421
|
+
maxSize = Number.POSITIVE_INFINITY,
|
|
422
|
+
defaultSize,
|
|
423
|
+
storageKey,
|
|
424
|
+
mobileBreakpoint = 768,
|
|
425
|
+
overlayClassName,
|
|
426
|
+
resizeAriaLabel,
|
|
427
|
+
className,
|
|
428
|
+
style,
|
|
429
|
+
panelStyle,
|
|
430
|
+
panelClassName,
|
|
431
|
+
container,
|
|
432
|
+
dismissOnInteractOutside = true,
|
|
433
|
+
onInteractOutside,
|
|
434
|
+
debugLayoutShift = false,
|
|
435
|
+
children,
|
|
436
|
+
...props
|
|
437
|
+
},
|
|
438
|
+
ref,
|
|
439
|
+
) => {
|
|
440
|
+
const contextContainer = useAppLayoutDrawerContainer()
|
|
441
|
+
const portalContainer = container !== undefined ? container : contextContainer
|
|
442
|
+
const open = React.useContext(DrawerOpenContext)
|
|
443
|
+
|
|
444
|
+
// Diagnostic: walk ancestors and snapshot horizontal-scroll metrics
|
|
445
|
+
// around the open transition. See `debugLayoutShift` prop doc.
|
|
446
|
+
React.useEffect(() => {
|
|
447
|
+
if (!debugLayoutShift) return
|
|
448
|
+
if (!open) return
|
|
449
|
+
if (typeof window === "undefined") return
|
|
450
|
+
if (!portalContainer) return
|
|
451
|
+
|
|
452
|
+
const collect = () => {
|
|
453
|
+
const list: Element[] = []
|
|
454
|
+
let el: Element | null = portalContainer
|
|
455
|
+
while (el) {
|
|
456
|
+
list.push(el)
|
|
457
|
+
el = el.parentElement
|
|
458
|
+
}
|
|
459
|
+
// Add documentElement explicitly in case ancestor chain stopped early.
|
|
460
|
+
if (!list.includes(document.documentElement)) {
|
|
461
|
+
list.push(document.documentElement)
|
|
462
|
+
}
|
|
463
|
+
return list
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const snapshot = (label: string) => {
|
|
467
|
+
const data = collect().map((el, i) => {
|
|
468
|
+
const cs = window.getComputedStyle(el)
|
|
469
|
+
const overflows = el.scrollWidth > el.clientWidth
|
|
470
|
+
const cls = String((el as HTMLElement).className ?? "")
|
|
471
|
+
.replace(/\s+/g, " ")
|
|
472
|
+
.slice(0, 60)
|
|
473
|
+
return {
|
|
474
|
+
depth: i,
|
|
475
|
+
tag:
|
|
476
|
+
el.tagName.toLowerCase() +
|
|
477
|
+
((el as HTMLElement).id ? "#" + (el as HTMLElement).id : ""),
|
|
478
|
+
class: cls,
|
|
479
|
+
"overflow-x": cs.overflowX,
|
|
480
|
+
scrollLeft: el.scrollLeft,
|
|
481
|
+
scrollWidth: el.scrollWidth,
|
|
482
|
+
clientWidth: el.clientWidth,
|
|
483
|
+
hOverflow: overflows ? "⚠️" : "",
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
// eslint-disable-next-line no-console
|
|
487
|
+
console.groupCollapsed(`[AppLayoutDrawer] ${label}`)
|
|
488
|
+
// eslint-disable-next-line no-console
|
|
489
|
+
console.table(data)
|
|
490
|
+
// eslint-disable-next-line no-console
|
|
491
|
+
console.groupEnd()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
snapshot("open: t0 (mount)")
|
|
495
|
+
const raf = requestAnimationFrame(() => snapshot("open: t≈16ms (RAF 1)"))
|
|
496
|
+
const t150 = window.setTimeout(() => snapshot("open: t≈150ms (mid-anim)"), 150)
|
|
497
|
+
const t350 = window.setTimeout(() => snapshot("open: t≈350ms (post-anim)"), 350)
|
|
498
|
+
|
|
499
|
+
return () => {
|
|
500
|
+
cancelAnimationFrame(raf)
|
|
501
|
+
clearTimeout(t150)
|
|
502
|
+
clearTimeout(t350)
|
|
503
|
+
snapshot("close")
|
|
504
|
+
}
|
|
505
|
+
}, [open, portalContainer, debugLayoutShift])
|
|
506
|
+
|
|
507
|
+
const resolvedSide: DrawerSide = side ?? "right"
|
|
508
|
+
const isHorizontal = HORIZONTAL_SIDES.has(resolvedSide)
|
|
509
|
+
const initialSize = defaultSize ?? (isHorizontal ? 560 : 480)
|
|
510
|
+
|
|
511
|
+
const [isMobile, setIsMobile] = React.useState(false)
|
|
512
|
+
React.useEffect(() => {
|
|
513
|
+
if (typeof window === "undefined") return
|
|
514
|
+
const mq = window.matchMedia(`(max-width: ${mobileBreakpoint - 1}px)`)
|
|
515
|
+
const update = () => setIsMobile(mq.matches)
|
|
516
|
+
update()
|
|
517
|
+
mq.addEventListener("change", update)
|
|
518
|
+
return () => mq.removeEventListener("change", update)
|
|
519
|
+
}, [mobileBreakpoint])
|
|
520
|
+
|
|
521
|
+
const { size, setSize } = useContainedResizableSize({
|
|
522
|
+
enabled: resizable,
|
|
523
|
+
isHorizontal,
|
|
524
|
+
minSize,
|
|
525
|
+
maxSize,
|
|
526
|
+
defaultSize: initialSize,
|
|
527
|
+
storageKey,
|
|
528
|
+
container: portalContainer,
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
const applyInlineSize = resizable && !isMobile
|
|
532
|
+
const sizeStyle: React.CSSProperties = applyInlineSize
|
|
533
|
+
? isHorizontal
|
|
534
|
+
? { width: size }
|
|
535
|
+
: { height: size }
|
|
536
|
+
: {}
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<DialogPrimitive.Portal container={portalContainer}>
|
|
540
|
+
{/* Overlay rendered manually: Radix's DialogPrimitive.Overlay returns
|
|
541
|
+
null in non-modal mode, so we render a plain div here. Setting
|
|
542
|
+
`pointer-events-auto` ensures it catches clicks on the main area
|
|
543
|
+
so they don't fall through to buttons underneath — that would
|
|
544
|
+
otherwise fire both Radix's outside-close AND the button's
|
|
545
|
+
own onClick, producing a close-then-reopen flicker. */}
|
|
546
|
+
<div
|
|
547
|
+
aria-hidden
|
|
548
|
+
data-state={open ? "open" : "closed"}
|
|
549
|
+
className={cn(
|
|
550
|
+
"absolute inset-0 z-[40] bg-black/50 outline-none data-[state=open]:pointer-events-auto data-[state=closed]:pointer-events-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
551
|
+
overlayClassName,
|
|
552
|
+
)}
|
|
553
|
+
/>
|
|
554
|
+
<DialogPrimitive.Content
|
|
555
|
+
ref={ref}
|
|
556
|
+
className={cn(appLayoutDrawerVariants({ side, flush }))}
|
|
557
|
+
style={style}
|
|
558
|
+
onInteractOutside={(event) => {
|
|
559
|
+
onInteractOutside?.(event)
|
|
560
|
+
if (event.defaultPrevented) return
|
|
561
|
+
// Chrome (header, sidebar) sits OUTSIDE the portal container.
|
|
562
|
+
// Those clicks never dismiss — they navigate, etc. Only clicks
|
|
563
|
+
// INSIDE the portal container (the overlay over main) can dismiss.
|
|
564
|
+
const target = event.target as Node | null
|
|
565
|
+
const isInsideContainer =
|
|
566
|
+
!!target && !!portalContainer?.contains(target)
|
|
567
|
+
if (!isInsideContainer) {
|
|
568
|
+
event.preventDefault()
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
if (!dismissOnInteractOutside) event.preventDefault()
|
|
572
|
+
}}
|
|
573
|
+
{...props}
|
|
574
|
+
>
|
|
575
|
+
{applyInlineSize ? (
|
|
576
|
+
<AppLayoutDrawerResizeHandle
|
|
577
|
+
side={resolvedSide}
|
|
578
|
+
size={size}
|
|
579
|
+
minSize={minSize}
|
|
580
|
+
maxSize={maxSize}
|
|
581
|
+
onSize={setSize}
|
|
582
|
+
ariaLabel={resizeAriaLabel}
|
|
583
|
+
/>
|
|
584
|
+
) : null}
|
|
585
|
+
<div
|
|
586
|
+
className={cn(
|
|
587
|
+
appLayoutDrawerPanelVariants({ side, flush }),
|
|
588
|
+
className,
|
|
589
|
+
panelClassName,
|
|
590
|
+
)}
|
|
591
|
+
style={{ ...sizeStyle, ...panelStyle }}
|
|
592
|
+
>
|
|
593
|
+
{children}
|
|
594
|
+
</div>
|
|
595
|
+
</DialogPrimitive.Content>
|
|
596
|
+
</DialogPrimitive.Portal>
|
|
597
|
+
)
|
|
598
|
+
},
|
|
599
|
+
)
|
|
600
|
+
AppLayoutDrawerContent.displayName = "AppLayoutDrawerContent"
|
|
601
|
+
|
|
602
|
+
// Sub-components are visually identical to the base Drawer's — re-export as
|
|
603
|
+
// aliases so the namespace stays consistent for consumers.
|
|
604
|
+
const AppLayoutDrawerHeader = DrawerHeader
|
|
605
|
+
const AppLayoutDrawerTitle = DrawerTitle
|
|
606
|
+
const AppLayoutDrawerDescription = DrawerDescription
|
|
607
|
+
const AppLayoutDrawerBody = DrawerBody
|
|
608
|
+
const AppLayoutDrawerFooter = DrawerFooter
|
|
609
|
+
|
|
610
|
+
export {
|
|
611
|
+
AppLayoutDrawerRoot as AppLayoutDrawer,
|
|
612
|
+
AppLayoutDrawerTrigger,
|
|
613
|
+
AppLayoutDrawerClose,
|
|
614
|
+
AppLayoutDrawerContent,
|
|
615
|
+
AppLayoutDrawerHeader,
|
|
616
|
+
AppLayoutDrawerTitle,
|
|
617
|
+
AppLayoutDrawerDescription,
|
|
618
|
+
AppLayoutDrawerBody,
|
|
619
|
+
AppLayoutDrawerFooter,
|
|
620
|
+
}
|