@nordmanai/nif 0.2.13 → 0.2.14
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.
- package/dist/index.js +2584 -2537
- package/dist/styles.css +1 -1
- package/dist/ui/components/nm-dropdown/nm-dropdown-item.d.ts +2 -0
- package/dist/ui/components/nm-dropdown/nm-dropdown.d.ts +4 -1
- package/dist/ui/components/nm-dropdown-menu/nm-dropdown-menu.d.ts +3 -2
- package/dist/ui/components/nm-sidebar/nm-sidebar.d.ts +15 -0
- package/dist/ui/config/nm-dropdown.d.ts +7 -0
- package/package.json +1 -1
- package/registry/index.json +6 -0
- package/registry/ui/components/nm-dropdown/nm-dropdown-item.ts +2 -0
- package/registry/ui/components/nm-dropdown/nm-dropdown.tsx +6 -2
- package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.css +11 -0
- package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.tsx +13 -3
- package/registry/ui/components/nm-sidebar/nm-sidebar.css +35 -0
- package/registry/ui/components/nm-sidebar/nm-sidebar.tsx +38 -0
- package/registry/ui/config/nm-dropdown.ts +7 -0
|
@@ -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
|
|
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;
|
|
@@ -112,6 +115,12 @@ export declare function NmSidebarMenuButton({ render, isActive, variant, size, t
|
|
|
112
115
|
export declare function NmSidebarMenuAction({ className, showOnHover, ...props }: ComponentProps<typeof NmSmallIconButton> & {
|
|
113
116
|
showOnHover?: boolean;
|
|
114
117
|
}): ReactNode;
|
|
118
|
+
/** Several actions at the end of a row, such as new and more. They reveal together when `showOnHover`. */
|
|
119
|
+
export declare function NmSidebarMenuActions({ className, showOnHover, ...props }: ComponentProps<'div'> & {
|
|
120
|
+
showOnHover?: boolean;
|
|
121
|
+
}): ReactNode;
|
|
122
|
+
/** A row of controls in the header or footer, such as a primary button and an icon button, spaced apart. */
|
|
123
|
+
export declare function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode;
|
|
115
124
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
116
125
|
export declare function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode;
|
|
117
126
|
/** Two lines in a row, such as a name and a plan in the footer. For a large menu button. */
|
|
@@ -137,4 +146,10 @@ export declare function NmSidebarMenuSubButton({ render, size, isActive, disable
|
|
|
137
146
|
export declare function NmSidebarChevron({ className, menu, ...props }: Omit<ComponentProps<typeof NmIcon>, 'id'> & {
|
|
138
147
|
menu?: boolean;
|
|
139
148
|
}): ReactNode;
|
|
149
|
+
/**
|
|
150
|
+
* A dropdown menu opened from the sidebar. It closes the moment the sidebar goes out of view
|
|
151
|
+
* (collapsed, a peek that ended, the drawer closed), so no menu outlives the panel it came from.
|
|
152
|
+
* While it is open the pointer may rest on it without ending a peek.
|
|
153
|
+
*/
|
|
154
|
+
export declare function NmSidebarDropdownMenu({ open: openProp, onOpenChange, ...props }: NmDropdownMenuProps): import("react").JSX.Element;
|
|
140
155
|
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.
|
|
3
|
+
"version": "0.2.14",
|
|
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": [
|
package/registry/index.json
CHANGED
|
@@ -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
|
|
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
|
|
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)}>
|
|
@@ -432,6 +432,41 @@
|
|
|
432
432
|
.nm-sidebar__menu-item:has(> .nm-sidebar__menu-action) > .nm-sidebar__menu-button {
|
|
433
433
|
padding-inline-end: var(--nm-icon-control);
|
|
434
434
|
}
|
|
435
|
+
.nm-sidebar__menu-item:has(> .nm-sidebar__menu-actions) > .nm-sidebar__menu-button {
|
|
436
|
+
padding-inline-end: calc(var(--nm-icon-control) * 2);
|
|
437
|
+
}
|
|
438
|
+
/* A group of actions at the end of a row. Its actions sit in flow inside it; the group takes the row's end. */
|
|
439
|
+
.nm-sidebar__menu-actions {
|
|
440
|
+
position: absolute;
|
|
441
|
+
inset-inline-end: var(--nm-space-4);
|
|
442
|
+
top: calc(50% - (var(--nm-icon-button) + var(--nm-space-6) * 2) / 2);
|
|
443
|
+
display: flex;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: var(--nm-space-2);
|
|
446
|
+
transition: opacity var(--reveal) var(--ease);
|
|
447
|
+
}
|
|
448
|
+
.nm-sidebar__menu-actions > .nm-sidebar__menu-action,
|
|
449
|
+
.nm-sidebar__menu-actions .nm-dropdown {
|
|
450
|
+
position: static;
|
|
451
|
+
}
|
|
452
|
+
.nm-sidebar__menu-actions[data-show-on-hover] {
|
|
453
|
+
opacity: 0;
|
|
454
|
+
}
|
|
455
|
+
.nm-sidebar__menu-item:hover > .nm-sidebar__menu-actions[data-show-on-hover],
|
|
456
|
+
.nm-sidebar__menu-item:focus-within > .nm-sidebar__menu-actions[data-show-on-hover],
|
|
457
|
+
.nm-sidebar__menu-item:has(> [data-active]) > .nm-sidebar__menu-actions[data-show-on-hover],
|
|
458
|
+
.nm-sidebar__menu-actions[data-show-on-hover]:has([data-popup-open]),
|
|
459
|
+
.nm-sidebar__menu-actions[data-show-on-hover]:has([aria-expanded='true']) {
|
|
460
|
+
opacity: 1;
|
|
461
|
+
}
|
|
462
|
+
/* A bar in the header or footer: its controls spread to the ends. */
|
|
463
|
+
.nm-sidebar__bar {
|
|
464
|
+
display: flex;
|
|
465
|
+
align-items: center;
|
|
466
|
+
justify-content: space-between;
|
|
467
|
+
gap: var(--nm-space-8);
|
|
468
|
+
padding: var(--nm-space-4);
|
|
469
|
+
}
|
|
435
470
|
|
|
436
471
|
/* The chevron of a row that opens. It points right when closed and down when open. */
|
|
437
472
|
.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);
|
|
@@ -477,6 +483,14 @@ export function NmSidebarMenuAction({ className, showOnHover = false, ...props }
|
|
|
477
483
|
);
|
|
478
484
|
}
|
|
479
485
|
|
|
486
|
+
/** Several actions at the end of a row, such as new and more. They reveal together when `showOnHover`. */
|
|
487
|
+
export function NmSidebarMenuActions({ className, showOnHover = false, ...props }: ComponentProps<'div'> & { showOnHover?: boolean }): ReactNode {
|
|
488
|
+
return <div data-slot="sidebar-menu-actions" data-show-on-hover={showOnHover ? '' : undefined} className={nmClass('nm-sidebar__menu-actions', className)} {...props} />;
|
|
489
|
+
}
|
|
490
|
+
/** A row of controls in the header or footer, such as a primary button and an icon button, spaced apart. */
|
|
491
|
+
export function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
492
|
+
return <div data-slot="sidebar-bar" className={nmClass('nm-sidebar__bar', className)} {...props} />;
|
|
493
|
+
}
|
|
480
494
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
481
495
|
export function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode {
|
|
482
496
|
return <NmChip data-slot="sidebar-menu-badge" size="sm" className={nmClass('nm-sidebar__menu-badge', className)} {...props} />;
|
|
@@ -549,3 +563,27 @@ export function NmSidebarMenuSubButton({
|
|
|
549
563
|
export function NmSidebarChevron({ className, menu = false, ...props }: Omit<ComponentProps<typeof NmIcon>, 'id'> & { menu?: boolean }): ReactNode {
|
|
550
564
|
return <NmIcon id="chevron" data-menu={menu ? '' : undefined} className={nmClass('nm-sidebar__chevron', className)} {...props} />;
|
|
551
565
|
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* A dropdown menu opened from the sidebar. It closes the moment the sidebar goes out of view
|
|
569
|
+
* (collapsed, a peek that ended, the drawer closed), so no menu outlives the panel it came from.
|
|
570
|
+
* While it is open the pointer may rest on it without ending a peek.
|
|
571
|
+
*/
|
|
572
|
+
export function NmSidebarDropdownMenu({ open: openProp, onOpenChange, ...props }: NmDropdownMenuProps) {
|
|
573
|
+
const visible = useSidebarVisible();
|
|
574
|
+
const [openState, setOpenState] = useState(false);
|
|
575
|
+
const open = (openProp ?? openState) && visible;
|
|
576
|
+
const handleOpenChange = useCallback<NonNullable<NmDropdownMenuProps['onOpenChange']>>(
|
|
577
|
+
(next, details) => {
|
|
578
|
+
setOpenState(next);
|
|
579
|
+
onOpenChange?.(next, details);
|
|
580
|
+
},
|
|
581
|
+
[onOpenChange],
|
|
582
|
+
);
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (!visible) {
|
|
585
|
+
setOpenState(false);
|
|
586
|
+
}
|
|
587
|
+
}, [visible]);
|
|
588
|
+
return <NmDropdownMenu open={open} onOpenChange={handleOpenChange} {...props} />;
|
|
589
|
+
}
|
|
@@ -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',
|