@nordmanai/nif 0.2.13 → 0.2.15

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.
@@ -13,6 +13,8 @@ export type NmDropdownItem = {
13
13
  danger?: boolean;
14
14
  disabled?: boolean;
15
15
  checked?: boolean;
16
+ /** The row that is the current choice in a list of alternatives. Shown by fill, not by a mark; for a toggle use `checked`. */
17
+ current?: boolean;
16
18
  /** Publish row state. The row stays inactive while busy or live. */
17
19
  mark?: 'idle' | 'busy' | 'live';
18
20
  children?: readonly NmDropdownItem[];
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode, type Ref } from 'react';
2
2
  import type { NmIconId } from '../../config/nm-icons.ts';
3
- import { NM_DROPDOWN_ALIGN, NM_DROPDOWN_REVEAL, NM_DROPDOWN_TONE, NM_DROPDOWN_TRIGGER } from '../../config/nm-dropdown.ts';
3
+ import { NM_DROPDOWN_ALIGN, NM_DROPDOWN_REVEAL, NM_DROPDOWN_SIDE, NM_DROPDOWN_TONE, NM_DROPDOWN_TRIGGER } from '../../config/nm-dropdown.ts';
4
4
  import type { NmDropdownItem } from './nm-dropdown-item';
5
5
  export type { NmDropdownItem };
