@nordmanai/nif 0.2.18 → 0.2.20
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 +440 -349
- package/dist/styles.css +1 -1
- package/dist/ui/components/nm-sidebar/nm-sidebar.d.ts +23 -1
- package/package.json +1 -1
- package/registry/index.json +1 -1
- package/registry/ui/components/nm-sidebar/nm-sidebar.css +71 -4
- package/registry/ui/components/nm-sidebar/nm-sidebar.tsx +165 -4
- package/registry/ui/styles/tokens.css +2 -0
|
@@ -29,6 +29,12 @@ type SidebarContextValue = {
|
|
|
29
29
|
peekOut: () => void;
|
|
30
30
|
collapsible: NmSidebarCollapsible;
|
|
31
31
|
setCollapsible: (mode: NmSidebarCollapsible) => void;
|
|
32
|
+
/** The width the user resized the sidebar to, in px. Null until resized: the sidebar then takes its token width. */
|
|
33
|
+
width: number | null;
|
|
34
|
+
/** Sets the resized width, or null to go back to the token width. Clamped by the min and max tokens in the CSS. */
|
|
35
|
+
setWidth: (width: number | null) => void;
|
|
36
|
+
resizing: boolean;
|
|
37
|
+
setResizing: (resizing: boolean) => void;
|
|
32
38
|
};
|
|
33
39
|
/** The sidebar's state: expanded or collapsed, peeking, on a desktop or a phone, and the function that toggles it. */
|
|
34
40
|
/** Whether the sidebar is in view: open or peeking on a desktop, the drawer open on a phone. */
|
|
@@ -46,12 +52,18 @@ export type NmSidebarProviderProps = ComponentProps<'div'> & {
|
|
|
46
52
|
peek?: boolean;
|
|
47
53
|
/** `fill` takes the room it is given. `sm` and `lg` are fixed heights, for a sidebar shown inside a page. */
|
|
48
54
|
height?: 'fill' | 'sm' | 'lg';
|
|
55
|
+
/** The resized width in px, when the app holds it. */
|
|
56
|
+
width?: number | null;
|
|
57
|
+
/** The starting resized width in px. Leave out for the token width. */
|
|
58
|
+
defaultWidth?: number;
|
|
59
|
+
/** Called while the sidebar is resized, with the new width, or null on reset. Store it to remember the width. */
|
|
60
|
+
onWidthChange?: (width: number | null) => void;
|
|
49
61
|
};
|
|
50
62
|
/**
|
|
51
63
|
* Holds the open state and hands it to every part inside. Wrap the sidebar and the main area in it.
|
|
52
64
|
* The shadcn Sidebar composition, built from NIF primitives and styled by NIF tokens.
|
|
53
65
|
*/
|
|
54
|
-
export declare function NmSidebarProvider({ defaultOpen, open: openProp, onOpenChange, shortcut, peek: peekEnabled, height, className, style, children, ...props }: NmSidebarProviderProps): ReactNode;
|
|
66
|
+
export declare function NmSidebarProvider({ defaultOpen, open: openProp, onOpenChange, shortcut, peek: peekEnabled, height, width: widthProp, defaultWidth, onWidthChange, className, style, children, ...props }: NmSidebarProviderProps): ReactNode;
|
|
55
67
|
export type NmSidebarProps = ComponentProps<'div'> & {
|
|
56
68
|
side?: NmSidebarSide;
|
|
57
69
|
variant?: NmSidebarVariant;
|
|
@@ -131,6 +143,16 @@ export declare function NmSidebarMenuActions({ className, showOnHover, ...props
|
|
|
131
143
|
export declare function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode;
|
|
132
144
|
/** The product name at the top of the sidebar. Static: it has a row's geometry so it lines up with the rows below, but no hover or press. */
|
|
133
145
|
export declare function NmSidebarBrand({ className, ...props }: ComponentProps<'div'>): ReactNode;
|
|
146
|
+
export type NmSidebarResizerProps = Omit<ComponentProps<'div'>, 'role' | 'onKeyDown' | 'onDoubleClick' | 'onPointerDown'> & {
|
|
147
|
+
/** The accessible name. */
|
|
148
|
+
label?: string;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The edge of the sidebar you drag to resize it. Put it inside `NmSidebar`. The width stays between the
|
|
152
|
+
* `--nm-width-sidebar-min` and `--nm-width-sidebar-max` tokens. Arrow keys resize, Home and End jump to the
|
|
153
|
+
* bounds, a double click or Enter goes back to the default width. Only shown while the sidebar is open.
|
|
154
|
+
*/
|
|
155
|
+
export declare function NmSidebarResizer({ className, label, ...props }: NmSidebarResizerProps): ReactNode;
|
|
134
156
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
135
157
|
export declare function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode;
|
|
136
158
|
/** Two lines in a row, such as a name and a plan in the footer. For a large menu button. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordmanai/nif",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
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,6 +1,7 @@
|
|
|
1
1
|
/* The provider holds the sidebar and the main area side by side. */
|
|
2
2
|
.nm-sidebar-provider {
|
|
3
|
-
|
|
3
|
+
/* The token width, or the width the user dragged to, always between the min and max tokens. */
|
|
4
|
+
--nm-sidebar-width: clamp(var(--nm-width-sidebar-min), var(--nm-sidebar-resized, var(--nm-width-sidebar)), var(--nm-width-sidebar-max));
|
|
4
5
|
--nm-sidebar-width-icon: var(--nm-width-sidebar-icon);
|
|
5
6
|
/* The sidebar shares the page's background; only its divider separates it. Floating and inset variants keep their raised card. */
|
|
6
7
|
--nm-sidebar-bg: var(--nm-canvas);
|
|
@@ -50,15 +51,56 @@
|
|
|
50
51
|
order: 1;
|
|
51
52
|
}
|
|
52
53
|
.nm-sidebar[data-variant='sidebar'][data-side='left'] {
|
|
53
|
-
border-inline-end: var(--nm-rule) solid var(--nm-border-
|
|
54
|
+
border-inline-end: var(--nm-rule) solid var(--nm-border-strong);
|
|
54
55
|
}
|
|
55
56
|
.nm-sidebar[data-variant='sidebar'][data-side='right'] {
|
|
56
|
-
border-inline-start: var(--nm-rule) solid var(--nm-border-
|
|
57
|
+
border-inline-start: var(--nm-rule) solid var(--nm-border-strong);
|
|
57
58
|
}
|
|
58
59
|
.nm-sidebar[data-collapsible='offcanvas'] {
|
|
59
60
|
width: 0;
|
|
60
61
|
border-width: 0;
|
|
61
62
|
}
|
|
63
|
+
/* The drag edge: a strip along the sidebar's end, over its border, with a line that shows on hover, focus and drag. */
|
|
64
|
+
.nm-sidebar__resizer {
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset-block: 0;
|
|
67
|
+
inset-inline-end: 0;
|
|
68
|
+
z-index: 1;
|
|
69
|
+
width: var(--nm-space-8);
|
|
70
|
+
cursor: col-resize;
|
|
71
|
+
touch-action: none;
|
|
72
|
+
outline: none;
|
|
73
|
+
}
|
|
74
|
+
.nm-sidebar[data-side='right'] .nm-sidebar__resizer {
|
|
75
|
+
inset-inline-end: auto;
|
|
76
|
+
inset-inline-start: 0;
|
|
77
|
+
}
|
|
78
|
+
.nm-sidebar__resizer::after {
|
|
79
|
+
content: '';
|
|
80
|
+
position: absolute;
|
|
81
|
+
inset-block: 0;
|
|
82
|
+
inset-inline-end: 0;
|
|
83
|
+
width: calc(var(--nm-rule) * 2);
|
|
84
|
+
background: transparent;
|
|
85
|
+
transition: background-color var(--reveal) var(--ease);
|
|
86
|
+
}
|
|
87
|
+
.nm-sidebar[data-side='right'] .nm-sidebar__resizer::after {
|
|
88
|
+
inset-inline-end: auto;
|
|
89
|
+
inset-inline-start: 0;
|
|
90
|
+
}
|
|
91
|
+
.nm-sidebar__resizer:hover::after,
|
|
92
|
+
.nm-sidebar__resizer:focus-visible::after,
|
|
93
|
+
.nm-sidebar-provider[data-resizing] .nm-sidebar__resizer::after {
|
|
94
|
+
background: var(--nm-accent);
|
|
95
|
+
}
|
|
96
|
+
/* While dragging, the width follows the pointer with no easing and nothing gets selected. */
|
|
97
|
+
.nm-sidebar-provider[data-resizing] {
|
|
98
|
+
cursor: col-resize;
|
|
99
|
+
user-select: none;
|
|
100
|
+
}
|
|
101
|
+
.nm-sidebar-provider[data-resizing] .nm-sidebar {
|
|
102
|
+
transition: none;
|
|
103
|
+
}
|
|
62
104
|
.nm-sidebar[data-collapsible='icon'],
|
|
63
105
|
.nm-sidebar[data-collapsible='toggle'] {
|
|
64
106
|
width: var(--nm-sidebar-width-icon);
|
|
@@ -173,6 +215,13 @@
|
|
|
173
215
|
.nm-sidebar__trigger {
|
|
174
216
|
flex-shrink: 0;
|
|
175
217
|
}
|
|
218
|
+
/* The toggle is a bare icon: no fill on hover, focus or while its peek is open. Focus keeps its ring. */
|
|
219
|
+
.nm-icon-button.nm-sidebar__trigger:hover:not(:disabled):not([aria-disabled='true']),
|
|
220
|
+
.nm-icon-button.nm-sidebar__trigger:focus-visible:not(:disabled):not([aria-disabled='true']),
|
|
221
|
+
.nm-icon-button.nm-sidebar__trigger[aria-expanded='true']:not(:disabled):not([aria-disabled='true']),
|
|
222
|
+
.nm-icon-button.nm-sidebar__trigger[aria-pressed='true']:not(:disabled):not([aria-disabled='true']) {
|
|
223
|
+
background: transparent;
|
|
224
|
+
}
|
|
176
225
|
/* Anchored: one toggle at the provider's top start corner, above the sidebar, that never moves. */
|
|
177
226
|
.nm-sidebar-provider > .nm-sidebar__trigger--anchored {
|
|
178
227
|
position: absolute;
|
|
@@ -523,7 +572,25 @@
|
|
|
523
572
|
height: var(--nm-icon-sm);
|
|
524
573
|
color: var(--nm-fg-subtle);
|
|
525
574
|
transform: rotate(-90deg);
|
|
526
|
-
|
|
575
|
+
/* Hidden until the row is hovered, focused or open. */
|
|
576
|
+
opacity: 0;
|
|
577
|
+
transition:
|
|
578
|
+
transform var(--reveal) var(--ease),
|
|
579
|
+
opacity var(--reveal) var(--ease);
|
|
580
|
+
}
|
|
581
|
+
.nm-sidebar__menu-button:hover > .nm-sidebar__chevron,
|
|
582
|
+
.nm-sidebar__menu-button:focus-visible > .nm-sidebar__chevron,
|
|
583
|
+
.nm-sidebar__menu-button[data-popup-open] > .nm-sidebar__chevron,
|
|
584
|
+
.nm-sidebar__menu-button[aria-expanded='true'] > .nm-sidebar__chevron,
|
|
585
|
+
.nm-sidebar__menu-button[data-panel-open] > .nm-sidebar__chevron,
|
|
586
|
+
.nm-sidebar__menu-button[data-open] > .nm-sidebar__chevron {
|
|
587
|
+
opacity: 1;
|
|
588
|
+
}
|
|
589
|
+
/* Nothing to hover on a touch screen: the chevrons stay. */
|
|
590
|
+
@media (hover: none) {
|
|
591
|
+
.nm-sidebar__chevron {
|
|
592
|
+
opacity: 1;
|
|
593
|
+
}
|
|
527
594
|
}
|
|
528
595
|
[aria-expanded='true'] > .nm-sidebar__chevron,
|
|
529
596
|
[data-panel-open] > .nm-sidebar__chevron,
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
type ComponentProps,
|
|
10
10
|
type CSSProperties,
|
|
11
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
12
|
+
type PointerEvent as ReactPointerEvent,
|
|
11
13
|
type ReactElement,
|
|
12
14
|
type ReactNode,
|
|
13
15
|
} from 'react';
|
|
@@ -56,6 +58,12 @@ type SidebarContextValue = {
|
|
|
56
58
|
peekOut: () => void;
|
|
57
59
|
collapsible: NmSidebarCollapsible;
|
|
58
60
|
setCollapsible: (mode: NmSidebarCollapsible) => void;
|
|
61
|
+
/** The width the user resized the sidebar to, in px. Null until resized: the sidebar then takes its token width. */
|
|
62
|
+
width: number | null;
|
|
63
|
+
/** Sets the resized width, or null to go back to the token width. Clamped by the min and max tokens in the CSS. */
|
|
64
|
+
setWidth: (width: number | null) => void;
|
|
65
|
+
resizing: boolean;
|
|
66
|
+
setResizing: (resizing: boolean) => void;
|
|
59
67
|
};
|
|
60
68
|
|
|
61
69
|
const SidebarContext = createContext<SidebarContextValue | null>(null);
|
|
@@ -103,8 +111,13 @@ export type NmSidebarProviderProps = ComponentProps<'div'> & {
|
|
|
103
111
|
peek?: boolean;
|
|
104
112
|
/** `fill` takes the room it is given. `sm` and `lg` are fixed heights, for a sidebar shown inside a page. */
|
|
105
113
|
height?: 'fill' | 'sm' | 'lg';
|
|
114
|
+
/** The resized width in px, when the app holds it. */
|
|
115
|
+
width?: number | null;
|
|
116
|
+
/** The starting resized width in px. Leave out for the token width. */
|
|
117
|
+
defaultWidth?: number;
|
|
118
|
+
/** Called while the sidebar is resized, with the new width, or null on reset. Store it to remember the width. */
|
|
119
|
+
onWidthChange?: (width: number | null) => void;
|
|
106
120
|
};
|
|
107
|
-
|
|
108
121
|
/**
|
|
109
122
|
* Holds the open state and hands it to every part inside. Wrap the sidebar and the main area in it.
|
|
110
123
|
* The shadcn Sidebar composition, built from NIF primitives and styled by NIF tokens.
|
|
@@ -116,6 +129,9 @@ export function NmSidebarProvider({
|
|
|
116
129
|
shortcut = true,
|
|
117
130
|
peek: peekEnabled = true,
|
|
118
131
|
height = 'fill',
|
|
132
|
+
width: widthProp,
|
|
133
|
+
defaultWidth,
|
|
134
|
+
onWidthChange,
|
|
119
135
|
className,
|
|
120
136
|
style,
|
|
121
137
|
children,
|
|
@@ -125,6 +141,18 @@ export function NmSidebarProvider({
|
|
|
125
141
|
const [openMobile, setOpenMobile] = useState(false);
|
|
126
142
|
const [openState, setOpenState] = useState(defaultOpen);
|
|
127
143
|
const [peek, setPeek] = useState(false);
|
|
144
|
+
const [widthState, setWidthState] = useState<number | null>(defaultWidth ?? null);
|
|
145
|
+
const [resizing, setResizing] = useState(false);
|
|
146
|
+
const width = widthProp === undefined ? widthState : widthProp;
|
|
147
|
+
const setWidth = useCallback(
|
|
148
|
+
(next: number | null) => {
|
|
149
|
+
onWidthChange?.(next);
|
|
150
|
+
if (widthProp === undefined) {
|
|
151
|
+
setWidthState(next);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
[onWidthChange, widthProp],
|
|
155
|
+
);
|
|
128
156
|
const [collapsible, setCollapsible] = useState<NmSidebarCollapsible>('offcanvas');
|
|
129
157
|
const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
130
158
|
const pointer = useRef<{ x: number; y: number } | null>(null);
|
|
@@ -215,12 +243,19 @@ export function NmSidebarProvider({
|
|
|
215
243
|
|
|
216
244
|
const state: NmSidebarState = open ? 'expanded' : 'collapsed';
|
|
217
245
|
const value = useMemo<SidebarContextValue>(
|
|
218
|
-
() => ({ state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar, peek: peek && !open, peekIn, peekOut, collapsible, setCollapsible }),
|
|
219
|
-
[state, open, setOpen, openMobile, isMobile, toggleSidebar, peek, peekIn, peekOut, collapsible],
|
|
246
|
+
() => ({ state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar, peek: peek && !open, peekIn, peekOut, collapsible, setCollapsible, width, setWidth, resizing, setResizing }),
|
|
247
|
+
[state, open, setOpen, openMobile, isMobile, toggleSidebar, peek, peekIn, peekOut, collapsible, width, setWidth, resizing],
|
|
220
248
|
);
|
|
221
249
|
return (
|
|
222
250
|
<SidebarContext.Provider value={value}>
|
|
223
|
-
<div
|
|
251
|
+
<div
|
|
252
|
+
data-slot="sidebar-wrapper"
|
|
253
|
+
data-height={height}
|
|
254
|
+
data-resizing={resizing ? '' : undefined}
|
|
255
|
+
className={nmClass('nm-sidebar-provider', className)}
|
|
256
|
+
style={width == null ? style : ({ ...style, ['--nm-sidebar-resized' as string]: `${width}px` } as CSSProperties)}
|
|
257
|
+
{...props}
|
|
258
|
+
>
|
|
224
259
|
{children}
|
|
225
260
|
</div>
|
|
226
261
|
</SidebarContext.Provider>
|
|
@@ -507,6 +542,132 @@ export function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): Re
|
|
|
507
542
|
export function NmSidebarBrand({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
508
543
|
return <div data-slot="sidebar-brand" className={nmClass('nm-sidebar__brand', className)} {...props} />;
|
|
509
544
|
}
|
|
545
|
+
export type NmSidebarResizerProps = Omit<ComponentProps<'div'>, 'role' | 'onKeyDown' | 'onDoubleClick' | 'onPointerDown'> & {
|
|
546
|
+
/** The accessible name. */
|
|
547
|
+
label?: string;
|
|
548
|
+
};
|
|
549
|
+
/** A token length in px. The sidebar bounds are plain rem tokens. */
|
|
550
|
+
function tokenPx(value: string): number {
|
|
551
|
+
const amount = parseFloat(value);
|
|
552
|
+
return value.trim().endsWith('rem') ? amount * parseFloat(getComputedStyle(document.documentElement).fontSize) : amount;
|
|
553
|
+
}
|
|
554
|
+
function clampWidth(value: number, min: number, max: number): number {
|
|
555
|
+
return Math.min(max, Math.max(min, value));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The edge of the sidebar you drag to resize it. Put it inside `NmSidebar`. The width stays between the
|
|
559
|
+
* `--nm-width-sidebar-min` and `--nm-width-sidebar-max` tokens. Arrow keys resize, Home and End jump to the
|
|
560
|
+
* bounds, a double click or Enter goes back to the default width. Only shown while the sidebar is open.
|
|
561
|
+
*/
|
|
562
|
+
export function NmSidebarResizer({ className, label = 'Resize sidebar', ...props }: NmSidebarResizerProps): ReactNode {
|
|
563
|
+
const { open, isMobile, setWidth, setResizing } = useSidebar();
|
|
564
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
565
|
+
const [range, setRange] = useState({ min: 0, max: 0, now: 0 });
|
|
566
|
+
const measure = useCallback(() => {
|
|
567
|
+
const element = ref.current;
|
|
568
|
+
const host = element?.closest<HTMLElement>('.nm-sidebar-provider');
|
|
569
|
+
const sidebar = element?.closest<HTMLElement>('.nm-sidebar');
|
|
570
|
+
if (!element || !host || !sidebar) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const style = getComputedStyle(host);
|
|
574
|
+
return {
|
|
575
|
+
host,
|
|
576
|
+
sidebar,
|
|
577
|
+
min: tokenPx(style.getPropertyValue('--nm-width-sidebar-min')),
|
|
578
|
+
max: tokenPx(style.getPropertyValue('--nm-width-sidebar-max')),
|
|
579
|
+
now: sidebar.getBoundingClientRect().width,
|
|
580
|
+
sign: (sidebar.getAttribute('data-side') === 'right') !== (style.direction === 'rtl') ? -1 : 1,
|
|
581
|
+
};
|
|
582
|
+
}, []);
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
const info = measure();
|
|
585
|
+
if (!info) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
const update = () => {
|
|
589
|
+
const next = measure();
|
|
590
|
+
if (next) {
|
|
591
|
+
setRange((prev) => (prev.min === next.min && prev.max === next.max && prev.now === next.now ? prev : { min: next.min, max: next.max, now: next.now }));
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
update();
|
|
595
|
+
const observer = new ResizeObserver(update);
|
|
596
|
+
observer.observe(info.sidebar);
|
|
597
|
+
return () => observer.disconnect();
|
|
598
|
+
}, [open, isMobile, measure]);
|
|
599
|
+
useEffect(() => () => setResizing(false), [setResizing]);
|
|
600
|
+
if (!open || isMobile) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
604
|
+
if (event.button !== 0) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const info = measure();
|
|
608
|
+
if (!info) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
event.preventDefault();
|
|
612
|
+
const target = event.currentTarget;
|
|
613
|
+
const startX = event.clientX;
|
|
614
|
+
const startWidth = info.now;
|
|
615
|
+
target.setPointerCapture(event.pointerId);
|
|
616
|
+
target.focus();
|
|
617
|
+
setResizing(true);
|
|
618
|
+
const move = (moveEvent: PointerEvent) => {
|
|
619
|
+
setWidth(clampWidth(startWidth + info.sign * (moveEvent.clientX - startX), info.min, info.max));
|
|
620
|
+
};
|
|
621
|
+
const end = () => {
|
|
622
|
+
setResizing(false);
|
|
623
|
+
target.removeEventListener('pointermove', move);
|
|
624
|
+
target.removeEventListener('pointerup', end);
|
|
625
|
+
target.removeEventListener('pointercancel', end);
|
|
626
|
+
};
|
|
627
|
+
target.addEventListener('pointermove', move);
|
|
628
|
+
target.addEventListener('pointerup', end);
|
|
629
|
+
target.addEventListener('pointercancel', end);
|
|
630
|
+
};
|
|
631
|
+
const onKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
632
|
+
const info = measure();
|
|
633
|
+
if (!info) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const step = (info.max - info.min) / 10;
|
|
637
|
+
const grow = event.key === 'ArrowRight' ? info.sign : event.key === 'ArrowLeft' ? -info.sign : 0;
|
|
638
|
+
if (grow !== 0) {
|
|
639
|
+
event.preventDefault();
|
|
640
|
+
setWidth(clampWidth(info.now + grow * step, info.min, info.max));
|
|
641
|
+
} else if (event.key === 'Home') {
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
setWidth(info.min);
|
|
644
|
+
} else if (event.key === 'End') {
|
|
645
|
+
event.preventDefault();
|
|
646
|
+
setWidth(info.max);
|
|
647
|
+
} else if (event.key === 'Enter') {
|
|
648
|
+
event.preventDefault();
|
|
649
|
+
setWidth(null);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
return (
|
|
653
|
+
<div
|
|
654
|
+
ref={ref}
|
|
655
|
+
role="separator"
|
|
656
|
+
aria-orientation="vertical"
|
|
657
|
+
aria-label={label}
|
|
658
|
+
aria-valuemin={Math.round(range.min)}
|
|
659
|
+
aria-valuemax={Math.round(range.max)}
|
|
660
|
+
aria-valuenow={Math.round(range.now)}
|
|
661
|
+
tabIndex={0}
|
|
662
|
+
data-slot="sidebar-resizer"
|
|
663
|
+
className={nmClass('nm-sidebar__resizer', className)}
|
|
664
|
+
onPointerDown={onPointerDown}
|
|
665
|
+
onKeyDown={onKeyDown}
|
|
666
|
+
onDoubleClick={() => setWidth(null)}
|
|
667
|
+
{...props}
|
|
668
|
+
/>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
510
671
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
511
672
|
export function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode {
|
|
512
673
|
return <NmChip data-slot="sidebar-menu-badge" size="sm" className={nmClass('nm-sidebar__menu-badge', className)} {...props} />;
|
|
@@ -209,6 +209,8 @@
|
|
|
209
209
|
--nm-width-panel-max: 80%;
|
|
210
210
|
/* The sidebar beside the main area: open, on a phone, and collapsed to icons (a square row inside the group padding). */
|
|
211
211
|
--nm-width-sidebar: 16rem;
|
|
212
|
+
--nm-width-sidebar-min: 13rem;
|
|
213
|
+
--nm-width-sidebar-max: 28rem;
|
|
212
214
|
--nm-width-sidebar-mobile: 18rem;
|
|
213
215
|
/* A drawer over the page from an edge. */
|
|
214
216
|
--nm-width-drawer: 20rem;
|