@sproutsocial/seeds-react-panel 0.3.7

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.
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { PanelContext } from "./PanelContext";
3
+ import type { TypePanelContext, TypePanelProviderProps } from "./PanelTypes";
4
+
5
+ export const PanelProvider = ({
6
+ children,
7
+ defaultOpen = false,
8
+ open: controlledOpen,
9
+ onOpenChange,
10
+ }: TypePanelProviderProps) => {
11
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
12
+ const isControlled = controlledOpen !== undefined;
13
+ const isPanelOpen = isControlled ? controlledOpen : internalOpen;
14
+
15
+ const setOpen = React.useCallback(
16
+ (next: boolean) => {
17
+ if (!isControlled) setInternalOpen(next);
18
+ onOpenChange?.(next);
19
+ },
20
+ [isControlled, onOpenChange]
21
+ );
22
+
23
+ const openPanel = React.useCallback(() => setOpen(true), [setOpen]);
24
+ const closePanel = React.useCallback(() => setOpen(false), [setOpen]);
25
+ const togglePanel = React.useCallback(
26
+ () => setOpen(!isPanelOpen),
27
+ [setOpen, isPanelOpen]
28
+ );
29
+
30
+ const value = React.useMemo<TypePanelContext>(
31
+ () => ({ isPanelOpen, openPanel, closePanel, togglePanel }),
32
+ [isPanelOpen, openPanel, closePanel, togglePanel]
33
+ );
34
+
35
+ return (
36
+ <PanelContext.Provider value={value}>{children}</PanelContext.Provider>
37
+ );
38
+ };
@@ -0,0 +1,166 @@
1
+ import * as React from "react";
2
+ import type {
3
+ TypeSystemCommonProps,
4
+ TypeStyledComponentsCommonProps,
5
+ } from "@sproutsocial/seeds-react-system-props";
6
+ import type { TypeBoxProps } from "@sproutsocial/seeds-react-box";
7
+ import type { TypeButtonProps } from "@sproutsocial/seeds-react-button";
8
+ import type {
9
+ TypeDrawerSnapPoint,
10
+ TypeDrawerActionProps,
11
+ } from "@sproutsocial/seeds-react-drawer";
12
+
13
+ export type { TypeDrawerActionProps as TypePanelActionProps };
14
+
15
+ export type PanelDirection = "left" | "right" | "bottom";
16
+
17
+ export interface TypePanelContext {
18
+ isPanelOpen: boolean;
19
+ togglePanel: () => void;
20
+ openPanel: () => void;
21
+ closePanel: () => void;
22
+ closeButtonLabel?: string;
23
+ }
24
+
25
+ export interface TypePanelProviderProps {
26
+ children: React.ReactNode;
27
+ /** Initial open state for uncontrolled usage. */
28
+ defaultOpen?: boolean;
29
+ /** Controlled open state. When provided, the provider becomes controlled. */
30
+ open?: boolean;
31
+ /** Called whenever the open state changes. Required for controlled usage. */
32
+ onOpenChange?: (open: boolean) => void;
33
+ }
34
+
35
+ export interface TypePanelProps
36
+ extends TypeStyledComponentsCommonProps,
37
+ TypeSystemCommonProps,
38
+ Omit<React.ComponentPropsWithoutRef<"div">, "color"> {
39
+ children: React.ReactNode;
40
+
41
+ /** Label for the close button. Usually "Close". */
42
+ closeButtonLabel: string;
43
+
44
+ /**
45
+ * Custom header content. When provided, replaces the auto-rendered default
46
+ * header entirely — `title` and `titleId` are ignored.
47
+ */
48
+ header?: React.ReactNode;
49
+
50
+ /** Custom footer content. Not rendered when omitted. */
51
+ footer?: React.ReactNode;
52
+
53
+ /** Title shown in the auto-rendered default header. Ignored when `header` is provided. */
54
+ title?: string;
55
+
56
+ /**
57
+ * Sets the `id` on the title element of the auto-rendered header. When the
58
+ * default header is used, Panel automatically wires `aria-labelledby` to this
59
+ * id so the dialog/aside has an accessible name. Pass an explicit
60
+ * `aria-labelledby` to override (e.g. when pointing at a different element).
61
+ * Ignored when `header` is provided — supply your own `aria-labelledby` in
62
+ * that case.
63
+ */
64
+ titleId?: string;
65
+
66
+ /** Side the panel anchors to on desktop. Defaults to "right". */
67
+ direction?: PanelDirection;
68
+
69
+ /**
70
+ * Width (px) when direction is "left" or "right", or height (px) when
71
+ * direction is "bottom". Defaults to 384 to match the existing web-app-core
72
+ * Panel.
73
+ */
74
+ width?: number;
75
+
76
+ /**
77
+ * Visual gap (px) between the panel and the adjacent content. Applied as
78
+ * margin on the side facing the content (left when direction="right",
79
+ * right when direction="left", top when direction="bottom"). Animates with
80
+ * the panel's open/close transition and collapses to 0 when closed so the
81
+ * gap does not remain visible alongside a zero-width panel. Defaults to 0.
82
+ */
83
+ gap?: number;
84
+
85
+ /**
86
+ * Forwarded to `useIsMobile`. Accepts a px number or any CSS media-query
87
+ * length. Below this width, the panel renders as an overlay bottom sheet
88
+ * (via seeds-react-drawer). Defaults to the theme's `breakpoints.sm`.
89
+ */
90
+ mobileBreakpoint?: number | string;
91
+
92
+ /** Applies only to the mobile bottom-sheet rendering. */
93
+ zIndex?: number;
94
+
95
+ /** Optional id used for data-qa-panel attributes. */
96
+ id?: string;
97
+
98
+ /**
99
+ * Snap points the panel can rest at when rendered as a mobile bottom sheet.
100
+ * Numbers 0–1 are viewport-height fractions; numbers > 1 are pixels; strings
101
+ * accept `px`/`rem` (e.g. `"480px"`, `"30rem"`). Order matters — the last
102
+ * entry is the "expanded" state. Ignored on the desktop side-panel rendering.
103
+ */
104
+ snapPoints?: TypeDrawerSnapPoint[];
105
+
106
+ /** Initial snap point for uncontrolled use. Mobile only. */
107
+ defaultSnapPoint?: TypeDrawerSnapPoint | null;
108
+
109
+ /** Controlled active snap point. Pair with `onSnapPointChange`. Mobile only. */
110
+ snapPoint?: TypeDrawerSnapPoint | null;
111
+
112
+ /** Fires when the active snap point changes. Mobile only. */
113
+ onSnapPointChange?: (snapPoint: TypeDrawerSnapPoint | null) => void;
114
+
115
+ /**
116
+ * When true, fast swipes can't skip past adjacent snap points. Mobile only.
117
+ */
118
+ snapToSequentialPoints?: boolean;
119
+
120
+ /**
121
+ * Action buttons shown in a floating rail above the panel on mobile (the
122
+ * bottom-sheet rendering). Ignored on desktop side-panel rendering — pass
123
+ * actions through `header`/`footer` slots there. The rail owns the close
124
+ * button so the default header omits its built-in close affordance.
125
+ */
126
+ actions?: TypeDrawerActionProps[];
127
+
128
+ /**
129
+ * Opt out of the floating mobile rail and render the `actions` inline in the
130
+ * header as pill buttons instead. Useful for nested bottom sheets and
131
+ * snap-point layouts where the floating rail would collide with surrounding
132
+ * UI. Defaults to `false`.
133
+ *
134
+ * When `header` is omitted, Panel auto-composes a mobile header that renders
135
+ * the title, the `actions` as pills, and a close-button pill. When `header`
136
+ * is provided, the consumer is responsible for rendering the actions inside
137
+ * their own header — `actions` is forwarded for context only.
138
+ */
139
+ actionsInHeader?: boolean;
140
+ }
141
+
142
+ export interface TypePanelHeaderProps extends TypeBoxProps {
143
+ title?: string;
144
+ /**
145
+ * When children are provided, `<Panel.CloseButton />` is NOT rendered
146
+ * automatically. Include it yourself to preserve keyboard accessibility.
147
+ */
148
+ children?: React.ReactNode;
149
+ /** Render-prop override that receives the parent panel context. */
150
+ render?: React.FC<TypePanelContext>;
151
+ }
152
+
153
+ export interface TypePanelContentProps extends TypeBoxProps {
154
+ children?: React.ReactNode;
155
+ }
156
+
157
+ export interface TypePanelFooterProps extends TypeBoxProps {
158
+ children?: React.ReactNode;
159
+ }
160
+
161
+ export interface TypePanelCloseButtonProps
162
+ extends Omit<TypeButtonProps, "children"> {
163
+ /** Render-prop override that receives the parent panel context. */
164
+ render?: React.FC<TypePanelContext>;
165
+ children?: React.ReactNode;
166
+ }
@@ -0,0 +1,354 @@
1
+ /* eslint-disable testing-library/prefer-screen-queries */
2
+
3
+ import React from "react";
4
+ import {
5
+ render as testRender,
6
+ waitFor,
7
+ screen,
8
+ act,
9
+ } from "@sproutsocial/seeds-react-testing-library";
10
+ import Panel, { PanelProvider, usePanelContext } from "../index";
11
+ import type { TypePanelProps } from "../PanelTypes";
12
+
13
+ const ToggleButton = ({ label = "toggle" }: { label?: string }) => {
14
+ const { togglePanel } = usePanelContext();
15
+ return <button onClick={togglePanel}>{label}</button>;
16
+ };
17
+
18
+ const renderPanel = ({
19
+ defaultOpen = false,
20
+ direction,
21
+ width,
22
+ children = (
23
+ <Panel.Content>
24
+ <p>Panel Content</p>
25
+ </Panel.Content>
26
+ ),
27
+ title = "Panel Header",
28
+ titleId = "panel-1-header",
29
+ closeButtonLabel = "close button",
30
+ ...rest
31
+ }: Partial<TypePanelProps> & { defaultOpen?: boolean } = {}) => {
32
+ return testRender(
33
+ <PanelProvider defaultOpen={defaultOpen}>
34
+ <div>
35
+ <ToggleButton />
36
+ <Panel
37
+ closeButtonLabel={closeButtonLabel}
38
+ direction={direction}
39
+ width={width}
40
+ title={title}
41
+ titleId={titleId}
42
+ {...rest}
43
+ >
44
+ {children}
45
+ </Panel>
46
+ </div>
47
+ </PanelProvider>
48
+ );
49
+ };
50
+
51
+ describe("Panel (desktop / inline mode)", () => {
52
+ it("renders children when open by default", () => {
53
+ renderPanel({ defaultOpen: true });
54
+ expect(screen.getByText(/panel content/i)).toBeInTheDocument();
55
+ });
56
+
57
+ it("toggles open and closed via context", async () => {
58
+ const { user } = renderPanel({ defaultOpen: false });
59
+ const panel = screen.getByRole("complementary");
60
+ expect(panel).toHaveAttribute("data-qa-panel-isopen", "false");
61
+
62
+ await act(async () => {
63
+ await user.click(screen.getByText("toggle"));
64
+ });
65
+
66
+ await waitFor(() => {
67
+ expect(panel).toHaveAttribute("data-qa-panel-isopen", "true");
68
+ });
69
+ expect(screen.getByText(/panel content/i)).toBeInTheDocument();
70
+ });
71
+
72
+ it("close button calls closePanel from context", async () => {
73
+ const { user } = renderPanel({ defaultOpen: true });
74
+
75
+ expect(screen.getByRole("complementary")).toHaveAttribute(
76
+ "data-qa-panel-isopen",
77
+ "true"
78
+ );
79
+
80
+ await act(async () => {
81
+ await user.click(screen.getByLabelText("close button"));
82
+ });
83
+
84
+ await waitFor(() => {
85
+ expect(screen.getByRole("complementary")).toHaveAttribute(
86
+ "data-qa-panel-isopen",
87
+ "false"
88
+ );
89
+ });
90
+ });
91
+
92
+ it("renders the title element with the provided id", () => {
93
+ renderPanel({ defaultOpen: true });
94
+ expect(screen.getByText("Panel Header")).toHaveAttribute(
95
+ "id",
96
+ "panel-1-header"
97
+ );
98
+ });
99
+
100
+ it("Panel.Content sets data-panel-content attribute", () => {
101
+ renderPanel({ defaultOpen: true });
102
+ expect(
103
+ screen.getByText(/panel content/i).closest("[data-panel-content]")
104
+ ).toBeInTheDocument();
105
+ });
106
+
107
+ it("accepts a custom header via the header prop", async () => {
108
+ const { user } = renderPanel({
109
+ defaultOpen: true,
110
+ header: (
111
+ <Panel.Header>
112
+ <h1>Custom Title</h1>
113
+ <Panel.CloseButton />
114
+ </Panel.Header>
115
+ ),
116
+ });
117
+
118
+ expect(screen.getByText("Custom Title")).toBeInTheDocument();
119
+
120
+ await act(async () => {
121
+ await user.click(screen.getByLabelText("close button"));
122
+ });
123
+
124
+ await waitFor(() => {
125
+ expect(screen.getByRole("complementary")).toHaveAttribute(
126
+ "data-qa-panel-isopen",
127
+ "false"
128
+ );
129
+ });
130
+ });
131
+
132
+ it("uses the provided width on desktop", () => {
133
+ renderPanel({ defaultOpen: true, width: 500 });
134
+ expect(screen.getByRole("complementary")).toHaveStyle("flex-basis: 500px");
135
+ });
136
+
137
+ it("collapses to flex-basis: 0 when closed", () => {
138
+ renderPanel({ defaultOpen: false });
139
+ expect(screen.getByRole("complementary")).toHaveStyle("flex-basis: 0px");
140
+ });
141
+ });
142
+
143
+ describe("Panel (mobile / drawer mode)", () => {
144
+ let originalMatchMedia: typeof window.matchMedia | undefined;
145
+
146
+ beforeEach(() => {
147
+ originalMatchMedia = window.matchMedia;
148
+ window.matchMedia = jest.fn().mockImplementation((query: string) => ({
149
+ matches: true,
150
+ media: query,
151
+ onchange: null,
152
+ addEventListener: jest.fn(),
153
+ removeEventListener: jest.fn(),
154
+ addListener: jest.fn(),
155
+ removeListener: jest.fn(),
156
+ dispatchEvent: jest.fn(),
157
+ }));
158
+ });
159
+
160
+ afterEach(() => {
161
+ if (originalMatchMedia) {
162
+ window.matchMedia = originalMatchMedia;
163
+ } else {
164
+ // @ts-expect-error allow deleting a non-optional dom property in tests
165
+ delete window.matchMedia;
166
+ }
167
+ });
168
+
169
+ it("defaults aria-labelledby to titleId when the default header is used", () => {
170
+ renderPanel({ defaultOpen: true });
171
+
172
+ expect(screen.getByRole("dialog")).toHaveAttribute(
173
+ "aria-labelledby",
174
+ "panel-1-header"
175
+ );
176
+ });
177
+
178
+ it("does not emit Drawer's missing-accessible-name warning", () => {
179
+ const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
180
+ renderPanel({ defaultOpen: true });
181
+ expect(warnSpy).not.toHaveBeenCalledWith(
182
+ expect.stringContaining("[Drawer] Missing accessible name")
183
+ );
184
+ warnSpy.mockRestore();
185
+ });
186
+
187
+ it("consumer aria-labelledby wins over the titleId default", () => {
188
+ renderPanel({
189
+ defaultOpen: true,
190
+ "aria-labelledby": "external-heading",
191
+ });
192
+
193
+ expect(screen.getByRole("dialog")).toHaveAttribute(
194
+ "aria-labelledby",
195
+ "external-heading"
196
+ );
197
+ });
198
+
199
+ it("forwards aria-label to the Drawer", () => {
200
+ renderPanel({
201
+ defaultOpen: true,
202
+ "aria-label": "Inspector",
203
+ });
204
+
205
+ expect(screen.getByRole("dialog")).toHaveAttribute(
206
+ "aria-label",
207
+ "Inspector"
208
+ );
209
+ });
210
+
211
+ it("forwards arbitrary data-* props to the Drawer", () => {
212
+ renderPanel({
213
+ defaultOpen: true,
214
+ // @ts-expect-error custom data attribute not in the typed prop list
215
+ "data-testid": "panel-mobile",
216
+ });
217
+
218
+ expect(screen.getByTestId("panel-mobile")).toBeInTheDocument();
219
+ });
220
+
221
+ it("does not default aria-labelledby when a custom header is provided", () => {
222
+ renderPanel({
223
+ defaultOpen: true,
224
+ header: <div>Custom Header</div>,
225
+ });
226
+
227
+ expect(screen.getByRole("dialog")).not.toHaveAttribute("aria-labelledby");
228
+ });
229
+
230
+ it("renders actions as pills in the header when actionsInHeader is set", async () => {
231
+ const onExpand = jest.fn();
232
+ const onShare = jest.fn();
233
+ const { user } = renderPanel({
234
+ defaultOpen: true,
235
+ actionsInHeader: true,
236
+ actions: [
237
+ {
238
+ "aria-label": "Expand",
239
+ iconName: "arrows-pointing-out-outline",
240
+ onClick: onExpand,
241
+ },
242
+ {
243
+ "aria-label": "Share",
244
+ iconName: "link-outline",
245
+ onClick: onShare,
246
+ },
247
+ ],
248
+ });
249
+
250
+ const expand = screen.getByLabelText("Expand");
251
+ const share = screen.getByLabelText("Share");
252
+ expect(expand).toBeInTheDocument();
253
+ expect(share).toBeInTheDocument();
254
+ // The auto-composed header also renders its own close pill so the
255
+ // suppressed-rail combination still has a way to dismiss the sheet.
256
+ expect(screen.getByLabelText("close button")).toBeInTheDocument();
257
+
258
+ await act(async () => {
259
+ await user.click(expand);
260
+ });
261
+ expect(onExpand).toHaveBeenCalledTimes(1);
262
+
263
+ await act(async () => {
264
+ await user.click(share);
265
+ });
266
+ expect(onShare).toHaveBeenCalledTimes(1);
267
+ });
268
+
269
+ it("forwards actions to the floating rail when actionsInHeader is not set", () => {
270
+ renderPanel({
271
+ defaultOpen: true,
272
+ actions: [
273
+ {
274
+ "aria-label": "Expand",
275
+ iconName: "arrows-pointing-out-outline",
276
+ },
277
+ ],
278
+ });
279
+
280
+ // Rail-mode: the rail owns the close button (with closeButtonLabel) and
281
+ // the action button is rendered inside the rail rather than as a header
282
+ // pill. Both are present, the rail's aria-label is set, and the
283
+ // auto-composed header is not used (no duplicate close pill).
284
+ expect(screen.getByLabelText("Drawer quick actions")).toBeInTheDocument();
285
+ expect(screen.getByLabelText("Expand")).toBeInTheDocument();
286
+ expect(screen.getAllByLabelText("close button")).toHaveLength(1);
287
+ });
288
+
289
+ it("does not auto-render the actions header when a custom header is provided", () => {
290
+ renderPanel({
291
+ defaultOpen: true,
292
+ actionsInHeader: true,
293
+ actions: [
294
+ {
295
+ "aria-label": "Expand",
296
+ iconName: "arrows-pointing-out-outline",
297
+ },
298
+ ],
299
+ header: <div>Custom Header</div>,
300
+ });
301
+
302
+ expect(screen.getByText("Custom Header")).toBeInTheDocument();
303
+ // The consumer owns rendering — Panel must not duplicate the action pill.
304
+ expect(screen.queryByLabelText("Expand")).not.toBeInTheDocument();
305
+ });
306
+ });
307
+
308
+ describe("PanelProvider (controlled)", () => {
309
+ it("calls onOpenChange when the panel toggles", async () => {
310
+ const onOpenChange = jest.fn();
311
+ const Controlled = () => {
312
+ const [open, setOpen] = React.useState(false);
313
+ return (
314
+ <PanelProvider
315
+ open={open}
316
+ onOpenChange={(next) => {
317
+ onOpenChange(next);
318
+ setOpen(next);
319
+ }}
320
+ >
321
+ <ToggleButton />
322
+ <Panel closeButtonLabel="close button">
323
+ <Panel.Content>
324
+ <p>controlled content</p>
325
+ </Panel.Content>
326
+ </Panel>
327
+ </PanelProvider>
328
+ );
329
+ };
330
+
331
+ const { user } = testRender(<Controlled />);
332
+ await act(async () => {
333
+ await user.click(screen.getByText("toggle"));
334
+ });
335
+
336
+ expect(onOpenChange).toHaveBeenCalledWith(true);
337
+ });
338
+ });
339
+
340
+ describe("usePanelContext", () => {
341
+ it("throws when used outside a PanelProvider", () => {
342
+ const Crash = () => {
343
+ usePanelContext();
344
+ return null;
345
+ };
346
+
347
+ // Suppress React's expected error console output for this assertion.
348
+ const spy = jest.spyOn(console, "error").mockImplementation(() => {});
349
+ expect(() => testRender(<Crash />)).toThrow(
350
+ /usePanelContext must be used within a <PanelProvider>/
351
+ );
352
+ spy.mockRestore();
353
+ });
354
+ });
@@ -0,0 +1,50 @@
1
+ import * as React from "react";
2
+ import Panel, {
3
+ PanelProvider,
4
+ usePanelContext,
5
+ type TypePanelProps,
6
+ type PanelDirection,
7
+ } from "../index";
8
+
9
+ // Compile-only type assertions. Not executed; presence of `_` flags unused warnings.
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ function _typeTests() {
12
+ // Required prop: closeButtonLabel
13
+ // @ts-expect-error closeButtonLabel is required
14
+ const _missing: React.ReactElement = <Panel>content</Panel>;
15
+
16
+ // Valid directions only
17
+ const _dir: PanelDirection = "right";
18
+ // @ts-expect-error "top" is not a supported direction
19
+ const _badDir: PanelDirection = "top";
20
+
21
+ // PanelProvider props
22
+ const _provider = (
23
+ <PanelProvider defaultOpen open={false} onOpenChange={() => {}}>
24
+ child
25
+ </PanelProvider>
26
+ );
27
+
28
+ // usePanelContext shape
29
+ const ctx: ReturnType<typeof usePanelContext> = {
30
+ isPanelOpen: true,
31
+ openPanel: () => {},
32
+ closePanel: () => {},
33
+ togglePanel: () => {},
34
+ };
35
+ void ctx;
36
+
37
+ // Full Panel props
38
+ const _props: TypePanelProps = {
39
+ children: null,
40
+ closeButtonLabel: "Close",
41
+ direction: "left",
42
+ width: 384,
43
+ mobileBreakpoint: 640,
44
+ };
45
+ void _props;
46
+ void _missing;
47
+ void _dir;
48
+ void _badDir;
49
+ void _provider;
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import Panel from "./Panel";
2
+
3
+ export default Panel;
4
+ export { Panel };
5
+ export { PanelContext, usePanelContext } from "./PanelContext";
6
+ export { PanelProvider } from "./PanelProvider";
7
+ export { PanelHeader } from "./PanelHeader";
8
+ export { PanelContent } from "./PanelContent";
9
+ export { PanelCloseButton } from "./PanelCloseButton";
10
+ export { PanelFooter } from "./PanelFooter";
11
+ export * from "./PanelTypes";
package/src/styles.ts ADDED
@@ -0,0 +1,71 @@
1
+ import styled, { css } from "styled-components";
2
+ import { COMMON } from "@sproutsocial/seeds-react-system-props";
3
+ import type { TypeSystemCommonProps } from "@sproutsocial/seeds-react-system-props";
4
+ import { MOTION_DURATION_MEDIUM } from "@sproutsocial/seeds-motion/unitless";
5
+ import Box from "@sproutsocial/seeds-react-box";
6
+ import type { PanelDirection } from "./PanelTypes";
7
+
8
+ interface PanelContainerProps extends TypeSystemCommonProps {
9
+ $isOpen: boolean;
10
+ $direction: PanelDirection;
11
+ $width: number;
12
+ $gap: number;
13
+ }
14
+
15
+ const gapSide = (direction: PanelDirection): "left" | "right" | "top" => {
16
+ if (direction === "right") return "left";
17
+ if (direction === "left") return "right";
18
+ return "top";
19
+ };
20
+
21
+ export const PanelContainer = styled.aside<PanelContainerProps>`
22
+ background: ${(props) => props.theme.colors.container.background.base};
23
+ flex-grow: 0;
24
+ flex-shrink: 0;
25
+ overflow: hidden;
26
+ transition: flex-basis ${MOTION_DURATION_MEDIUM}s ease-in-out,
27
+ margin ${MOTION_DURATION_MEDIUM}s ease-in-out;
28
+ flex-basis: ${(props) => (props.$isOpen ? `${props.$width}px` : "0px")};
29
+ margin-${(props) => gapSide(props.$direction)}: ${(props) =>
30
+ props.$isOpen && props.$gap ? `${props.$gap}px` : "0px"};
31
+ border-radius: ${({ theme }) => theme.radii[800]};
32
+
33
+ ${(props) =>
34
+ props.$direction === "bottom"
35
+ ? css`
36
+ width: 100%;
37
+ `
38
+ : css`
39
+ height: 100%;
40
+ `}
41
+
42
+ ${COMMON}
43
+ `;
44
+
45
+ export const PanelInner = styled.div<{
46
+ $direction: PanelDirection;
47
+ $width: number;
48
+ }>`
49
+ ${(props) =>
50
+ props.$direction === "bottom"
51
+ ? css`
52
+ min-height: ${props.$width}px;
53
+ width: 100%;
54
+ `
55
+ : css`
56
+ min-width: ${props.$width}px;
57
+ height: 100%;
58
+ `}
59
+ display: flex;
60
+ flex-direction: column;
61
+ overflow: hidden;
62
+ `;
63
+
64
+ export const Content = styled(Box)`
65
+ overflow-y: auto;
66
+ `;
67
+
68
+ export const Footer = styled(Box)`
69
+ flex: 0 0 auto;
70
+ overflow: hidden;
71
+ `;