6
6
  export type NmDropdownProps = {
@@ -11,6 +11,7 @@ export type NmDropdownProps = {
11
11
  trigger?: (typeof NM_DROPDOWN_TRIGGER)[keyof typeof NM_DROPDOWN_TRIGGER];
12
12
  tone?: (typeof NM_DROPDOWN_TONE)[keyof typeof NM_DROPDOWN_TONE];
13
13
  align?: (typeof NM_DROPDOWN_ALIGN)[keyof typeof NM_DROPDOWN_ALIGN];
14
+ side?: (typeof NM_DROPDOWN_SIDE)[keyof typeof NM_DROPDOWN_SIDE];
14
15
  icon?: NmIconId;
15
16
  imageSrc?: string;
16
17
  displayLabel?: string;
@@ -22,6 +23,8 @@ export type NmDropdownProps = {
22
23
  /** `hover` hides the trigger until the host with `data-nm-menu` is hovered or focused. */
23
24
  reveal?: (typeof NM_DROPDOWN_REVEAL)[keyof typeof NM_DROPDOWN_REVEAL];
24
25
  open?: boolean;
26
+ /** Opens when the trigger is hovered, as well as on click. */
27
+ openOnHover?: boolean;
25
28
  triggerClassName?: string;
26
29
  triggerRef?: Ref<HTMLButtonElement>;
27
30
  onOpenChange?: (open: boolean) => void;
@@ -2,7 +2,8 @@ import { type ComponentProps } from 'react';
2
2
  import { Menu as MenuPrimitive } from '../../lib/nm-motion.ts';
3
3
  import type { NmDropdownItem } from '../nm-dropdown/nm-dropdown-item.ts';
4
4
  /** Base UI Menu, in the shadcn dropdown-menu composition, styled by NIF tokens. */
5
- export declare function NmDropdownMenu(props: MenuPrimitive.Root.Props): import("react").JSX.Element;
5
+ export type NmDropdownMenuProps = MenuPrimitive.Root.Props;
6
+ export declare function NmDropdownMenu(props: NmDropdownMenuProps): import("react").JSX.Element;
6
7
  export declare function NmDropdownMenuTrigger(props: MenuPrimitive.Trigger.Props): import("react").JSX.Element;
7
8
  type PositionProps = Pick<MenuPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>;
8
9
  export declare function NmDropdownMenuContent({ align, alignOffset, side, sideOffset, className, ...props }: MenuPrimitive.Popup.Props & PositionProps): import("react").JSX.Element;
@@ -29,7 +30,7 @@ export declare function NmDropdownMenuText({ title, description }: {
29
30
  export declare function NmDropdownMenuDot({ status }: {
30
31
  status?: 'ok' | 'error';
31
32
  }): import("react").JSX.Element;
32
- /** A list of NmDropdownItem as animated menu rows: branches become submenus, `checked` rows keep a mark. */
33
+ /** A list of NmDropdownItem as animated menu rows: branches become submenus, `checked` rows keep a mark, the `current` row (or the `selected` id) is filled. */
33
34
  export declare function NmDropdownMenuItems({ items, selected, onSelect, onBranchExpand, }: {
34
35
  items: readonly NmDropdownItem[];
35
36
  selected?: string | null;
@@ -2,6 +2,7 @@ import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
2
2
  import { type NmSidebarButtonSize, type NmSidebarButtonVariant, type NmSidebarCollapsible, type NmSidebarSide, type NmSidebarState, type NmSidebarVariant } from '../../config/nm-sidebar.ts';
3
3
  import type { NmIconId } from '../../config/nm-icons.ts';
4
4
  import { NmChip } from '../nm-chip/nm-chip.tsx';
5
+ import { type NmDropdownMenuProps } from '../nm-dropdown-menu/nm-dropdown-menu.tsx';
5
6
  import { NmIcon } from '../nm-icon/nm-icon.tsx';
6
7
  import { NmInput } from '../nm-input/nm-input.tsx';
7
8
  import { NmSeparator } from '../nm-separator/nm-separator.tsx';
@@ -30,6 +31,8 @@ type SidebarContextValue = {
30
31
  setCollapsible: (mode: NmSidebarCollapsible) => void;
31
32
  };
32
33
  /** The sidebar's state: expanded or collapsed, peeking, on a desktop or a phone, and the function that toggles it. */
34
+ /** Whether the sidebar is in view: open or peeking on a desktop, the drawer open on a phone. */
35
+ export declare function useSidebarVisible(): boolean;
33
36
  /** The sidebar context when there is one. For a part that lives in a bar shared by pages with and without a sidebar. */
34
37
  export declare function useOptionalSidebar(): SidebarContextValue | null;
35
38
  export declare function useSidebar(): SidebarContextValue;
@@ -67,9 +70,17 @@ export type NmSidebarTriggerProps = Omit<ComponentProps<'button'>, 'aria-label'>
67
70
  icon?: NmIconId;
68
71
  /** On by default: a tooltip with the name and the ⌘B shortcut. */
69
72
  tooltip?: boolean;
73
+ /**
74
+ * Keeps the toggle at one spot of the provider whether the sidebar is open or closed: it is taken out of
75
+ * the flow and anchored at the top start corner, above the sidebar. The header leaves room for it, and
76
+ * `NmSidebarTriggerSpace` holds its place in a bar while the sidebar is closed.
77
+ */
78
+ anchored?: boolean;
70
79
  };
71
80
  /** The button that opens and closes the sidebar. In the main area's bar, or in the sidebar's own header. Resting on it peeks the sidebar. */
72
- export declare function NmSidebarTrigger({ className, label, icon, tooltip, onClick, onPointerEnter, onPointerLeave, ...props }: NmSidebarTriggerProps): ReactNode;
81
+ export declare function NmSidebarTrigger({ className, label, icon, tooltip, anchored, onClick, onPointerEnter, onPointerLeave, ...props }: NmSidebarTriggerProps): ReactNode;
82
+ /** Holds the anchored trigger's place in a bar while the sidebar is closed, so the bar's content starts beside it. */
83
+ export declare function NmSidebarTriggerSpace({ className, ...props }: ComponentProps<'span'>): ReactNode;
73
84
  /** A thin strip on the sidebar's edge. Clicking it toggles the sidebar. */
74
85
  export declare function NmSidebarRail({ className, label, ...props }: ComponentProps<'button'> & {
75
86
  label?: string;
@@ -112,6 +123,12 @@ export declare function NmSidebarMenuButton({ render, isActive, variant, size, t
112
123
  export declare function NmSidebarMenuAction({ className, showOnHover, ...props }: ComponentProps<typeof NmSmallIconButton> & {
113
124
  showOnHover?: boolean;
114
125
  }): ReactNode;
126
+ /** Several actions at the end of a row, such as new and more. They reveal together when `showOnHover`. */
127
+ export declare function NmSidebarMenuActions({ className, showOnHover, ...props }: ComponentProps<'div'> & {
128
+ showOnHover?: boolean;
129
+ }): ReactNode;
130
+ /** A row of controls in the header or footer, such as a primary button and an icon button, spaced apart. */
131
+ export declare function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode;
115
132
  /** A count or a short word at the end of a row. The NIF Chip, small. */
116
133
  export declare function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode;
117
134
  /** Two lines in a row, such as a name and a plan in the footer. For a large menu button. */
@@ -137,4 +154,10 @@ export declare function NmSidebarMenuSubButton({ render, size, isActive, disable
137
154
  export declare function NmSidebarChevron({ className, menu, ...props }: Omit<ComponentProps<typeof NmIcon>, 'id'> & {
138
155
  menu?: boolean;
139
156
  }): ReactNode;
157
+ /**
158
+ * A dropdown menu opened from the sidebar. It closes the moment the sidebar goes out of view
159
+ * (collapsed, a peek that ended, the drawer closed), so no menu outlives the panel it came from.
160
+ * While it is open the pointer may rest on it without ending a peek.
161
+ */
162
+ export declare function NmSidebarDropdownMenu({ open: openProp, onOpenChange, ...props }: NmDropdownMenuProps): import("react").JSX.Element;
140
163
  export {};
@@ -47,6 +47,13 @@ export declare const NM_DROPDOWN_ALIGN: {
47
47
  /** Match the hosting `.nm-rail__row` pill width and left edge. */
48
48
  readonly row: 'row';
49
49
  };
50
+ /** Which side of the trigger the menu opens on. `right` for a menu that must not cover a side panel's rows. */
51
+ export declare const NM_DROPDOWN_SIDE: {
52
+ readonly bottom: 'bottom';
53
+ readonly top: 'top';
54
+ readonly right: 'right';
55
+ readonly left: 'left';
56
+ };
50
57
  /** Icon menus stay hidden until the host row or field is hovered or focused. */
51
58
  export declare const NM_DROPDOWN_REVEAL: {
52
59
  readonly hover: 'hover';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordmanai/nif",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Nordman Interface Framework: the one UI and UX system for every Nordman product. Base UI, in the shadcn composition, styled by NIF tokens.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.13",
2
+ "version": "0.2.14",
3
3
  "versions": {
4
4
  "@animate-ui/react": "^2.4.2",
5
5
  "@base-ui/react": "^1.9.0",
@@ -5013,6 +5013,10 @@
5013
5013
  "components/nm-chip/nm-chip.tsx",
5014
5014
  "components/nm-drawer/nm-drawer.css",
5015
5015
  "components/nm-drawer/nm-drawer.tsx",
5016
+ "components/nm-dropdown-menu/nm-dropdown-menu.css",
5017
+ "components/nm-dropdown-menu/nm-dropdown-menu.tsx",
5018
+ "components/nm-dropdown/nm-dropdown-item.ts",
5019
+ "components/nm-dropdown/nm-dropdown.css",
5016
5020
  "components/nm-icon/animated/activity.tsx",
5017
5021
  "components/nm-icon/animated/archive.tsx",
5018
5022
  "components/nm-icon/animated/arrow-down.tsx",
@@ -5178,6 +5182,8 @@
5178
5182
  "uses": [
5179
5183
  "chip",
5180
5184
  "drawer",
5185
+ "dropdown",
5186
+ "dropdown-menu",
5181
5187
  "icon",
5182
5188
  "input",
5183
5189
  "kbd",
@@ -14,6 +14,8 @@ export type NmDropdownItem = {
14
14
  danger?: boolean;
15
15
  disabled?: boolean;
16
16
  checked?: boolean;
17
+ /** The row that is the current choice in a list of alternatives. Shown by fill, not by a mark; for a toggle use `checked`. */
18
+ current?: boolean;
17
19
  /** Publish row state. The row stays inactive while busy or live. */
18
20
  mark?: 'idle' | 'busy' | 'live';
19
21
  children?: readonly NmDropdownItem[];
@@ -4,6 +4,7 @@ import {
4
4
  NM_DROPDOWN_ALIGN,
5
5
  NM_DROPDOWN_CLASSES,
6
6
  NM_DROPDOWN_REVEAL,
7
+ NM_DROPDOWN_SIDE,
7
8
  NM_DROPDOWN_TONE,
8
9
  NM_DROPDOWN_TRIGGER,
9
10
  } from '../../config/nm-dropdown.ts';
@@ -46,6 +47,7 @@ export type NmDropdownProps = {
46
47
  trigger?: (typeof NM_DROPDOWN_TRIGGER)[keyof typeof NM_DROPDOWN_TRIGGER];
47
48
  tone?: (typeof NM_DROPDOWN_TONE)[keyof typeof NM_DROPDOWN_TONE];
48
49
  align?: (typeof NM_DROPDOWN_ALIGN)[keyof typeof NM_DROPDOWN_ALIGN];
50
+ side?: (typeof NM_DROPDOWN_SIDE)[keyof typeof NM_DROPDOWN_SIDE];
49
51
  icon?: NmIconId;
50
52
  imageSrc?: string;
51
53
  displayLabel?: string;
@@ -57,6 +59,8 @@ export type NmDropdownProps = {
57
59
  /** `hover` hides the trigger until the host with `data-nm-menu` is hovered or focused. */
58
60
  reveal?: (typeof NM_DROPDOWN_REVEAL)[keyof typeof NM_DROPDOWN_REVEAL];
59
61
  open?: boolean;
62
+ /** Opens when the trigger is hovered, as well as on click. */
63
+ openOnHover?: boolean;
60
64
  triggerClassName?: string;
61
65
  triggerRef?: Ref<HTMLButtonElement>;
62
66
  onOpenChange?: (open: boolean) => void;
@@ -173,8 +177,8 @@ export function NmDropdown(props: NmDropdownProps): ReactNode {
173
177
  data-reveal={props.reveal}
174
178
  >
175
179
  <NmDropdownMenu open={open} onOpenChange={setOpen}>
176
- <NmDropdownMenuTrigger render={triggerElement} />
177
- <NmDropdownMenuContent align={props.align === NM_DROPDOWN_ALIGN.end ? 'end' : 'start'}>
180
+ <NmDropdownMenuTrigger render={triggerElement} openOnHover={props.openOnHover} />
181
+ <NmDropdownMenuContent align={props.align === NM_DROPDOWN_ALIGN.end ? 'end' : 'start'} side={props.side}>
178
182
  <NmDropdownMenuItems
179
183
  items={items}
180
184
  selected={props.selected}
@@ -70,6 +70,17 @@
70
70
  background: var(--nm-line);
71
71
  }
72
72
 
73
+ .nm-menu__item[data-current] {
74
+ background: var(--nm-line);
75
+ color: var(--nm-fg);
76
+ }
77
+ .nm-menu__item[data-current] .nm-icon {
78
+ color: var(--nm-fg);
79
+ }
80
+ .nm-menu__item[data-current]:hover,
81
+ .nm-menu__item[data-current][data-highlighted] {
82
+ background: var(--nm-raised);
83
+ }
73
84
  .nm-menu__item[data-disabled] {
74
85
  pointer-events: none;
75
86
  opacity: 0.5;
@@ -8,7 +8,9 @@ import type { NmDropdownItem } from '../nm-dropdown/nm-dropdown-item.ts';
8
8
  const InGroup = createContext(false);
9
9
 
10
10
  /** Base UI Menu, in the shadcn dropdown-menu composition, styled by NIF tokens. */
11
- export function NmDropdownMenu(props: MenuPrimitive.Root.Props) {
11
+ export type NmDropdownMenuProps = MenuPrimitive.Root.Props;
12
+
13
+ export function NmDropdownMenu(props: NmDropdownMenuProps) {
12
14
  return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
13
15
  }
14
16
 
@@ -150,7 +152,7 @@ function itemBody(item: NmDropdownItem) {
150
152
  );
151
153
  }
152
154
 
153
- /** A list of NmDropdownItem as animated menu rows: branches become submenus, `checked` rows keep a mark. */
155
+ /** A list of NmDropdownItem as animated menu rows: branches become submenus, `checked` rows keep a mark, the `current` row (or the `selected` id) is filled. */
154
156
  export function NmDropdownMenuItems({
155
157
  items,
156
158
  selected,
@@ -178,7 +180,15 @@ export function NmDropdownMenuItems({
178
180
  </NmDropdownMenuSub>
179
181
  );
180
182
  }
181
- const checked = item.checked ?? (selected != null ? item.id === selected : undefined);
183
+ const current = item.current === true || (selected != null && item.id === selected);
184
+ if (current) {
185
+ return (
186
+ <NmDropdownMenuItem key={item.id} data-current="" aria-current="true" disabled={item.disabled} onClick={() => onSelect?.(item.id)}>
187
+ {itemBody(item)}
188
+ </NmDropdownMenuItem>
189
+ );
190
+ }
191
+ const checked = item.checked;
182
192
  if (checked !== undefined) {
183
193
  return (
184
194
  <NmDropdownMenuCheckboxItem key={item.id} checked={checked} disabled={item.disabled} onClick={() => onSelect?.(item.id)}>
@@ -171,6 +171,29 @@
171
171
  .nm-sidebar__trigger {
172
172
  flex-shrink: 0;
173
173
  }
174
+ /* Anchored: one toggle at the provider's top start corner, above the sidebar, that never moves. */
175
+ .nm-sidebar-provider > .nm-sidebar__trigger--anchored {
176
+ position: absolute;
177
+ top: var(--nm-space-12);
178
+ inset-inline-start: var(--nm-space-12);
179
+ z-index: calc(var(--nm-rail-z) + 1);
180
+ }
181
+ /* The header leaves the anchored toggle's room and lines its first row up with it. */
182
+ .nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header {
183
+ padding-block-start: var(--nm-space-12);
184
+ padding-inline-start: calc(var(--nm-space-12) + var(--nm-icon-control) + var(--nm-space-8));
185
+ min-height: calc(var(--nm-space-12) + var(--nm-icon-control) + var(--nm-space-8));
186
+ }
187
+ .nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__menu > .nm-sidebar__menu-item:first-child > .nm-sidebar__menu-button {
188
+ min-height: var(--nm-icon-control);
189
+ }
190
+ /* Holds the anchored toggle's place in a bar while the sidebar is closed. */
191
+ .nm-sidebar__trigger-space {
192
+ display: inline-block;
193
+ flex-shrink: 0;
194
+ width: var(--nm-icon-control);
195
+ height: var(--nm-icon-control);
196
+ }
174
197
  /* A header with the toggle in it is a row: the brand or switcher, then the toggle at the end. */
175
198
  .nm-sidebar__header:has(> .nm-sidebar__trigger) {
176
199
  flex-direction: row;
@@ -432,6 +455,44 @@
432
455
  .nm-sidebar__menu-item:has(> .nm-sidebar__menu-action) > .nm-sidebar__menu-button {
433
456
  padding-inline-end: var(--nm-icon-control);
434
457
  }
458
+ .nm-sidebar__menu-item:has(> .nm-sidebar__menu-actions) > .nm-sidebar__menu-button {
459
+ padding-inline-end: var(--nm-icon-control);
460
+ }
461
+ .nm-sidebar__menu-item:has(> .nm-sidebar__menu-actions > :nth-child(2)) > .nm-sidebar__menu-button {
462
+ padding-inline-end: calc(var(--nm-icon-control) * 2);
463
+ }
464
+ /* A group of actions at the end of a row. Its actions sit in flow inside it; the group takes the row's end. */
465
+ .nm-sidebar__menu-actions {
466
+ position: absolute;
467
+ inset-inline-end: var(--nm-space-4);
468
+ top: calc(50% - (var(--nm-icon-button) + var(--nm-space-6) * 2) / 2);
469
+ display: flex;
470
+ align-items: center;
471
+ gap: var(--nm-space-2);
472
+ transition: opacity var(--reveal) var(--ease);
473
+ }
474
+ .nm-sidebar__menu-actions > .nm-sidebar__menu-action,
475
+ .nm-sidebar__menu-actions .nm-dropdown {
476
+ position: static;
477
+ }
478
+ .nm-sidebar__menu-actions[data-show-on-hover] {
479
+ opacity: 0;
480
+ }
481
+ .nm-sidebar__menu-item:hover > .nm-sidebar__menu-actions[data-show-on-hover],
482
+ .nm-sidebar__menu-item:focus-within > .nm-sidebar__menu-actions[data-show-on-hover],
483
+ .nm-sidebar__menu-item:has(> [data-active]) > .nm-sidebar__menu-actions[data-show-on-hover],
484
+ .nm-sidebar__menu-actions[data-show-on-hover]:has([data-popup-open]),
485
+ .nm-sidebar__menu-actions[data-show-on-hover]:has([aria-expanded='true']) {
486
+ opacity: 1;
487
+ }
488
+ /* A bar in the header or footer: its controls spread to the ends. */
489
+ .nm-sidebar__bar {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: space-between;
493
+ gap: var(--nm-space-8);
494
+ padding: var(--nm-space-4);
495
+ }
435
496
 
436
497
  /* The chevron of a row that opens. It points right when closed and down when open. */
437
498
  .nm-sidebar__chevron {
@@ -26,6 +26,7 @@ import {
26
26
  } from '../../config/nm-sidebar.ts';
27
27
  import type { NmIconId } from '../../config/nm-icons.ts';
28
28
  import { NmChip } from '../nm-chip/nm-chip.tsx';
29
+ import { NmDropdownMenu, type NmDropdownMenuProps } from '../nm-dropdown-menu/nm-dropdown-menu.tsx';
29
30
  import { NmDrawer, NmDrawerContent } from '../nm-drawer/nm-drawer.tsx';
30
31
  import { NmIcon } from '../nm-icon/nm-icon.tsx';
31
32
  import { NmInput } from '../nm-input/nm-input.tsx';
@@ -63,6 +64,11 @@ const SidebarContext = createContext<SidebarContextValue | null>(null);
63
64
  const PEEK_KEEPS = '[data-slot="sidebar"], [data-slot="sidebar-trigger"], [data-slot="tooltip-content"], [role="menu"], [role="dialog"]';
64
65
 
65
66
  /** The sidebar's state: expanded or collapsed, peeking, on a desktop or a phone, and the function that toggles it. */
67
+ /** Whether the sidebar is in view: open or peeking on a desktop, the drawer open on a phone. */
68
+ export function useSidebarVisible(): boolean {
69
+ const { open, peek, isMobile, openMobile } = useSidebar();
70
+ return isMobile ? openMobile : open || peek;
71
+ }
66
72
  /** The sidebar context when there is one. For a part that lives in a bar shared by pages with and without a sidebar. */
67
73
  export function useOptionalSidebar(): SidebarContextValue | null {
68
74
  return useContext(SidebarContext);
@@ -289,10 +295,16 @@ export type NmSidebarTriggerProps = Omit<ComponentProps<'button'>, 'aria-label'>
289
295
  icon?: NmIconId;
290
296
  /** On by default: a tooltip with the name and the ⌘B shortcut. */
291
297
  tooltip?: boolean;
298
+ /**
299
+ * Keeps the toggle at one spot of the provider whether the sidebar is open or closed: it is taken out of
300
+ * the flow and anchored at the top start corner, above the sidebar. The header leaves room for it, and
301
+ * `NmSidebarTriggerSpace` holds its place in a bar while the sidebar is closed.
302
+ */
303
+ anchored?: boolean;
292
304
  };
293
305
 
294
306
  /** The button that opens and closes the sidebar. In the main area's bar, or in the sidebar's own header. Resting on it peeks the sidebar. */
295
- export function NmSidebarTrigger({ className, label, icon = 'panel-left', tooltip = true, onClick, onPointerEnter, onPointerLeave, ...props }: NmSidebarTriggerProps): ReactNode {
307
+ export function NmSidebarTrigger({ className, label, icon = 'panel-left', tooltip = true, anchored = false, onClick, onPointerEnter, onPointerLeave, ...props }: NmSidebarTriggerProps): ReactNode {
296
308
  const { open, isMobile, toggleSidebar, peekIn, peekOut } = useSidebar();
297
309
  const name = label ?? (open || isMobile ? 'Close sidebar' : 'Open sidebar');
298
310
  const button = (
@@ -301,7 +313,8 @@ export function NmSidebarTrigger({ className, label, icon = 'panel-left', toolti
301
313
  glyph={icon}
302
314
  label={name}
303
315
  tone="quiet"
304
- className={nmClass('nm-sidebar__trigger', className)}
316
+ data-anchored={anchored ? 'true' : undefined}
317
+ className={nmClass('nm-sidebar__trigger', anchored ? 'nm-sidebar__trigger--anchored' : null, className)}
305
318
  onClick={(event) => {
306
319
  onClick?.(event);
307
320
  toggleSidebar();
@@ -331,6 +344,11 @@ export function NmSidebarTrigger({ className, label, icon = 'panel-left', toolti
331
344
  );
332
345
  }
333
346
 
347
+ /** Holds the anchored trigger's place in a bar while the sidebar is closed, so the bar's content starts beside it. */
348
+ export function NmSidebarTriggerSpace({ className, ...props }: ComponentProps<'span'>): ReactNode {
349
+ return <span aria-hidden="true" data-slot="sidebar-trigger-space" className={nmClass('nm-sidebar__trigger-space', className)} {...props} />;
350
+ }
351
+
334
352
  /** A thin strip on the sidebar's edge. Clicking it toggles the sidebar. */
335
353
  export function NmSidebarRail({ className, label = 'Toggle sidebar', ...props }: ComponentProps<'button'> & { label?: string }): ReactNode {
336
354
  const { toggleSidebar } = useSidebar();
@@ -477,6 +495,14 @@ export function NmSidebarMenuAction({ className, showOnHover = false, ...props }
477
495
  );
478
496
  }
479
497
 
498
+ /** Several actions at the end of a row, such as new and more. They reveal together when `showOnHover`. */
499
+ export function NmSidebarMenuActions({ className, showOnHover = false, ...props }: ComponentProps<'div'> & { showOnHover?: boolean }): ReactNode {
500
+ return <div data-slot="sidebar-menu-actions" data-show-on-hover={showOnHover ? '' : undefined} className={nmClass('nm-sidebar__menu-actions', className)} {...props} />;
501
+ }
502
+ /** A row of controls in the header or footer, such as a primary button and an icon button, spaced apart. */
503
+ export function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode {
504
+ return <div data-slot="sidebar-bar" className={nmClass('nm-sidebar__bar', className)} {...props} />;
505
+ }
480
506
  /** A count or a short word at the end of a row. The NIF Chip, small. */
481
507
  export function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode {
482
508
  return <NmChip data-slot="sidebar-menu-badge" size="sm" className={nmClass('nm-sidebar__menu-badge', className)} {...props} />;
@@ -549,3 +575,27 @@ export function NmSidebarMenuSubButton({
549
575
  export function NmSidebarChevron({ className, menu = false, ...props }: Omit<ComponentProps<typeof NmIcon>, 'id'> & { menu?: boolean }): ReactNode {
550
576
  return <NmIcon id="chevron" data-menu={menu ? '' : undefined} className={nmClass('nm-sidebar__chevron', className)} {...props} />;
551
577
  }
578
+
579
+ /**
580
+ * A dropdown menu opened from the sidebar. It closes the moment the sidebar goes out of view
581
+ * (collapsed, a peek that ended, the drawer closed), so no menu outlives the panel it came from.
582
+ * While it is open the pointer may rest on it without ending a peek.
583
+ */
584
+ export function NmSidebarDropdownMenu({ open: openProp, onOpenChange, ...props }: NmDropdownMenuProps) {
585
+ const visible = useSidebarVisible();
586
+ const [openState, setOpenState] = useState(false);
587
+ const open = (openProp ?? openState) && visible;
588
+ const handleOpenChange = useCallback<NonNullable<NmDropdownMenuProps['onOpenChange']>>(
589
+ (next, details) => {
590
+ setOpenState(next);
591
+ onOpenChange?.(next, details);
592
+ },
593
+ [onOpenChange],
594
+ );
595
+ useEffect(() => {
596
+ if (!visible) {
597
+ setOpenState(false);
598
+ }
599
+ }, [visible]);
600
+ return <NmDropdownMenu open={open} onOpenChange={handleOpenChange} {...props} />;
601
+ }
@@ -51,6 +51,13 @@ export const NM_DROPDOWN_ALIGN = {
51
51
  row: 'row',
52
52
  } as const;
53
53
 
54
+ /** Which side of the trigger the menu opens on. `right` for a menu that must not cover a side panel's rows. */
55
+ export const NM_DROPDOWN_SIDE = {
56
+ bottom: 'bottom',
57
+ top: 'top',
58
+ right: 'right',
59
+ left: 'left',
60
+ } as const;
54
61
  /** Icon menus stay hidden until the host row or field is hovered or focused. */
55
62
  export const NM_DROPDOWN_REVEAL = {
56
63
  hover: 'hover',