@galaxy-io/dls 2.10.3 → 2.10.5

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,16 +1,19 @@
1
1
  import Text, { TextSize, TextVariant } from "../text/Text.js";
2
2
  import GalaxyAlchemyWordmark from "./GalaxyAlchemyWordmark.js";
3
3
  import GalaxyFilamentWordmark from "./GalaxyFilamentWordmark.js";
4
+ import { useAppFrameContext } from "../layout/internal/AppFrameContext.js";
4
5
  /* empty css */
5
6
  import { styled } from "@linaria/react";
6
7
  import { forwardRef } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { cx } from "@linaria/core";
8
10
  //#region src/brand/PoweredBy.tsx
9
11
  /**
10
12
  * The credit at the foot of a Galaxy module's sidebar: "Powered by" and the product wordmark, as
11
13
  * one link that opens the product's site in a new tab. A 40px row (tall enough to centre on the
12
14
  * Galaxy host's 24px theme toggle beside it) with `body_sm` tertiary text and the wordmark sized so
13
- * every product reads at the same x-height (Filament 15px, Alchemy 12px); its accessible name is
15
+ * every product reads at the same x-height (Filament 15px, Alchemy 12px), its letters on the text's
16
+ * baseline (an inline mark shifted by its own descent, not two boxes centred); its accessible name is
14
17
  * `ariaLabel` plus " (opens in a new tab)".
15
18
  *
16
19
  * `wordmark` is an enum of the product wordmarks, never a node: a product wordmark always stands
@@ -18,8 +21,9 @@ import { jsx, jsxs } from "react/jsx-runtime";
18
21
  * (`Link isExternal` would draw one after the logo); the new tab is in the name.
19
22
  *
20
23
  * Hover is a monochrome step on the mark, `text.tertiary` to `text.secondary`, the DLS's own hover
21
- * idiom (a color change, never opacity). Focus is the `FOCUS_RING`. Not a nav destination
22
- * (`NavItem`), not a body link (`Link`).
24
+ * idiom (a color change, never opacity). Focus is the `FOCUS_RING`. In a collapsed `AppFrame` it
25
+ * renders nothing: the icon rail has no room for the credit. Not a nav destination (`NavItem`),
26
+ * not a body link (`Link`).
23
27
  */
24
28
  /** The product whose wordmark the credit shows. */
