@sproutsocial/seeds-react-panel 1.0.11 → 1.1.2

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.
@@ -1,11 +1,58 @@
1
1
  import * as React from "react";
2
+ import { needsStyledComponents } from "@sproutsocial/seeds-react-system-props";
2
3
  import { Footer } from "./styles";
4
+ import { cn } from "./PanelShared";
3
5
  import type { TypePanelFooterProps } from "./PanelTypes";
4
6
 
5
- export const PanelFooter = ({ children, ...rest }: TypePanelFooterProps) => (
7
+ /**
8
+ * Styled-components footer — the backward-compatible fallback. Routed to when
9
+ * the consumer passes styled-system props (e.g. `<Panel.Footer bg="...">`).
10
+ */
11
+ const PanelFooterStyled = ({ children, ...rest }: TypePanelFooterProps) => (
6
12
  <Footer p={450} data-panel-footer="" {...rest}>
7
13
  {children}
8
14
  </Footer>
9
15
  );
10
16
 
17
+ /**
18
+ * Tailwind/CSS-class footer — the preferred path. Renders a plain
19
+ * `<div data-panel-footer>` with the `seeds-panel-footer` class from panel.css.
20
+ *
21
+ * `margin-bottom: var(--bleed, 0px)` lifts the footer above the Drawer's mobile
22
+ * bottom-sheet bleed (see styles.ts). It is asserted by the jsdom tests and the
23
+ * `--bleed` var is only set at runtime by the mobile Drawer, so it is applied
24
+ * inline (falling back to 0px on desktop).
25
+ */
26
+ const PanelFooterTailwind = ({ children, ...rest }: TypePanelFooterProps) => {
27
+ const { className, style, ...domRest } = rest as {
28
+ className?: string;
29
+ style?: React.CSSProperties;
30
+ } & Record<string, unknown>;
31
+
32
+ return (
33
+ <div
34
+ className={cn("seeds-panel-footer", className)}
35
+ style={{ marginBottom: "var(--bleed, 0px)", ...style }}
36
+ data-panel-footer=""
37
+ {...domRest}
38
+ >
39
+ {children}
40
+ </div>
41
+ );
42
+ };
43
+
44
+ /**
45
+ * Hybrid footer router. Routes on the leftover props after the footer's own
46
+ * props are removed.
47
+ */
48
+ export const PanelFooter = (props: TypePanelFooterProps) => {
49
+ const { children, ...rest } = props;
50
+
51
+ if (needsStyledComponents(rest)) {
52
+ return <PanelFooterStyled {...props} />;
53
+ }
54
+
55
+ return <PanelFooterTailwind {...props} />;
56
+ };
57
+
11
58
  PanelFooter.displayName = "Panel.Footer";
@@ -1,41 +1,90 @@
1
1
  import Box from "@sproutsocial/seeds-react-box";
2
2
  import Text from "@sproutsocial/seeds-react-text";
3
+ import { needsStyledComponents } from "@sproutsocial/seeds-react-system-props";
3
4
  import { usePanelContext } from "./PanelContext";
4
5
  import { PanelCloseButton } from "./PanelCloseButton";
6
+ import { cn } from "./PanelShared";
5
7
  import type { TypePanelHeaderProps } from "./PanelTypes";
6
8
 
7
- export const PanelHeader = ({
9
+ /**
10
+ * Styled-components header — the backward-compatible fallback. The hybrid
11
+ * router sends consumers here whenever they pass styled-system props (e.g.
12
+ * `<Panel.Header alignItems="center">`) or a `styled()` extension, so the full
13
+ * `Box` prop surface keeps working.
14
+ */
15
+ const PanelHeaderStyled = ({
8
16
  title = "",
9
17
  id = undefined,
10
18
  children,
11
19
  render,
12
20
  ...rest
13
- }: TypePanelHeaderProps) => {
14
- const panelContext = usePanelContext();
21
+ }: TypePanelHeaderProps) => (
22
+ <Box
23
+ display="flex"
24
+ flex="0 0 auto"
25
+ justifyContent="space-between"
26
+ alignItems="center"
27
+ p={400}
28
+ borderBottom="1px solid"
29
+ borderColor="container.border.base"
30
+ {...rest}
31
+ >
32
+ {children || (
33
+ <>
34
+ <Text.Headline id={id}>{title}</Text.Headline>
35
+ <PanelCloseButton />
36
+ </>
37
+ )}
38
+ </Box>
39
+ );
15
40
 
16
- if (render) {
17
- return render(panelContext);
18
- }
41
+ /**
42
+ * Tailwind/CSS-class header — the preferred path. Renders a plain `<div>` with
43
+ * the `seeds-panel-header` class from panel.css instead of a styled `Box`. The
44
+ * `render` prop is handled by the hybrid before reaching here.
45
+ */
46
+ const PanelHeaderTailwind = ({
47
+ title = "",
48
+ id = undefined,
49
+ children,
50
+ render,
51
+ ...rest
52
+ }: TypePanelHeaderProps) => {
53
+ const { className, ...domRest } = rest as {
54
+ className?: string;
55
+ } & Record<string, unknown>;
19
56
 
20
57
  return (
21
- <Box
22
- display="flex"
23
- flex="0 0 auto"
24
- justifyContent="space-between"
25
- alignItems="center"
26
- p={400}
27
- borderBottom="1px solid"
28
- borderColor="container.border.base"
29
- {...rest}
30
- >
58
+ <div className={cn("seeds-panel-header", className)} {...domRest}>
31
59
  {children || (
32
60
  <>
33
61
  <Text.Headline id={id}>{title}</Text.Headline>
34
62
  <PanelCloseButton />
35
63
  </>
36
64
  )}
37
- </Box>
65
+ </div>
38
66
  );
39
67
  };
40
68
 
69
+ /**
70
+ * Hybrid header router. The `render` escape hatch short-circuits before any
71
+ * routing (matching the original always-styled behaviour); otherwise it routes
72
+ * on the leftover props after the header's own props are removed.
73
+ */
74
+ export const PanelHeader = (props: TypePanelHeaderProps) => {
75
+ const panelContext = usePanelContext();
76
+
77
+ if (props.render) {
78
+ return props.render(panelContext);
79
+ }
80
+
81
+ const { title, id, children, render, ...rest } = props;
82
+
83
+ if (needsStyledComponents(rest)) {
84
+ return <PanelHeaderStyled {...props} />;
85
+ }
86
+
87
+ return <PanelHeaderTailwind {...props} />;
88
+ };
89
+
41
90
  PanelHeader.displayName = "Panel.Header";
@@ -0,0 +1,52 @@
1
+ import { needsStyledComponents } from "@sproutsocial/seeds-react-system-props";
2
+ import { PanelStyled } from "./PanelStyled";
3
+ import { PanelTailwind } from "./PanelTailwind";
4
+ import type { TypePanelProps } from "./PanelTypes";
5
+
6
+ /**
7
+ * Hybrid Panel router. Chooses the styled-components path when the consumer
8
+ * passes a styled-system (`COMMON`) prop or a `styled()` extension, otherwise
9
+ * the preferred Tailwind path.
10
+ *
11
+ * The routing decision runs on the *leftover* props after Panel's own domain
12
+ * props are destructured — critically, `width` and `zIndex` are Panel domain
13
+ * props whose names also appear in `STYLED_SYSTEM_PROPS`. Gating on the full
14
+ * prop object would push almost every real Panel usage (anything setting
15
+ * `width`) onto the styled path and defeat the migration, so we exclude the
16
+ * domain props first and gate only on `rest`.
17
+ */
18
+ export const PanelHybrid = (props: TypePanelProps) => {
19
+ const {
20
+ children,
21
+ closeButtonLabel,
22
+ header,
23
+ headerless,
24
+ footer,
25
+ title,
26
+ titleId,
27
+ direction,
28
+ width,
29
+ gap,
30
+ mobileBreakpoint,
31
+ zIndex,
32
+ id,
33
+ snapPoints,
34
+ defaultSnapPoint,
35
+ snapPoint,
36
+ onSnapPointChange,
37
+ snapToSequentialPoints,
38
+ actions,
39
+ actionsInHeader,
40
+ "aria-labelledby": ariaLabelledBy,
41
+ "aria-label": ariaLabel,
42
+ ...rest
43
+ } = props;
44
+
45
+ if (needsStyledComponents(rest)) {
46
+ return <PanelStyled {...props} />;
47
+ }
48
+
49
+ return <PanelTailwind {...props} />;
50
+ };
51
+
52
+ PanelHybrid.displayName = "Panel";
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+ import Drawer from "@sproutsocial/seeds-react-drawer";
3
+ import { PanelContext } from "./PanelContext";
4
+ import { PanelContent } from "./PanelContent";
5
+ import { PanelMobileActionsHeader } from "./PanelMobileActionsHeader";
6
+ import type { TypePanelShell } from "./PanelShared";
7
+
8
+ /**
9
+ * Mobile bottom-sheet rendering, shared verbatim by both the styled and the
10
+ * Tailwind shells. The mobile path always renders through `Drawer`, which is
11
+ * itself already migrated to the hybrid pattern, so nothing here depends on the
12
+ * root routing gate — the two shells render an identical mobile tree.
13
+ */
14
+ export const PanelMobile = ({ shell }: { shell: TypePanelShell }) => {
15
+ const {
16
+ rest,
17
+ children,
18
+ closeButtonLabel,
19
+ header,
20
+ headerless,
21
+ footer,
22
+ title,
23
+ titleId,
24
+ zIndex,
25
+ snapPoints,
26
+ defaultSnapPoint,
27
+ snapPoint,
28
+ onSnapPointChange,
29
+ snapToSequentialPoints,
30
+ actions,
31
+ actionsInHeader,
32
+ ariaLabel,
33
+ isPanelOpen,
34
+ closePanel,
35
+ enrichedValue,
36
+ effectiveAriaLabelledBy,
37
+ } = shell;
38
+
39
+ // With `actionsInHeader`, Drawer suppresses the floating rail and expects the
40
+ // consumer to render the actions inside a custom header. Auto-compose one
41
+ // (title + action pills + close pill) when the consumer hasn't supplied their
42
+ // own header; otherwise we'd drop `actions` on the floor.
43
+ const shouldRenderActionsHeader =
44
+ !headerless &&
45
+ header == null &&
46
+ actionsInHeader === true &&
47
+ !!actions &&
48
+ actions.length > 0;
49
+
50
+ // Headerless suppresses the built-in header entirely; the children own all
51
+ // chrome in that mode.
52
+ let defaultMobileHeader: React.ReactNode = null;
53
+ if (!headerless) {
54
+ defaultMobileHeader = shouldRenderActionsHeader ? (
55
+ <PanelMobileActionsHeader
56
+ title={title}
57
+ titleId={titleId}
58
+ actions={actions!}
59
+ />
60
+ ) : (
61
+ <Drawer.Header title={title} id={titleId} />
62
+ );
63
+ }
64
+
65
+ return (
66
+ <PanelContext.Provider value={enrichedValue}>
67
+ <Drawer
68
+ {...rest}
69
+ open={isPanelOpen}
70
+ onClose={closePanel}
71
+ direction="bottom"
72
+ closeButtonLabel={closeButtonLabel}
73
+ zIndex={zIndex}
74
+ snapPoints={snapPoints}
75
+ defaultSnapPoint={defaultSnapPoint}
76
+ snapPoint={snapPoint}
77
+ onSnapPointChange={onSnapPointChange}
78
+ snapToSequentialPoints={snapToSequentialPoints}
79
+ actions={actions}
80
+ actionsInHeader={actionsInHeader}
81
+ aria-labelledby={effectiveAriaLabelledBy}
82
+ aria-label={ariaLabel}
83
+ >
84
+ {header ?? defaultMobileHeader}
85
+ <PanelContent>{children}</PanelContent>
86
+ {footer}
87
+ </Drawer>
88
+ </PanelContext.Provider>
89
+ );
90
+ };
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import Box from "@sproutsocial/seeds-react-box";
3
2
  import Button from "@sproutsocial/seeds-react-button";
4
3
  import Drawer from "@sproutsocial/seeds-react-drawer";
5
4
  import Icon from "@sproutsocial/seeds-react-icon";
@@ -20,6 +19,10 @@ interface TypePanelMobileActionsHeaderProps {
20
19
  * pattern in `seeds-react-peek-in/PeekIn.tsx`. The Drawer's floating rail is
21
20
  * suppressed in this combination (`actionsInHeader` opts out), so without this
22
21
  * header the actions would render nowhere.
22
+ *
23
+ * This header is composed internally by Panel (it receives no consumer
24
+ * styled-system props), so its chrome is emitted directly as `seeds-panel-*`
25
+ * classes from panel.css rather than through a styled `Box`.
23
26
  */
24
27
  export const PanelMobileActionsHeader = ({
25
28
  title,
@@ -28,14 +31,7 @@ export const PanelMobileActionsHeader = ({
28
31
  }: TypePanelMobileActionsHeaderProps) => (
29
32
  <Drawer.Header
30
33
  render={(ctx) => (
31
- <Box
32
- display="flex"
33
- alignItems="center"
34
- justifyContent="space-between"
35
- pt={400}
36
- px={450}
37
- flex="0 0 auto"
38
- >
34
+ <div className="seeds-panel-mobile-actions-header">
39
35
  <Text
40
36
  as="h2"
41
37
  fontSize={400}
@@ -45,7 +41,7 @@ export const PanelMobileActionsHeader = ({
45
41
  >
46
42
  {title}
47
43
  </Text>
48
- <Box display="flex" alignItems="center" gap={200}>
44
+ <div className="seeds-panel-mobile-actions-buttons">
49
45
  {actions.map((action, i) => (
50
46
  <Button
51
47
  key={i}
@@ -64,8 +60,8 @@ export const PanelMobileActionsHeader = ({
64
60
  >
65
61
  <Icon aria-hidden name="x-outline" />
66
62
  </Button>
67
- </Box>
68
- </Box>
63
+ </div>
64
+ </div>
69
65
  )}
70
66
  />
71
67
  );
@@ -0,0 +1,189 @@
1
+ import * as React from "react";
2
+ import { useIsMobile } from "@sproutsocial/seeds-react-hooks";
3
+ import { usePanelContext } from "./PanelContext";
4
+ import type {
5
+ PanelDirection,
6
+ TypePanelProps,
7
+ TypePanelContext,
8
+ } from "./PanelTypes";
9
+
10
+ /**
11
+ * Class-name merge helper. Copied intentionally rather than imported from
12
+ * `@sproutsocial/seeds-react-utilities` — `cn` is not exported there, so each
13
+ * migrated Tailwind component carries its own copy (mirrors AvatarTailwind /
14
+ * DrawerShared). Because Panel is a multi-part component, the helper lives once
15
+ * here and is imported by every Tailwind sub-component instead of being
16
+ * duplicated in each file.
17
+ */
18
+ export function cn(
19
+ ...inputs: (string | undefined | null | false | Record<string, boolean>)[]
20
+ ): string {
21
+ const classes: string[] = [];
22
+
23
+ for (const input of inputs) {
24
+ if (!input) continue;
25
+
26
+ if (typeof input === "string") {
27
+ classes.push(input);
28
+ } else if (typeof input === "object") {
29
+ for (const [key, value] of Object.entries(input)) {
30
+ if (value) {
31
+ classes.push(key);
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ return classes.join(" ");
38
+ }
39
+
40
+ /**
41
+ * Dynamic geometry for the outer `<aside>` on the Tailwind path. These values
42
+ * depend on runtime props (open state, width, gap, direction) and are asserted
43
+ * as computed styles by the jsdom tests, so they must be inline `style` rather
44
+ * than CSS-variable-driven classes (panel.css is not loaded under jsdom). The
45
+ * static chrome (background, radius, transition, overflow, flex-grow/shrink)
46
+ * lives in panel.css.
47
+ *
48
+ * The gap sits on the margin edge facing the adjacent content: left when the
49
+ * panel anchors right, right when it anchors left, top when it anchors to the
50
+ * bottom (mirrors `gapSide` in styles.ts).
51
+ */
52
+ export const getPanelContainerStyle = (
53
+ direction: PanelDirection,
54
+ width: number,
55
+ gap: number,
56
+ isPanelOpen: boolean
57
+ ): React.CSSProperties => {
58
+ const gapValue = isPanelOpen && gap ? `${gap}px` : "0px";
59
+ const style: React.CSSProperties = {
60
+ flexBasis: isPanelOpen ? `${width}px` : "0px",
61
+ };
62
+
63
+ if (direction === "bottom") {
64
+ style.width = "100%";
65
+ } else {
66
+ style.alignSelf = "stretch";
67
+ }
68
+
69
+ if (direction === "right") {
70
+ style.marginLeft = gapValue;
71
+ } else if (direction === "left") {
72
+ style.marginRight = gapValue;
73
+ } else {
74
+ style.marginTop = gapValue;
75
+ }
76
+
77
+ return style;
78
+ };
79
+
80
+ /**
81
+ * Dynamic geometry for the inner layout `<div>` on the Tailwind path. The
82
+ * direction-dependent min-width/min-height come from the `width` prop.
83
+ */
84
+ export const getPanelInnerStyle = (
85
+ direction: PanelDirection,
86
+ width: number
87
+ ): React.CSSProperties =>
88
+ direction === "bottom"
89
+ ? { minHeight: `${width}px`, width: "100%" }
90
+ : { minWidth: `${width}px`, height: "100%" };
91
+
92
+ /**
93
+ * Shared render logic for both the styled and the Tailwind shells: reads the
94
+ * PanelProvider context, enriches it (closeButtonLabel/headerless), freezes the
95
+ * last-rendered children during the collapse, resolves the mobile breakpoint,
96
+ * and computes the auto-wired accessible name. Centralising it here guarantees
97
+ * the two shells stay behaviourally identical — only the desktop container
98
+ * markup differs between them.
99
+ */
100
+ export const usePanelShell = (props: TypePanelProps) => {
101
+ const {
102
+ children,
103
+ closeButtonLabel,
104
+ header,
105
+ headerless = false,
106
+ footer,
107
+ title,
108
+ titleId,
109
+ direction = "right",
110
+ width = 384,
111
+ gap = 4,
112
+ mobileBreakpoint,
113
+ zIndex,
114
+ id,
115
+ snapPoints,
116
+ defaultSnapPoint,
117
+ snapPoint,
118
+ onSnapPointChange,
119
+ snapToSequentialPoints,
120
+ actions,
121
+ actionsInHeader,
122
+ "aria-labelledby": ariaLabelledByProp,
123
+ "aria-label": ariaLabel,
124
+ ...rest
125
+ } = props;
126
+
127
+ const panelContext = usePanelContext();
128
+ const { isPanelOpen, closePanel } = panelContext;
129
+ const isMobile = useIsMobile(mobileBreakpoint);
130
+
131
+ // Expose closeButtonLabel through context so PanelCloseButton can read it
132
+ // without prop drilling — same pattern as DrawerContext.
133
+ const enrichedValue = React.useMemo<TypePanelContext>(
134
+ () => ({ ...panelContext, closeButtonLabel, headerless }),
135
+ [panelContext, closeButtonLabel, headerless]
136
+ );
137
+
138
+ // Freeze the last-rendered children during the collapse so consumers that
139
+ // conditionally render content don't produce an empty panel shell while the
140
+ // flex-basis animation runs.
141
+ const prevChildrenRef = React.useRef<React.ReactNode>(null);
142
+ if (isPanelOpen) {
143
+ prevChildrenRef.current = children;
144
+ }
145
+ const renderedChildren = isPanelOpen ? children : prevChildrenRef.current;
146
+
147
+ // When the consumer relies on the default header, its title element renders
148
+ // with `id={titleId}`. Auto-wire `aria-labelledby` so the dialog/aside has an
149
+ // accessible name without forcing every caller to repeat the id.
150
+ const effectiveAriaLabelledBy =
151
+ ariaLabelledByProp ?? (!headerless && header == null ? titleId : undefined);
152
+
153
+ return {
154
+ rest,
155
+ children,
156
+ closeButtonLabel,
157
+ header,
158
+ headerless,
159
+ footer,
160
+ title,
161
+ titleId,
162
+ direction,
163
+ width,
164
+ gap,
165
+ zIndex,
166
+ id,
167
+ snapPoints,
168
+ defaultSnapPoint,
169
+ snapPoint,
170
+ onSnapPointChange,
171
+ snapToSequentialPoints,
172
+ actions,
173
+ actionsInHeader,
174
+ ariaLabel,
175
+ isPanelOpen,
176
+ closePanel,
177
+ isMobile,
178
+ enrichedValue,
179
+ renderedChildren,
180
+ effectiveAriaLabelledBy,
181
+ };
182
+ };
183
+
184
+ /**
185
+ * The shared shell values consumed by both the styled and the Tailwind desktop
186
+ * shells and by the shared mobile render. Inferred from `usePanelShell` so the
187
+ * `rest` pass-through keeps its precise type (mirrors `TypeDrawerShell`).
188
+ */
189
+ export type TypePanelShell = ReturnType<typeof usePanelShell>;
@@ -0,0 +1,62 @@
1
+ import { PanelContext } from "./PanelContext";
2
+ import { PanelHeader } from "./PanelHeader";
3
+ import { PanelContent } from "./PanelContent";
4
+ import { PanelMobile } from "./PanelMobile";
5
+ import { PanelContainer, PanelInner } from "./styles";
6
+ import { usePanelShell } from "./PanelShared";
7
+ import type { TypePanelProps } from "./PanelTypes";
8
+
9
+ /**
10
+ * Styled-components shell — the backward-compatible fallback. The hybrid router
11
+ * sends consumers here whenever they pass a `COMMON`-mixin styled-system prop
12
+ * (e.g. `mt`, `mb`, `mr`) or a `styled()` extension, so the full styled prop
13
+ * surface on the outer container keeps working exactly as before.
14
+ */
15
+ export const PanelStyled = (props: TypePanelProps) => {
16
+ const shell = usePanelShell(props);
17
+ const {
18
+ rest,
19
+ header,
20
+ headerless,
21
+ footer,
22
+ title,
23
+ titleId,
24
+ direction,
25
+ width,
26
+ gap,
27
+ id,
28
+ ariaLabel,
29
+ isPanelOpen,
30
+ isMobile,
31
+ enrichedValue,
32
+ renderedChildren,
33
+ effectiveAriaLabelledBy,
34
+ } = shell;
35
+
36
+ if (isMobile) {
37
+ return <PanelMobile shell={shell} />;
38
+ }
39
+
40
+ return (
41
+ <PanelContext.Provider value={enrichedValue}>
42
+ <PanelContainer
43
+ {...rest}
44
+ $isOpen={isPanelOpen}
45
+ $direction={direction}
46
+ $width={width}
47
+ $gap={gap}
48
+ data-qa-panel={id}
49
+ data-qa-panel-isopen={isPanelOpen}
50
+ aria-labelledby={effectiveAriaLabelledBy}
51
+ aria-label={ariaLabel}
52
+ >
53
+ <PanelInner $direction={direction} $width={width}>
54
+ {header ??
55
+ (headerless ? null : <PanelHeader title={title} id={titleId} />)}
56
+ <PanelContent>{renderedChildren}</PanelContent>
57
+ {footer}
58
+ </PanelInner>
59
+ </PanelContainer>
60
+ </PanelContext.Provider>
61
+ );
62
+ };
@@ -0,0 +1,86 @@
1
+ import * as React from "react";
2
+ import { PanelContext } from "./PanelContext";
3
+ import { PanelHeader } from "./PanelHeader";
4
+ import { PanelContent } from "./PanelContent";
5
+ import { PanelMobile } from "./PanelMobile";
6
+ import {
7
+ cn,
8
+ usePanelShell,
9
+ getPanelContainerStyle,
10
+ getPanelInnerStyle,
11
+ } from "./PanelShared";
12
+ import type { TypePanelProps } from "./PanelTypes";
13
+
14
+ /**
15
+ * Tailwind/CSS-class shell — the preferred path, routed to by the hybrid when
16
+ * the consumer passes no styled-system props or `styled()` extension. Renders a
17
+ * plain `<aside>` (implicit `complementary` role) with the `seeds-panel` classes
18
+ * from panel.css instead of a styled container. Runtime geometry (flex-basis,
19
+ * gap margin, direction shape, inner min-width/min-height) is applied inline
20
+ * because the jsdom tests assert those as computed styles and panel.css is not
21
+ * loaded there. The mobile path renders identically to the styled shell via
22
+ * the shared `PanelMobile`.
23
+ */
24
+ export const PanelTailwind = (props: TypePanelProps) => {
25
+ const shell = usePanelShell(props);
26
+ const {
27
+ rest,
28
+ header,
29
+ headerless,
30
+ footer,
31
+ title,
32
+ titleId,
33
+ direction,
34
+ width,
35
+ gap,
36
+ id,
37
+ ariaLabel,
38
+ isPanelOpen,
39
+ isMobile,
40
+ enrichedValue,
41
+ renderedChildren,
42
+ effectiveAriaLabelledBy,
43
+ } = shell;
44
+
45
+ if (isMobile) {
46
+ return <PanelMobile shell={shell} />;
47
+ }
48
+
49
+ // `rest` carries only non-styled pass-through props on this path (the gate
50
+ // routes styled-system props to PanelStyled). Peel off className/style so we
51
+ // can merge them onto the component's own class and inline geometry rather
52
+ // than letting a spread clobber them.
53
+ const { className, style, ...domRest } = rest as {
54
+ className?: string;
55
+ style?: React.CSSProperties;
56
+ } & Record<string, unknown>;
57
+
58
+ const containerStyle = getPanelContainerStyle(
59
+ direction,
60
+ width,
61
+ gap,
62
+ isPanelOpen
63
+ );
64
+ const innerStyle = getPanelInnerStyle(direction, width);
65
+
66
+ return (
67
+ <PanelContext.Provider value={enrichedValue}>
68
+ <aside
69
+ {...domRest}
70
+ className={cn("seeds-panel", className)}
71
+ style={{ ...containerStyle, ...style }}
72
+ data-qa-panel={id}
73
+ data-qa-panel-isopen={isPanelOpen}
74
+ aria-labelledby={effectiveAriaLabelledBy}
75
+ aria-label={ariaLabel}
76
+ >
77
+ <div className="seeds-panel-inner" style={innerStyle}>
78
+ {header ??
79
+ (headerless ? null : <PanelHeader title={title} id={titleId} />)}
80
+ <PanelContent>{renderedChildren}</PanelContent>
81
+ {footer}
82
+ </div>
83
+ </aside>
84
+ </PanelContext.Provider>
85
+ );
86
+ };