@wallarm-org/design-system 1.24.0 → 1.26.0

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.
Files changed (41) hide show
  1. package/README.md +2 -0
  2. package/dist/components/AppShell/story-content/_storyAccountDropdown.d.ts +5 -2
  3. package/dist/components/AppShell/story-content/_storyAccountDropdown.js +110 -55
  4. package/dist/components/AppShell/story-content/_storyConfigRenderer.d.ts +4 -1
  5. package/dist/components/AppShell/story-content/_storyConfigRenderer.js +8 -5
  6. package/dist/components/AppShell/story-content/index.d.ts +1 -1
  7. package/dist/components/AppShell/story-content/index.js +1 -1
  8. package/dist/components/DropdownMenu/DropdownMenuRadioItem.js +1 -1
  9. package/dist/components/NavPanel/NavPanelBack.js +1 -1
  10. package/dist/components/NavPanel/NavPanelGroupItem.js +8 -1
  11. package/dist/components/NavPanel/NavPanelGroupLabel.js +1 -1
  12. package/dist/components/NavPanel/NavPanelSkeleton.js +1 -1
  13. package/dist/components/NavPanel/classes.js +6 -6
  14. package/dist/components/NavRail/NavRail.d.ts +7 -0
  15. package/dist/components/NavRail/NavRail.js +5 -3
  16. package/dist/components/NavRail/NavRailBody.js +6 -1
  17. package/dist/components/NavRail/NavRailContext.d.ts +2 -1
  18. package/dist/components/NavRail/NavRailContext.js +1 -1
  19. package/dist/components/NavRail/NavRailFooter.js +6 -1
  20. package/dist/components/NavRail/NavRailItem.d.ts +4 -0
  21. package/dist/components/NavRail/NavRailItem.js +33 -7
  22. package/dist/components/NavRail/NavRailSkeleton.js +24 -4
  23. package/dist/components/NavRail/classes.d.ts +13 -1
  24. package/dist/components/NavRail/classes.js +55 -9
  25. package/dist/components/NavRail/index.d.ts +1 -1
  26. package/dist/components/SearchModal/SearchModal.js +1 -0
  27. package/dist/components/Skeleton/Skeleton.d.ts +1 -1
  28. package/dist/components/Skeleton/Skeleton.js +1 -0
  29. package/dist/components/ThemeProvider/ThemeProvider.d.ts +4 -1
  30. package/dist/components/ThemeProvider/ThemeProvider.js +18 -2
  31. package/dist/components/ThemeProvider/ThemeProviderContext.d.ts +3 -1
  32. package/dist/components/ThemeProvider/ThemeProviderContext.js +3 -1
  33. package/dist/components/ThemeProvider/index.d.ts +1 -1
  34. package/dist/components/ThemeProvider/types.d.ts +2 -0
  35. package/dist/index.d.ts +2 -2
  36. package/dist/metadata/components.json +56 -9
  37. package/dist/theme/components/app-shell.css +11 -0
  38. package/dist/theme/index.css +4 -0
  39. package/dist/theme/radius.css +1 -0
  40. package/dist/theme/semantic.css +79 -0
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -37,6 +37,8 @@ export const App = () => {
37
37
  };
38
38
  ```
39
39
 
40
+ `ThemeProvider` also controls the Frame style (`useTheme().setFrameStyle`, `'neutral' | 'branded'`) - see [Installation Guide](docs/installation.md) for details.
41
+
40
42
  ```css
41
43
  /* App.css */
42
44
  @import 'tailwindcss';
