@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.
- package/dist/index.js +2600 -2544
- 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 +24 -1
- package/dist/ui/config/nm-dropdown.d.ts +7 -0
- package/package.json +1 -1
- package/registry/index.json +7 -1
- 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 +61 -0
- package/registry/ui/components/nm-sidebar/nm-sidebar.tsx +52 -2
- 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;
|
|
@@ -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.
|
|
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": [
|
package/registry/index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.2.
|
|
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
|
|
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)}>
|
|
@@ -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
|
-
|
|
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',
|