@nordmanai/nif 0.2.17 → 0.2.19
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 +446 -348
- package/dist/styles.css +1 -1
- package/dist/ui/components/nm-sidebar/nm-sidebar.d.ts +25 -1
- package/package.json +1 -1
- package/registry/index.json +1 -1
- package/registry/ui/components/nm-sidebar/nm-sidebar.css +67 -6
- package/registry/ui/components/nm-sidebar/nm-sidebar.tsx +169 -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;
|
|
@@ -129,6 +141,18 @@ export declare function NmSidebarMenuActions({ className, showOnHover, ...props
|
|
|
129
141
|
}): ReactNode;
|
|
130
142
|
/** A row of controls in the header or footer, such as a primary button and an icon button, spaced apart. */
|
|
131
143
|
export declare function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode;
|
|
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. */
|
|
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;
|
|
132
156
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
133
157
|
export declare function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode;
|
|
134
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.19",
|
|
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);
|
|
@@ -177,18 +219,37 @@
|
|
|
177
219
|
.nm-sidebar-provider > .nm-sidebar__trigger--anchored {
|
|
178
220
|
position: absolute;
|
|
179
221
|
top: var(--nm-space-12);
|
|
180
|
-
|
|
222
|
+
/* Its icon sits where a row's icon sits: the group padding, the row border, then the row's icon padding. */
|
|
223
|
+
inset-inline-start: calc(var(--nm-space-8) + var(--nm-rule));
|
|
181
224
|
z-index: calc(var(--nm-rail-z) + 1);
|
|
182
225
|
}
|
|
183
226
|
/* The header leaves the anchored toggle's room and lines its first row up with it. */
|
|
184
227
|
.nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header {
|
|
185
228
|
padding-block-start: var(--nm-space-12);
|
|
186
|
-
|
|
229
|
+
/* The first row's text starts where a row's text starts: after the icon column. */
|
|
230
|
+
padding-inline-start: calc(var(--nm-space-8) + var(--nm-icon-button) + var(--nm-space-8));
|
|
187
231
|
min-height: calc(var(--nm-space-12) + var(--nm-icon-control) + var(--nm-space-8));
|
|
188
232
|
}
|
|
189
|
-
.nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__menu > .nm-sidebar__menu-item:first-child > .nm-sidebar__menu-button
|
|
233
|
+
.nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__menu > .nm-sidebar__menu-item:first-child > .nm-sidebar__menu-button,
|
|
234
|
+
.nm-sidebar-provider:has(> .nm-sidebar__trigger--anchored) .nm-sidebar__header > .nm-sidebar__brand {
|
|
190
235
|
min-height: var(--nm-icon-control);
|
|
191
236
|
}
|
|
237
|
+
/* The product name: a row's geometry and type, semibold, with nothing to hover or press. */
|
|
238
|
+
.nm-sidebar__brand {
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: var(--nm-space-8);
|
|
242
|
+
box-sizing: border-box;
|
|
243
|
+
width: 100%;
|
|
244
|
+
height: var(--control-h);
|
|
245
|
+
padding: 0 var(--nm-icon-pad);
|
|
246
|
+
border: var(--nm-rule) solid transparent;
|
|
247
|
+
color: var(--nm-fg);
|
|
248
|
+
font: inherit;
|
|
249
|
+
font-size: var(--nm-type-ui);
|
|
250
|
+
font-weight: var(--nm-weight-semibold);
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
}
|
|
192
253
|
/* Holds the anchored toggle's place in a bar while the sidebar is closed. */
|
|
193
254
|
.nm-sidebar__trigger-space {
|
|
194
255
|
display: inline-block;
|
|
@@ -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>
|
|
@@ -503,6 +538,136 @@ export function NmSidebarMenuActions({ className, showOnHover = false, ...props
|
|
|
503
538
|
export function NmSidebarBar({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
504
539
|
return <div data-slot="sidebar-bar" className={nmClass('nm-sidebar__bar', className)} {...props} />;
|
|
505
540
|
}
|
|
541
|
+
/** 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. */
|
|
542
|
+
export function NmSidebarBrand({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
543
|
+
return <div data-slot="sidebar-brand" className={nmClass('nm-sidebar__brand', className)} {...props} />;
|
|
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
|
+
}
|
|
506
671
|
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
507
672
|
export function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode {
|
|
508
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;
|