feat(desktop): hover-reveal collapsed sidebars as fixed overlays (#41670)
* feat(desktop): hover-reveal collapsed chat sidebar as a fixed overlay When the sessions sidebar is collapsed, hovering the left edge now floats it back in as a fixed overlay over the main content instead of just being hidden. The collapsed grid track stays at 0px so the panel never reserves space — it slides over whatever's underneath and retracts on pointer-leave. - PaneShell: new hoverReveal prop. When a pane is collapsed + hoverReveal, render an edge hot-zone + a side-anchored floating panel (absolute, full height, honors any persisted resize width) that slides in on hover/focus. - ChatSidebar: force the (otherwise opacity-0 when collapsed) sidebar fully visible + interactive while the overlay is revealed, via an in-data-[pane-hover-reveal=open] variant. - desktop-controller: opt the chat-sidebar pane into hoverReveal. * feat(desktop): lower window minWidth 900→400 Lets the window shrink to a narrow rail (e.g. for the collapsed hover-reveal sidebar) instead of being floored at 900px. * fix(desktop): render full sidebar content in hover-reveal overlay The hover-reveal overlay showed only the nav rail — session rows, search, pinned/recents were gated behind `sidebarOpen` (false while collapsed), so they never mounted in the floated panel. Add a $sidebarRevealed store the PaneShell overlay drives via a new onHoverRevealChange callback, and gate ChatSidebar's content on `sidebarOpen || sidebarRevealed` (contentVisible) instead of raw open state. The overlay now shows the complete sidebar. * fix(desktop): drop shadow on hover-reveal sidebar overlay * feat(desktop): hover-reveal the file-browser sidebar too The reveal mechanism already lives in the shared Pane primitive — the right rail just opts in with hoverReveal. Its content renders unconditionally, so (unlike the chat sidebar) it needs no extra content-visibility gating. * clean(desktop): tighten hover-reveal pane code KISS pass — flatten the translate ternary, derive a single `revealed`, inline the edge style, drop the redundant set-guard, and trim comments to the house one-liner style. No behavior change. * fix(desktop): stop hiding sidebar nav labels on narrow windows The nav labels (New session, Skills, …) and the ⌘N hint were gated on a viewport breakpoint (max-[46.25rem]:hidden), so shrinking the window hid them even when the sidebar itself was wide — including in the hover-reveal overlay. Drop the gate; the label already truncates (min-w-0 flex-1) so it ellipsizes gracefully in a narrow rail, and contentVisible already hides it when collapsed to the icon rail. * feat(desktop): auto-collapse both sidebars below 600px into hover-reveal Add a Pane `forceCollapsed` prop — collapses the track without writing to the store (so the saved open state restores when the window widens) while keeping hoverReveal alive (unlike `disabled`, which suppresses it). desktop-controller watches (max-width: 600px) and force-collapses the chat sidebar + file browser, so on a narrow window both rails get out of the way and the hover-reveal overlay becomes the way in. * feat(desktop): hover-intent + refined easing for sidebar reveal - Gate the reveal on pointer velocity: the full-height edge hot-zone now only arms on a slow, deliberate pass (<=0.55 px/ms). Fast sweeps toward the titlebar/statusbar — or off the window — blow past the threshold and never trigger, so the wide hit area stops being a nuisance. - Swap the slide easing to cubic-bezier(0.32,0.72,0,1) at 260ms (snappy-out, soft-land) for a more serious-app feel. * fix(desktop): don't reveal sidebar during window resize Resizing the window parks the cursor on the screen edge and fires slow pointermoves over the hot-zone, reading as deliberate intent. Guard the reveal on (a) e.buttons !== 0 — any button-held drag, incl. edge-resize — and (b) a 250ms cooldown after any window resize event. * feat(desktop): hoverIntent-style poll gate + inert contents during slide Replace the single-sample velocity check (too eager — fired on any one slow move, incl. resize drift) with a port of Brian Cherne's hoverIntent: poll the pointer every 90ms and only arm once it has *settled* (moved <5px between two consecutive polls inside the edge zone). Fly-bys, pass-throughs, and resize drift never produce two close samples in a row, so they don't trigger. Also keep the revealed panel's CONTENTS pointer-events-none until the slide-in transition finishes (onTransitionEnd → settled), so you can't misclick a session row mid-animation. Resets on retract. * fix(desktop): no cursor/hit-test leak before reveal settles The edge hot-zone showed cursor:pointer the instant the pointer touched it — before the panel was armed or in view. And contents were inert but the panel itself still hit-tested, so the cursor could flip mid-slide. Fix: hot-zone is cursor-default (it's invisible), and the whole panel is pointer-events-none until revealed && settled, so the cursor never changes or lands on a row before the slide-in finishes. * fix(desktop): geometry-driven close so revealed panel always retracts The revealed panel relied on its own onPointerLeave to close — but a panel that slid in under a still cursor (or whose contents were inert during the slide) never fires enter/leave, so it got stuck open (esp. the file browser). onTransitionEnd also bubbled from the file-tree's own row transitions, tripping the settled flag wrongly. Replace with a document-level pointermove watcher that closes once the cursor leaves the panel's bounding rect + a 24px grace — independent of pointer-events state or what the contents do. Gate interactivity on a simple slide-duration timer (interactive) instead of the fragile transitionEnd, so the cursor still can't flip or land on a row before the panel is in view. * feat(desktop): make sidebar toggle shortcuts reveal when force-collapsed mod+b / mod+j were no-ops on a narrow (force-collapsed) window — they flipped the store but the pane ignores it. Now the toggle handlers also dispatch PANE_TOGGLE_REVEAL_EVENT; a force-collapsed Pane listens (only while overlayActive) and flips its hover-reveal, so the shortcut floats the rail in (and back out) at this responsive breakpoint. * refactor(desktop): name the 600px sidebar collapse breakpoint Hoist the inline '(max-width: 600px)' literal into SIDEBAR_COLLAPSE_BREAKPOINT_PX + SIDEBAR_COLLAPSE_MEDIA_QUERY in layout-constants, so the responsive collapse point is a single named source of truth instead of a magic string in the controller. * tweak(desktop): sidebar auto-collapse breakpoint 600px -> 768px 768 is the standard md breakpoint and a more honest 'no room to dock' point. * tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms * Revert "tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms" This reverts commit 6009a132008105f5f871370d554a5097be3090af. * perf(desktop): pre-mount hover-reveal contents to kill slide-in stall The reveal mounted the (heavy, virtualized) sidebar contents in the same frame the slide started, so the browser stalled painting the transform until the mount finished — a ~100-200ms beat before the panel moved, very visible on the instant keyboard toggle (hover masked it via the 90ms intent poll). Report overlayActive (collapsed-overlay mode) rather than the live reveal state to the mount consumer, so contents stay mounted off-screen while collapsed and reveal is a pure transform. Visibility is still driven separately by the data-pane-hover-reveal attr + the slide transform. * fix(desktop): make reveal hotkey spammable Two throttles on the reveal toggle: - The handler fired both the reveal event AND toggleSidebarOpen() per press; the store write hits localStorage synchronously every keystroke + recomputes the grid, janking rapid presses. When collapsed, only dispatch the reveal event (the store toggle was a no-op anyway). - The geometry close-watcher slammed a keyboard-opened panel shut on the first stray pointermove (trackpad jitter), fighting hotkey spam. Keyboard reveals now ignore geometry until the cursor actually enters the panel, then the mouse takes over. * fix(desktop): inset reveal hot-zone past the OS window-resize gutter The hot-zone sat flush at the window edge (left-0/right-0), overlapping the OS resize grab strip — reaching to drag-resize naturally slows the cursor there, which hoverIntent reads as settled and reveals before the resize drag even starts. Inset the hot-zone 8px so the outermost edge stays a pure resize/drag region and only an intentful move just inside it arms a reveal. * fix(desktop): keep reveal hot-zone at edge, gate arming past resize gutter Insetting the hot-zone made it unreachable when moving fast. Instead, anchor the zone flush at the edge (w-4, always captures the pointer) but only ARM the reveal when the cursor settles >=8px in from the edge — so a resize-reach that parks on the outermost OS grab strip never triggers, while a deliberate move into the zone still does. Keeps polling while in the gutter so moving inward still arms. * refactor(desktop): rebuild hover-reveal as pure CSS, delete the JS state machine The hand-rolled pointer state machine (hoverIntent poll, refs, timers, document pointermove geometry-close, interactive gate, resize cooldowns, keyboard-held suppression) was fragile and side/instance-specific — hover broke on the right rail, keyboard toggles triggered phantom animations, resize popped it open. Replace all of it with the native primitive: CSS group-hover drives the slide transform; a transition-delay on enter (instant on leave) is the hover-intent gate (a fast pass-by doesn't dwell long enough to open); a thin edge trigger inset past the OS resize grab strip arms it; and a single `forced` bool (data-forced, toggled by the keyboard event) pins it open. Side-agnostic by construction — group-hover doesn't care which edge or which pane. Net: ~200 lines of imperative pointer logic → ~40 lines of declarative CSS. * fix(desktop): don't animate hover-reveal panel across viewport on side flip Flipping panes changed the off-screen transform from -translateX (off the left) to +translateX (off the right). transition-transform interpolated between them, passing through translate-x-0 (fully on-screen) mid-way — so the hidden panel visibly slid across the window to reach its new hiding spot. Key the panel on side so it remounts off-screen on the new edge with no transition to play. * clean(desktop): tighten hover-reveal markup KISS pass on the CSS-driven reveal: reuse the existing `side` instead of a local `left`, move the static duration/ease to inline style (drop two single-use CSS vars + their arbitrary-value classes, keep only the state-dependent enter-delay var), and trim comments to the house one-liner density. No behavior change. * fix(desktop): inset titlebar past traffic lights when sidebar is force-collapsed The titlebar content inset (clearing the macOS traffic lights) keyed off the stored sidebarOpen/fileBrowserOpen, but below the collapse breakpoint both rails are force-collapsed so the left edge is uncovered while the store still says open — content (the intro wordmark) overflowed under the lights. Gate leftEdgePaneOpen on !narrowViewport using the shared SIDEBAR_COLLAPSE_MEDIA_QUERY. Also rename the now-misleading reveal plumbing to match what it actually does: onHoverRevealChange -> onOverlayActiveChange, $sidebarRevealed -> $sidebarOverlayMounted (+ setter/consumer). It reports/stores collapsed-overlay mode (mount gate), not live reveal state. * feat(desktop): small --nous-shadow lift on revealed hover-reveal panels Add a --nous-shadow token (white-based on light, black-based on dark) and apply it to the floating sidebar panel only while revealed (group-hover / data-forced) so it reads as lifted off the surface. No shadow on the off-screen panel. * feat(desktop): shadow-reveal lift on revealed hover-reveal panels Mirror the --shadow-nous layered falloff into a new --shadow-reveal token whose drop color flips per mode (white on light, black on dark) via --shadow-reveal-raw set in :root / :root.dark. Apply the generated shadow-reveal utility to the floated panel only while revealed (group-hover / data-forced). Leaves the shared --shadow-nous untouched. * feat(desktop): use tuned reveal shadow, drop per-mode token Replace the --shadow-reveal token machinery with Brooklyn's tuned literal (0 -18px 18px -5px #0000003b) inline per-panel via --reveal-shadow, y-offset sign flipped for the right side. Same color both modes. Reverts styles.css to pristine (token removed). * fix(desktop): use the reveal shadow verbatim, don't invert it per side Flipping the y-offset sign for the right side inverted the shadow's direction (cast-up -> cast-down), making it read heavier — not a mirror. The mirror axis for a left/right panel is offset-x, which is 0 here, so both sides take the tuned value as-is: 0 -18px 18px -5px #0000003b. * clean(desktop): hoist reveal shadow to a named const Move the inline reveal-shadow literal to HOVER_REVEAL_SHADOW alongside the other HOVER_REVEAL_* tuning consts; drop the now-stale per-side comment. * fix(desktop): truncate titlebar title before the right tool cluster The session title used a hardcoded max-w-[52vw] that's blind to where the right-side tools start, so it ran under them at narrow widths / with pane tools present. Bound the title container by the same vars the titlebar drag region uses (--titlebar-content-inset + --titlebar-tools-right + --titlebar-tools-width) so it truncates exactly at the cluster's left edge. * fix(desktop): responsive markdown tables — floor width + nowrap headers The wrapper had overflow-x-auto but the table was w-full with auto layout, so instead of scrolling it crushed columns until even header words broke mid-word (Tim/e, Nig/ht). Add a min-w-[18rem] floor so it scrolls horizontally when the column is narrower than readable, and whitespace-nowrap on th so headers never break mid-word. Above the floor it still wraps cells naturally. * fix intro
This commit is contained in:
parent
86e5efb0ae
commit
d65b513f23
14 changed files with 226 additions and 65 deletions
|
|
@ -425,7 +425,7 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
|
|||
<div className="aui-md-table my-2 max-w-full overflow-x-auto rounded-[0.375rem] border border-border">
|
||||
<table
|
||||
className={cn(
|
||||
'm-0 w-full border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
'm-0 w-full min-w-[18rem] border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -438,7 +438,7 @@ function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTex
|
|||
th: ({ className, ...props }: ComponentProps<'th'>) => (
|
||||
<th
|
||||
className={cn(
|
||||
'px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
|
||||
'whitespace-nowrap px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -150,10 +150,7 @@ export const Thread: FC<{
|
|||
)
|
||||
|
||||
const emptyPlaceholder = intro ? (
|
||||
<div
|
||||
className="flex min-h-0 w-full flex-col items-center justify-center"
|
||||
style={{ paddingBottom: 'var(--composer-measured-height)' }}
|
||||
>
|
||||
<div className="flex min-h-0 w-full flex-col items-center justify-center pt-[var(--composer-measured-height)]">
|
||||
<Intro {...intro} />
|
||||
</div>
|
||||
) : undefined
|
||||
|
|
@ -470,9 +467,7 @@ const ReasoningAccordionGroup: FC<{ children?: ReactNode; endIndex: number; star
|
|||
s =>
|
||||
s.thread.isRunning &&
|
||||
s.message.status?.type === 'running' &&
|
||||
s.message.parts
|
||||
.slice(Math.max(0, startIndex))
|
||||
.some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
|
||||
s.message.parts.slice(Math.max(0, startIndex)).some(p => p?.type === 'reasoning' && p.status?.type !== 'complete')
|
||||
)
|
||||
|
||||
// A reasoning group with no actual text is pure noise — drop the whole
|
||||
|
|
|
|||
|
|
@ -160,14 +160,14 @@ export function Intro({ personality, seed }: IntroProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none flex w-full min-w-0 flex-col items-center justify-center px-3 py-6 text-center text-muted-foreground sm:px-6 lg:px-8"
|
||||
className="pointer-events-none flex w-full min-w-0 flex-col items-center justify-center px-0.5 py-6 text-center text-muted-foreground sm:px-6 lg:px-8"
|
||||
data-slot="aui_intro"
|
||||
>
|
||||
<div className="w-full min-w-0">
|
||||
<p
|
||||
aria-label={WORDMARK}
|
||||
className="fit-text mx-auto mb-3 w-[88%] font-['Collapse'] font-bold uppercase leading-[0.9] tracking-[0.08em] text-midground mix-blend-plus-lighter dark:text-foreground/90"
|
||||
style={{ '--fit-text-line-height': '0.9', '--fit-text-min': '2.75rem' } as CSSProperties}
|
||||
className="fit-text mx-auto mb-1 w-[calc(100%-1rem)] font-['Collapse'] font-bold uppercase leading-[0.9] tracking-[0.08em] text-midground mix-blend-plus-lighter dark:text-foreground/90"
|
||||
style={{ '--fit-min': '2.75rem' } as CSSProperties}
|
||||
>
|
||||
<span>
|
||||
<span>{WORDMARK}</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type { PaneShellContextValue, PaneSlot } from './context'
|
||||
export { PaneShellContext } from './context'
|
||||
export { Pane, PaneMain, PaneShell } from './pane-shell'
|
||||
export { Pane, PANE_TOGGLE_REVEAL_EVENT, PaneMain, PaneShell } from './pane-shell'
|
||||
export type { PaneMainProps, PaneProps, PaneShellProps } from './pane-shell'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -31,6 +32,12 @@ export interface PaneProps {
|
|||
defaultOpen?: boolean
|
||||
/** Forces the pane closed (track→0, aria-hidden) without writing to the store — for transient route gates. */
|
||||
disabled?: boolean
|
||||
/** Like disabled, but keeps hoverReveal alive — collapses the track without writing to the store (e.g. narrow window). */
|
||||
forceCollapsed?: boolean
|
||||
/** When collapsed, float the contents over the main column on hover/focus instead of hiding them (track stays 0px). */
|
||||
hoverReveal?: boolean
|
||||
/** Called with true while the pane is a collapsed hover-reveal overlay, so the consumer can keep contents mounted (ready to slide). */
|
||||
onOverlayActiveChange?: (overlayActive: boolean) => void
|
||||
id: string
|
||||
maxWidth?: WidthValue
|
||||
minWidth?: WidthValue
|
||||
|
|
@ -53,6 +60,7 @@ export interface PaneShellProps {
|
|||
interface CollectedPane {
|
||||
defaultOpen: boolean
|
||||
disabled: boolean
|
||||
forceCollapsed: boolean
|
||||
id: string
|
||||
resizable: boolean
|
||||
side: PaneSide
|
||||
|
|
@ -62,6 +70,22 @@ interface CollectedPane {
|
|||
const DEFAULT_WIDTH = '16rem'
|
||||
const DEFAULT_RESIZE_MIN_WIDTH = 160
|
||||
|
||||
// Hover-reveal slide. The enter delay is a pure-CSS hover-intent gate: a fast
|
||||
// pass-by doesn't dwell on the trigger long enough for the delay to elapse.
|
||||
const HOVER_REVEAL_SLIDE_MS = 220
|
||||
const HOVER_REVEAL_ENTER_DELAY_MS = 130
|
||||
const HOVER_REVEAL_EASE = 'cubic-bezier(0.32,0.72,0,1)'
|
||||
// Offset shadow lifting the revealed panel off the content (same both sides;
|
||||
// the mirror axis is offset-x, which is 0). Same color on light + dark.
|
||||
const HOVER_REVEAL_SHADOW = '0px -18px 18px -5px #00000012'
|
||||
// Edge trigger strip, inset past the OS window-resize grab area.
|
||||
const HOVER_REVEAL_TRIGGER_WIDTH = 14
|
||||
const HOVER_REVEAL_EDGE_GUTTER = 6
|
||||
|
||||
// Fired (window CustomEvent<{ id }>) to toggle a force-collapsed pane's reveal
|
||||
// from the keyboard, since its store-open toggle is a no-op while collapsed.
|
||||
export const PANE_TOGGLE_REVEAL_EVENT = 'hermes:pane-toggle-reveal'
|
||||
|
||||
const widthToCss = (value: WidthValue | undefined, fallback: string) =>
|
||||
value === undefined ? fallback : typeof value === 'number' ? `${value}px` : value
|
||||
|
||||
|
|
@ -110,6 +134,7 @@ function collectPanes(children: ReactNode) {
|
|||
const entry: CollectedPane = {
|
||||
defaultOpen: props.defaultOpen ?? true,
|
||||
disabled: props.disabled ?? false,
|
||||
forceCollapsed: props.forceCollapsed ?? false,
|
||||
id: props.id,
|
||||
resizable: props.resizable ?? false,
|
||||
side: props.side,
|
||||
|
|
@ -124,7 +149,7 @@ function collectPanes(children: ReactNode) {
|
|||
|
||||
function trackForPane(pane: CollectedPane, states: Record<string, { open: boolean; widthOverride?: number }>) {
|
||||
const stateOpen = states[pane.id]?.open ?? pane.defaultOpen
|
||||
const open = !pane.disabled && stateOpen
|
||||
const open = !pane.disabled && !pane.forceCollapsed && stateOpen
|
||||
|
||||
if (!open) {
|
||||
return { open: false, track: '0px' }
|
||||
|
|
@ -193,14 +218,29 @@ export function Pane({
|
|||
className,
|
||||
defaultOpen = true,
|
||||
disabled = false,
|
||||
hoverReveal = false,
|
||||
id,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
resizable = false
|
||||
onOverlayActiveChange,
|
||||
resizable = false,
|
||||
width
|
||||
}: PaneProps) {
|
||||
const ctx = useContext(PaneShellContext)
|
||||
const paneStates = useStore($paneStates)
|
||||
const registered = useRef(false)
|
||||
const paneRef = useRef<HTMLDivElement | null>(null)
|
||||
// Keyboard (mod+b / mod+j) pins the reveal open while collapsed; hover is CSS.
|
||||
const [forced, setForced] = useState(false)
|
||||
|
||||
const slot = ctx?.paneById.get(id)
|
||||
const open = Boolean(slot?.open && !disabled)
|
||||
const side = slot?.side ?? 'left'
|
||||
// Collapsed + hoverReveal: float the pane contents over the main column on
|
||||
// hover/focus instead of hiding them. Honors any persisted resize width.
|
||||
const overlayActive = !open && hoverReveal && !disabled
|
||||
const override = resizable ? paneStates[id]?.widthOverride : undefined
|
||||
const overlayWidth = override !== undefined ? `${override}px` : widthToCss(width, DEFAULT_WIDTH)
|
||||
|
||||
useEffect(() => {
|
||||
if (registered.current) {
|
||||
|
|
@ -211,12 +251,34 @@ export function Pane({
|
|||
ensurePaneRegistered(id, { open: defaultOpen })
|
||||
}, [defaultOpen, id])
|
||||
|
||||
const slot = ctx?.paneById.get(id)
|
||||
const open = Boolean(slot?.open && !disabled)
|
||||
// Keyboard toggle pins/unpins the reveal while collapsed; clear when no longer
|
||||
// a collapsed overlay (reopened / widened).
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !overlayActive) {
|
||||
setForced(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const onToggle = (e: Event) => {
|
||||
if ((e as CustomEvent<{ id: string }>).detail?.id === id) {
|
||||
setForced(v => !v)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle)
|
||||
|
||||
return () => window.removeEventListener(PANE_TOGGLE_REVEAL_EVENT, onToggle)
|
||||
}, [id, overlayActive])
|
||||
|
||||
// Keep contents mounted while collapsed so reveal is a pure CSS transform.
|
||||
useEffect(() => {
|
||||
onOverlayActiveChange?.(overlayActive)
|
||||
}, [onOverlayActiveChange, overlayActive])
|
||||
|
||||
const canResize = open && resizable
|
||||
const lo = widthToPx(minWidth) ?? DEFAULT_RESIZE_MIN_WIDTH
|
||||
const hi = widthToPx(maxWidth) ?? Number.POSITIVE_INFINITY
|
||||
const side = slot?.side ?? 'left'
|
||||
|
||||
const startResize = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
|
|
@ -273,6 +335,58 @@ export function Pane({
|
|||
return null
|
||||
}
|
||||
|
||||
// Collapsed hover-reveal track: a 0px, pointer-transparent grid cell holding a
|
||||
// thin edge trigger + the floating panel (both absolute, escaping the zero
|
||||
// box). group-hover (or data-forced from the keyboard) drives the slide; the
|
||||
// enter-delay is the hover-intent gate. No JS pointer math.
|
||||
if (overlayActive) {
|
||||
const edge = side === 'left' ? 'left' : 'right'
|
||||
const offscreen = side === 'left' ? '-translate-x-[calc(100%+1rem)]' : 'translate-x-[calc(100%+1rem)]'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group/reveal pointer-events-none relative row-start-1 min-w-0', className)}
|
||||
data-forced={forced ? '' : undefined}
|
||||
data-pane-hover-reveal={forced ? 'open' : 'closed'}
|
||||
data-pane-id={id}
|
||||
data-pane-open="false"
|
||||
data-pane-side={side}
|
||||
ref={paneRef}
|
||||
style={{ gridColumn: `${slot.column} / ${slot.column + 1}` }}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-auto absolute inset-y-0 z-30 [-webkit-app-region:no-drag]"
|
||||
style={{ [edge]: HOVER_REVEAL_EDGE_GUTTER, width: HOVER_REVEAL_TRIGGER_WIDTH }}
|
||||
/>
|
||||
|
||||
{/* Keyed on side so flipping panes remounts off-screen on the new edge
|
||||
instead of transitioning the transform across the viewport. */}
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-y-0 z-30 overflow-hidden transition-transform delay-0',
|
||||
offscreen,
|
||||
'group-hover/reveal:pointer-events-auto group-hover/reveal:translate-x-0 group-hover/reveal:delay-[var(--reveal-enter-delay)] group-hover/reveal:shadow-[var(--reveal-shadow)]',
|
||||
'group-data-[forced]/reveal:pointer-events-auto group-data-[forced]/reveal:translate-x-0 group-data-[forced]/reveal:delay-0 group-data-[forced]/reveal:shadow-[var(--reveal-shadow)]'
|
||||
)}
|
||||
key={edge}
|
||||
style={
|
||||
{
|
||||
[edge]: 0,
|
||||
width: overlayWidth,
|
||||
'--reveal-shadow': HOVER_REVEAL_SHADOW,
|
||||
transitionDuration: `${HOVER_REVEAL_SLIDE_MS}ms`,
|
||||
transitionTimingFunction: HOVER_REVEAL_EASE,
|
||||
'--reveal-enter-delay': `${HOVER_REVEAL_ENTER_DELAY_MS}ms`
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden={!open}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue