@lepid-labs/ui-react 1.0.0 → 1.0.2
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/AppHeader.d.ts +36 -0
- package/dist/AppHeader.js +32 -0
- package/dist/AppShell.d.ts +41 -0
- package/dist/AppShell.js +64 -0
- package/dist/ButtonGroup.d.ts +13 -0
- package/dist/ButtonGroup.js +14 -0
- package/dist/CodeBlock.d.ts +11 -0
- package/dist/CodeBlock.js +30 -0
- package/dist/HoldButton.d.ts +23 -0
- package/dist/HoldButton.js +112 -0
- package/dist/IconButton.d.ts +18 -0
- package/dist/IconButton.js +9 -0
- package/dist/LogBlock.d.ts +22 -0
- package/dist/LogBlock.js +30 -0
- package/dist/Page.d.ts +51 -0
- package/dist/Page.js +30 -0
- package/dist/SideNav.d.ts +27 -0
- package/dist/SideNav.js +21 -0
- package/dist/StatusCard.d.ts +42 -0
- package/dist/StatusCard.js +35 -0
- package/dist/Stepper.d.ts +19 -0
- package/dist/Stepper.js +12 -0
- package/dist/Table.d.ts +17 -0
- package/dist/Table.js +29 -0
- package/dist/Tabs.js +4 -1
- package/dist/button-group.d.ts +7 -0
- package/dist/button-group.js +11 -0
- package/dist/copy-state.d.ts +14 -0
- package/dist/copy-state.js +31 -0
- package/dist/feedback.d.ts +5 -1
- package/dist/feedback.js +2 -2
- package/dist/hold.d.ts +31 -0
- package/dist/hold.js +82 -0
- package/dist/icon-button.d.ts +23 -0
- package/dist/icon-button.js +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +23 -0
- package/dist/log-state.d.ts +20 -0
- package/dist/log-state.js +21 -0
- package/dist/poly.d.ts +6 -0
- package/dist/poly.js +1 -0
- package/dist/shell-state.d.ts +22 -0
- package/dist/shell-state.js +25 -0
- package/dist/slide-dir.d.ts +5 -0
- package/dist/slide-dir.js +12 -0
- package/dist/status-card.d.ts +15 -0
- package/dist/status-card.js +27 -0
- package/dist/stepper-state.d.ts +12 -0
- package/dist/stepper-state.js +24 -0
- package/dist/table-state.d.ts +28 -0
- package/dist/table-state.js +29 -0
- package/dist/useSlideDir.d.ts +7 -0
- package/dist/useSlideDir.js +26 -0
- package/package.json +3 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import type { PolyProps } from "./poly.js";
|
|
3
|
+
export interface AppHeaderProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
/** Logo mark and/or app name. */
|
|
5
|
+
brand?: ReactNode;
|
|
6
|
+
/** Makes the brand a link (usually home). */
|
|
7
|
+
brandHref?: string;
|
|
8
|
+
/** Usually a `TopNav`. */
|
|
9
|
+
nav?: ReactNode;
|
|
10
|
+
/** Right-aligned: search, theme switcher, account menu. */
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
/** Accessible name of the side-nav toggle. */
|
|
13
|
+
toggleLabel?: string;
|
|
14
|
+
}
|
|
15
|
+
/** The shell's sticky top bar. Inside an `AppShell` with a side nav it leads
|
|
16
|
+
* with the toggle that collapses the rail (wide) or opens the drawer (narrow). */
|
|
17
|
+
export declare function AppHeader({ brand, brandHref, nav, actions, toggleLabel, className, children, ...rest }: AppHeaderProps): import("react").JSX.Element;
|
|
18
|
+
export interface ShellToggleProps extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
/** The side-nav toggle: collapses the rail (wide) or opens the drawer
|
|
22
|
+
* (narrow). `AppHeader` renders one; place it yourself (e.g. in the
|
|
23
|
+
* `SideNav` footer) in a shell without a header. Renders nothing outside an
|
|
24
|
+
* `AppShell` with a nav. */
|
|
25
|
+
export declare function ShellToggle({ label, className, children, ...rest }: ShellToggleProps): import("react").JSX.Element | null;
|
|
26
|
+
export type TopNavProps = HTMLAttributes<HTMLElement>;
|
|
27
|
+
/** Horizontal page links for the header; scrolls sideways when narrow. */
|
|
28
|
+
export declare function TopNav({ className, ...rest }: TopNavProps): import("react").JSX.Element;
|
|
29
|
+
export type TopNavItemProps<E extends ElementType = "a"> = PolyProps<E, {
|
|
30
|
+
active?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}>;
|
|
34
|
+
/** A header link. `active` sets `aria-current="page"` (router links that set
|
|
35
|
+
* it themselves light up the same way). */
|
|
36
|
+
export declare function TopNavItem<E extends ElementType = "a">({ as, active, className, ...rest }: TopNavItemProps<E>): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useShell } from "./AppShell.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** The shell's sticky top bar. Inside an `AppShell` with a side nav it leads
|
|
5
|
+
* with the toggle that collapses the rail (wide) or opens the drawer (narrow). */
|
|
6
|
+
export function AppHeader({ brand, brandHref, nav, actions, toggleLabel = "Toggle navigation", className, children, ...rest }) {
|
|
7
|
+
return (_jsxs("header", { className: cx("ld-shell__header", className), ...rest, children: [_jsx(ShellToggle, { label: toggleLabel }), brand != null &&
|
|
8
|
+
(brandHref ? (_jsx("a", { className: "ld-shell__brand", href: brandHref, children: brand })) : (_jsx("span", { className: "ld-shell__brand", children: brand }))), nav, children, actions != null && _jsx("div", { className: "ld-shell__actions", children: actions })] }));
|
|
9
|
+
}
|
|
10
|
+
/** The side-nav toggle: collapses the rail (wide) or opens the drawer
|
|
11
|
+
* (narrow). `AppHeader` renders one; place it yourself (e.g. in the
|
|
12
|
+
* `SideNav` footer) in a shell without a header. Renders nothing outside an
|
|
13
|
+
* `AppShell` with a nav. */
|
|
14
|
+
export function ShellToggle({ label = "Toggle navigation", className, children, ...rest }) {
|
|
15
|
+
const shell = useShell();
|
|
16
|
+
if (!shell?.hasNav)
|
|
17
|
+
return null;
|
|
18
|
+
return (_jsx("button", { type: "button", className: cx("ld-shell__toggle", className), "aria-label": label, "aria-controls": shell.navId, "aria-expanded": shell.expanded, onClick: shell.toggleNav, ...rest, children: children ?? _jsx(MenuIcon, {}) }));
|
|
19
|
+
}
|
|
20
|
+
/** Horizontal page links for the header; scrolls sideways when narrow. */
|
|
21
|
+
export function TopNav({ className, ...rest }) {
|
|
22
|
+
return _jsx("nav", { className: cx("ld-topnav", className), ...rest });
|
|
23
|
+
}
|
|
24
|
+
/** A header link. `active` sets `aria-current="page"` (router links that set
|
|
25
|
+
* it themselves light up the same way). */
|
|
26
|
+
export function TopNavItem({ as, active, className, ...rest }) {
|
|
27
|
+
const Tag = as ?? "a";
|
|
28
|
+
return (_jsx(Tag, { className: cx("ld-topnav__item", className), ...(active ? { "aria-current": "page" } : {}), ...rest }));
|
|
29
|
+
}
|
|
30
|
+
function MenuIcon() {
|
|
31
|
+
return (_jsx("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M3 5.5h14M3 10h14M3 14.5h14" }) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
export interface ShellContextValue {
|
|
3
|
+
/** The shell has a side nav (so the header shows the toggle). */
|
|
4
|
+
hasNav: boolean;
|
|
5
|
+
/** The viewport is below the 48rem breakpoint (drawer / tab bar mode). */
|
|
6
|
+
narrow: boolean;
|
|
7
|
+
/** Wide viewports: the nav is an icon rail. */
|
|
8
|
+
collapsed: boolean;
|
|
9
|
+
/** The nav is visibly expanded on the current viewport. */
|
|
10
|
+
expanded: boolean;
|
|
11
|
+
navId: string;
|
|
12
|
+
toggleNav: () => void;
|
|
13
|
+
closeNav: () => void;
|
|
14
|
+
}
|
|
15
|
+
/** The enclosing AppShell's nav state, or null outside one. */
|
|
16
|
+
export declare function useShell(): ShellContextValue | null;
|
|
17
|
+
/** True below the shell breakpoint; false during SSR. */
|
|
18
|
+
export declare function useShellNarrow(): boolean;
|
|
19
|
+
export interface AppShellProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
20
|
+
/** Usually an `AppHeader`. Leave out for a shell without a top bar. */
|
|
21
|
+
header?: ReactNode;
|
|
22
|
+
/** Usually a `SideNav`. Leave out for a shell without a left-hand nav. */
|
|
23
|
+
nav?: ReactNode;
|
|
24
|
+
footer?: ReactNode;
|
|
25
|
+
/** Page content, rendered inside `<main>`. */
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
/** Below 48rem the nav becomes a slide-in drawer or a bottom tab bar. The
|
|
28
|
+
* drawer opens from the header's toggle, so the default is "drawer" with a
|
|
29
|
+
* header and "tabbar" without one. */
|
|
30
|
+
mobileNav?: "drawer" | "tabbar";
|
|
31
|
+
/** Controlled icon-rail state for wide viewports. */
|
|
32
|
+
navCollapsed?: boolean;
|
|
33
|
+
defaultNavCollapsed?: boolean;
|
|
34
|
+
onNavCollapsedChange?: (collapsed: boolean) => void;
|
|
35
|
+
/** Text of the skip link to main content; `false` drops it. */
|
|
36
|
+
skipLink?: ReactNode | false;
|
|
37
|
+
}
|
|
38
|
+
/** Application frame: optional sticky header, optional left-hand nav (icon
|
|
39
|
+
* rail when collapsed, drawer or tab bar on narrow screens), main, optional
|
|
40
|
+
* footer. Paints the theme's page background. */
|
|
41
|
+
export declare function AppShell({ header, nav, footer, children, mobileNav, navCollapsed, defaultNavCollapsed, onNavCollapsedChange, skipLink, className, ...rest }: AppShellProps): import("react").JSX.Element;
|
package/dist/AppShell.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useId, useState, useSyncExternalStore, } from "react";
|
|
3
|
+
import { navExpanded, shellAttrs, SHELL_NARROW_QUERY, toggleNav } from "./shell-state.js";
|
|
4
|
+
const ShellContext = createContext(null);
|
|
5
|
+
/** The enclosing AppShell's nav state, or null outside one. */
|
|
6
|
+
export function useShell() {
|
|
7
|
+
return useContext(ShellContext);
|
|
8
|
+
}
|
|
9
|
+
const subscribeNarrow = (onChange) => {
|
|
10
|
+
const mq = window.matchMedia(SHELL_NARROW_QUERY);
|
|
11
|
+
mq.addEventListener("change", onChange);
|
|
12
|
+
return () => mq.removeEventListener("change", onChange);
|
|
13
|
+
};
|
|
14
|
+
/** True below the shell breakpoint; false during SSR. */
|
|
15
|
+
export function useShellNarrow() {
|
|
16
|
+
return useSyncExternalStore(subscribeNarrow, () => window.matchMedia(SHELL_NARROW_QUERY).matches, () => false);
|
|
17
|
+
}
|
|
18
|
+
/** Application frame: optional sticky header, optional left-hand nav (icon
|
|
19
|
+
* rail when collapsed, drawer or tab bar on narrow screens), main, optional
|
|
20
|
+
* footer. Paints the theme's page background. */
|
|
21
|
+
export function AppShell({ header, nav, footer, children, mobileNav = header == null ? "tabbar" : "drawer", navCollapsed, defaultNavCollapsed = false, onNavCollapsedChange, skipLink = "Skip to content", className, ...rest }) {
|
|
22
|
+
const narrow = useShellNarrow();
|
|
23
|
+
const [innerCollapsed, setInnerCollapsed] = useState(defaultNavCollapsed);
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const collapsed = navCollapsed ?? innerCollapsed;
|
|
26
|
+
const state = { collapsed, open: open && narrow };
|
|
27
|
+
const navId = useId();
|
|
28
|
+
const mainId = useId();
|
|
29
|
+
const toggle = useCallback(() => {
|
|
30
|
+
const next = toggleNav({ collapsed, open }, narrow);
|
|
31
|
+
if (next.open !== open)
|
|
32
|
+
setOpen(next.open);
|
|
33
|
+
if (next.collapsed !== collapsed) {
|
|
34
|
+
if (navCollapsed === undefined)
|
|
35
|
+
setInnerCollapsed(next.collapsed);
|
|
36
|
+
onNavCollapsedChange?.(next.collapsed);
|
|
37
|
+
}
|
|
38
|
+
}, [collapsed, open, narrow, navCollapsed, onNavCollapsedChange]);
|
|
39
|
+
const close = useCallback(() => setOpen(false), []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!state.open)
|
|
42
|
+
return;
|
|
43
|
+
const onKey = (e) => e.key === "Escape" && setOpen(false);
|
|
44
|
+
document.addEventListener("keydown", onKey);
|
|
45
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
46
|
+
}, [state.open]);
|
|
47
|
+
// Following a link out of the drawer closes it.
|
|
48
|
+
const onNavClick = (e) => {
|
|
49
|
+
if (narrow && e.target.closest("a"))
|
|
50
|
+
setOpen(false);
|
|
51
|
+
};
|
|
52
|
+
const ctx = {
|
|
53
|
+
hasNav: nav != null,
|
|
54
|
+
narrow,
|
|
55
|
+
collapsed,
|
|
56
|
+
expanded: navExpanded(state, narrow),
|
|
57
|
+
navId,
|
|
58
|
+
toggleNav: toggle,
|
|
59
|
+
closeNav: close,
|
|
60
|
+
};
|
|
61
|
+
const modifiers = [mobileNav === "tabbar" && "ld-shell--tabbar", className];
|
|
62
|
+
const attrs = shellAttrs(state, modifiers.filter(Boolean).join(" "));
|
|
63
|
+
return (_jsx(ShellContext.Provider, { value: ctx, children: _jsxs("div", { ...attrs, ...rest, children: [skipLink !== false && (_jsx("a", { className: "ld-shell__skip", href: `#${mainId}`, children: skipLink })), header, nav != null && (_jsxs(_Fragment, { children: [_jsx("aside", { className: "ld-shell__nav", id: navId, onClick: onNavClick, children: nav }), _jsx("div", { className: "ld-shell__scrim", "aria-hidden": "true", onClick: close })] })), _jsx("main", { className: "ld-shell__main", id: mainId, tabIndex: -1, children: children }), footer != null && _jsx("footer", { className: "ld-shell__footer", children: footer })] }) }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** Stretch to the full row with equal-width segments. Maps to .ld-btn-group--block. */
|
|
4
|
+
block?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** One long track with `Button`s lined up inside it. The theme strips the
|
|
7
|
+
* segments' own chrome, so children are plain `Button`s; name the group with
|
|
8
|
+
* `aria-label`. For a toggle / segmented control set `aria-pressed` on each
|
|
9
|
+
* `Button` — the theme lights the pressed segment from that attribute, and
|
|
10
|
+
* which one is pressed stays the caller's state. With a single pressed segment
|
|
11
|
+
* the theme slides one pill between segments; the group stamps `data-ld-dir`
|
|
12
|
+
* so the pill's leading edge reaches the new segment first. */
|
|
13
|
+
export declare function ButtonGroup({ block, className, ...rest }: ButtonGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { buttonGroupAttrs } from "./button-group.js";
|
|
3
|
+
import { useSlideDir } from "./useSlideDir.js";
|
|
4
|
+
/** One long track with `Button`s lined up inside it. The theme strips the
|
|
5
|
+
* segments' own chrome, so children are plain `Button`s; name the group with
|
|
6
|
+
* `aria-label`. For a toggle / segmented control set `aria-pressed` on each
|
|
7
|
+
* `Button` — the theme lights the pressed segment from that attribute, and
|
|
8
|
+
* which one is pressed stays the caller's state. With a single pressed segment
|
|
9
|
+
* the theme slides one pill between segments; the group stamps `data-ld-dir`
|
|
10
|
+
* so the pill's leading edge reaches the new segment first. */
|
|
11
|
+
export function ButtonGroup({ block, className, ...rest }) {
|
|
12
|
+
const ref = useSlideDir('.ld-btn--active, [aria-pressed="true"]');
|
|
13
|
+
return _jsx("div", { ...buttonGroupAttrs(block, className), ...rest, ref: ref });
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
export type CodeBlockProps = HTMLAttributes<HTMLPreElement> & {
|
|
3
|
+
/** Accessible name and tooltip of the copy button. Default "Copy code". */
|
|
4
|
+
copyLabel?: string;
|
|
5
|
+
/** Class for the `.ld-pre-wrap` wrapper (`className` goes on the `<pre>`). */
|
|
6
|
+
wrapClassName?: string;
|
|
7
|
+
};
|
|
8
|
+
/** `.ld-pre` with a copy-to-clipboard control in its top-right corner. Copies
|
|
9
|
+
* the block's text content; for ~1.5s the button shows a check (or a danger
|
|
10
|
+
* tint on failure) and a polite live region announces the result. */
|
|
11
|
+
export declare function CodeBlock({ copyLabel, wrapClassName, className, children, ...rest }: CodeBlockProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { IconButton } from "./IconButton.js";
|
|
4
|
+
import { COPIED_MS, copyAnnouncement, copyDataAttr, copyText } from "./copy-state.js";
|
|
5
|
+
import { cx } from "./cx.js";
|
|
6
|
+
const glyph = {
|
|
7
|
+
viewBox: "0 0 20 20",
|
|
8
|
+
fill: "none",
|
|
9
|
+
stroke: "currentColor",
|
|
10
|
+
strokeWidth: 1.6,
|
|
11
|
+
strokeLinecap: "round",
|
|
12
|
+
strokeLinejoin: "round",
|
|
13
|
+
"aria-hidden": true,
|
|
14
|
+
};
|
|
15
|
+
/** `.ld-pre` with a copy-to-clipboard control in its top-right corner. Copies
|
|
16
|
+
* the block's text content; for ~1.5s the button shows a check (or a danger
|
|
17
|
+
* tint on failure) and a polite live region announces the result. */
|
|
18
|
+
export function CodeBlock({ copyLabel = "Copy code", wrapClassName, className, children, ...rest }) {
|
|
19
|
+
const pre = useRef(null);
|
|
20
|
+
const timer = useRef(undefined);
|
|
21
|
+
const [status, setStatus] = useState("idle");
|
|
22
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
23
|
+
async function copy() {
|
|
24
|
+
const result = await copyText(pre.current?.textContent ?? "");
|
|
25
|
+
clearTimeout(timer.current);
|
|
26
|
+
setStatus(result);
|
|
27
|
+
timer.current = setTimeout(() => setStatus("idle"), COPIED_MS);
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs("div", { className: cx("ld-pre-wrap", wrapClassName), children: [_jsx("pre", { ref: pre, className: cx("ld-pre", className), ...rest, children: children }), _jsxs(IconButton, { label: copyLabel, className: "ld-pre-copy", "data-ld-copy": copyDataAttr(status), onClick: copy, children: [_jsxs("svg", { className: "ld-pre-copy__icon", ...glyph, children: [_jsx("rect", { x: "7", y: "7", width: "10", height: "10", rx: "1.5" }), _jsx("path", { d: "M13 4.5V4a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h.5" })] }), _jsx("svg", { className: "ld-pre-copy__check", ...glyph, children: _jsx("path", { d: "m4 10.5 4 4 8-9" }) })] }), _jsx("span", { className: "ld-pre-copy-status", "aria-live": "polite", children: copyAnnouncement(status) })] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ButtonProps } from "./Button.js";
|
|
3
|
+
export interface HoldButtonProps extends Omit<ButtonProps, "onClick"> {
|
|
4
|
+
/** Commitment window in ms. Defaults to the theme's --ld-hold-duration (300 ms). */
|
|
5
|
+
duration?: number;
|
|
6
|
+
/** Fires once when the hold completes. */
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
/** Optional secondary text under the label (announced via aria-describedby). */
|
|
9
|
+
hint?: ReactNode;
|
|
10
|
+
/** Show the live percentage. Default true. */
|
|
11
|
+
showMeter?: boolean;
|
|
12
|
+
/** Content of the circular well the ring wraps — usually an icon. */
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
/** Keyboard escape hatch: a plain Space/Enter tap fires immediately instead
|
|
15
|
+
* of requiring a held key. Pair with a confirm dialog in onConfirm. */
|
|
16
|
+
confirmOnKeyboardTap?: boolean;
|
|
17
|
+
/** Announced to assistive tech once the hold fires. Default "Confirmed". */
|
|
18
|
+
confirmedLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Hold-to-confirm button: the ring around the icon fills over the commitment
|
|
21
|
+
* window and the action fires only when it completes. Releasing early cancels.
|
|
22
|
+
* Renders .ld-btn.ld-btn--hold with the ring/body/meter subparts. */
|
|
23
|
+
export declare function HoldButton({ duration, onConfirm, hint, showMeter, icon, confirmOnKeyboardTap, confirmedLabel, variant, size, className, style, children, disabled, onPointerDown, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, onKeyUp, onBlur, ...rest }: HoldButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { createHoldController } from "./hold.js";
|
|
5
|
+
const SR_ONLY = {
|
|
6
|
+
position: "absolute",
|
|
7
|
+
width: 1,
|
|
8
|
+
height: 1,
|
|
9
|
+
margin: -1,
|
|
10
|
+
padding: 0,
|
|
11
|
+
overflow: "hidden",
|
|
12
|
+
clip: "rect(0 0 0 0)",
|
|
13
|
+
whiteSpace: "nowrap",
|
|
14
|
+
border: 0,
|
|
15
|
+
};
|
|
16
|
+
const HOLD_KEYS = new Set([" ", "Enter"]);
|
|
17
|
+
/** Parse a CSS <time> ("300ms" | "0.3s") into ms; undefined if unparseable. */
|
|
18
|
+
function parseCssTime(raw) {
|
|
19
|
+
const m = raw.trim().match(/^([\d.]+)\s*(ms|s)$/);
|
|
20
|
+
if (!m)
|
|
21
|
+
return undefined;
|
|
22
|
+
const n = Number(m[1]);
|
|
23
|
+
return m[2] === "s" ? n * 1000 : n;
|
|
24
|
+
}
|
|
25
|
+
/** Hold-to-confirm button: the ring around the icon fills over the commitment
|
|
26
|
+
* window and the action fires only when it completes. Releasing early cancels.
|
|
27
|
+
* Renders .ld-btn.ld-btn--hold with the ring/body/meter subparts. */
|
|
28
|
+
export function HoldButton({ duration, onConfirm, hint, showMeter = true, icon, confirmOnKeyboardTap = false, confirmedLabel = "Confirmed", variant = "default", size = "md", className, style, children, disabled, onPointerDown, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, onKeyUp, onBlur, ...rest }) {
|
|
29
|
+
const ref = useRef(null);
|
|
30
|
+
const ctl = useRef(null);
|
|
31
|
+
const latest = useRef({ duration, onConfirm });
|
|
32
|
+
latest.current = { duration, onConfirm };
|
|
33
|
+
const hintId = useId();
|
|
34
|
+
const [progress, setProgress] = useState(0);
|
|
35
|
+
const [state, setState] = useState("idle");
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const c = createHoldController({
|
|
38
|
+
duration: () => {
|
|
39
|
+
const explicit = latest.current.duration;
|
|
40
|
+
if (explicit !== undefined)
|
|
41
|
+
return explicit;
|
|
42
|
+
const el = ref.current;
|
|
43
|
+
const fromTheme = el && parseCssTime(getComputedStyle(el).getPropertyValue("--ld-hold-duration"));
|
|
44
|
+
return fromTheme ?? 300;
|
|
45
|
+
},
|
|
46
|
+
onProgress: setProgress,
|
|
47
|
+
onConfirm: () => latest.current.onConfirm(),
|
|
48
|
+
onStateChange: setState,
|
|
49
|
+
});
|
|
50
|
+
ctl.current = c;
|
|
51
|
+
return () => c.dispose();
|
|
52
|
+
}, []);
|
|
53
|
+
const start = useCallback(() => {
|
|
54
|
+
if (!disabled)
|
|
55
|
+
ctl.current?.start();
|
|
56
|
+
}, [disabled]);
|
|
57
|
+
const cancel = useCallback(() => ctl.current?.cancel(), []);
|
|
58
|
+
const handlePointerDown = (e) => {
|
|
59
|
+
onPointerDown?.(e);
|
|
60
|
+
if (e.defaultPrevented || e.button !== 0 || !e.isPrimary)
|
|
61
|
+
return;
|
|
62
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
63
|
+
start();
|
|
64
|
+
};
|
|
65
|
+
const handlePointerUp = (e) => {
|
|
66
|
+
onPointerUp?.(e);
|
|
67
|
+
cancel();
|
|
68
|
+
};
|
|
69
|
+
const handlePointerCancel = (e) => {
|
|
70
|
+
onPointerCancel?.(e);
|
|
71
|
+
cancel();
|
|
72
|
+
};
|
|
73
|
+
const handleLostCapture = (e) => {
|
|
74
|
+
onLostPointerCapture?.(e);
|
|
75
|
+
cancel();
|
|
76
|
+
};
|
|
77
|
+
const handleKeyDown = (e) => {
|
|
78
|
+
onKeyDown?.(e);
|
|
79
|
+
if (e.defaultPrevented)
|
|
80
|
+
return;
|
|
81
|
+
if (e.key === "Escape") {
|
|
82
|
+
cancel();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!HOLD_KEYS.has(e.key))
|
|
86
|
+
return;
|
|
87
|
+
e.preventDefault(); // suppress the native click that Space/Enter would synthesize
|
|
88
|
+
if (e.repeat)
|
|
89
|
+
return;
|
|
90
|
+
if (confirmOnKeyboardTap) {
|
|
91
|
+
if (!disabled)
|
|
92
|
+
ctl.current?.fire();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
start();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleKeyUp = (e) => {
|
|
99
|
+
onKeyUp?.(e);
|
|
100
|
+
if (HOLD_KEYS.has(e.key)) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
cancel();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const handleBlur = (e) => {
|
|
106
|
+
onBlur?.(e);
|
|
107
|
+
cancel();
|
|
108
|
+
};
|
|
109
|
+
const percent = Math.round(progress * 100);
|
|
110
|
+
const classes = cx("ld-btn", "ld-btn--hold", variant !== "default" && `ld-btn--${variant}`, size === "sm" && "ld-btn--sm", className);
|
|
111
|
+
return (_jsxs("button", { ref: ref, type: "button", className: classes, style: { ...style, "--ld-hold": progress }, "data-ld-hold": state === "idle" ? undefined : state, "aria-describedby": hint !== undefined ? hintId : undefined, disabled: disabled, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, onLostPointerCapture: handleLostCapture, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onBlur: handleBlur, ...rest, children: [_jsx("span", { className: "ld-btn__ring", "aria-hidden": "true", children: icon }), _jsxs("span", { className: "ld-btn__body", children: [_jsx("span", { className: "ld-btn__label", children: children }), hint !== undefined && (_jsx("span", { className: "ld-btn__hint", id: hintId, children: hint }))] }), showMeter && (_jsxs("span", { className: "ld-btn__meter", "aria-hidden": "true", children: [percent, "%"] })), _jsx("span", { style: SR_ONLY, "aria-live": "polite", children: state === "fired" ? confirmedLabel : "" })] }));
|
|
112
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from "react";
|
|
2
|
+
import type { PolyProps } from "./poly.js";
|
|
3
|
+
export type IconButtonProps<E extends ElementType = "button"> = PolyProps<E, {
|
|
4
|
+
/** Accessible name (`aria-label`), and the tooltip unless `title` is set. Required: the control has no text. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Lit state. Maps to .ld-icon-btn--active; on a link also `aria-current="page"`.
|
|
7
|
+
* For a toggle button pass `aria-pressed` instead — the theme lights that too. */
|
|
8
|
+
active?: boolean;
|
|
9
|
+
/** "danger" tints the glyph red on hover. Maps to .ld-icon-btn--danger. */
|
|
10
|
+
variant?: "default" | "danger";
|
|
11
|
+
title?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** The icon (an inline `<svg>`; the theme sizes it to 1.25em). */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}>;
|
|
16
|
+
/** Chromeless square icon-only control. Renders a `<button type="button">`,
|
|
17
|
+
* or `as` — `"a"` or a router Link — for navigation. */
|
|
18
|
+
export declare function IconButton<E extends ElementType = "button">({ as, label, active, variant, title, className, ...rest }: IconButtonProps<E>): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { iconButtonAttrs } from "./icon-button.js";
|
|
3
|
+
/** Chromeless square icon-only control. Renders a `<button type="button">`,
|
|
4
|
+
* or `as` — `"a"` or a router Link — for navigation. */
|
|
5
|
+
export function IconButton({ as, label, active, variant, title, className, ...rest }) {
|
|
6
|
+
const Tag = as ?? "button";
|
|
7
|
+
const attrs = iconButtonAttrs({ label, active, variant, title, isButton: Tag === "button", className });
|
|
8
|
+
return _jsx(Tag, { ...attrs, ...rest });
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
export type LogBlockProps = HTMLAttributes<HTMLPreElement> & {
|
|
3
|
+
/** Keep the view on the newest line as content grows — but only while the
|
|
4
|
+
* reader is already at the bottom; scrolling up pauses it. */
|
|
5
|
+
follow?: boolean;
|
|
6
|
+
/** Height cap (number = px). Defaults to the theme's 420px. */
|
|
7
|
+
maxHeight?: number | string;
|
|
8
|
+
/** Announce appended lines to assistive tech (role="log"). Turn off for very
|
|
9
|
+
* chatty streams. Default true. */
|
|
10
|
+
live?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Terminal/log `<pre>`: bounded, scrolling, wrapped mono output (.ld-pre--log). */
|
|
13
|
+
export declare const LogBlock: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLPreElement> & {
|
|
14
|
+
/** Keep the view on the newest line as content grows — but only while the
|
|
15
|
+
* reader is already at the bottom; scrolling up pauses it. */
|
|
16
|
+
follow?: boolean;
|
|
17
|
+
/** Height cap (number = px). Defaults to the theme's 420px. */
|
|
18
|
+
maxHeight?: number | string;
|
|
19
|
+
/** Announce appended lines to assistive tech (role="log"). Turn off for very
|
|
20
|
+
* chatty streams. Default true. */
|
|
21
|
+
live?: boolean;
|
|
22
|
+
} & import("react").RefAttributes<HTMLPreElement>>;
|
package/dist/LogBlock.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { isPinnedToBottom, logAttrs } from "./log-state.js";
|
|
4
|
+
function setRef(ref, value) {
|
|
5
|
+
if (typeof ref === "function")
|
|
6
|
+
ref(value);
|
|
7
|
+
else if (ref)
|
|
8
|
+
ref.current = value;
|
|
9
|
+
}
|
|
10
|
+
/** Terminal/log `<pre>`: bounded, scrolling, wrapped mono output (.ld-pre--log). */
|
|
11
|
+
export const LogBlock = forwardRef(function LogBlock({ follow, maxHeight, live, className, style, onScroll, children, ...rest }, ref) {
|
|
12
|
+
const el = useRef(null);
|
|
13
|
+
const pinned = useRef(true);
|
|
14
|
+
const attrs = logAttrs({ maxHeight, live, className });
|
|
15
|
+
const attach = useCallback((node) => {
|
|
16
|
+
el.current = node;
|
|
17
|
+
setRef(ref, node);
|
|
18
|
+
}, [ref]);
|
|
19
|
+
function handleScroll(e) {
|
|
20
|
+
const t = e.currentTarget;
|
|
21
|
+
pinned.current = isPinnedToBottom(t.scrollTop, t.scrollHeight, t.clientHeight);
|
|
22
|
+
onScroll?.(e);
|
|
23
|
+
}
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
const node = el.current;
|
|
26
|
+
if (follow && node && pinned.current)
|
|
27
|
+
node.scrollTop = node.scrollHeight;
|
|
28
|
+
}, [follow, children]);
|
|
29
|
+
return (_jsx("pre", { ref: attach, ...attrs, style: { ...attrs.style, ...style }, onScroll: handleScroll, ...rest, children: children }));
|
|
30
|
+
});
|
package/dist/Page.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface PageHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
3
|
+
title: ReactNode;
|
|
4
|
+
/** Small line above the title: a section name or breadcrumb. */
|
|
5
|
+
eyebrow?: ReactNode;
|
|
6
|
+
subtitle?: ReactNode;
|
|
7
|
+
/** Right-aligned buttons. */
|
|
8
|
+
actions?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function PageHeader({ title, eyebrow, subtitle, actions, className, ...rest }: PageHeaderProps): import("react").JSX.Element;
|
|
11
|
+
export interface PageProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
12
|
+
/** Column width: narrow 48rem (chat, prose, forms), default 72rem, wide 96rem, full. */
|
|
13
|
+
width?: "narrow" | "default" | "wide" | "full";
|
|
14
|
+
/** Renders a `PageHeader` when set. */
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
eyebrow?: ReactNode;
|
|
17
|
+
subtitle?: ReactNode;
|
|
18
|
+
actions?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/** A centered page column inside `AppShell`'s main. */
|
|
21
|
+
export declare function Page({ width, title, eyebrow, subtitle, actions, className, children, ...rest }: PageProps): import("react").JSX.Element;
|
|
22
|
+
export interface GridProps extends HTMLAttributes<HTMLDivElement> {
|
|
23
|
+
/** Smallest column width before the grid drops a column (default 18rem). */
|
|
24
|
+
min?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Dashboard grid: as many equal columns of at least `min` as fit. */
|
|
27
|
+
export declare function Grid({ min, className, style, ...rest }: GridProps): import("react").JSX.Element;
|
|
28
|
+
export interface SplitProps extends HTMLAttributes<HTMLDivElement> {
|
|
29
|
+
/** The master pane. `children` is the detail. */
|
|
30
|
+
list: ReactNode;
|
|
31
|
+
/** Preferred list width (default 20rem). The panes stack once the detail would drop below 60%. */
|
|
32
|
+
listWidth?: string;
|
|
33
|
+
}
|
|
34
|
+
/** List + detail. */
|
|
35
|
+
export declare function Split({ list, listWidth, className, style, children, ...rest }: SplitProps): import("react").JSX.Element;
|
|
36
|
+
export interface AsideLayoutProps extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
/** Sticky side column: table of contents, related items, filters. */
|
|
38
|
+
aside: ReactNode;
|
|
39
|
+
/** Preferred aside width (default 16rem). */
|
|
40
|
+
asideWidth?: string;
|
|
41
|
+
/** Accessible name for the aside landmark. */
|
|
42
|
+
asideLabel?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Content with a sticky right-hand aside; stacks when narrow. */
|
|
45
|
+
export declare function AsideLayout({ aside, asideWidth, asideLabel, className, style, children, ...rest }: AsideLayoutProps): import("react").JSX.Element;
|
|
46
|
+
export interface CenterProps extends HTMLAttributes<HTMLDivElement> {
|
|
47
|
+
/** Width of the centered column (default 26rem). */
|
|
48
|
+
maxWidth?: string;
|
|
49
|
+
}
|
|
50
|
+
/** One narrow column centered in the remaining height: sign in, empty states. */
|
|
51
|
+
export declare function Center({ maxWidth, className, style, ...rest }: CenterProps): import("react").JSX.Element;
|
package/dist/Page.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Sets layout custom properties alongside any caller style. */
|
|
4
|
+
function vars(style, entries) {
|
|
5
|
+
const set = Object.entries(entries).filter(([, v]) => v !== undefined);
|
|
6
|
+
return set.length ? { ...Object.fromEntries(set), ...style } : style;
|
|
7
|
+
}
|
|
8
|
+
export function PageHeader({ title, eyebrow, subtitle, actions, className, ...rest }) {
|
|
9
|
+
return (_jsxs("header", { className: cx("ld-page__header", className), ...rest, children: [_jsxs("div", { className: "ld-page__heading", children: [eyebrow != null && _jsx("p", { className: "ld-page__eyebrow", children: eyebrow }), _jsx("h1", { className: "ld-page__title", children: title }), subtitle != null && _jsx("p", { className: "ld-page__subtitle", children: subtitle })] }), actions != null && _jsx("div", { className: "ld-page__actions", children: actions })] }));
|
|
10
|
+
}
|
|
11
|
+
/** A centered page column inside `AppShell`'s main. */
|
|
12
|
+
export function Page({ width = "default", title, eyebrow, subtitle, actions, className, children, ...rest }) {
|
|
13
|
+
return (_jsxs("div", { className: cx("ld-page", width !== "default" && `ld-page--${width}`, className), ...rest, children: [title != null && _jsx(PageHeader, { title: title, eyebrow: eyebrow, subtitle: subtitle, actions: actions }), children] }));
|
|
14
|
+
}
|
|
15
|
+
/** Dashboard grid: as many equal columns of at least `min` as fit. */
|
|
16
|
+
export function Grid({ min, className, style, ...rest }) {
|
|
17
|
+
return _jsx("div", { className: cx("ld-grid", className), style: vars(style, { "--ld-grid-min": min }), ...rest });
|
|
18
|
+
}
|
|
19
|
+
/** List + detail. */
|
|
20
|
+
export function Split({ list, listWidth, className, style, children, ...rest }) {
|
|
21
|
+
return (_jsxs("div", { className: cx("ld-split", className), style: vars(style, { "--ld-split-list": listWidth }), ...rest, children: [_jsx("div", { className: "ld-split__list", children: list }), _jsx("div", { className: "ld-split__detail", children: children })] }));
|
|
22
|
+
}
|
|
23
|
+
/** Content with a sticky right-hand aside; stacks when narrow. */
|
|
24
|
+
export function AsideLayout({ aside, asideWidth, asideLabel, className, style, children, ...rest }) {
|
|
25
|
+
return (_jsxs("div", { className: cx("ld-aside-layout", className), style: vars(style, { "--ld-aside-w": asideWidth }), ...rest, children: [_jsx("div", { className: "ld-aside-layout__main", children: children }), _jsx("aside", { className: "ld-aside-layout__aside", "aria-label": asideLabel, children: aside })] }));
|
|
26
|
+
}
|
|
27
|
+
/** One narrow column centered in the remaining height: sign in, empty states. */
|
|
28
|
+
export function Center({ maxWidth, className, style, ...rest }) {
|
|
29
|
+
return _jsx("div", { className: cx("ld-center", className), style: vars(style, { "--ld-center-max": maxWidth }), ...rest });
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ElementType, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import type { PolyProps } from "./poly.js";
|
|
3
|
+
export interface SideNavProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
/** Pinned to the bottom of the nav: settings, account, version. */
|
|
5
|
+
footer?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
/** The left-hand nav: `SideNavSection`s of `SideNavItem`s. Pass it as
|
|
8
|
+
* `AppShell`'s `nav`. Give it an `aria-label` ("Main"). */
|
|
9
|
+
export declare function SideNav({ footer, className, children, ...rest }: SideNavProps): import("react").JSX.Element;
|
|
10
|
+
export interface SideNavSectionProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
11
|
+
/** Group heading; hidden on the collapsed rail (a rule separates groups). */
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function SideNavSection({ title, className, children, ...rest }: SideNavSectionProps): import("react").JSX.Element;
|
|
15
|
+
export type SideNavItemProps<E extends ElementType = "a"> = PolyProps<E, {
|
|
16
|
+
/** Shown alone on the collapsed rail and the tab bar — give every item one there. */
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
/** Trailing count or status, e.g. `<Badge size="sm">3</Badge>`. */
|
|
19
|
+
badge?: ReactNode;
|
|
20
|
+
/** Sets `aria-current="page"`. */
|
|
21
|
+
active?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}>;
|
|
25
|
+
/** One nav destination. Renders an `<a>`, or `as` (a router Link). On the
|
|
26
|
+
* collapsed rail a string label doubles as the hover tooltip. */
|
|
27
|
+
export declare function SideNavItem<E extends ElementType = "a">({ as, icon, badge, active, className, children, ...rest }: SideNavItemProps<E>): import("react").JSX.Element;
|
package/dist/SideNav.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { useShell } from "./AppShell.js";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
/** The left-hand nav: `SideNavSection`s of `SideNavItem`s. Pass it as
|
|
6
|
+
* `AppShell`'s `nav`. Give it an `aria-label` ("Main"). */
|
|
7
|
+
export function SideNav({ footer, className, children, ...rest }) {
|
|
8
|
+
return (_jsxs("nav", { className: cx("ld-sidenav", className), ...rest, children: [children, footer != null && _jsx("div", { className: "ld-sidenav__footer", children: footer })] }));
|
|
9
|
+
}
|
|
10
|
+
export function SideNavSection({ title, className, children, ...rest }) {
|
|
11
|
+
const headingId = useId();
|
|
12
|
+
return (_jsxs("div", { className: cx("ld-sidenav__section", className), role: "group", "aria-labelledby": title != null ? headingId : undefined, ...rest, children: [title != null && (_jsx("div", { className: "ld-sidenav__heading", id: headingId, children: title })), children] }));
|
|
13
|
+
}
|
|
14
|
+
/** One nav destination. Renders an `<a>`, or `as` (a router Link). On the
|
|
15
|
+
* collapsed rail a string label doubles as the hover tooltip. */
|
|
16
|
+
export function SideNavItem({ as, icon, badge, active, className, children, ...rest }) {
|
|
17
|
+
const shell = useShell();
|
|
18
|
+
const Tag = as ?? "a";
|
|
19
|
+
const tooltip = shell?.collapsed && !shell.narrow && typeof children === "string" ? children : undefined;
|
|
20
|
+
return (_jsxs(Tag, { className: cx("ld-sidenav__item", className), title: tooltip, ...(active ? { "aria-current": "page" } : {}), ...rest, children: [icon != null && (_jsx("span", { className: "ld-sidenav__icon", "aria-hidden": "true", children: icon })), _jsx("span", { className: "ld-sidenav__label", children: children }), badge != null && _jsx("span", { className: "ld-sidenav__badge", children: badge })] }));
|
|
21
|
+
}
|