Skip to content

Commit f53e042

Browse files
committed
Naming and docs
1 parent 0d44191 commit f53e042

File tree

2 files changed

+63
-28
lines changed

2 files changed

+63
-28
lines changed

packages/gitbook/src/components/AIChat/AIChat.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,17 @@ export function AIChat() {
7272
<SideSheet
7373
side="right"
7474
open={chat.opened}
75-
onClose={() => chatController.close()}
75+
onOpenChange={(open) => {
76+
if (open) {
77+
chatController.open();
78+
} else {
79+
chatController.close();
80+
}
81+
}}
7682
data-testid="ai-chat"
77-
withShim={true}
83+
withScrim={true}
7884
className={tcls(
79-
'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'
85+
'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'
8086
)}
8187
>
8288
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base">

packages/gitbook/src/components/primitives/SideSheet.tsx

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,39 @@ import React from 'react';
77
import { useIsMobile } from '../hooks/useIsMobile';
88
import { Button } from './Button';
99

10+
/**
11+
* SideSheet - A slide-in panel component that can appear from the left or right side.
12+
*
13+
* Supports both controlled and uncontrolled modes:
14+
* - Controlled: Provide both `open` and `onOpenChange` props. Parent manages state.
15+
* - Uncontrolled: Omit `open` prop. Component manages its own state internally.
16+
*/
1017
export function SideSheet(
1118
props: {
19+
/** Which side the sheet slides in from */
1220
side: 'left' | 'right';
13-
open?: boolean;
21+
/**
22+
* Optional CSS class to monitor and sync with `document.body.classList`.
23+
* When set, a MutationObserver watches for the class and syncs the sheet state accordingly.
24+
* Adding this class opens the sheet, removing it closes it.
25+
* Works in both controlled and uncontrolled modes.
26+
*/
1427
toggleClass?: string;
28+
/**
29+
* Modal behavior: true (always modal), false (never modal), or 'mobile' (modal only on mobile).
30+
* Defaults to 'mobile'.
31+
*/
1532
modal?: true | false | 'mobile';
16-
onClose?: () => void;
17-
withShim?: boolean;
33+
/**
34+
* Controls visibility. If provided, component is controlled (parent manages state).
35+
* If undefined, component is uncontrolled (manages its own state).
36+
*/
37+
open?: boolean;
38+
/** Called when the open state changes. Receives the new state (true/false). Only used in controlled mode. */
39+
onOpenChange?: (open: boolean) => void;
40+
/** Show a backdrop overlay when modal */
41+
withScrim?: boolean;
42+
/** Show a close button when modal */
1843
withCloseButton?: boolean;
1944
} & React.HTMLAttributes<HTMLDivElement>
2045
) {
@@ -25,33 +50,35 @@ export function SideSheet(
2550
toggleClass,
2651
open: openState,
2752
modal = 'mobile',
28-
withShim,
53+
withScrim,
2954
withCloseButton,
30-
onClose,
55+
onOpenChange,
3156
...rest
3257
} = props;
3358

3459
const isMobile = useIsMobile();
3560
const isModal = modal === 'mobile' ? isMobile : modal;
3661

62+
// Internal state for uncontrolled mode (only used when open prop is undefined)
3763
const [open, setOpen] = React.useState(openState ?? false);
3864

39-
// Use prop if provided (controlled), otherwise use internal state (uncontrolled)
65+
// Determine actual open state: controlled (from prop) or uncontrolled (from internal state)
4066
const isOpen = openState !== undefined ? openState : open;
4167

4268
const handleClose = React.useCallback(() => {
4369
if (openState !== undefined) {
44-
// Controlled mode: notify parent
45-
onClose?.();
70+
// Controlled mode: parent manages state, notify via callback with new state
71+
onOpenChange?.(false);
4672
} else {
47-
// Uncontrolled mode: update internal state
73+
// Uncontrolled mode: update internal state and sync body class if needed
4874
setOpen(false);
4975
if (toggleClass) {
5076
document.body.classList.remove(toggleClass);
5177
}
5278
}
53-
}, [openState, onClose, toggleClass]);
79+
}, [openState, onOpenChange, toggleClass]);
5480

81+
// Sync the sheet state with the body class if the toggleClass is set
5582
React.useEffect(() => {
5683
if (!toggleClass) {
5784
return;
@@ -62,17 +89,13 @@ export function SideSheet(
6289
if (mutation.attributeName === 'class') {
6390
const shouldBeOpen = document.body.classList.contains(toggleClass);
6491
if (openState !== undefined) {
65-
// Controlled mode: notify parent if state should change
92+
// Controlled mode: sync with parent's state
93+
// Notify parent of state change via onOpenChange
6694
if (shouldBeOpen !== openState) {
67-
if (shouldBeOpen) {
68-
// Opening via class - no callback, just sync
69-
// Parent should handle this via toggleClass observation
70-
} else {
71-
onClose?.();
72-
}
95+
onOpenChange?.(shouldBeOpen);
7396
}
7497
} else {
75-
// Uncontrolled mode: update internal state
98+
// Uncontrolled mode: sync internal state with body class
7699
setOpen(shouldBeOpen);
77100
}
78101
}
@@ -83,13 +106,17 @@ export function SideSheet(
83106
observer.observe(document.body, { attributes: true });
84107

85108
return () => observer.disconnect();
86-
}, [toggleClass, openState, onClose]);
109+
}, [toggleClass, openState, onOpenChange]);
87110

88111
return (
89112
<>
90-
{isModal && withShim ? (
91-
<SideSheetShim className={isOpen ? '' : 'hidden opacity-0'} onClick={handleClose} />
113+
{isModal && withScrim ? (
114+
<SideSheetScrim
115+
className={isOpen ? '' : 'hidden opacity-0 backdrop-blur-none'}
116+
onClick={handleClose}
117+
/>
92118
) : null}
119+
93120
<aside
94121
className={tcls(
95122
'side-sheet',
@@ -100,7 +127,7 @@ export function SideSheet(
100127
: side === 'left'
101128
? 'hydrated:animate-exit-to-left'
102129
: 'hydrated:animate-exit-to-right',
103-
'fixed inset-y-0 z-50',
130+
'fixed inset-y-0 z-41', // Above the side sheet scrim on z-40
104131
side === 'left' ? 'left-0' : 'right-0',
105132
withCloseButton ? (side === 'left' ? 'mr-16' : 'ml-16') : '',
106133
isOpen ? '' : 'hidden',
@@ -125,11 +152,12 @@ export function SideSheet(
125152
);
126153
}
127154

128-
export function SideSheetShim(props: { className?: ClassValue; onClick?: () => void }) {
155+
/** Backdrop overlay shown behind the modal sheet */
156+
export function SideSheetScrim(props: { className?: ClassValue; onClick?: () => void }) {
129157
const { className, onClick } = props;
130158
return (
131159
<div
132-
id="side-sheet-shim"
160+
id="side-sheet-scrim"
133161
onClick={() => {
134162
onClick?.();
135163
}}
@@ -139,13 +167,14 @@ export function SideSheetShim(props: { className?: ClassValue; onClick?: () => v
139167
}
140168
}}
141169
className={tcls(
142-
'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',
170+
'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',
143171
className
144172
)}
145173
/>
146174
);
147175
}
148176

177+
/** Close button displayed outside the sheet when modal */
149178
export function SideSheetCloseButton(props: { className?: ClassValue; onClick?: () => void }) {
150179
const { className, onClick } = props;
151180
const language = useLanguage();

0 commit comments

Comments
 (0)