@nordmanai/nif 0.2.5 → 0.2.9
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.d.ts +5 -0
- package/dist/index.js +6915 -6376
- package/dist/styles.css +1 -1
- package/dist/ui/components/nm-alert-dialog/nm-alert-dialog.d.ts +17 -0
- package/dist/ui/components/nm-dialog/nm-dialog.d.ts +4 -0
- package/dist/ui/components/nm-drawer/nm-drawer.d.ts +18 -0
- package/dist/ui/components/nm-sidebar/nm-sidebar.d.ts +134 -0
- package/dist/ui/config/nm-appearance.d.ts +13 -1
- package/dist/ui/config/nm-drawer.d.ts +3 -0
- package/dist/ui/config/nm-sidebar.d.ts +29 -0
- package/dist/ui/lib/appearance/appearanceState.d.ts +2 -1
- package/package.json +1 -1
- package/registry/index.json +212 -1
- package/registry/ui/components/nm-ai/nm-ai.css +3 -3
- package/registry/ui/components/nm-alert/nm-alert.css +1 -1
- package/registry/ui/components/nm-alert-dialog/nm-alert-dialog.css +4 -0
- package/registry/ui/components/nm-alert-dialog/nm-alert-dialog.tsx +43 -0
- package/registry/ui/components/nm-button/nm-button.css +2 -2
- package/registry/ui/components/nm-button-group/nm-button-group.css +1 -1
- package/registry/ui/components/nm-calendar/nm-calendar.css +1 -1
- package/registry/ui/components/nm-card/nm-card.css +2 -2
- package/registry/ui/components/nm-chatbox/nm-chatbox.css +1 -1
- package/registry/ui/components/nm-checkbox/nm-checkbox.css +2 -2
- package/registry/ui/components/nm-chip/nm-chip.css +1 -1
- package/registry/ui/components/nm-code-block/nm-code-block.css +1 -1
- package/registry/ui/components/nm-collection/nm-collection.css +2 -2
- package/registry/ui/components/nm-color-picker/nm-color-picker.css +1 -1
- package/registry/ui/components/nm-combobox/nm-combobox.css +2 -2
- package/registry/ui/components/nm-command/nm-command.css +1 -1
- package/registry/ui/components/nm-composer/nm-composer.css +3 -3
- package/registry/ui/components/nm-date-picker/nm-date-picker.css +2 -2
- package/registry/ui/components/nm-dialog/nm-dialog.css +16 -1
- package/registry/ui/components/nm-dialog/nm-dialog.tsx +8 -0
- package/registry/ui/components/nm-drawer/nm-drawer.css +70 -0
- package/registry/ui/components/nm-drawer/nm-drawer.tsx +41 -0
- package/registry/ui/components/nm-dropdown/nm-dropdown.css +2 -2
- package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.css +1 -1
- package/registry/ui/components/nm-field/nm-field.css +6 -6
- package/registry/ui/components/nm-hover-card/nm-hover-card.css +1 -1
- package/registry/ui/components/nm-input/nm-input.css +3 -3
- package/registry/ui/components/nm-input-group/nm-input-group.css +2 -2
- package/registry/ui/components/nm-message/nm-message.css +1 -1
- package/registry/ui/components/nm-native-select/nm-native-select.css +2 -2
- package/registry/ui/components/nm-popover/nm-popover.css +1 -1
- package/registry/ui/components/nm-prose/nm-prose.css +3 -3
- package/registry/ui/components/nm-questionnaire/nm-questionnaire.css +3 -3
- package/registry/ui/components/nm-radio-group/nm-radio-group.css +2 -2
- package/registry/ui/components/nm-segmented/nm-segmented.css +1 -1
- package/registry/ui/components/nm-select/nm-select.css +2 -2
- package/registry/ui/components/nm-separator/nm-separator.css +1 -1
- package/registry/ui/components/nm-side-panel/nm-side-panel.css +1 -1
- package/registry/ui/components/nm-sidebar/nm-sidebar.css +647 -0
- package/registry/ui/components/nm-sidebar/nm-sidebar.tsx +532 -0
- package/registry/ui/components/nm-spinner/nm-spinner.css +1 -1
- package/registry/ui/components/nm-switch/nm-switch.css +1 -1
- package/registry/ui/components/nm-table/nm-table.css +4 -4
- package/registry/ui/components/nm-tabs/nm-tabs.css +1 -1
- package/registry/ui/components/nm-textarea/nm-textarea.css +5 -5
- package/registry/ui/components/nm-toggle/nm-toggle.css +1 -1
- package/registry/ui/components/nordman-icon-button/nordman-icon-button.css +2 -2
- package/registry/ui/components/nordman-login/nordman-login.css +3 -3
- package/registry/ui/components/nordman-toggle/nordman-toggle.css +1 -1
- package/registry/ui/config/nm-appearance.ts +17 -0
- package/registry/ui/config/nm-drawer.ts +3 -0
- package/registry/ui/config/nm-sidebar.ts +37 -0
- package/registry/ui/lib/appearance/appearanceState.ts +8 -0
- package/registry/ui/lib/appearance/applyAppearance.ts +3 -1
- package/registry/ui/lib/appearance/deriveTokens.ts +2 -0
- package/registry/ui/styles/light-appearance.css +3 -3
- package/registry/ui/styles/tokens.css +31 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ComponentProps,
|
|
10
|
+
type CSSProperties,
|
|
11
|
+
type ReactElement,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
15
|
+
import { nmClass } from '../../lib/nm-class.ts';
|
|
16
|
+
import {
|
|
17
|
+
NM_SIDEBAR_MOBILE_QUERY,
|
|
18
|
+
NM_SIDEBAR_PEEK_LEAVE_MS,
|
|
19
|
+
NM_SIDEBAR_SHORTCUT,
|
|
20
|
+
type NmSidebarButtonSize,
|
|
21
|
+
type NmSidebarButtonVariant,
|
|
22
|
+
type NmSidebarCollapsible,
|
|
23
|
+
type NmSidebarSide,
|
|
24
|
+
type NmSidebarState,
|
|
25
|
+
type NmSidebarVariant,
|
|
26
|
+
} from '../../config/nm-sidebar.ts';
|
|
27
|
+
import type { NmIconId } from '../../config/nm-icons.ts';
|
|
28
|
+
import { NmChip } from '../nm-chip/nm-chip.tsx';
|
|
29
|
+
import { NmDrawer, NmDrawerContent } from '../nm-drawer/nm-drawer.tsx';
|
|
30
|
+
import { NmIcon } from '../nm-icon/nm-icon.tsx';
|
|
31
|
+
import { NmInput } from '../nm-input/nm-input.tsx';
|
|
32
|
+
import { NmKbd } from '../nm-kbd/nm-kbd.tsx';
|
|
33
|
+
import { NmSeparator } from '../nm-separator/nm-separator.tsx';
|
|
34
|
+
import { NmSkeleton } from '../nm-skeleton/nm-skeleton.tsx';
|
|
35
|
+
import { NmSmallIconButton } from '../nm-small-icon-button/nm-small-icon-button.tsx';
|
|
36
|
+
import { NordmanIconButton } from '../nordman-icon-button/nordman-icon-button.tsx';
|
|
37
|
+
import { NmTooltip, NmTooltipContent, NmTooltipTrigger } from '../nm-tooltip/nm-tooltip.tsx';
|
|
38
|
+
|
|
39
|
+
/** Replace the element a part renders, as Base UI does: give it an `<a>` and the part is a link. */
|
|
40
|
+
type RenderProp = ReactElement | undefined;
|
|
41
|
+
|
|
42
|
+
type SidebarContextValue = {
|
|
43
|
+
state: NmSidebarState;
|
|
44
|
+
open: boolean;
|
|
45
|
+
setOpen: (open: boolean) => void;
|
|
46
|
+
openMobile: boolean;
|
|
47
|
+
setOpenMobile: (open: boolean) => void;
|
|
48
|
+
isMobile: boolean;
|
|
49
|
+
toggleSidebar: () => void;
|
|
50
|
+
/** The collapsed sidebar shown in full over the page while the pointer is on it or on the toggle. */
|
|
51
|
+
peek: boolean;
|
|
52
|
+
/** Starts a peek. Nothing happens when the sidebar is open, on a phone, or when peeking is off. */
|
|
53
|
+
peekIn: (event?: { clientX: number; clientY: number }) => void;
|
|
54
|
+
/** Ends a peek after a short grace, so the pointer can travel from the toggle into the sidebar. */
|
|
55
|
+
peekOut: () => void;
|
|
56
|
+
collapsible: NmSidebarCollapsible;
|
|
57
|
+
setCollapsible: (mode: NmSidebarCollapsible) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SidebarContext = createContext<SidebarContextValue | null>(null);
|
|
61
|
+
|
|
62
|
+
/** Where the pointer may rest without ending a peek: the sidebar, its toggle, a tooltip, or a menu opened from a row. */
|
|
63
|
+
const PEEK_KEEPS = '[data-slot="sidebar"], [data-slot="sidebar-trigger"], [data-slot="tooltip-content"], [role="menu"], [role="dialog"]';
|
|
64
|
+
|
|
65
|
+
/** The sidebar's state: expanded or collapsed, peeking, on a desktop or a phone, and the function that toggles it. */
|
|
66
|
+
export function useSidebar(): SidebarContextValue {
|
|
67
|
+
const context = useContext(SidebarContext);
|
|
68
|
+
if (!context) {
|
|
69
|
+
throw new Error('useSidebar must be used within a NmSidebarProvider.');
|
|
70
|
+
}
|
|
71
|
+
return context;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function useMediaQuery(query: string): boolean {
|
|
75
|
+
const [matches, setMatches] = useState(() => (typeof window === 'undefined' ? false : window.matchMedia(query).matches));
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const list = window.matchMedia(query);
|
|
78
|
+
const on = () => setMatches(list.matches);
|
|
79
|
+
on();
|
|
80
|
+
list.addEventListener('change', on);
|
|
81
|
+
return () => list.removeEventListener('change', on);
|
|
82
|
+
}, [query]);
|
|
83
|
+
return matches;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type NmSidebarProviderProps = ComponentProps<'div'> & {
|
|
87
|
+
defaultOpen?: boolean;
|
|
88
|
+
open?: boolean;
|
|
89
|
+
onOpenChange?: (open: boolean) => void;
|
|
90
|
+
/** ⌘B / Ctrl+B toggles the sidebar. Off when several sidebars share a page. */
|
|
91
|
+
shortcut?: boolean;
|
|
92
|
+
/** On by default: resting the pointer on the toggle or on the collapsed sidebar shows it in full, over the page, until the pointer leaves. */
|
|
93
|
+
peek?: boolean;
|
|
94
|
+
/** `fill` takes the room it is given. `sm` and `lg` are fixed heights, for a sidebar shown inside a page. */
|
|
95
|
+
height?: 'fill' | 'sm' | 'lg';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Holds the open state and hands it to every part inside. Wrap the sidebar and the main area in it.
|
|
100
|
+
* The shadcn Sidebar composition, built from NIF primitives and styled by NIF tokens.
|
|
101
|
+
*/
|
|
102
|
+
export function NmSidebarProvider({
|
|
103
|
+
defaultOpen = true,
|
|
104
|
+
open: openProp,
|
|
105
|
+
onOpenChange,
|
|
106
|
+
shortcut = true,
|
|
107
|
+
peek: peekEnabled = true,
|
|
108
|
+
height = 'fill',
|
|
109
|
+
className,
|
|
110
|
+
style,
|
|
111
|
+
children,
|
|
112
|
+
...props
|
|
113
|
+
}: NmSidebarProviderProps): ReactNode {
|
|
114
|
+
const isMobile = useMediaQuery(NM_SIDEBAR_MOBILE_QUERY);
|
|
115
|
+
const [openMobile, setOpenMobile] = useState(false);
|
|
116
|
+
const [openState, setOpenState] = useState(defaultOpen);
|
|
117
|
+
const [peek, setPeek] = useState(false);
|
|
118
|
+
const [collapsible, setCollapsible] = useState<NmSidebarCollapsible>('offcanvas');
|
|
119
|
+
const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
120
|
+
const pointer = useRef<{ x: number; y: number } | null>(null);
|
|
121
|
+
const open = openProp ?? openState;
|
|
122
|
+
const setOpen = useCallback(
|
|
123
|
+
(next: boolean) => {
|
|
124
|
+
onOpenChange?.(next);
|
|
125
|
+
if (openProp == null) {
|
|
126
|
+
setOpenState(next);
|
|
127
|
+
}
|
|
128
|
+
setPeek(false);
|
|
129
|
+
},
|
|
130
|
+
[onOpenChange, openProp],
|
|
131
|
+
);
|
|
132
|
+
const toggleSidebar = useCallback(() => {
|
|
133
|
+
if (isMobile) {
|
|
134
|
+
setOpenMobile((value) => !value);
|
|
135
|
+
} else {
|
|
136
|
+
setOpen(!open);
|
|
137
|
+
}
|
|
138
|
+
}, [isMobile, open, setOpen]);
|
|
139
|
+
const clearLeave = () => {
|
|
140
|
+
if (leaveTimer.current != null) {
|
|
141
|
+
clearTimeout(leaveTimer.current);
|
|
142
|
+
leaveTimer.current = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const peekIn = useCallback(
|
|
146
|
+
(event?: { clientX: number; clientY: number }) => {
|
|
147
|
+
clearLeave();
|
|
148
|
+
if (event) {
|
|
149
|
+
pointer.current = { x: event.clientX, y: event.clientY };
|
|
150
|
+
}
|
|
151
|
+
if (peekEnabled && !open && !isMobile && collapsible !== 'none') {
|
|
152
|
+
setPeek(true);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
[peekEnabled, open, isMobile, collapsible],
|
|
156
|
+
);
|
|
157
|
+
const pointerInside = () => {
|
|
158
|
+
const at = pointer.current;
|
|
159
|
+
if (at == null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return document.elementFromPoint(at.x, at.y)?.closest(PEEK_KEEPS) != null;
|
|
163
|
+
};
|
|
164
|
+
const peekOut = useCallback(() => {
|
|
165
|
+
clearLeave();
|
|
166
|
+
leaveTimer.current = setTimeout(() => {
|
|
167
|
+
leaveTimer.current = null;
|
|
168
|
+
if (!pointerInside()) {
|
|
169
|
+
setPeek(false);
|
|
170
|
+
}
|
|
171
|
+
}, NM_SIDEBAR_PEEK_LEAVE_MS);
|
|
172
|
+
}, []);
|
|
173
|
+
useEffect(() => clearLeave, []);
|
|
174
|
+
// While peeking, the pointer's position decides, not boundary events: the panel opening under a still pointer
|
|
175
|
+
// moves the toggle out from under it, and a menu opened from a row is portaled outside the sidebar.
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!peek) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const onMove = (event: PointerEvent) => {
|
|
181
|
+
pointer.current = { x: event.clientX, y: event.clientY };
|
|
182
|
+
if (pointerInside()) {
|
|
183
|
+
clearLeave();
|
|
184
|
+
} else if (leaveTimer.current == null) {
|
|
185
|
+
peekOut();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
document.addEventListener('pointermove', onMove);
|
|
189
|
+
return () => document.removeEventListener('pointermove', onMove);
|
|
190
|
+
}, [peek, peekOut]);
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!shortcut) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const onKey = (event: KeyboardEvent) => {
|
|
197
|
+
if (event.key === NM_SIDEBAR_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
toggleSidebar();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
window.addEventListener('keydown', onKey);
|
|
203
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
204
|
+
}, [shortcut, toggleSidebar]);
|
|
205
|
+
|
|
206
|
+
const state: NmSidebarState = open ? 'expanded' : 'collapsed';
|
|
207
|
+
const value = useMemo<SidebarContextValue>(
|
|
208
|
+
() => ({ state, open, setOpen, openMobile, setOpenMobile, isMobile, toggleSidebar, peek: peek && !open, peekIn, peekOut, collapsible, setCollapsible }),
|
|
209
|
+
[state, open, setOpen, openMobile, isMobile, toggleSidebar, peek, peekIn, peekOut, collapsible],
|
|
210
|
+
);
|
|
211
|
+
return (
|
|
212
|
+
<SidebarContext.Provider value={value}>
|
|
213
|
+
<div data-slot="sidebar-wrapper" data-height={height} className={nmClass('nm-sidebar-provider', className)} style={style} {...props}>
|
|
214
|
+
{children}
|
|
215
|
+
</div>
|
|
216
|
+
</SidebarContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type NmSidebarProps = ComponentProps<'div'> & {
|
|
221
|
+
side?: NmSidebarSide;
|
|
222
|
+
variant?: NmSidebarVariant;
|
|
223
|
+
collapsible?: NmSidebarCollapsible;
|
|
224
|
+
/** The accessible name of the sidebar on a phone, where it opens over the page. */
|
|
225
|
+
label?: string;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The sidebar itself. Beside the main area on a desktop, where it slides out of view, collapses to a column
|
|
230
|
+
* of icons, or collapses to its toggle alone. Over the page on a phone, in a drawer.
|
|
231
|
+
*/
|
|
232
|
+
export function NmSidebar({
|
|
233
|
+
side = 'left',
|
|
234
|
+
variant = 'sidebar',
|
|
235
|
+
collapsible = 'offcanvas',
|
|
236
|
+
label = 'Sidebar',
|
|
237
|
+
className,
|
|
238
|
+
children,
|
|
239
|
+
...props
|
|
240
|
+
}: NmSidebarProps): ReactNode {
|
|
241
|
+
const { isMobile, state, openMobile, setOpenMobile, peek, peekIn, peekOut, setCollapsible } = useSidebar();
|
|
242
|
+
useEffect(() => setCollapsible(collapsible), [collapsible, setCollapsible]);
|
|
243
|
+
|
|
244
|
+
if (collapsible === 'none') {
|
|
245
|
+
return (
|
|
246
|
+
<div data-slot="sidebar" data-side={side} data-variant={variant} data-collapsible="none" className={nmClass('nm-sidebar', className)} {...props}>
|
|
247
|
+
<div data-slot="sidebar-inner" className="nm-sidebar__inner">{children}</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isMobile) {
|
|
253
|
+
return (
|
|
254
|
+
<NmDrawer open={openMobile} onOpenChange={setOpenMobile}>
|
|
255
|
+
<NmDrawerContent data-slot="sidebar" data-mobile="" side={side} aria-label={label} className={nmClass('nm-sidebar nm-sidebar__sheet', className)}>
|
|
256
|
+
<div data-slot="sidebar-inner" className="nm-sidebar__inner">{children}</div>
|
|
257
|
+
</NmDrawerContent>
|
|
258
|
+
</NmDrawer>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const collapsed = state === 'collapsed';
|
|
263
|
+
return (
|
|
264
|
+
<div
|
|
265
|
+
data-slot="sidebar"
|
|
266
|
+
data-state={state}
|
|
267
|
+
data-collapsible={collapsed && !peek ? collapsible : ''}
|
|
268
|
+
data-mode={collapsible}
|
|
269
|
+
data-peek={collapsed && peek ? '' : undefined}
|
|
270
|
+
data-variant={variant}
|
|
271
|
+
data-side={side}
|
|
272
|
+
className={nmClass('nm-sidebar', className)}
|
|
273
|
+
onPointerEnter={collapsed ? peekIn : undefined}
|
|
274
|
+
onPointerLeave={collapsed ? peekOut : undefined}
|
|
275
|
+
{...props}
|
|
276
|
+
>
|
|
277
|
+
<div data-slot="sidebar-inner" className="nm-sidebar__inner">{children}</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export type NmSidebarTriggerProps = Omit<ComponentProps<'button'>, 'aria-label'> & {
|
|
283
|
+
/** The accessible name. The tooltip says Open or Close by state. */
|
|
284
|
+
label?: string;
|
|
285
|
+
icon?: NmIconId;
|
|
286
|
+
/** On by default: a tooltip with the name and the ⌘B shortcut. */
|
|
287
|
+
tooltip?: boolean;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** 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. */
|
|
291
|
+
export function NmSidebarTrigger({ className, label, icon = 'panel-left', tooltip = true, onClick, onPointerEnter, onPointerLeave, ...props }: NmSidebarTriggerProps): ReactNode {
|
|
292
|
+
const { open, isMobile, toggleSidebar, peekIn, peekOut } = useSidebar();
|
|
293
|
+
const name = label ?? (open || isMobile ? 'Close sidebar' : 'Open sidebar');
|
|
294
|
+
const button = (
|
|
295
|
+
<NordmanIconButton
|
|
296
|
+
data-slot="sidebar-trigger"
|
|
297
|
+
glyph={icon}
|
|
298
|
+
label={name}
|
|
299
|
+
tone="quiet"
|
|
300
|
+
className={nmClass('nm-sidebar__trigger', className)}
|
|
301
|
+
onClick={(event) => {
|
|
302
|
+
onClick?.(event);
|
|
303
|
+
toggleSidebar();
|
|
304
|
+
}}
|
|
305
|
+
onPointerEnter={(event) => {
|
|
306
|
+
onPointerEnter?.(event);
|
|
307
|
+
peekIn(event);
|
|
308
|
+
}}
|
|
309
|
+
onPointerLeave={(event) => {
|
|
310
|
+
onPointerLeave?.(event);
|
|
311
|
+
peekOut();
|
|
312
|
+
}}
|
|
313
|
+
{...props}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
if (!tooltip) {
|
|
317
|
+
return button;
|
|
318
|
+
}
|
|
319
|
+
return (
|
|
320
|
+
<NmTooltip>
|
|
321
|
+
<NmTooltipTrigger render={button} />
|
|
322
|
+
<NmTooltipContent side="bottom">
|
|
323
|
+
{name}
|
|
324
|
+
<NmKbd>⌘B</NmKbd>
|
|
325
|
+
</NmTooltipContent>
|
|
326
|
+
</NmTooltip>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** A thin strip on the sidebar's edge. Clicking it toggles the sidebar. */
|
|
331
|
+
export function NmSidebarRail({ className, label = 'Toggle sidebar', ...props }: ComponentProps<'button'> & { label?: string }): ReactNode {
|
|
332
|
+
const { toggleSidebar } = useSidebar();
|
|
333
|
+
return (
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
data-slot="sidebar-rail"
|
|
337
|
+
aria-label={label}
|
|
338
|
+
tabIndex={-1}
|
|
339
|
+
title={label}
|
|
340
|
+
className={nmClass('nm-sidebar__rail', className)}
|
|
341
|
+
onClick={toggleSidebar}
|
|
342
|
+
{...props}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The main area beside the sidebar. With the inset variant it is the raised card. */
|
|
348
|
+
export function NmSidebarInset({ className, ...props }: ComponentProps<'main'>): ReactNode {
|
|
349
|
+
return <main data-slot="sidebar-inset" className={nmClass('nm-sidebar__inset', className)} {...props} />;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** A search field in the sidebar. The NIF Input. */
|
|
353
|
+
export function NmSidebarInput({ className, ...props }: ComponentProps<typeof NmInput>): ReactNode {
|
|
354
|
+
return <NmInput data-slot="sidebar-input" className={nmClass('nm-sidebar__input', className as string)} {...props} />;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Sticky at the top: a brand, a title or a workspace switcher, and the toggle when the sidebar collapses to it. */
|
|
358
|
+
export function NmSidebarHeader({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
359
|
+
return <div data-slot="sidebar-header" className={nmClass('nm-sidebar__header', className)} {...props} />;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Sticky at the bottom: the person, settings or actions. */
|
|
363
|
+
export function NmSidebarFooter({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
364
|
+
return <div data-slot="sidebar-footer" className={nmClass('nm-sidebar__footer', className)} {...props} />;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The NIF Separator, fitted to the sidebar's padding. */
|
|
368
|
+
export function NmSidebarSeparator({ className, ...props }: ComponentProps<typeof NmSeparator>): ReactNode {
|
|
369
|
+
return <NmSeparator data-slot="sidebar-separator" className={nmClass('nm-sidebar__separator', className as string)} {...props} />;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The region between the header and the footer. It scrolls. */
|
|
373
|
+
export function NmSidebarContent({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
374
|
+
return <div data-slot="sidebar-content" className={nmClass('nm-sidebar__content', className)} {...props} />;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Related navigation, with a label, an action and content. */
|
|
378
|
+
export function NmSidebarGroup({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
379
|
+
return <div data-slot="sidebar-group" className={nmClass('nm-sidebar__group', className)} {...props} />;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function NmSidebarGroupLabel({ className, render, ref, ...props }: ComponentProps<'div'> & { render?: RenderProp }): ReactNode {
|
|
383
|
+
return useRender({
|
|
384
|
+
render,
|
|
385
|
+
ref,
|
|
386
|
+
defaultTagName: 'div',
|
|
387
|
+
props: { 'data-slot': 'sidebar-group-label', className: nmClass('nm-sidebar__group-label', className), ...props },
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** An icon button at the end of a group label, such as add. The NIF small icon button, quiet. */
|
|
392
|
+
export function NmSidebarGroupAction({ className, ...props }: ComponentProps<typeof NmSmallIconButton>): ReactNode {
|
|
393
|
+
return <NmSmallIconButton data-slot="sidebar-group-action" tone="quiet" className={nmClass('nm-sidebar__group-action', className)} {...props} />;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function NmSidebarGroupContent({ className, ...props }: ComponentProps<'div'>): ReactNode {
|
|
397
|
+
return <div data-slot="sidebar-group-content" className={nmClass('nm-sidebar__group-content', className)} {...props} />;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function NmSidebarMenu({ className, ...props }: ComponentProps<'ul'>): ReactNode {
|
|
401
|
+
return <ul data-slot="sidebar-menu" className={nmClass('nm-sidebar__menu', className)} {...props} />;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function NmSidebarMenuItem({ className, ...props }: ComponentProps<'li'>): ReactNode {
|
|
405
|
+
return <li data-slot="sidebar-menu-item" className={nmClass('nm-sidebar__menu-item', className)} {...props} />;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export type NmSidebarMenuButtonProps = ComponentProps<'button'> & {
|
|
409
|
+
render?: RenderProp;
|
|
410
|
+
isActive?: boolean;
|
|
411
|
+
variant?: NmSidebarButtonVariant;
|
|
412
|
+
size?: NmSidebarButtonSize;
|
|
413
|
+
/** Shown beside the icon when the sidebar is collapsed to icons. A string, or the props of the tooltip content. */
|
|
414
|
+
tooltip?: string | ComponentProps<typeof NmTooltipContent>;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** A row in the menu: an icon and a label, as a button or a link. Active, disabled, three sizes and two variants. */
|
|
418
|
+
export function NmSidebarMenuButton({
|
|
419
|
+
render,
|
|
420
|
+
isActive = false,
|
|
421
|
+
variant = 'default',
|
|
422
|
+
size = 'default',
|
|
423
|
+
tooltip,
|
|
424
|
+
className,
|
|
425
|
+
ref,
|
|
426
|
+
...props
|
|
427
|
+
}: NmSidebarMenuButtonProps): ReactNode {
|
|
428
|
+
const { isMobile, state, peek } = useSidebar();
|
|
429
|
+
const button = useRender({
|
|
430
|
+
render,
|
|
431
|
+
ref,
|
|
432
|
+
defaultTagName: 'button',
|
|
433
|
+
props: {
|
|
434
|
+
type: render ? undefined : 'button',
|
|
435
|
+
'data-slot': 'sidebar-menu-button',
|
|
436
|
+
'data-size': size,
|
|
437
|
+
'data-active': isActive ? '' : undefined,
|
|
438
|
+
'data-variant': variant,
|
|
439
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
440
|
+
className: nmClass('nm-sidebar__menu-button', className),
|
|
441
|
+
...props,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
if (tooltip == null) {
|
|
445
|
+
return button;
|
|
446
|
+
}
|
|
447
|
+
const content = typeof tooltip === 'string' ? { children: tooltip } : tooltip;
|
|
448
|
+
// The tooltip exists only while the label is out of view: collapsed to icons, not peeking, not on a phone.
|
|
449
|
+
const shows = state === 'collapsed' && !peek && !isMobile;
|
|
450
|
+
return (
|
|
451
|
+
<NmTooltip>
|
|
452
|
+
<NmTooltipTrigger render={button} />
|
|
453
|
+
{shows ? <NmTooltipContent side="right" align="center" {...content} /> : null}
|
|
454
|
+
</NmTooltip>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** An icon button at the end of a row, such as more. The NIF small icon button, quiet. `showOnHover` keeps it hidden until the row is hovered or focused. */
|
|
459
|
+
export function NmSidebarMenuAction({ className, showOnHover = false, ...props }: ComponentProps<typeof NmSmallIconButton> & { showOnHover?: boolean }): ReactNode {
|
|
460
|
+
return (
|
|
461
|
+
<NmSmallIconButton
|
|
462
|
+
data-slot="sidebar-menu-action"
|
|
463
|
+
data-show-on-hover={showOnHover ? '' : undefined}
|
|
464
|
+
tone="quiet"
|
|
465
|
+
className={nmClass('nm-sidebar__menu-action', className)}
|
|
466
|
+
{...props}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A count or a short word at the end of a row. The NIF Chip, small. */
|
|
472
|
+
export function NmSidebarMenuBadge({ className, ...props }: ComponentProps<typeof NmChip>): ReactNode {
|
|
473
|
+
return <NmChip data-slot="sidebar-menu-badge" size="sm" className={nmClass('nm-sidebar__menu-badge', className)} {...props} />;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Two lines in a row, such as a name and a plan in the footer. For a large menu button. */
|
|
477
|
+
export function NmSidebarMenuText({ title, description, className, ...props }: ComponentProps<'span'> & { title: string; description?: string }): ReactNode {
|
|
478
|
+
return (
|
|
479
|
+
<span data-slot="sidebar-menu-text" className={nmClass('nm-sidebar__text', className)} {...props}>
|
|
480
|
+
<span>{title}</span>
|
|
481
|
+
{description != null ? <span>{description}</span> : null}
|
|
482
|
+
</span>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** A row's shape while its content loads. The icon and the text are random widths, so a list of them reads as a list. */
|
|
487
|
+
export function NmSidebarMenuSkeleton({ className, showIcon = false, ...props }: ComponentProps<'div'> & { showIcon?: boolean }): ReactNode {
|
|
488
|
+
const [width] = useState(() => `${Math.floor(Math.random() * 40) + 50}%`);
|
|
489
|
+
return (
|
|
490
|
+
<div data-slot="sidebar-menu-skeleton" className={nmClass('nm-sidebar__menu-skeleton', className)} {...props}>
|
|
491
|
+
{showIcon ? <NmSkeleton className="nm-sidebar__menu-skeleton-icon" /> : null}
|
|
492
|
+
<NmSkeleton className="nm-sidebar__menu-skeleton-text" style={{ ['--nm-sidebar-skeleton-width' as string]: width } as CSSProperties} />
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Rows under a row, indented by a line. */
|
|
498
|
+
export function NmSidebarMenuSub({ className, ...props }: ComponentProps<'ul'>): ReactNode {
|
|
499
|
+
return <ul data-slot="sidebar-menu-sub" className={nmClass('nm-sidebar__menu-sub', className)} {...props} />;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function NmSidebarMenuSubItem({ className, ...props }: ComponentProps<'li'>): ReactNode {
|
|
503
|
+
return <li data-slot="sidebar-menu-sub-item" className={nmClass('nm-sidebar__menu-sub-item', className)} {...props} />;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function NmSidebarMenuSubButton({
|
|
507
|
+
render,
|
|
508
|
+
size = 'default',
|
|
509
|
+
isActive = false,
|
|
510
|
+
className,
|
|
511
|
+
ref,
|
|
512
|
+
...props
|
|
513
|
+
}: ComponentProps<'a'> & { render?: RenderProp; size?: Exclude<NmSidebarButtonSize, 'lg'>; isActive?: boolean }): ReactNode {
|
|
514
|
+
return useRender({
|
|
515
|
+
render,
|
|
516
|
+
ref,
|
|
517
|
+
defaultTagName: 'a',
|
|
518
|
+
props: {
|
|
519
|
+
'data-slot': 'sidebar-menu-sub-button',
|
|
520
|
+
'data-size': size,
|
|
521
|
+
'data-active': isActive ? '' : undefined,
|
|
522
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
523
|
+
className: nmClass('nm-sidebar__menu-sub-button', className),
|
|
524
|
+
...props,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** A chevron at the end of a row. In a collapsible row it points right and turns down when the row is open. With `menu` it points down, for a row that opens a menu. */
|
|
530
|
+
export function NmSidebarChevron({ className, menu = false, ...props }: Omit<ComponentProps<typeof NmIcon>, 'id'> & { menu?: boolean }): ReactNode {
|
|
531
|
+
return <NmIcon id="chevron" data-menu={menu ? '' : undefined} className={nmClass('nm-sidebar__chevron', className)} {...props} />;
|
|
532
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
box-sizing: border-box;
|
|
4
4
|
width: var(--nm-icon);
|
|
5
5
|
height: var(--nm-icon);
|
|
6
|
-
border: 2px solid var(--nm-
|
|
6
|
+
border: 2px solid var(--nm-border-strong);
|
|
7
7
|
border-top-color: var(--nm-fg);
|
|
8
8
|
border-radius: 50%;
|
|
9
9
|
animation: nm-spinner-turn 0.8s linear infinite;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
width: calc(var(--nm-switch-thumb) * 2);
|
|
8
8
|
height: var(--nm-switch-thumb);
|
|
9
9
|
padding: var(--nm-space-2);
|
|
10
|
-
border: var(--nm-rule) solid var(--nm-
|
|
10
|
+
border: var(--nm-rule) solid var(--nm-border-strong);
|
|
11
11
|
border-radius: var(--toggle-r);
|
|
12
12
|
background: var(--nm-panel);
|
|
13
13
|
cursor: pointer;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
.nm-table__head {
|
|
34
34
|
padding: var(--nm-table-pad-y) var(--nm-table-pad-x);
|
|
35
|
-
border-bottom: var(--nm-rule) solid var(--nm-
|
|
35
|
+
border-bottom: var(--nm-rule) solid var(--nm-border-strong);
|
|
36
36
|
color: var(--nm-fg-muted);
|
|
37
37
|
font-size: var(--nm-type-ui-sm);
|
|
38
38
|
font-weight: var(--nm-weight-medium);
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
|
|
76
76
|
.nm-table[data-variant='default'] .nm-table__body .nm-table__row:not(:last-child),
|
|
77
77
|
.nm-table[data-variant='striped'] .nm-table__body .nm-table__row:not(:last-child) {
|
|
78
|
-
border-bottom: var(--nm-rule) solid var(--nm-
|
|
78
|
+
border-bottom: var(--nm-rule) solid var(--nm-border);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
.nm-table[data-variant='striped'] .nm-table__body .nm-table__row:nth-child(even) {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
|
|
85
85
|
.nm-table[data-variant='lined'] .nm-table__head,
|
|
86
86
|
.nm-table[data-variant='lined'] .nm-table__cell {
|
|
87
|
-
border: var(--nm-rule) solid var(--nm-
|
|
87
|
+
border: var(--nm-rule) solid var(--nm-border);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
.nm-table[data-variant='plain'] .nm-table__head {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
.nm-table__footer {
|
|
115
|
-
border-top: var(--nm-rule) solid var(--nm-
|
|
115
|
+
border-top: var(--nm-rule) solid var(--nm-border-strong);
|
|
116
116
|
background: var(--nm-raised);
|
|
117
117
|
font-weight: var(--nm-weight-medium);
|
|
118
118
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
width: 100%;
|
|
5
5
|
min-height: calc(var(--control-h) * 2.5);
|
|
6
6
|
padding: var(--field-area-py) var(--control-px);
|
|
7
|
-
border: var(--nm-rule) solid var(--nm-
|
|
7
|
+
border: var(--nm-rule) solid var(--nm-border-strong);
|
|
8
8
|
border-radius: var(--nm-control-r);
|
|
9
9
|
background: var(--nm-raised);
|
|
10
10
|
color: var(--nm-fg);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.nm-textarea:hover:not(:disabled) {
|
|
27
|
-
border-color: var(--nm-stroke);
|
|
27
|
+
border-color: var(--nm-border-stroke);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.nm-textarea:focus-visible {
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
flex: none;
|
|
103
103
|
flex-direction: column;
|
|
104
104
|
width: var(--nm-side-panel-basis, 50%);
|
|
105
|
-
border-inline-start: var(--nm-rule) solid var(--nm-divider);
|
|
105
|
+
border-inline-start: var(--nm-rule) solid var(--nm-border-divider);
|
|
106
106
|
background: var(--nm-canvas);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
height: 100%;
|
|
134
134
|
min-height: 100%;
|
|
135
135
|
padding: var(--nm-space-16);
|
|
136
|
-
border: var(--nm-rule) solid var(--nm-
|
|
136
|
+
border: var(--nm-rule) solid var(--nm-border-strong);
|
|
137
137
|
border-radius: var(--nm-control-r);
|
|
138
138
|
outline: none;
|
|
139
139
|
resize: none;
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
height: 100%;
|
|
157
157
|
padding: var(--nm-space-16);
|
|
158
158
|
overflow-y: auto;
|
|
159
|
-
border: var(--nm-rule) solid var(--nm-
|
|
159
|
+
border: var(--nm-rule) solid var(--nm-border-strong);
|
|
160
160
|
border-radius: var(--nm-control-r);
|
|
161
161
|
background: var(--nm-raised);
|
|
162
162
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
/* Raised sits on a surface of its own with an edge, so it reads on any background. Panel has no edge. */
|
|
34
34
|
.nm-icon-button--raised {
|
|
35
35
|
background: var(--nm-raised);
|
|
36
|
-
box-shadow: inset 0 0 0 var(--nm-rule) var(--nm-
|
|
36
|
+
box-shadow: inset 0 0 0 var(--nm-rule) var(--nm-border-strong);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.nm-icon-button--quiet {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
.nm-icon-button--quiet.nm-icon-button--active:not(:disabled) {
|
|
49
49
|
background: var(--nm-raised);
|
|
50
50
|
color: var(--nm-fg);
|
|
51
|
-
box-shadow: inset 0 0 0 var(--nm-rule) var(--nm-
|
|
51
|
+
box-shadow: inset 0 0 0 var(--nm-rule) var(--nm-border);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
.nm-icon-button--solid {
|