@lepid-labs/ui-react 1.0.1 → 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/CodeBlock.d.ts +11 -0
- package/dist/CodeBlock.js +30 -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/Table.d.ts +17 -0
- package/dist/Table.js +29 -0
- package/dist/copy-state.d.ts +14 -0
- package/dist/copy-state.js +31 -0
- package/dist/icon-button.d.ts +23 -0
- package/dist/icon-button.js +24 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -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/table-state.d.ts +28 -0
- package/dist/table-state.js +29 -0
- package/package.json +1 -1
|
@@ -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,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,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
|
+
}
|
package/dist/Table.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HTMLAttributes, KeyboardEvent, MouseEvent, TableHTMLAttributes } from "react";
|
|
2
|
+
export type TableProps = TableHTMLAttributes<HTMLTableElement> & {
|
|
3
|
+
/** Clickable rows: pointer cursor, focus ring, selected state. Maps to .ld-table--interactive. */
|
|
4
|
+
interactive?: boolean;
|
|
5
|
+
};
|
|
6
|
+
/** Styled `<table>`. Compose with native thead/tbody/th/td; use TableRow for clickable body rows. */
|
|
7
|
+
export declare function Table({ interactive, className, ...rest }: TableProps): import("react").JSX.Element;
|
|
8
|
+
export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
|
|
9
|
+
/** Row activation from a click or Enter/Space. Makes the row focusable. Clicks on
|
|
10
|
+
* nested controls (links, buttons, inputs) and text selections don't activate. */
|
|
11
|
+
onActivate?: (event: MouseEvent<HTMLTableRowElement> | KeyboardEvent<HTMLTableRowElement>) => void;
|
|
12
|
+
/** Lit selected state (`aria-selected`). Omit for rows that can't be selected. */
|
|
13
|
+
selected?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** Body row. With `onActivate` it is keyboard-operable; pair it with `<Table interactive>`
|
|
16
|
+
* so the theme shows the pointer and focus ring. */
|
|
17
|
+
export declare function TableRow({ onActivate, selected, onClick, onKeyDown, ...rest }: TableRowProps): import("react").JSX.Element;
|
package/dist/Table.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { isRowActivationClick, isRowActivationKey, ROW_NESTED_CONTROLS, tableAttrs, tableRowAttrs } from "./table-state.js";
|
|
3
|
+
/** Styled `<table>`. Compose with native thead/tbody/th/td; use TableRow for clickable body rows. */
|
|
4
|
+
export function Table({ interactive, className, ...rest }) {
|
|
5
|
+
return _jsx("table", { ...tableAttrs({ interactive, className }), ...rest });
|
|
6
|
+
}
|
|
7
|
+
/** Body row. With `onActivate` it is keyboard-operable; pair it with `<Table interactive>`
|
|
8
|
+
* so the theme shows the pointer and focus ring. */
|
|
9
|
+
export function TableRow({ onActivate, selected, onClick, onKeyDown, ...rest }) {
|
|
10
|
+
if (!onActivate)
|
|
11
|
+
return _jsx("tr", { ...tableRowAttrs({ selected }), onClick: onClick, onKeyDown: onKeyDown, ...rest });
|
|
12
|
+
function handleClick(e) {
|
|
13
|
+
onClick?.(e);
|
|
14
|
+
if (e.defaultPrevented)
|
|
15
|
+
return;
|
|
16
|
+
const hit = e.target.closest(ROW_NESTED_CONTROLS);
|
|
17
|
+
const nested = hit !== null && hit !== e.currentTarget && e.currentTarget.contains(hit);
|
|
18
|
+
if (isRowActivationClick(nested, window.getSelection()?.toString() ?? ""))
|
|
19
|
+
onActivate(e);
|
|
20
|
+
}
|
|
21
|
+
function handleKeyDown(e) {
|
|
22
|
+
onKeyDown?.(e);
|
|
23
|
+
if (e.defaultPrevented || !isRowActivationKey(e.key, e.target === e.currentTarget))
|
|
24
|
+
return;
|
|
25
|
+
e.preventDefault(); // Space would scroll the page
|
|
26
|
+
onActivate(e);
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("tr", { ...tableRowAttrs({ activatable: true, selected }), onClick: handleClick, onKeyDown: handleKeyDown, ...rest }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** How long the copied / failed state holds before the control reverts. */
|
|
2
|
+
export declare const COPIED_MS = 1500;
|
|
3
|
+
export type CopyStatus = "idle" | "copied" | "failed";
|
|
4
|
+
/** The slice of `navigator.clipboard` the copy control needs. */
|
|
5
|
+
export interface ClipboardLike {
|
|
6
|
+
writeText(text: string): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Write `text` to the clipboard. Never throws: a missing clipboard (insecure
|
|
9
|
+
* context, old browser) or a rejected write (permission denied) is "failed". */
|
|
10
|
+
export declare function copyText(text: string, clipboard?: ClipboardLike | undefined): Promise<"copied" | "failed">;
|
|
11
|
+
/** Value for the button's `data-ld-copy` hook; absent while idle. */
|
|
12
|
+
export declare function copyDataAttr(status: CopyStatus): "copied" | "failed" | undefined;
|
|
13
|
+
/** Text for the polite live region; empty while idle so nothing is announced. */
|
|
14
|
+
export declare function copyAnnouncement(status: CopyStatus): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Pure copy logic behind CodeBlock, kept DOM-free so it can be tested under
|
|
2
|
+
// node:test without a renderer (so it imports nothing — Node runs the .ts
|
|
3
|
+
// source and cannot resolve the .js specifiers the build uses).
|
|
4
|
+
/** How long the copied / failed state holds before the control reverts. */
|
|
5
|
+
export const COPIED_MS = 1500;
|
|
6
|
+
/** Write `text` to the clipboard. Never throws: a missing clipboard (insecure
|
|
7
|
+
* context, old browser) or a rejected write (permission denied) is "failed". */
|
|
8
|
+
export async function copyText(text, clipboard = globalThis.navigator
|
|
9
|
+
?.clipboard) {
|
|
10
|
+
if (!clipboard)
|
|
11
|
+
return "failed";
|
|
12
|
+
try {
|
|
13
|
+
await clipboard.writeText(text);
|
|
14
|
+
return "copied";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "failed";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Value for the button's `data-ld-copy` hook; absent while idle. */
|
|
21
|
+
export function copyDataAttr(status) {
|
|
22
|
+
return status === "idle" ? undefined : status;
|
|
23
|
+
}
|
|
24
|
+
/** Text for the polite live region; empty while idle so nothing is announced. */
|
|
25
|
+
export function copyAnnouncement(status) {
|
|
26
|
+
if (status === "copied")
|
|
27
|
+
return "Copied to clipboard";
|
|
28
|
+
if (status === "failed")
|
|
29
|
+
return "Copy failed";
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface IconButtonOptions {
|
|
2
|
+
/** Accessible name; also the hover tooltip unless `title` is given. */
|
|
3
|
+
label: string;
|
|
4
|
+
/** Lit state. On a link it also sets `aria-current="page"`. */
|
|
5
|
+
active?: boolean;
|
|
6
|
+
/** "danger" tints the glyph red on hover. */
|
|
7
|
+
variant?: "default" | "danger";
|
|
8
|
+
/** Explicit tooltip; overrides the one derived from `label`. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** The rendered element is a native `<button>` (not `<a>` / a router Link). */
|
|
11
|
+
isButton?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IconButtonAttrs {
|
|
15
|
+
className: string;
|
|
16
|
+
"aria-label": string;
|
|
17
|
+
title: string;
|
|
18
|
+
type?: "button";
|
|
19
|
+
"aria-current"?: "page";
|
|
20
|
+
}
|
|
21
|
+
/** Class list and ARIA for an icon-only control. A native button defaults to
|
|
22
|
+
* `type="button"` so it never submits a surrounding form. */
|
|
23
|
+
export declare function iconButtonAttrs({ label, active, variant, title, isButton, className, }: IconButtonOptions): IconButtonAttrs;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Pure attribute logic behind IconButton, kept DOM-free so it can be tested
|
|
2
|
+
// under node:test without a renderer (so it imports nothing — Node runs the
|
|
3
|
+
// .ts source and cannot resolve the .js specifiers the build uses).
|
|
4
|
+
/** Class list and ARIA for an icon-only control. A native button defaults to
|
|
5
|
+
* `type="button"` so it never submits a surrounding form. */
|
|
6
|
+
export function iconButtonAttrs({ label, active, variant = "default", title, isButton = true, className, }) {
|
|
7
|
+
const attrs = {
|
|
8
|
+
className: [
|
|
9
|
+
"ld-icon-btn",
|
|
10
|
+
active && "ld-icon-btn--active",
|
|
11
|
+
variant === "danger" && "ld-icon-btn--danger",
|
|
12
|
+
className,
|
|
13
|
+
]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(" "),
|
|
16
|
+
"aria-label": label,
|
|
17
|
+
title: title ?? label,
|
|
18
|
+
};
|
|
19
|
+
if (isButton)
|
|
20
|
+
attrs.type = "button";
|
|
21
|
+
else if (active)
|
|
22
|
+
attrs["aria-current"] = "page";
|
|
23
|
+
return attrs;
|
|
24
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export { Button, type ButtonProps } from "./Button.js";
|
|
2
2
|
export { ButtonGroup, type ButtonGroupProps } from "./ButtonGroup.js";
|
|
3
3
|
export { buttonGroupAttrs } from "./button-group.js";
|
|
4
|
+
export { IconButton, type IconButtonProps } from "./IconButton.js";
|
|
5
|
+
export { iconButtonAttrs, type IconButtonOptions } from "./icon-button.js";
|
|
4
6
|
export { HoldButton, type HoldButtonProps } from "./HoldButton.js";
|
|
5
7
|
export { createHoldController, type HoldController, type HoldOptions, type HoldState } from "./hold.js";
|
|
6
8
|
export { Card, type CardProps } from "./Card.js";
|
|
9
|
+
export { Table, TableRow, type TableProps, type TableRowProps } from "./Table.js";
|
|
10
|
+
export { tableAttrs, tableRowAttrs, isRowActivationKey, isRowActivationClick, ROW_NESTED_CONTROLS, type TableOptions, type TableRowOptions, } from "./table-state.js";
|
|
11
|
+
export { LogBlock, type LogBlockProps } from "./LogBlock.js";
|
|
12
|
+
export { logAttrs, isPinnedToBottom, LOG_PIN_TOLERANCE, type LogOptions } from "./log-state.js";
|
|
13
|
+
export { CodeBlock, type CodeBlockProps } from "./CodeBlock.js";
|
|
14
|
+
export { copyText, copyDataAttr, copyAnnouncement, COPIED_MS, type CopyStatus, type ClipboardLike, } from "./copy-state.js";
|
|
7
15
|
export { StatusCard, StatusCardRows, StatusCardRow, StatusCardFooter, StatusCardEmpty, type StatusCardProps, type StatusCardRowProps, type StatusCardFooterProps, } from "./StatusCard.js";
|
|
8
16
|
export { attentionTone, statusCardAttrs, type StatusTone } from "./status-card.js";
|
|
9
17
|
export { Stepper, type StepperProps, type StepItem } from "./Stepper.js";
|
|
@@ -16,3 +24,9 @@ export { Tabs, type TabsProps, type TabItem } from "./Tabs.js";
|
|
|
16
24
|
export { slideDir, type SlideDir } from "./slide-dir.js";
|
|
17
25
|
export { useSlideDir } from "./useSlideDir.js";
|
|
18
26
|
export { cx } from "./cx.js";
|
|
27
|
+
export { AppShell, useShell, useShellNarrow, type AppShellProps, type ShellContextValue } from "./AppShell.js";
|
|
28
|
+
export { AppHeader, ShellToggle, TopNav, TopNavItem, type AppHeaderProps, type ShellToggleProps, type TopNavProps, type TopNavItemProps } from "./AppHeader.js";
|
|
29
|
+
export { SideNav, SideNavSection, SideNavItem, type SideNavProps, type SideNavSectionProps, type SideNavItemProps, } from "./SideNav.js";
|
|
30
|
+
export { Page, PageHeader, Grid, Split, AsideLayout, Center, type PageProps, type PageHeaderProps, type GridProps, type SplitProps, type AsideLayoutProps, type CenterProps, } from "./Page.js";
|
|
31
|
+
export { navExpanded, shellAttrs, toggleNav, SHELL_NARROW_QUERY, type ShellNavState } from "./shell-state.js";
|
|
32
|
+
export type { PolyProps } from "./poly.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export { Button } from "./Button.js";
|
|
2
2
|
export { ButtonGroup } from "./ButtonGroup.js";
|
|
3
3
|
export { buttonGroupAttrs } from "./button-group.js";
|
|
4
|
+
export { IconButton } from "./IconButton.js";
|
|
5
|
+
export { iconButtonAttrs } from "./icon-button.js";
|
|
4
6
|
export { HoldButton } from "./HoldButton.js";
|
|
5
7
|
export { createHoldController } from "./hold.js";
|
|
6
8
|
export { Card } from "./Card.js";
|
|
9
|
+
export { Table, TableRow } from "./Table.js";
|
|
10
|
+
export { tableAttrs, tableRowAttrs, isRowActivationKey, isRowActivationClick, ROW_NESTED_CONTROLS, } from "./table-state.js";
|
|
11
|
+
export { LogBlock } from "./LogBlock.js";
|
|
12
|
+
export { logAttrs, isPinnedToBottom, LOG_PIN_TOLERANCE } from "./log-state.js";
|
|
13
|
+
export { CodeBlock } from "./CodeBlock.js";
|
|
14
|
+
export { copyText, copyDataAttr, copyAnnouncement, COPIED_MS, } from "./copy-state.js";
|
|
7
15
|
export { StatusCard, StatusCardRows, StatusCardRow, StatusCardFooter, StatusCardEmpty, } from "./StatusCard.js";
|
|
8
16
|
export { attentionTone, statusCardAttrs } from "./status-card.js";
|
|
9
17
|
export { Stepper } from "./Stepper.js";
|
|
@@ -16,3 +24,8 @@ export { Tabs } from "./Tabs.js";
|
|
|
16
24
|
export { slideDir } from "./slide-dir.js";
|
|
17
25
|
export { useSlideDir } from "./useSlideDir.js";
|
|
18
26
|
export { cx } from "./cx.js";
|
|
27
|
+
export { AppShell, useShell, useShellNarrow } from "./AppShell.js";
|
|
28
|
+
export { AppHeader, ShellToggle, TopNav, TopNavItem } from "./AppHeader.js";
|
|
29
|
+
export { SideNav, SideNavSection, SideNavItem, } from "./SideNav.js";
|
|
30
|
+
export { Page, PageHeader, Grid, Split, AsideLayout, Center, } from "./Page.js";
|
|
31
|
+
export { navExpanded, shellAttrs, toggleNav, SHELL_NARROW_QUERY } from "./shell-state.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** How close to the bottom (px) still counts as "reading the tail". */
|
|
2
|
+
export declare const LOG_PIN_TOLERANCE = 8;
|
|
3
|
+
export interface LogOptions {
|
|
4
|
+
/** Cap on the block's height (number = px); sets --ld-log-max-height. */
|
|
5
|
+
maxHeight?: number | string;
|
|
6
|
+
/** Announce appended lines to assistive tech (role="log"). Default true. */
|
|
7
|
+
live?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LogAttrs {
|
|
11
|
+
className: string;
|
|
12
|
+
role?: "log";
|
|
13
|
+
style?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/** Class list, live-region role, and height variable for the `<pre>`. */
|
|
16
|
+
export declare function logAttrs({ maxHeight, live, className }: LogOptions): LogAttrs;
|
|
17
|
+
/** True when the viewport sits at (or within tolerance of) the bottom — the
|
|
18
|
+
* reader is following the tail, so new output should keep it there. Content
|
|
19
|
+
* that doesn't overflow is always pinned. */
|
|
20
|
+
export declare function isPinnedToBottom(scrollTop: number, scrollHeight: number, clientHeight: number, tolerance?: number): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Pure attribute and follow logic behind LogBlock, kept DOM-free so it can be
|
|
2
|
+
// tested under node:test without a renderer (so it imports nothing — Node runs
|
|
3
|
+
// the .ts source and cannot resolve .js specifiers).
|
|
4
|
+
/** How close to the bottom (px) still counts as "reading the tail". */
|
|
5
|
+
export const LOG_PIN_TOLERANCE = 8;
|
|
6
|
+
/** Class list, live-region role, and height variable for the `<pre>`. */
|
|
7
|
+
export function logAttrs({ maxHeight, live = true, className }) {
|
|
8
|
+
const attrs = { className: ["ld-pre", "ld-pre--log", className].filter(Boolean).join(" ") };
|
|
9
|
+
if (live)
|
|
10
|
+
attrs.role = "log";
|
|
11
|
+
if (maxHeight !== undefined) {
|
|
12
|
+
attrs.style = { "--ld-log-max-height": typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight };
|
|
13
|
+
}
|
|
14
|
+
return attrs;
|
|
15
|
+
}
|
|
16
|
+
/** True when the viewport sits at (or within tolerance of) the bottom — the
|
|
17
|
+
* reader is following the tail, so new output should keep it there. Content
|
|
18
|
+
* that doesn't overflow is always pinned. */
|
|
19
|
+
export function isPinnedToBottom(scrollTop, scrollHeight, clientHeight, tolerance = LOG_PIN_TOLERANCE) {
|
|
20
|
+
return scrollHeight - clientHeight - scrollTop <= tolerance;
|
|
21
|
+
}
|
package/dist/poly.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType } from "react";
|
|
2
|
+
/** Props of a component that renders `as` (default `<a>`) — pass a router's
|
|
3
|
+
* Link to keep client-side navigation. Own props win over the element's. */
|
|
4
|
+
export type PolyProps<E extends ElementType, Own> = Own & {
|
|
5
|
+
as?: E;
|
|
6
|
+
} & Omit<ComponentPropsWithoutRef<E>, keyof Own | "as">;
|
package/dist/poly.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Below this width the side nav leaves the grid and becomes a drawer. The
|
|
2
|
+
* theme CSS hard-codes the same 48rem (media queries cannot read tokens). */
|
|
3
|
+
export declare const SHELL_NARROW_QUERY = "(max-width: 48rem)";
|
|
4
|
+
export interface ShellNavState {
|
|
5
|
+
/** Wide viewports: the nav is an icon-only rail. Kept across resizes. */
|
|
6
|
+
collapsed: boolean;
|
|
7
|
+
/** Narrow viewports: the drawer is open. Ignored by the CSS when wide. */
|
|
8
|
+
open: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The one toggle button means "collapse the rail" when wide and "open the
|
|
11
|
+
* drawer" when narrow; each state survives the other viewport untouched. */
|
|
12
|
+
export declare function toggleNav(state: ShellNavState, narrow: boolean): ShellNavState;
|
|
13
|
+
/** Whether the nav is visibly expanded, for the toggle's aria-expanded. */
|
|
14
|
+
export declare function navExpanded(state: ShellNavState, narrow: boolean): boolean;
|
|
15
|
+
export interface ShellAttrs {
|
|
16
|
+
className: string;
|
|
17
|
+
"data-ld-nav-collapsed"?: "";
|
|
18
|
+
"data-ld-nav-open"?: "";
|
|
19
|
+
}
|
|
20
|
+
/** Class list and state attributes for the `.ld-shell` root. The theme CSS
|
|
21
|
+
* reads only these two presence attributes, so plain HTML can set them too. */
|
|
22
|
+
export declare function shellAttrs(state: ShellNavState, className?: string): ShellAttrs;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Pure state logic behind AppShell, kept DOM-free so it can be tested under
|
|
2
|
+
// node:test without a renderer (so it imports nothing — Node runs the .ts
|
|
3
|
+
// source and cannot resolve the .js specifiers the build uses).
|
|
4
|
+
/** Below this width the side nav leaves the grid and becomes a drawer. The
|
|
5
|
+
* theme CSS hard-codes the same 48rem (media queries cannot read tokens). */
|
|
6
|
+
export const SHELL_NARROW_QUERY = "(max-width: 48rem)";
|
|
7
|
+
/** The one toggle button means "collapse the rail" when wide and "open the
|
|
8
|
+
* drawer" when narrow; each state survives the other viewport untouched. */
|
|
9
|
+
export function toggleNav(state, narrow) {
|
|
10
|
+
return narrow ? { ...state, open: !state.open } : { ...state, collapsed: !state.collapsed };
|
|
11
|
+
}
|
|
12
|
+
/** Whether the nav is visibly expanded, for the toggle's aria-expanded. */
|
|
13
|
+
export function navExpanded(state, narrow) {
|
|
14
|
+
return narrow ? state.open : !state.collapsed;
|
|
15
|
+
}
|
|
16
|
+
/** Class list and state attributes for the `.ld-shell` root. The theme CSS
|
|
17
|
+
* reads only these two presence attributes, so plain HTML can set them too. */
|
|
18
|
+
export function shellAttrs(state, className) {
|
|
19
|
+
const attrs = { className: ["ld-shell", className].filter(Boolean).join(" ") };
|
|
20
|
+
if (state.collapsed)
|
|
21
|
+
attrs["data-ld-nav-collapsed"] = "";
|
|
22
|
+
if (state.open)
|
|
23
|
+
attrs["data-ld-nav-open"] = "";
|
|
24
|
+
return attrs;
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Descendants that own their own click/keys; a row never double-fires for them. */
|
|
2
|
+
export declare const ROW_NESTED_CONTROLS = "a[href], button, input, select, textarea, summary, label, [role=\"button\"], [role=\"link\"], [contenteditable=\"true\"]";
|
|
3
|
+
export interface TableOptions {
|
|
4
|
+
/** Rows are clickable: pointer cursor, focus ring, selected state. */
|
|
5
|
+
interactive?: boolean;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Class list for the `<table>`. */
|
|
9
|
+
export declare function tableAttrs({ interactive, className }: TableOptions): {
|
|
10
|
+
className: string;
|
|
11
|
+
};
|
|
12
|
+
export interface TableRowOptions {
|
|
13
|
+
/** The row has an activation handler, so it joins the tab order. */
|
|
14
|
+
activatable?: boolean;
|
|
15
|
+
/** Selected state; omitted leaves aria-selected off entirely. */
|
|
16
|
+
selected?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface TableRowAttrs {
|
|
19
|
+
tabIndex?: 0;
|
|
20
|
+
"aria-selected"?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Focus and selection ARIA for a body row. */
|
|
23
|
+
export declare function tableRowAttrs({ activatable, selected }: TableRowOptions): TableRowAttrs;
|
|
24
|
+
/** Enter or Space on the focused row itself (not a control inside it) activates it. */
|
|
25
|
+
export declare function isRowActivationKey(key: string, fromRow: boolean): boolean;
|
|
26
|
+
/** A click activates the row unless it landed on a nested control (`hit` is the
|
|
27
|
+
* closest ROW_NESTED_CONTROLS match inside the row) or finished a text selection. */
|
|
28
|
+
export declare function isRowActivationClick(hitNestedControl: boolean, selectedText: string): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Pure attribute and activation logic behind Table / TableRow, kept DOM-free
|
|
2
|
+
// so it can be tested under node:test without a renderer (so it imports
|
|
3
|
+
// nothing — Node runs the .ts source and cannot resolve .js specifiers).
|
|
4
|
+
/** Descendants that own their own click/keys; a row never double-fires for them. */
|
|
5
|
+
export const ROW_NESTED_CONTROLS = 'a[href], button, input, select, textarea, summary, label, [role="button"], [role="link"], [contenteditable="true"]';
|
|
6
|
+
/** Class list for the `<table>`. */
|
|
7
|
+
export function tableAttrs({ interactive, className }) {
|
|
8
|
+
return {
|
|
9
|
+
className: ["ld-table", interactive && "ld-table--interactive", className].filter(Boolean).join(" "),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Focus and selection ARIA for a body row. */
|
|
13
|
+
export function tableRowAttrs({ activatable, selected }) {
|
|
14
|
+
const attrs = {};
|
|
15
|
+
if (activatable)
|
|
16
|
+
attrs.tabIndex = 0;
|
|
17
|
+
if (selected !== undefined)
|
|
18
|
+
attrs["aria-selected"] = selected;
|
|
19
|
+
return attrs;
|
|
20
|
+
}
|
|
21
|
+
/** Enter or Space on the focused row itself (not a control inside it) activates it. */
|
|
22
|
+
export function isRowActivationKey(key, fromRow) {
|
|
23
|
+
return fromRow && (key === "Enter" || key === " ");
|
|
24
|
+
}
|
|
25
|
+
/** A click activates the row unless it landed on a nested control (`hit` is the
|
|
26
|
+
* closest ROW_NESTED_CONTROLS match inside the row) or finished a text selection. */
|
|
27
|
+
export function isRowActivationClick(hitNestedControl, selectedText) {
|
|
28
|
+
return !hitNestedControl && selectedText === "";
|
|
29
|
+
}
|