@@ -1,5 +1,8 @@
1
- import type { Theme } from '../../ThemeProvider';
2
- export type SidebarMode = 'adaptive' | 'expanded';
1
+ import { type NavRailMode } from '../../NavRail';
2
+ import { type Theme } from '../../ThemeProvider';
3
+ export type SidebarMode = 'adaptive' | 'expanded' | 'collapsed' | 'compact';
4
+ /** Adaptive collapses the rail inside a product and expands it on Home; the other modes pin it. */
5
+ export declare const railModeFor: (mode: SidebarMode, onHome: boolean) => NavRailMode;
3
6
  export declare const AccountDropdown: ({ sidebarMode, onSidebarModeChange, theme, onThemeChange, }: {
4
7
  sidebarMode: SidebarMode;
5
8
  onSidebarModeChange: (mode: SidebarMode) => void;
@@ -1,20 +1,60 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Check, LogOut, PanelLeftDashed, PencilRuler, Settings, User } from "../../../icons/index.js";
3
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemContent, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, DropdownMenuTriggerItem } from "../../DropdownMenu/index.js";
2
+ import { Fragment } from "react";
3
+ import { LogOut, PanelLeftDashed, PencilRuler, Settings, User, UserRound } from "../../../icons/index.js";
4
+ import { cn } from "../../../utils/cn.js";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuItemContent, DropdownMenuItemIcon, DropdownMenuItemText, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuTriggerItem } from "../../DropdownMenu/index.js";
4
6
  import { NavRailItem } from "../../NavRail/index.js";
5
7
  import { HStack, VStack } from "../../Stack/index.js";
6
8
  import { Text } from "../../Text/index.js";
9
+ import { useTheme } from "../../ThemeProvider/index.js";
10
+ const railModeFor = (mode, onHome)=>{
11
+ if ('adaptive' === mode) return onHome ? 'expanded' : 'collapsed';
12
+ return mode;
13
+ };
7
14
  const USER_NAME = 'Meow Meow';
8
15
  const USER_EMAIL = 'meow@meow.com';
16
+ const THEMES = [
17
+ 'light',
18
+ 'dark'
19
+ ];
20
+ const SIDEBAR_MODE_GROUPS = [
21
+ [
22
+ 'adaptive'
23
+ ],
24
+ [
25
+ 'expanded',
26
+ 'collapsed'
27
+ ],
28
+ [
29
+ 'compact'
30
+ ]
31
+ ];
32
+ const SIDEBAR_MODES = SIDEBAR_MODE_GROUPS.flat();
9
33
  const THEME_LABELS = {
10
34
  light: 'Light',
11
35
  dark: 'Dark'
12
36
  };
37
+ const FRAME_STYLES = [
38
+ 'neutral',
39
+ 'branded'
40
+ ];
41
+ const FRAME_STYLE_LABELS = {
42
+ neutral: 'Neutral',
43
+ branded: 'Branded'
44
+ };
45
+ const FRAME_STYLE_SWATCH = {
46
+ neutral: 'bg-icon-secondary',
47
+ branded: 'bg-icon-brand'
48
+ };
13
49
  const SIDEBAR_MODE_LABELS = {
14
50
  adaptive: 'Adaptive',
15
- expanded: 'Always expanded'
51
+ expanded: 'Always expanded',
52
+ collapsed: 'Always collapsed',
53
+ compact: 'Compact'
16
54
  };
