Naming and docs

This commit is contained in:
Zeno Kapitein
2025-12-02 12:52:02 +01:00
parent 0d44191666
commit f53e0427e4
2 changed files with 63 additions and 28 deletions
@@ -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();