mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-05 12:11:29 +00:00
Naming and docs
This commit is contained in:
@@ -72,11 +72,17 @@ export function AIChat() {
|
||||
<SideSheet
|
||||
side="right"
|
||||
open={chat.opened}
|
||||
onClose={() => chatController.close()}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
chatController.open();
|
||||
} else {
|
||||
chatController.close();
|
||||
}
|
||||
}}
|
||||
data-testid="ai-chat"
|
||||
withShim={true}
|
||||
withScrim={true}
|
||||
className={tcls(
|
||||
'ai-chat z-40 mx-auto not-hydrated:hidden w-96 max-w-full pl-8 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
|
||||
'ai-chat mx-auto not-hydrated:hidden w-96 max-w-full pl-8 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
|
||||
)}
|
||||
>
|
||||
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base">
|
||||
|
||||
@@ -7,14 +7,39 @@ import React from 'react';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { Button } from './Button';
|
||||
|
||||
/**
|
||||
* SideSheet - A slide-in panel component that can appear from the left or right side.
|
||||
*
|
||||
* Supports both controlled and uncontrolled modes:
|
||||
* - Controlled: Provide both `open` and `onOpenChange` props. Parent manages state.
|
||||
* - Uncontrolled: Omit `open` prop. Component manages its own state internally.
|
||||
*/
|
||||
export function SideSheet(
|
||||
props: {
|
||||
/** Which side the sheet slides in from */
|
||||
side: 'left' | 'right';
|
||||
open?: boolean;
|
||||
/**
|
||||
* Optional CSS class to monitor and sync with `document.body.classList`.
|
||||
* When set, a MutationObserver watches for the class and syncs the sheet state accordingly.
|
||||
* Adding this class opens the sheet, removing it closes it.
|
||||
* Works in both controlled and uncontrolled modes.
|
||||
*/
|
||||
toggleClass?: string;
|
||||
/**
|
||||
* Modal behavior: true (always modal), false (never modal), or 'mobile' (modal only on mobile).
|
||||
* Defaults to 'mobile'.
|
||||
*/
|
||||
modal?: true | false | 'mobile';
|
||||
onClose?: () => void;
|
||||
withShim?: boolean;
|
||||
/**
|
||||
* Controls visibility. If provided, component is controlled (parent manages state).
|
||||
* If undefined, component is uncontrolled (manages its own state).
|
||||
*/
|
||||
open?: boolean;
|
||||
/** Called when the open state changes. Receives the new state (true/false). Only used in controlled mode. */
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/** Show a backdrop overlay when modal */
|
||||
withScrim?: boolean;
|
||||
/** Show a close button when modal */
|
||||
withCloseButton?: boolean;
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
) {
|
||||
@@ -25,33 +50,35 @@ export function SideSheet(
|
||||
toggleClass,
|
||||
open: openState,
|
||||
modal = 'mobile',
|
||||
withShim,
|
||||
withScrim,
|
||||
withCloseButton,
|
||||
onClose,
|
||||
onOpenChange,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isModal = modal === 'mobile' ? isMobile : modal;
|
||||
|
||||
// Internal state for uncontrolled mode (only used when open prop is undefined)
|
||||
const [open, setOpen] = React.useState(openState ?? false);
|
||||
|
||||
// Use prop if provided (controlled), otherwise use internal state (uncontrolled)
|
||||
// Determine actual open state: controlled (from prop) or uncontrolled (from internal state)
|
||||
const isOpen = openState !== undefined ? openState : open;
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (openState !== undefined) {
|
||||
// Controlled mode: notify parent
|
||||
onClose?.();
|
||||
// Controlled mode: parent manages state, notify via callback with new state
|
||||
onOpenChange?.(false);
|
||||
} else {
|
||||
// Uncontrolled mode: update internal state
|
||||
// Uncontrolled mode: update internal state and sync body class if needed
|
||||
setOpen(false);
|
||||
if (toggleClass) {
|
||||
document.body.classList.remove(toggleClass);
|
||||
}
|
||||
}
|
||||
}, [openState, onClose, toggleClass]);
|
||||
}, [openState, onOpenChange, toggleClass]);
|
||||
|
||||
// Sync the sheet state with the body class if the toggleClass is set
|
||||
React.useEffect(() => {
|
||||
if (!toggleClass) {
|
||||
return;
|
||||
@@ -62,17 +89,13 @@ export function SideSheet(
|
||||
if (mutation.attributeName === 'class') {
|
||||
const shouldBeOpen = document.body.classList.contains(toggleClass);
|
||||
if (openState !== undefined) {
|
||||
// Controlled mode: notify parent if state should change
|
||||
// Controlled mode: sync with parent's state
|
||||
// Notify parent of state change via onOpenChange
|
||||
if (shouldBeOpen !== openState) {
|
||||
if (shouldBeOpen) {
|
||||
// Opening via class - no callback, just sync
|
||||
// Parent should handle this via toggleClass observation
|
||||
} else {
|
||||
onClose?.();
|
||||
}
|
||||
onOpenChange?.(shouldBeOpen);
|
||||
}
|
||||
} else {
|
||||
// Uncontrolled mode: update internal state
|
||||
// Uncontrolled mode: sync internal state with body class
|
||||
setOpen(shouldBeOpen);
|
||||
}
|
||||
}
|
||||
@@ -83,13 +106,17 @@ export function SideSheet(
|
||||
observer.observe(document.body, { attributes: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [toggleClass, openState, onClose]);
|
||||
}, [toggleClass, openState, onOpenChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModal && withShim ? (
|
||||
<SideSheetShim className={isOpen ? '' : 'hidden opacity-0'} onClick={handleClose} />
|
||||
{isModal && withScrim ? (
|
||||
<SideSheetScrim
|
||||
className={isOpen ? '' : 'hidden opacity-0 backdrop-blur-none'}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<aside
|
||||
className={tcls(
|
||||
'side-sheet',
|
||||
@@ -100,7 +127,7 @@ export function SideSheet(
|
||||
: side === 'left'
|
||||
? 'hydrated:animate-exit-to-left'
|
||||
: 'hydrated:animate-exit-to-right',
|
||||
'fixed inset-y-0 z-50',
|
||||
'fixed inset-y-0 z-41', // Above the side sheet scrim on z-40
|
||||
side === 'left' ? 'left-0' : 'right-0',
|
||||
withCloseButton ? (side === 'left' ? 'mr-16' : 'ml-16') : '',
|
||||
isOpen ? '' : 'hidden',
|
||||
@@ -125,11 +152,12 @@ export function SideSheet(
|
||||
);
|
||||
}
|
||||
|
||||
export function SideSheetShim(props: { className?: ClassValue; onClick?: () => void }) {
|
||||
/** Backdrop overlay shown behind the modal sheet */
|
||||
export function SideSheetScrim(props: { className?: ClassValue; onClick?: () => void }) {
|
||||
const { className, onClick } = props;
|
||||
return (
|
||||
<div
|
||||
id="side-sheet-shim"
|
||||
id="side-sheet-scrim"
|
||||
onClick={() => {
|
||||
onClick?.();
|
||||
}}
|
||||
@@ -139,13 +167,14 @@ export function SideSheetShim(props: { className?: ClassValue; onClick?: () => v
|
||||
}
|
||||
}}
|
||||
className={tcls(
|
||||
'fixed inset-0 z-40 items-start bg-tint-base/3 not-hydrated:opacity-0 starting:opacity-0 backdrop-blur-md transition-[opacity,display,filter] transition-discrete duration-250',
|
||||
'fixed inset-0 z-40 items-start bg-tint-base/3 not-hydrated:opacity-0 starting:opacity-0 backdrop-blur-md starting:backdrop-blur-none transition-[opacity,display,backdrop-filter] transition-discrete duration-250 dark:bg-tint-base/9',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Close button displayed outside the sheet when modal */
|
||||
export function SideSheetCloseButton(props: { className?: ClassValue; onClick?: () => void }) {
|
||||
const { className, onClick } = props;
|
||||
const language = useLanguage();
|
||||
|
||||
Reference in New Issue
Block a user