17
- const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChange })=>/*#__PURE__*/ jsxs(DropdownMenu, {
55
+ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChange })=>{
56
+ const { frameStyle, setFrameStyle } = useTheme();
57
+ return /*#__PURE__*/ jsxs(DropdownMenu, {
18
58
  positioning: {
19
59
  placement: 'right-end',
20
60
  gutter: 6,
@@ -25,8 +65,9 @@ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChang
25
65
  children: [
26
66
  /*#__PURE__*/ jsx(DropdownMenuTrigger, {
27
67
  children: /*#__PURE__*/ jsx(NavRailItem, {
28
- icon: User,
29
- label: USER_NAME
68
+ icon: UserRound,
69
+ label: USER_NAME,
70
+ avatar: true
30
71
  })
31
72
  }),
32
73
  /*#__PURE__*/ jsxs(DropdownMenuContent, {
@@ -79,12 +120,16 @@ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChang
79
120
  /*#__PURE__*/ jsxs(DropdownMenuItemContent, {
80
121
  children: [
81
122
  /*#__PURE__*/ jsx(DropdownMenuItemText, {
82
- children: "Theme"
123
+ children: "Appearance"
83
124
  }),
84
- /*#__PURE__*/ jsx(Text, {
125
+ /*#__PURE__*/ jsxs(Text, {
85
126
  size: "xs",
86
127
  color: "secondary",
87
- children: THEME_LABELS[theme]
128
+ children: [
129
+ THEME_LABELS[theme],
130
+ " and ",
131
+ FRAME_STYLE_LABELS[frameStyle].toLowerCase()
132
+ ]
88
133
  })
89
134
  ]
90
135
  })
@@ -93,27 +138,42 @@ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChang
93
138
  /*#__PURE__*/ jsxs(DropdownMenuContent, {
94
139
  className: "w-132",
95
140
  children: [
96
- /*#__PURE__*/ jsxs(DropdownMenuItem, {
97
- onSelect: ()=>onThemeChange('light'),
98
- children: [
99
- /*#__PURE__*/ jsx(DropdownMenuItemText, {
100
- children: "Light"
101
- }),
102
- 'light' === theme && /*#__PURE__*/ jsx(DropdownMenuShortcut, {
103
- children: /*#__PURE__*/ jsx(Check, {})
104
- })
105
- ]
141
+ /*#__PURE__*/ jsx(DropdownMenuRadioGroup, {
142
+ value: theme,
143
+ onValueChange: ({ value })=>{
144
+ const next = THEMES.find((t)=>t === value);
145
+ if (next) onThemeChange(next);
146
+ },
147
+ children: THEMES.map((t)=>/*#__PURE__*/ jsx(DropdownMenuRadioItem, {
148
+ value: t,
149
+ children: /*#__PURE__*/ jsx(DropdownMenuItemText, {
150
+ children: THEME_LABELS[t]
151
+ })
152
+ }, t))
106
153
  }),
107
- /*#__PURE__*/ jsxs(DropdownMenuItem, {
108
- onSelect: ()=>onThemeChange('dark'),
109
- children: [
110
- /*#__PURE__*/ jsx(DropdownMenuItemText, {
111
- children: "Dark"
112
- }),
113
- 'dark' === theme && /*#__PURE__*/ jsx(DropdownMenuShortcut, {
114
- children: /*#__PURE__*/ jsx(Check, {})
115
- })
116
- ]
154
+ /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
155
+ /*#__PURE__*/ jsx(DropdownMenuRadioGroup, {
156
+ value: frameStyle,
157
+ onValueChange: ({ value })=>{
158
+ const next = FRAME_STYLES.find((s)=>s === value);
159
+ if (next) setFrameStyle(next);
160
+ },
161
+ children: FRAME_STYLES.map((s)=>/*#__PURE__*/ jsxs(DropdownMenuRadioItem, {
162
+ value: s,
163
+ children: [
164
+ /*#__PURE__*/ jsx(DropdownMenuItemIcon, {
165
+ children: /*#__PURE__*/ jsx("span", {
166
+ className: "flex size-16 items-center justify-center",
167
+ children: /*#__PURE__*/ jsx("span", {
168
+ className: cn('size-8 rounded-full', FRAME_STYLE_SWATCH[s])
169
+ })
170
+ })
171
+ }),
172
+ /*#__PURE__*/ jsx(DropdownMenuItemText, {
173
+ children: FRAME_STYLE_LABELS[s]
174
+ })
175
+ ]
176
+ }, s))
117
177
  })
118
178
  ]
119
179
  })
@@ -140,32 +200,26 @@ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChang
140
200
  })
141
201
  ]
142
202
  }),
143
- /*#__PURE__*/ jsxs(DropdownMenuContent, {
203
+ /*#__PURE__*/ jsx(DropdownMenuContent, {
144
204
  className: "w-200",
145
- children: [
146
- /*#__PURE__*/ jsxs(DropdownMenuItem, {
147
- onSelect: ()=>onSidebarModeChange('adaptive'),
148
- children: [
149
- /*#__PURE__*/ jsx(DropdownMenuItemText, {
150
- children: "Adaptive"
151
- }),
152
- 'adaptive' === sidebarMode && /*#__PURE__*/ jsx(DropdownMenuShortcut, {
153
- children: /*#__PURE__*/ jsx(Check, {})
154
- })
155
- ]
156
- }),
157
- /*#__PURE__*/ jsxs(DropdownMenuItem, {
158
- onSelect: ()=>onSidebarModeChange('expanded'),
159
- children: [
160
- /*#__PURE__*/ jsx(DropdownMenuItemText, {
161
- children: "Always expanded"
162
- }),
163
- 'expanded' === sidebarMode && /*#__PURE__*/ jsx(DropdownMenuShortcut, {
164
- children: /*#__PURE__*/ jsx(Check, {})
165
- })
166
- ]
167
- })
168
- ]
205
+ children: /*#__PURE__*/ jsx(DropdownMenuRadioGroup, {
206
+ value: sidebarMode,
207
+ onValueChange: ({ value })=>{
208
+ const next = SIDEBAR_MODES.find((m)=>m === value);
209
+ if (next) onSidebarModeChange(next);
210
+ },
211
+ children: SIDEBAR_MODE_GROUPS.map((group, index)=>/*#__PURE__*/ jsxs(Fragment, {
212
+ children: [
213
+ index > 0 && /*#__PURE__*/ jsx(DropdownMenuSeparator, {}),
214
+ group.map((m)=>/*#__PURE__*/ jsx(DropdownMenuRadioItem, {
215
+ value: m,
216
+ children: /*#__PURE__*/ jsx(DropdownMenuItemText, {
217
+ children: SIDEBAR_MODE_LABELS[m]
218
+ })
219
+ }, m))
220
+ ]
221
+ }, group.join('-')))
222
+ })
169
223
  })
170
224
  ]
171
225
  }),
@@ -184,4 +238,5 @@ const AccountDropdown = ({ sidebarMode, onSidebarModeChange, theme, onThemeChang
184
238
  })
185
239
  ]
186
240
  });
187
- export { AccountDropdown };
241
+ };
242
+ export { AccountDropdown, railModeFor };
@@ -3,7 +3,10 @@ import { type Product } from './_storyLib';
3
3
  export interface ConfigRemoteProps {
4
4
  config: NavConfig;
5
5
  basePath?: string;
6
+ /** Fake a 2s load whenever the product changes, to show the panel and content skeletons. */
7
+ simulateLoading?: boolean;
6
8
  }
7
- export declare const RemoteForProduct: ({ product }: {
9
+ export declare const RemoteForProduct: ({ product, simulateLoading, }: {
8
10
  product: Product;
11
+ simulateLoading?: boolean;
9
12
  }) => import("react").JSX.Element;
@@ -31,16 +31,18 @@ const RemotePageContent = ()=>{
31
31
  ]
32
32
  });
33
33
  };
34
- const ConfigRemote = ({ config, basePath })=>{
35
- const [loading, setLoading] = useState(true);
34
+ const ConfigRemote = ({ config, basePath, simulateLoading = true })=>{
35
+ const [loading, setLoading] = useState(simulateLoading);
36
36
  useEffect(()=>{
37
+ if (!simulateLoading) return;
37
38
  setLoading(true);
38
39
  const timer = setTimeout(()=>{
39
40
  setLoading(false);
40
41
  }, 2000);
41
42
  return ()=>clearTimeout(timer);
42
43
  }, [
43
- config.productLabel
44
+ config.productLabel,
45
+ simulateLoading
44
46
  ]);
45
47
  return /*#__PURE__*/ jsx(RemoteShell, {
46
48
  config: config,
@@ -67,12 +69,13 @@ const ConfigRemote = ({ config, basePath })=>{
67
69
  })
68
70
  });
69
71
  };
70
- const RemoteForProduct = ({ product })=>{
72
+ const RemoteForProduct = ({ product, simulateLoading = true })=>{
71
73
  if ('home' === product) return /*#__PURE__*/ jsx(HomeContent, {});
72
74
  const { config } = PRODUCT_CONFIGS[product];
73
75
  return /*#__PURE__*/ jsx(ConfigRemote, {
74
76
  config: config,
75
- basePath: `/${product}`
77
+ basePath: `/${product}`,
78
+ simulateLoading: simulateLoading
76
79
  });
77
80
  };
78
81
  export { RemoteForProduct };
@@ -1,4 +1,4 @@
1
- export { AccountDropdown, type SidebarMode } from './_storyAccountDropdown';
1
+ export { AccountDropdown, railModeFor, type SidebarMode } from './_storyAccountDropdown';
2
2
  export { RemoteForProduct } from './_storyConfigRenderer';
3
3
  export { HeaderActions } from './_storyHeaderActions';
4
4
  export { HomeContent } from './_storyHomeContent';
@@ -1,4 +1,4 @@
1
- export { AccountDropdown } from "./_storyAccountDropdown.js";
1
+ export { AccountDropdown, railModeFor } from "./_storyAccountDropdown.js";
2
2
  export { RemoteForProduct } from "./_storyConfigRenderer.js";
3
3
  export { HeaderActions } from "./_storyHeaderActions.js";
4
4
  export { HomeContent } from "./_storyHomeContent.js";
@@ -15,7 +15,7 @@ const DropdownMenuRadioItem = ({ children, value, disabled, closeOnSelect, varia
15
15
  "data-testid": testId,
16
16
  className: cn(dropdownMenuItemVariants({
17
17
  variant
18
- }), className),
18
+ }), 'data-[state=checked]:bg-states-primary-active', className),
19
19
  children: [
20
20
  children,
21
21
  /*#__PURE__*/ jsx(DropdownMenuItemIndicator, {
@@ -11,7 +11,7 @@ const NavPanelBack = ({ ref, className, children, ...props })=>{
11
11
  type: "button",
12
12
  "data-slot": "nav-panel-back",
13
13
  "data-testid": testId,
14
- className: cn('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-6 p-8 text-sm text-text-primary opacity-85 transition-colors outline-none hover:opacity-100 hover:overlay-states-primary-hover focus-visible:opacity-100 focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed', className),
14
+ className: cn('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-10 p-8 text-sm text-text-primary transition-colors outline-none hover:overlay-states-primary-hover focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed', className),
15
15
  children: [
16
16
  /*#__PURE__*/ jsx(CornerUpLeft, {
17
17
  size: "md"
@@ -4,7 +4,7 @@ import { cn } from "../../utils/cn.js";
4
4
  import { useTestId } from "../../utils/testId.js";
5
5
  import { Text } from "../Text/index.js";
6
6
  import { navPanelGroupItemVariants } from "./classes.js";
7
- import { useNavPanelDepth, useNavPanelIndent } from "./NavPanelContext.js";
7
+ import { NAV_PANEL_BASE_PADDING, useNavPanelDepth, useNavPanelIndent } from "./NavPanelContext.js";
8
8
  const NavPanelGroupItem = ({ ref, asChild = false, icon: Icon, active = false, className, children, ...props })=>{
9
9
  const testId = useTestId('group-item');
10
10
  const depth = useNavPanelDepth();
@@ -33,6 +33,13 @@ const NavPanelGroupItem = ({ ref, asChild = false, icon: Icon, active = false, c
33
33
  size: "sm",
34
34
  truncate: true,
35
35
  children: children
36
+ }),
37
+ active && depth > 0 && /*#__PURE__*/ jsx("span", {
38
+ "aria-hidden": true,
39
+ className: "absolute top-0 bottom-0 hidden w-px bg-border-brand branded:block",
40
+ style: {
41
+ left: depth * indent - NAV_PANEL_BASE_PADDING
42
+ }
36
43
  })
37
44
  ]
38
45
  });
@@ -22,7 +22,7 @@ const NavPanelGroupLabel = ({ ref, icon: Icon, className, children, ...props })=
22
22
  style: {
23
23
  paddingLeft: 8 + depth * indent
24
24
  },
25
- className: cn('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-6 py-8 pr-8 text-sm text-text-primary opacity-85 transition-colors outline-none hover:opacity-100 hover:overlay-states-primary-hover focus-visible:opacity-100 focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed', className),
25
+ className: cn('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-10 py-8 pr-8 text-sm text-text-primary transition-colors outline-none hover:overlay-states-primary-hover focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed', className),
26
26
  children: [
27
27
  Icon && /*#__PURE__*/ jsx("span", {
28
28
  className: "flex shrink-0 items-center justify-center",
@@ -15,7 +15,7 @@ const NavPanelSkeleton = ({ ref, className, count = 6, ...props })=>{
15
15
  }, (_, i)=>/*#__PURE__*/ jsx(Skeleton, {
16
16
  width: "100%",
17
17
  height: "28px",
18
- rounded: 6
18
+ rounded: 10
19
19
  }, i))
20
20
  });
21
21
  };
@@ -1,20 +1,20 @@
1
1
  import { cva } from "class-variance-authority";
2
- const navPanelItemVariants = cva('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-6 p-8 text-sm text-text-primary transition-colors outline-none', {
2
+ const navPanelItemVariants = cva('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-10 p-8 text-sm text-text-primary transition-colors outline-none', {
3
3
  variants: {
4
4
  active: {
5
- true: 'overlay-states-primary-active font-semibold',
6
- false: 'opacity-85 hover:opacity-100 hover:overlay-states-primary-hover focus-visible:opacity-100 focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed'
5
+ true: 'overlay-states-primary-active font-semibold branded:overlay-states-brand-active branded:text-text-brand',
6
+ false: 'hover:overlay-states-primary-hover focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed'
7
7
  }
8
8
  },
9
9
  defaultVariants: {
10
10
  active: false
11
11
  }
12
12
  });
13
- const navPanelGroupItemVariants = cva('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-6 py-8 pr-8 text-sm text-text-primary transition-colors outline-none', {
13
+ const navPanelGroupItemVariants = cva('overlay flex h-32 shrink-0 w-full cursor-pointer items-center gap-8 rounded-10 py-8 pr-8 text-sm text-text-primary transition-colors outline-none', {
14
14
  variants: {
15
15
  active: {
16
- true: 'overlay-states-primary-active font-semibold',
17
- false: 'opacity-85 hover:opacity-100 hover:overlay-states-primary-hover focus-visible:opacity-100 focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed'
16
+ true: 'overlay-states-primary-active font-semibold branded:overlay-states-brand-active branded:text-text-brand',
17
+ false: 'hover:overlay-states-primary-hover focus-visible:overlay-states-primary-hover active:overlay-states-primary-pressed'
18
18
  }
19
19
  },
20
20
  defaultVariants: {
@@ -1,8 +1,15 @@
1
1
  import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
2
2
  import { type TestableProps } from '../../utils/testId';
3
+ import { type NavRailMode } from './NavRailContext';
3
4
  export interface NavRailProps extends HTMLAttributes<HTMLElement>, TestableProps {
4
5
  ref?: Ref<HTMLElement>;
5
6
  children?: ReactNode;
7
+ /**
8
+ * How the rail lays out its items: `expanded` (icon beside label), `collapsed` (icon only) or
9
+ * `compact` (icon over a short label). Wins over `collapsed` when both are set.
10
+ */
11
+ mode?: NavRailMode;
12
+ /** Shorthand for `mode='collapsed'`, kept so existing hosts keep working. Prefer `mode`. */
6
13
  collapsed?: boolean;
7
14
  }
8
15
  export declare const NavRail: FC<NavRailProps>;
@@ -6,8 +6,9 @@ import { cn } from "../../utils/cn.js";
6
6
  import { TestIdProvider } from "../../utils/testId.js";
7
7
  import { navRailVariants } from "./classes.js";
8
8
  import { NavRailContextProvider } from "./NavRailContext.js";
9
- const NavRail = ({ ref, collapsed = false, className, children, 'data-testid': testId, ...props })=>{
9
+ const NavRail = ({ ref, mode: modeProp, collapsed = false, className, children, 'data-testid': testId, ...props })=>{
10
10
  const internalRef = useRef(null);
11
+ const mode = modeProp ?? (collapsed ? 'collapsed' : 'expanded');
11
12
  const focusPanel = useCallback(()=>{
12
13
  const panel = document.querySelector('[data-slot="nav-panel"]');
13
14
  const target = panel?.querySelector('[tabindex="0"]');
@@ -37,16 +38,17 @@ const NavRail = ({ ref, collapsed = false, className, children, 'data-testid': t
37
38
  value: testId,
38
39
  children: /*#__PURE__*/ jsx(NavRailContextProvider, {
39
40
  value: {
40
- collapsed
41
+ mode
41
42
  },
42
43
  children: /*#__PURE__*/ jsx("nav", {
43
44
  ...props,
44
45
  ref: composeRefs(internalRef, ref),
45
46
  "aria-label": "Global navigation",
46
47
  "data-slot": "nav-rail",
48
+ "data-mode": mode,
47
49
  "data-testid": testId,
48
50
  className: cn(navRailVariants({
49
- collapsed
51
+ mode
50
52
  }), className),
51
53
  children: children
52
54
  })
@@ -1,14 +1,19 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../utils/cn.js";
3
3
  import { useTestId } from "../../utils/testId.js";
4
+ import { navRailBodyVariants } from "./classes.js";
5
+ import { useNavRailContext } from "./NavRailContext.js";
4
6
  const NavRailBody = ({ ref, className, children, ...props })=>{
7
+ const { mode } = useNavRailContext();
5
8
  const testId = useTestId('body');
6
9
  return /*#__PURE__*/ jsx("div", {
7
10
  ...props,
8
11
  ref: ref,
9
12
  "data-slot": "nav-rail-body",
10
13
  "data-testid": testId,
11
- className: cn('flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overscroll-none [scrollbar-width:thin]', className),
14
+ className: cn(navRailBodyVariants({
15
+ mode
16
+ }), className),
12
17
  children: children
13
18
  });
14
19
  };
@@ -1,5 +1,6 @@
1
+ export type NavRailMode = 'expanded' | 'collapsed' | 'compact';
1
2
  interface NavRailContextValue {
2
- collapsed: boolean;
3
+ mode: NavRailMode;
3
4
  }
4
5
  export declare const NavRailContextProvider: import("react").Provider<NavRailContextValue>;
5
6
  export declare function useNavRailContext(): NavRailContextValue;
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext } from "react";
2
2
  const NavRailCtx = /*#__PURE__*/ createContext({
3
- collapsed: false
3
+ mode: 'expanded'
4
4
  });
5
5
  const NavRailContextProvider = NavRailCtx.Provider;
6
6
  function useNavRailContext() {
@@ -1,14 +1,19 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../../utils/cn.js";
3
3
  import { useTestId } from "../../utils/testId.js";
4
+ import { navRailFooterVariants } from "./classes.js";
5
+ import { useNavRailContext } from "./NavRailContext.js";
4
6
  const NavRailFooter = ({ ref, className, children, ...props })=>{
7
+ const { mode } = useNavRailContext();
5
8
  const testId = useTestId('footer');
6
9
  return /*#__PURE__*/ jsx("div", {
7
10
  ...props,
8
11
  ref: ref,
9
12
  "data-slot": "nav-rail-footer",
10
13
  "data-testid": testId,
11
- className: cn('mt-auto flex flex-col gap-2', className),
14
+ className: cn(navRailFooterVariants({
15
+ mode
16
+ }), className),
12
17
  children: children
13
18
  });
14
19
  };
@@ -8,5 +8,9 @@ export interface NavRailItemProps extends AnchorHTMLAttributes<HTMLAnchorElement
8
8
  label: ReactNode;
9
9
  shortcut?: string[];
10
10
  active?: boolean;
11
+ /** Seats the icon on a soft plate, for the signed-in user's item at the foot of the rail. */
12
+ avatar?: boolean;
13
+ /** User photo for the plate (implies `avatar`). The icon stays underneath and returns if the photo fails to load. */
14
+ avatarSrc?: string;
11
15
  }
12
16
  export declare const NavRailItem: FC<NavRailItemProps>;
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Fragment, useRef } from "react";
2
+ import { Fragment, useRef, useState } from "react";
3
3
  import { composeRefs } from "@radix-ui/react-compose-refs";
4
4
  import { Slot, Slottable } from "@radix-ui/react-slot";
5
5
  import { cn } from "../../utils/cn.js";
@@ -8,35 +8,61 @@ import { Kbd } from "../Kbd/index.js";
8
8
  import { Tooltip } from "../Tooltip/index.js";
9
9
  import { TooltipContent } from "../Tooltip/TooltipContent.js";
10
10
  import { TooltipTrigger } from "../Tooltip/TooltipTrigger.js";
11
- import { navRailItemVariants } from "./classes.js";
11
+ import { navRailItemCompactLabelClassName, navRailItemVariants } from "./classes.js";
12
12
  import { useNavRailContext } from "./NavRailContext.js";
13
13
  import { useShortcut } from "./useShortcut.js";
14
- const NavRailItem = ({ ref, asChild = false, icon: Icon, label, shortcut, active = false, className, children, 'data-testid': testIdProp, ...props })=>{
15
- const { collapsed } = useNavRailContext();
14
+ const NavRailItem = ({ ref, asChild = false, icon: Icon, label, shortcut, active = false, avatar = false, avatarSrc, className, children, 'data-testid': testIdProp, ...props })=>{
15
+ const { mode } = useNavRailContext();
16
16
  const testId = useTestId('item', testIdProp);
17
17
  const Comp = asChild ? Slot : 'a';
18
18
  const internalRef = useRef(null);
19
+ const [failedSrc, setFailedSrc] = useState();
20
+ const photo = avatarSrc && avatarSrc !== failedSrc ? avatarSrc : void 0;
21
+ const hasPlate = avatar || void 0 !== avatarSrc;
22
+ const showsLabel = 'expanded' === mode || 'compact' === mode && !hasPlate;
23
+ const fallbackName = showsLabel || 'string' != typeof label ? void 0 : label;
19
24
  useShortcut(shortcut, internalRef);
20
25
  const element = /*#__PURE__*/ jsxs(Comp, {
21
26
  ...props,
22
27
  ref: composeRefs(internalRef, ref),
28
+ "aria-label": props['aria-label'] ?? fallbackName,
23
29
  "aria-current": active ? 'page' : void 0,
24
30
  "data-slot": "nav-rail-item",
25
31
  "data-testid": testId,
26
32
  className: cn(navRailItemVariants({
27
- active
33
+ mode,
34
+ active,
35
+ avatar: hasPlate
28
36
  }), className),
29
37
  children: [
30
38
  /*#__PURE__*/ jsx("span", {
31
39
  className: "flex shrink-0 items-center justify-center",
32
- children: /*#__PURE__*/ jsx(Icon, {
40
+ children: hasPlate ? /*#__PURE__*/ jsxs("span", {
41
+ "data-slot": "nav-rail-item-avatar",
42
+ className: cn('-m-4 grid size-24 place-items-center overflow-hidden rounded-8 bg-states-primary-hover branded:bg-states-brand-hover branded:text-icon-brand *:col-start-1 *:row-start-1', !photo && 'border border-border-primary branded:border-border-brand'),
43
+ children: [
44
+ /*#__PURE__*/ jsx(Icon, {
45
+ size: "md"
46
+ }),
47
+ photo && /*#__PURE__*/ jsx("img", {
48
+ src: photo,
49
+ alt: "",
50
+ className: "size-full object-cover",
51
+ onError: ()=>setFailedSrc(photo)
52
+ })
53
+ ]
54
+ }) : /*#__PURE__*/ jsx(Icon, {
33
55
  size: "md"
34
56
  })
35
57
  }),
36
- !collapsed && /*#__PURE__*/ jsx("span", {
58
+ 'expanded' === mode && /*#__PURE__*/ jsx("span", {
37
59
  className: cn('truncate whitespace-nowrap transition-[opacity,width] duration-200 pl-8'),
38
60
  children: label
39
61
  }),
62
+ showsLabel && 'compact' === mode && /*#__PURE__*/ jsx("span", {
63
+ className: navRailItemCompactLabelClassName,
64
+ children: label
65
+ }),
40
66
  /*#__PURE__*/ jsx(Slottable, {
41
67
  children: children
42
68
  })