25
29
  var PoweredByWordmark = /* @__PURE__ */ function(PoweredByWordmark) {
@@ -41,15 +45,30 @@ var POWERED_BY_WORDMARK_TO_SIZE_PX_MAP = {
41
45
  ["FILAMENT"]: 15,
42
46
  ["ALCHEMY"]: 12
43
47
  };
48
+ /**
49
+ * How far each wordmark's letters hang below their baseline, as a fraction of the artwork's
50
+ * height: Filament's italic f tail (1200 − 910.55 of 1109 units), Alchemy's y (252 − 201.35 of
51
+ * 252). Measured from the artwork's ink, so the mark can sit on the text's own baseline.
52
+ */
53
+ /** The baseline shift per wordmark: its letters' descent in px at its size, below the text's baseline. */
54
+ /**
55
+ * Each wordmark on the text's baseline: inline (the wordmarks are blocks), an SVG's bottom edge
56
+ * sits on the baseline, so `vertical-align` drops it by the letters' descent. The browser places the baseline (font metrics,
57
+ * pixel snapping), so the mark and "Powered by" always share it.
58
+ */
59
+ var POWERED_BY_WORDMARK_TO_BASELINE_CSS_MAP = {
60
+ ["FILAMENT"]: "gx-PoweredBy-FILAMENT-f1hilrad",
61
+ ["ALCHEMY"]: "gx-PoweredBy-ALCHEMY-a1s356ob"
62
+ };
44
63
  /** What is appended to the name, as `Link isExternal` does. */
45
64
  var NEW_TAB_SUFFIX = " (opens in a new tab)";
46
65
  /**
47
- * The row: 40px (32 + 8), 8px in from the sides like a `NavItem`'s content, the text and the mark
48
- * 8px apart on one centre line. The radius gives the focus ring its corners.
66
+ * The row: 40px (32 + 8), 8px in from the sides like a `NavItem`'s content. The text and the mark
67
+ * share one line (and so one baseline), 8px apart. The radius gives the focus ring its corners.
49
68
  */
50
69
  var Root = /*#__PURE__*/ styled("a")({
51
70
  name: "Root",
52
- class: "gx-PoweredBy-r1hilrad",
71
+ class: "gx-PoweredBy-r1ez0mfr",
53
72
  propsAsIs: false
54
73
  });
55
74
  /**
@@ -57,18 +76,31 @@ var Root = /*#__PURE__*/ styled("a")({
57
76
  * already transitions `color` on `duration.fast`. Declared in this module, whose CSS follows the
58
77
  * wordmarks' (it imports them), so it wins over their `text.primary`.
59
78
  */
60
- var MARK_CSS = "gx-PoweredBy-MARK_CSS-m1s356ob";
79
+ var MARK_CSS = "gx-PoweredBy-MARK_CSS-mi05aex";
61
80
  /** The wordmark component per product: both take a px `size`, an `ariaLabel` and a class. */
62
81
  var POWERED_BY_WORDMARK_TO_COMPONENT_MAP = {
63
82
  ["FILAMENT"]: GalaxyFilamentWordmark,
64
83
  ["ALCHEMY"]: GalaxyAlchemyWordmark
65
84
  };
66
- /** "Powered by" never wraps or shrinks: the row is short and the mark follows it. */
67
- var LABEL_CSS = "gx-PoweredBy-LABEL_CSS-l1ez0mfr";
85
+ /**
86
+ * The line holding "Powered by" and the mark. It never wraps or shrinks, and its line height is the
87
+ * row's 40px, so the mark's descent stays inside the line box and never moves the text: the baseline
88
+ * lands where a centred `body_sm` line puts it, level with the middle of an icon beside the row.
89
+ * Declared after Text's own CSS (this module imports it), so the line height wins.
90
+ */
91
+ var LABEL_CSS = "gx-PoweredBy-LABEL_CSS-llnwtgc";
92
+ /** The 8px between "Powered by" and the mark, as inline padding (no margins). */
93
+ var MarkSlot = /*#__PURE__*/ styled("span")({
94
+ name: "MarkSlot",
95
+ class: "gx-PoweredBy-MarkSlot-mh367b4",
96
+ propsAsIs: false
97
+ });
68
98
  /** The sidebar credit: "Powered by" and a product wordmark, linking to the product in a new tab. */
69
99
  var PoweredBy = forwardRef(function PoweredBy({ wordmark, href, ariaLabel, id, className, style, ...rest }, ref) {
100
+ const frame = useAppFrameContext();
70
101
  const Wordmark = POWERED_BY_WORDMARK_TO_COMPONENT_MAP[wordmark];
71
- return /* @__PURE__ */ jsxs(Root, {
102
+ if (frame?.isCollapsed) return null;
103
+ return /* @__PURE__ */ jsx(Root, {
72
104
  "aria-label": `${ariaLabel}${NEW_TAB_SUFFIX}`,
73
105
  ...rest,
74
106
  ref,
@@ -78,17 +110,17 @@ var PoweredBy = forwardRef(function PoweredBy({ wordmark, href, ariaLabel, id, c
78
110
  rel: "noopener noreferrer",
79
111
  className,
80
112
  style,
81
- children: [/* @__PURE__ */ jsx(Text, {
113
+ children: /* @__PURE__ */ jsxs(Text, {
82
114
  as: "span",
83
115
  size: TextSize.BODY_SM,
84
116
  variant: TextVariant.TERTIARY,
85
117
  className: LABEL_CSS,
86
- children: "Powered by"
87
- }), /* @__PURE__ */ jsx(Wordmark, {
88
- size: POWERED_BY_WORDMARK_TO_SIZE_PX_MAP[wordmark],
89
- ariaLabel: "",
90
- className: MARK_CSS
91
- })]
118
+ children: ["Powered by", /* @__PURE__ */ jsx(MarkSlot, { children: /* @__PURE__ */ jsx(Wordmark, {
119
+ size: POWERED_BY_WORDMARK_TO_SIZE_PX_MAP[wordmark],
120
+ ariaLabel: "",
121
+ className: cx(MARK_CSS, POWERED_BY_WORDMARK_TO_BASELINE_CSS_MAP[wordmark])
122
+ }) })]
123
+ })
92
124
  });
93
125
  });
94
126
  PoweredBy.displayName = "PoweredBy";
@@ -123,7 +123,7 @@ var CRON_INPUT_FOCUS_RETRY_FRAMES = 10;
123
123
  /** The neutral hint shown when `cronstrue` cannot read the expression. */
124
124
  var CRON_INPUT_HINT = "Enter five fields: minute, hour, day of month, month, day of week.";
125
125
  /** The caption on the divider between the builder and the expression. */
126
- var CRON_INPUT_DIVIDER_LABEL = "or";
126
+ var CRON_INPUT_DIVIDER_LABEL = "OR";
127
127
  /** Each builder row's label; the label column is as wide as the longest, whichever rows show. */
128
128
  var ROW_LABELS = {
129
129
  frequency: "Frequency",
@@ -1,9 +1,28 @@
1
1
  import { type AriaAttributes, type CSSProperties, type ReactNode } from "react";
2
- /** The sidebar column's width in px, the same in every Galaxy module (Filament, Alchemy). */
3
- export declare const APP_FRAME_SIDEBAR_WIDTH = 240;
2
+ /** The sidebar column's default width in px, the same in every Galaxy module (Filament, Alchemy). */
3
+ export declare const APP_FRAME_SIDEBAR_WIDTH = 220;
4
+ /**
5
+ * The collapsed column's width in px: 8px inset, SidebarNav's 8px, a 32px rail item, 8px, 8px. A
6
+ * rail icon's centre lands 1px from where the expanded item drew it.
7
+ */
8
+ export declare const APP_FRAME_SIDEBAR_COLLAPSED_WIDTH = 64;
4
9
  export interface AppFrameProps extends AriaAttributes {
5
10
  /** The sidebar column's content: a `SidebarNav` with no `header`. It fills the column. */
6
11
  sidebar: ReactNode;
12
+ /**
13
+ * The sidebar column's width while expanded, inset included. A number is px.
14
+ * @default APP_FRAME_SIDEBAR_WIDTH (220)
15
+ */
16
+ sidebarWidth?: number | string;
17
+ /** Controlled: the sidebar is the icon rail (`APP_FRAME_SIDEBAR_COLLAPSED_WIDTH`). */
18
+ isCollapsed?: boolean;
19
+ /**
20
+ * Uncontrolled: whether the sidebar starts as the icon rail.
21
+ * @default false
22
+ */
23
+ defaultIsCollapsed?: boolean;
24
+ /** Called with the next collapsed state when `AppFrameToggle` is pressed. */
25
+ onCollapsedChange?: (isCollapsed: boolean) => void;
7
26
  /** The page, usually a `PageLayout`. The column is a flex column that never overflows the frame. */
8
27
  children: ReactNode;
9
28
  /** DOM id on the root `<div>`. */
@@ -17,4 +36,23 @@ export interface AppFrameProps extends AriaAttributes {
17
36
  }
18
37
  /** A Galaxy module's frame: the sidebar column, a hairline, and the page. */
19
38
  declare const AppFrame: import("react").ForwardRefExoticComponent<AppFrameProps & import("react").RefAttributes<HTMLDivElement>>;
39
+ export interface AppFrameToggleProps extends AriaAttributes {
40
+ /** DOM id on the `<button>`. */
41
+ id?: string;
42
+ /** Any `data-*` attribute, forwarded to the `<button>`. */
43
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
44
+ /** Extra class on the `<button>`. */
45
+ className?: string;
46
+ /** Inline style on the `<button>`; only for setting CSS custom properties. */
47
+ style?: CSSProperties;
48
+ }
49
+ /**
50
+ * The button that collapses and expands the enclosing AppFrame's sidebar: a `TERTIARY` `SMALL`
51
+ * icon button (`SidebarSimpleIcon`, the host's own), named and tooltipped "Collapse sidebar" /
52
+ * "Expand sidebar", with `aria-expanded` and `aria-controls` on the column. Mod+B presses it (the
53
+ * tooltip shows the shortcut, `aria-keyshortcuts` announces it), also from inside a field. Put it in the
54
+ * SidebarNav footer, opposite `PoweredBy` in a `SPACE_BETWEEN` row; on the rail PoweredBy steps out
55
+ * and the row centres the toggle under the icons. Renders nothing outside an AppFrame.
56
+ */
57
+ export declare const AppFrameToggle: import("react").ForwardRefExoticComponent<AppFrameToggleProps & import("react").RefAttributes<HTMLButtonElement>>;
20
58
  export default AppFrame;
@@ -1,9 +1,14 @@
1
1
  import { Orientation } from "../theme/enums.js";
2
+ import { useControlledState } from "../hooks/useControlledState.js";
3
+ import { getCssSizeValue } from "../utils/css.js";
4
+ import { AppFrameContext, useAppFrameContext } from "./internal/AppFrameContext.js";
2
5
  import Divider from "./Divider.js";
6
+ import Button, { ButtonSize, ButtonVariant } from "../buttons/Button.js";
3
7
  /* empty css */
4
8
  import { styled } from "@linaria/react";
5
- import { forwardRef } from "react";
9
+ import { forwardRef, useId, useMemo } from "react";
6
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { SidebarSimpleIcon } from "@phosphor-icons/react";
7
12
  //#region src/layout/AppFrame.tsx
8
13
  /**
9
14
  * The product frame of a Galaxy module: a fixed sidebar column, a vertical hairline, and a content
@@ -23,12 +28,29 @@ import { jsx, jsxs } from "react/jsx-runtime";
23
28
  * it a `footer` and its items only, and `hasDividers={false}` when the footer should run on
24
29
  * without a line.
25
30
  *
26
- * There is no width prop (layout props live on Box, Flex, FlexItem and Grid): the sidebar is
27
- * `APP_FRAME_SIDEBAR_WIDTH`, 240px, the width every Galaxy module uses. Not a page (`PageLayout`),
28
- * not resizable panes (`ResizablePanels`).
31
+ * The sidebar is `APP_FRAME_SIDEBAR_WIDTH`, 220px, the width every Galaxy module uses;
32
+ * `sidebarWidth` changes it (a wrapper cannot reach the column, so it is the one layout prop here).
33
+ *
34
+ * **Collapsing.** `isCollapsed` / `defaultIsCollapsed` / `onCollapsedChange` narrow the column to
35
+ * the icon rail, `APP_FRAME_SIDEBAR_COLLAPSED_WIDTH` (64px: the inset, SidebarNav's padding and a
36
+ * 32px item), so the icons barely move. The width eases over `duration.base` (the Galaxy host's own
37
+ * sidebar motion) and the column clips while it moves; reduced motion snaps it. A SidebarNav in the
38
+ * column follows the frame (its own `isCollapsed` still wins), `PoweredBy` steps out on the rail,
39
+ * and `AppFrameToggle` is the button that flips it: put it in the nav's footer, at the end of the
40
+ * PoweredBy row; it also binds Mod+B (⌘B on Mac, Ctrl+B elsewhere), the Galaxy host's own sidebar
41
+ * shortcut, while it is mounted.
42
+ *
43
+ * Not a page (`PageLayout`), not resizable panes (`ResizablePanels`).
44
+ */
45
+ /** The collapse shortcut: Mod+B, ⌘B on Mac and Ctrl+B elsewhere, as in the Galaxy host. */
46
+ var APP_FRAME_TOGGLE_HOT_KEYS = ["mod", "b"];
47
+ /** The sidebar column's default width in px, the same in every Galaxy module (Filament, Alchemy). */
48
+ var APP_FRAME_SIDEBAR_WIDTH = 220;
49
+ /**
50
+ * The collapsed column's width in px: 8px inset, SidebarNav's 8px, a 32px rail item, 8px, 8px. A
51
+ * rail icon's centre lands 1px from where the expanded item drew it.
29
52
  */
30
- /** The sidebar column's width in px, the same in every Galaxy module (Filament, Alchemy). */
31
- var APP_FRAME_SIDEBAR_WIDTH = 240;
53
+ var APP_FRAME_SIDEBAR_COLLAPSED_WIDTH = 64;
32
54
  /** The frame: a row that fills the host's box on the sidebar's fill, clipped. */
33
55
  var Root = /*#__PURE__*/ styled("div")({
34
56
  name: "Root",
@@ -36,9 +58,10 @@ var Root = /*#__PURE__*/ styled("div")({
36
58
  propsAsIs: false
37
59
  });
38
60
  /**
39
- * The sidebar column: fixed width, inset `8 8 0`. With SidebarNav's own 8px the first item sits
40
- * 16px from the top and both sides, on the Galaxy host's Home button (the host contract), and the
41
- * footer runs to the bottom edge.
61
+ * The sidebar column: fixed width (`sidebarWidth`, or the rail), inset `8 8 0`. With SidebarNav's
62
+ * own 8px the first item sits 16px from the top and both sides, on the Galaxy host's Home button
63
+ * (the host contract), and the footer runs to the bottom edge. It clips so the labels never spill
64
+ * while the width moves.
42
65
  */
43
66
  var Sidebar = /*#__PURE__*/ styled("div")({
44
67
  name: "Sidebar",
@@ -52,20 +75,72 @@ var Content = /*#__PURE__*/ styled("div")({
52
75
  propsAsIs: false
53
76
  });
54
77
  /** A Galaxy module's frame: the sidebar column, a hairline, and the page. */
55
- var AppFrame = forwardRef(function AppFrame({ sidebar, children, id, className, style, ...rest }, ref) {
56
- return /* @__PURE__ */ jsxs(Root, {
57
- ...rest,
58
- ref,
59
- id,
60
- className,
61
- style,
62
- children: [
63
- /* @__PURE__ */ jsx(Sidebar, { children: sidebar }),
64
- /* @__PURE__ */ jsx(Divider, { orientation: Orientation.VERTICAL }),
65
- /* @__PURE__ */ jsx(Content, { children })
66
- ]
78
+ var AppFrame = forwardRef(function AppFrame({ sidebar, sidebarWidth = 220, isCollapsed: isCollapsedProp, defaultIsCollapsed = false, onCollapsedChange, children, id, className, style, ...rest }, ref) {
79
+ const [isCollapsed, setIsCollapsed] = useControlledState({
80
+ value: isCollapsedProp,
81
+ defaultValue: defaultIsCollapsed,
82
+ onChange: onCollapsedChange
83
+ });
84
+ const sidebarId = useId();
85
+ const context = useMemo(() => ({
86
+ isCollapsed,
87
+ setIsCollapsed,
88
+ sidebarId
89
+ }), [
90
+ isCollapsed,
91
+ setIsCollapsed,
92
+ sidebarId
93
+ ]);
94
+ const width = isCollapsed ? `64px` : getCssSizeValue(sidebarWidth);
95
+ return /* @__PURE__ */ jsx(AppFrameContext.Provider, {
96
+ value: context,
97
+ children: /* @__PURE__ */ jsxs(Root, {
98
+ ...rest,
99
+ ref,
100
+ id,
101
+ className,
102
+ style,
103
+ children: [
104
+ /* @__PURE__ */ jsx(Sidebar, {
105
+ id: sidebarId,
106
+ style: { "--gx-app-frame-sidebar-width": width },
107
+ children: sidebar
108
+ }),
109
+ /* @__PURE__ */ jsx(Divider, { orientation: Orientation.VERTICAL }),
110
+ /* @__PURE__ */ jsx(Content, { children })
111
+ ]
112
+ })
67
113
  });
68
114
  });
69
115
  AppFrame.displayName = "AppFrame";
116
+ /**
117
+ * The button that collapses and expands the enclosing AppFrame's sidebar: a `TERTIARY` `SMALL`
118
+ * icon button (`SidebarSimpleIcon`, the host's own), named and tooltipped "Collapse sidebar" /
119
+ * "Expand sidebar", with `aria-expanded` and `aria-controls` on the column. Mod+B presses it (the
120
+ * tooltip shows the shortcut, `aria-keyshortcuts` announces it), also from inside a field. Put it in the
121
+ * SidebarNav footer, opposite `PoweredBy` in a `SPACE_BETWEEN` row; on the rail PoweredBy steps out
122
+ * and the row centres the toggle under the icons. Renders nothing outside an AppFrame.
123
+ */
124
+ var AppFrameToggle = forwardRef(function AppFrameToggle(props, ref) {
125
+ const frame = useAppFrameContext();
126
+ if (frame === null) return null;
127
+ const { isCollapsed, setIsCollapsed, sidebarId } = frame;
128
+ const label = isCollapsed ? "Expand sidebar" : "Collapse sidebar";
129
+ return /* @__PURE__ */ jsx(Button, {
130
+ "aria-expanded": !isCollapsed,
131
+ "aria-controls": sidebarId,
132
+ ...props,
133
+ ref,
134
+ variant: ButtonVariant.TERTIARY,
135
+ size: ButtonSize.SMALL,
136
+ icon: SidebarSimpleIcon,
137
+ ariaLabel: label,
138
+ tooltip: label,
139
+ hotKeys: APP_FRAME_TOGGLE_HOT_KEYS,
140
+ shouldBindHotKey: true,
141
+ onClick: () => setIsCollapsed(!isCollapsed)
142
+ });
143
+ });
144
+ AppFrameToggle.displayName = "AppFrameToggle";
70
145
  //#endregion
71
- export { APP_FRAME_SIDEBAR_WIDTH, AppFrame as default };
146
+ export { APP_FRAME_SIDEBAR_COLLAPSED_WIDTH, APP_FRAME_SIDEBAR_WIDTH, AppFrameToggle, AppFrame as default };
@@ -0,0 +1,9 @@
1
+ export interface AppFrameContextValue {
2
+ isCollapsed: boolean;
3
+ setIsCollapsed: (isCollapsed: boolean) => void;
4
+ /** The sidebar column's DOM id, for the toggle's `aria-controls`. */
5
+ sidebarId: string;
6
+ }
7
+ export declare const AppFrameContext: import("react").Context<AppFrameContextValue | null>;
8
+ /** The enclosing AppFrame's collapse state, or `null` outside one. */
9
+ export declare function useAppFrameContext(): AppFrameContextValue | null;
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/layout/internal/AppFrameContext.ts
3
+ /**
4
+ * What AppFrame shares with the parts in its sidebar column: whether the column is the icon rail,
5
+ * and the setter `AppFrameToggle` calls. `SidebarNav` reads `isCollapsed` when its own prop is
6
+ * unset; `PoweredBy` renders nothing on the rail. `null` outside an AppFrame.
7
+ */
8
+ var AppFrameContext = createContext(null);
9
+ /** The enclosing AppFrame's collapse state, or `null` outside one. */
10
+ function useAppFrameContext() {
11
+ return useContext(AppFrameContext);
12
+ }
13
+ //#endregion
14
+ export { AppFrameContext, useAppFrameContext };
@@ -13,7 +13,7 @@ export interface SidebarNavProps extends AriaAttributes {
13
13
  */
14
14
  hasDividers?: boolean;
15
15
  /**
16
- * Icon rail: items show only their icon (labels and counts move into tooltips), group labels become dividers. Give the parent a 48px width.
16
+ * Icon rail: items show only their icon (labels and counts move into tooltips), group labels become dividers. Give the parent a 48px width. Inside an `AppFrame` it follows the frame's collapsed state when unset.
17
17
  * @default false
18
18
  */
19
19
  isCollapsed?: boolean;
@@ -3,6 +3,7 @@ import { Placement } from "../theme/enums.js";
3
3
  import Text, { TextSize, TextTransform, TextVariant, TextWeight } from "../text/Text.js";
4
4
  import { useControlledOpenState } from "../hooks/useControlledOpenState.js";
5
5
  import Tooltip from "../tooltip/Tooltip.js";
6
+ import { useAppFrameContext } from "../layout/internal/AppFrameContext.js";
6
7
  import Divider from "../layout/Divider.js";
7
8
  import ScrollArea from "../layout/ScrollArea.js";
8
9
  import Icon, { IconVariant } from "../icons/Icon.js";
@@ -62,6 +63,7 @@ var Header = /*#__PURE__*/ styled("div")({
62
63
  class: "gx-SidebarNav-Header-h7sesvp",
63
64
  propsAsIs: false
64
65
  });
66
+ /** Items stand 8px apart, expanded and on the rail: the Galaxy host's own sidebar rhythm. */
65
67
  var List = /*#__PURE__*/ styled("ul")({
66
68
  name: "List",
67
69
  class: "gx-SidebarNav-List-lxg8t2f",
@@ -130,7 +132,11 @@ var GroupDivider = /*#__PURE__*/ styled("div")({
130
132
  class: "gx-SidebarNav-GroupDivider-gyqt4ix",
131
133
  propsAsIs: false
132
134
  });
133
- /** Rail mode: the header content and the footer content center in the 48px column. */
135
+ /**
136
+ * Rail mode: the header content and the footer content center in the 48px column. A section drops
137
+ * its 12px top padding and spaces its divider row like an item: the 24px row centres the line, so
138
+ * the space above it (the list's 8px gap + 11.5px) matches the space below (11.5px + 8px).
139
+ */
134
140
  var ROOT_COLLAPSED_CSS = "gx-SidebarNav-ROOT_COLLAPSED_CSS-rieaizt";
135
141
  /** The scrolling list takes the height between header and footer. */
136
142
  var LIST_SCROLL_CSS = "gx-SidebarNav-LIST_SCROLL_CSS-lo6ca5i";
@@ -145,7 +151,9 @@ var ITEM_COLLAPSED_CSS = "gx-SidebarNav-ITEM_COLLAPSED_CSS-i1ietnnw";
145
151
  var LABEL_CSS = "gx-SidebarNav-LABEL_CSS-l1d4bor0";
146
152
  var VISUALLY_HIDDEN_CSS = "gx-SidebarNav-VISUALLY_HIDDEN_CSS-v1rmtlmv";
147
153
  /** The primary navigation column: header, scrolling list of items and sections, footer. */
148
- var SidebarNav = forwardRef(function SidebarNav({ children, header, footer, hasDividers = true, isCollapsed = false, ariaLabel, id, className, style, ...rest }, ref) {
154
+ var SidebarNav = forwardRef(function SidebarNav({ children, header, footer, hasDividers = true, isCollapsed: isCollapsedProp, ariaLabel, id, className, style, ...rest }, ref) {
155
+ const frame = useAppFrameContext();
156
+ const isCollapsed = isCollapsedProp ?? frame?.isCollapsed ?? false;
149
157
  const name = ariaLabel || rest["aria-label"] || (rest["aria-labelledby"] ? void 0 : DEFAULT_ARIA_LABEL);
150
158
  const listContext = useMemo(() => ({
151
159
  isCollapsed,