@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.
Files changed (54) hide show
  1. package/dist/AppHeader.d.ts +36 -0
  2. package/dist/AppHeader.js +32 -0
  3. package/dist/AppShell.d.ts +41 -0
  4. package/dist/AppShell.js +64 -0
  5. package/dist/ButtonGroup.d.ts +13 -0
  6. package/dist/ButtonGroup.js +14 -0
  7. package/dist/CodeBlock.d.ts +11 -0
  8. package/dist/CodeBlock.js +30 -0
  9. package/dist/HoldButton.d.ts +23 -0
  10. package/dist/HoldButton.js +112 -0
  11. package/dist/IconButton.d.ts +18 -0
  12. package/dist/IconButton.js +9 -0
  13. package/dist/LogBlock.d.ts +22 -0
  14. package/dist/LogBlock.js +30 -0
  15. package/dist/Page.d.ts +51 -0
  16. package/dist/Page.js +30 -0
  17. package/dist/SideNav.d.ts +27 -0
  18. package/dist/SideNav.js +21 -0
  19. package/dist/StatusCard.d.ts +42 -0
  20. package/dist/StatusCard.js +35 -0
  21. package/dist/Stepper.d.ts +19 -0
  22. package/dist/Stepper.js +12 -0
  23. package/dist/Table.d.ts +17 -0
  24. package/dist/Table.js +29 -0
  25. package/dist/Tabs.js +4 -1
  26. package/dist/button-group.d.ts +7 -0
  27. package/dist/button-group.js +11 -0
  28. package/dist/copy-state.d.ts +14 -0
  29. package/dist/copy-state.js +31 -0
  30. package/dist/feedback.d.ts +5 -1
  31. package/dist/feedback.js +2 -2
  32. package/dist/hold.d.ts +31 -0
  33. package/dist/hold.js +82 -0
  34. package/dist/icon-button.d.ts +23 -0
  35. package/dist/icon-button.js +24 -0
  36. package/dist/index.d.ts +24 -0
  37. package/dist/index.js +23 -0
  38. package/dist/log-state.d.ts +20 -0
  39. package/dist/log-state.js +21 -0
  40. package/dist/poly.d.ts +6 -0
  41. package/dist/poly.js +1 -0
  42. package/dist/shell-state.d.ts +22 -0
  43. package/dist/shell-state.js +25 -0
  44. package/dist/slide-dir.d.ts +5 -0
  45. package/dist/slide-dir.js +12 -0
  46. package/dist/status-card.d.ts +15 -0
  47. package/dist/status-card.js +27 -0
  48. package/dist/stepper-state.d.ts +12 -0
  49. package/dist/stepper-state.js +24 -0
  50. package/dist/table-state.d.ts +28 -0
  51. package/dist/table-state.js +29 -0
  52. package/dist/useSlideDir.d.ts +7 -0
  53. package/dist/useSlideDir.js +26 -0
  54. package/package.json +3 -2
@@ -0,0 +1,42 @@
1
+ import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
2
+ import { type StatusTone } from "./status-card.js";
3
+ export interface StatusCardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "prefix"> {
4
+ title: ReactNode;
5
+ /** Renders the title as a link. */
6
+ href?: string;
7
+ /** Open `href` in a new tab (adds the noopener/noreferrer rel). */
8
+ external?: boolean;
9
+ /** Faint lead-in before the title — an owner, group, or namespace. */
10
+ prefix?: ReactNode;
11
+ /** Colors the left status edge. */
12
+ tone?: StatusTone;
13
+ /** Outline + glow ring calling for action; `true` means warning. */
14
+ attention?: boolean | StatusTone;
15
+ /** Bump (e.g. `Date.now()`) whenever the card's data changes to replay the flash. */
16
+ changedAt?: number;
17
+ /** Faint icon in the top-left corner (a lock, a pin). */
18
+ watermark?: ReactNode;
19
+ /** Right side of the head: badges, icon links. */
20
+ meta?: ReactNode;
21
+ /** Faint trailing text after `meta` — typically a relative time. */
22
+ note?: ReactNode;
23
+ }
24
+ export declare function StatusCard({ title, href, external, prefix, tone, attention, changedAt, watermark, meta, note, className, children, ...rest }: StatusCardProps): import("react").JSX.Element;
25
+ /** `<ul>` of rows — the card's list body. */
26
+ export declare function StatusCardRows({ className, ...rest }: HTMLAttributes<HTMLUListElement>): import("react").JSX.Element;
27
+ export interface StatusCardRowProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "prefix"> {
28
+ /** Narrow right-aligned lead column — an id, a number. */
29
+ leading?: ReactNode;
30
+ /** Compact trailing cluster — badges, counts, icons. */
31
+ trailing?: ReactNode;
32
+ external?: boolean;
33
+ }
34
+ /** One row: `<li>` wrapping an `<a>` when `href` is set, else a `<div>`. */
35
+ export declare function StatusCardRow({ leading, trailing, external, href, className, children, ...rest }: StatusCardRowProps): import("react").JSX.Element;
36
+ export interface StatusCardFooterProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
37
+ external?: boolean;
38
+ }
39
+ /** Faint footer line — an overflow link ("4 more…") or a plain summary. */
40
+ export declare function StatusCardFooter({ external, href, className, ...rest }: StatusCardFooterProps): import("react").JSX.Element;
41
+ /** Empty-state line under the head ("No open issues"). */
42
+ export declare function StatusCardEmpty({ className, ...rest }: HTMLAttributes<HTMLParagraphElement>): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { replayFlash, statusCardAttrs } from "./status-card.js";
5
+ export function StatusCard({ title, href, external, prefix, tone, attention, changedAt, watermark, meta, note, className, children, ...rest }) {
6
+ const root = useRef(null);
7
+ useEffect(() => {
8
+ if (changedAt && root.current)
9
+ replayFlash(root.current);
10
+ }, [changedAt]);
11
+ const heading = (_jsxs(_Fragment, { children: [prefix !== undefined && _jsx("span", { className: "ld-status-card__prefix", children: prefix }), title] }));
12
+ return (_jsxs("div", { ref: root, ...statusCardAttrs(tone, attention, className), ...rest, children: [watermark !== undefined && (_jsx("span", { className: "ld-status-card__watermark", "aria-hidden": "true", children: watermark })), _jsxs("div", { className: "ld-status-card__head", children: [href ? (_jsx("a", { className: "ld-status-card__title", href: href, ...linkTarget(external), children: heading })) : (_jsx("span", { className: "ld-status-card__title", children: heading })), (meta !== undefined || note !== undefined) && (_jsxs("div", { className: "ld-status-card__meta", children: [meta, note !== undefined && _jsx("span", { className: "ld-status-card__note", children: note })] }))] }), children] }));
13
+ }
14
+ /** `<ul>` of rows — the card's list body. */
15
+ export function StatusCardRows({ className, ...rest }) {
16
+ return _jsx("ul", { className: cx("ld-status-card__rows", className), ...rest });
17
+ }
18
+ /** One row: `<li>` wrapping an `<a>` when `href` is set, else a `<div>`. */
19
+ export function StatusCardRow({ leading, trailing, external, href, className, children, ...rest }) {
20
+ const body = (_jsxs(_Fragment, { children: [leading !== undefined && _jsx("span", { className: "ld-status-card__row-lead", children: leading }), _jsx("span", { className: "ld-status-card__row-main", children: children }), trailing !== undefined && _jsx("span", { className: "ld-status-card__row-trail", children: trailing })] }));
21
+ const cls = cx("ld-status-card__row", className);
22
+ return (_jsx("li", { children: href ? (_jsx("a", { className: cls, href: href, ...linkTarget(external), ...rest, children: body })) : (_jsx("div", { className: cls, ...rest, children: body })) }));
23
+ }
24
+ /** Faint footer line — an overflow link ("4 more…") or a plain summary. */
25
+ export function StatusCardFooter({ external, href, className, ...rest }) {
26
+ const cls = cx("ld-status-card__footer", className);
27
+ return href ? (_jsx("a", { className: cls, href: href, ...linkTarget(external), ...rest })) : (_jsx("div", { className: cls, ...rest }));
28
+ }
29
+ /** Empty-state line under the head ("No open issues"). */
30
+ export function StatusCardEmpty({ className, ...rest }) {
31
+ return _jsx("p", { className: cx("ld-status-card__empty", className), ...rest });
32
+ }
33
+ function linkTarget(external) {
34
+ return external ? { target: "_blank", rel: "noopener noreferrer" } : {};
35
+ }
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export interface StepItem {
3
+ id: string;
4
+ label: ReactNode;
5
+ /** Icon inside the node. Defaults to a check mark once complete, else the step number. */
6
+ icon?: ReactNode;
7
+ }
8
+ export interface StepperProps extends HTMLAttributes<HTMLOListElement> {
9
+ steps: StepItem[];
10
+ /** Index of the step in progress. Earlier steps are complete, later ones upcoming;
11
+ * pass `steps.length` when everything is done. */
12
+ current: number;
13
+ /** Extra class for every step `<li>`. */
14
+ stepClassName?: string;
15
+ }
16
+ /** Horizontal milestone rail: labelled step nodes on a progress track. The
17
+ * theme CSS fills the rail up to the current node from the state modifiers,
18
+ * so `current` is the only piece of state. */
19
+ export declare function Stepper({ steps, current, stepClassName, className, ...rest }: StepperProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "./cx.js";
3
+ import { stepAttrs, stepState } from "./stepper-state.js";
4
+ /** Horizontal milestone rail: labelled step nodes on a progress track. The
5
+ * theme CSS fills the rail up to the current node from the state modifiers,
6
+ * so `current` is the only piece of state. */
7
+ export function Stepper({ steps, current, stepClassName, className, ...rest }) {
8
+ return (_jsx("ol", { className: cx("ld-stepper", className), ...rest, children: steps.map((step, index) => (_jsxs("li", { ...stepAttrs(index, current, stepClassName), children: [_jsx("span", { className: "ld-stepper__node", "aria-hidden": "true", children: step.icon ?? (stepState(index, current) === "complete" ? _jsx(CheckIcon, {}) : index + 1) }), _jsx("span", { className: "ld-stepper__label", children: step.label })] }, step.id))) }));
9
+ }
10
+ function CheckIcon() {
11
+ return (_jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M3 8.5l3 3 7-7" }) }));
12
+ }
@@ -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
+ }
package/dist/Tabs.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { cx } from "./cx.js";
4
+ import { useSlideDir } from "./useSlideDir.js";
4
5
  export function Tabs({ items, active, defaultActive, onChange }) {
5
6
  const [internal, setInternal] = useState(defaultActive ?? items[0]?.id);
6
7
  const current = active ?? internal;
@@ -9,5 +10,7 @@ export function Tabs({ items, active, defaultActive, onChange }) {
9
10
  onChange?.(id);
10
11
  };
11
12
  const activeItem = items.find((t) => t.id === current);
12
- return (_jsxs("div", { children: [_jsx("div", { role: "tablist", className: "ld-tabs", children: items.map((t) => (_jsx("button", { role: "tab", type: "button", "aria-selected": t.id === current, className: cx("ld-tab", t.id === current && "ld-tab--active"), onClick: () => select(t.id), children: t.label }, t.id))) }), _jsx("div", { role: "tabpanel", className: "ld-tabpanel", children: activeItem?.content })] }));
13
+ // data-ld-dir for the theme's sliding underline: which edge leads
14
+ const tablist = useSlideDir('[aria-selected="true"]');
15
+ return (_jsxs("div", { children: [_jsx("div", { ref: tablist, role: "tablist", className: "ld-tabs", children: items.map((t) => (_jsx("button", { role: "tab", type: "button", "aria-selected": t.id === current, className: cx("ld-tab", t.id === current && "ld-tab--active"), onClick: () => select(t.id), children: t.label }, t.id))) }), _jsx("div", { role: "tabpanel", className: "ld-tabpanel", children: activeItem?.content })] }));
13
16
  }
@@ -0,0 +1,7 @@
1
+ export interface ButtonGroupAttrs {
2
+ className: string;
3
+ role: "group";
4
+ }
5
+ /** Root class list and role for the group track. `block` stretches it to the
6
+ * row with equal-width segments. */
7
+ export declare function buttonGroupAttrs(block?: boolean, className?: string): ButtonGroupAttrs;
@@ -0,0 +1,11 @@
1
+ // Pure attribute logic behind ButtonGroup, 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
+ /** Root class list and role for the group track. `block` stretches it to the
5
+ * row with equal-width segments. */
6
+ export function buttonGroupAttrs(block, className) {
7
+ return {
8
+ className: ["ld-btn-group", block && "ld-btn-group--block", className].filter(Boolean).join(" "),
9
+ role: "group",
10
+ };
11
+ }
@@ -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
+ }
@@ -2,8 +2,12 @@ import type { HTMLAttributes, ReactNode } from "react";
2
2
  export type SemanticVariant = "info" | "success" | "warning" | "danger";
3
3
  export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
4
4
  variant?: SemanticVariant | "primary";
5
+ /** `sm` is the 9px dashboard pill used in StatusCard meta rows. */
6
+ size?: "sm";
7
+ /** Solid warning fill with a breathing glow — the one call to action. */
8
+ pulse?: boolean;
5
9
  }
6
- export declare function Badge({ variant, className, ...rest }: BadgeProps): import("react").JSX.Element;
10
+ export declare function Badge({ variant, size, pulse, className, ...rest }: BadgeProps): import("react").JSX.Element;
7
11
  export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
8
12
  variant?: SemanticVariant;
9
13
  title?: ReactNode;
package/dist/feedback.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "./cx.js";
3
- export function Badge({ variant, className, ...rest }) {
4
- return _jsx("span", { className: cx("ld-badge", variant && `ld-badge--${variant}`, className), ...rest });
3
+ export function Badge({ variant, size, pulse, className, ...rest }) {
4
+ return (_jsx("span", { className: cx("ld-badge", variant && `ld-badge--${variant}`, size && `ld-badge--${size}`, pulse && "ld-badge--pulse", className), ...rest }));
5
5
  }
6
6
  export function Alert({ variant = "info", title, className, children, ...rest }) {
7
7
  return (_jsxs("div", { role: "alert", className: cx("ld-alert", `ld-alert--${variant}`, className), ...rest, children: [title !== undefined && _jsx("span", { className: "ld-alert__title", children: title }), children] }));
package/dist/hold.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** Hold-to-confirm state machine, framework-free so it can be unit-tested
2
+ * without a DOM. HoldButton drives it from pointer/keyboard events. */
3
+ export type HoldState = "idle" | "holding" | "fired";
4
+ export interface HoldOptions {
5
+ /** Commitment window in ms, resolved at the moment a hold starts. */
6
+ duration: () => number;
7
+ /** Called with progress in [0, 1] on every frame, and with 0 on reset. */
8
+ onProgress: (progress: number) => void;
9
+ /** Fires exactly once per completed hold. */
10
+ onConfirm: () => void;
11
+ onStateChange?: (state: HoldState) => void;
12
+ /** How long the "fired" state lingers before returning to idle (ms). */
13
+ firedFor?: number;
14
+ now?: () => number;
15
+ requestFrame?: (cb: () => void) => number;
16
+ cancelFrame?: (id: number) => void;
17
+ setTimer?: (cb: () => void, ms: number) => number;
18
+ clearTimer?: (id: number) => void;
19
+ }
20
+ export interface HoldController {
21
+ readonly state: HoldState;
22
+ /** Begin a hold. Ignored unless idle. */
23
+ start(): void;
24
+ /** Abort an in-progress hold without firing. Ignored unless holding. */
25
+ cancel(): void;
26
+ /** Skip the hold and fire immediately (keyboard-tap escape hatch). */
27
+ fire(): void;
28
+ /** Cancel everything and drop pending timers. */
29
+ dispose(): void;
30
+ }
31
+ export declare function createHoldController(opts: HoldOptions): HoldController;
package/dist/hold.js ADDED
@@ -0,0 +1,82 @@
1
+ /** Hold-to-confirm state machine, framework-free so it can be unit-tested
2
+ * without a DOM. HoldButton drives it from pointer/keyboard events. */
3
+ export function createHoldController(opts) {
4
+ const now = opts.now ?? (() => performance.now());
5
+ const requestFrame = opts.requestFrame ?? ((cb) => requestAnimationFrame(cb));
6
+ const cancelFrame = opts.cancelFrame ?? ((id) => cancelAnimationFrame(id));
7
+ const setTimer = opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
8
+ const clearTimer = opts.clearTimer ?? ((id) => clearTimeout(id));
9
+ const firedFor = opts.firedFor ?? 600;
10
+ let state = "idle";
11
+ let startedAt = 0;
12
+ let duration = 0;
13
+ let frame;
14
+ let timer;
15
+ const setState = (next) => {
16
+ if (state === next)
17
+ return;
18
+ state = next;
19
+ opts.onStateChange?.(next);
20
+ };
21
+ const stopFrame = () => {
22
+ if (frame !== undefined)
23
+ cancelFrame(frame);
24
+ frame = undefined;
25
+ };
26
+ const fire = () => {
27
+ stopFrame();
28
+ setState("fired");
29
+ opts.onProgress(1);
30
+ opts.onConfirm();
31
+ timer = setTimer(() => {
32
+ timer = undefined;
33
+ setState("idle");
34
+ opts.onProgress(0);
35
+ }, firedFor);
36
+ };
37
+ const tick = () => {
38
+ frame = undefined;
39
+ if (state !== "holding")
40
+ return;
41
+ const elapsed = now() - startedAt;
42
+ if (elapsed >= duration) {
43
+ fire();
44
+ return;
45
+ }
46
+ opts.onProgress(Math.max(0, elapsed / duration));
47
+ frame = requestFrame(tick);
48
+ };
49
+ return {
50
+ get state() {
51
+ return state;
52
+ },
53
+ start() {
54
+ if (state !== "idle")
55
+ return;
56
+ duration = Math.max(0, opts.duration());
57
+ startedAt = now();
58
+ setState("holding");
59
+ opts.onProgress(0);
60
+ frame = requestFrame(tick);
61
+ },
62
+ cancel() {
63
+ if (state !== "holding")
64
+ return;
65
+ stopFrame();
66
+ setState("idle");
67
+ opts.onProgress(0);
68
+ },
69
+ fire() {
70
+ if (state === "fired")
71
+ return;
72
+ fire();
73
+ },
74
+ dispose() {
75
+ stopFrame();
76
+ if (timer !== undefined)
77
+ clearTimer(timer);
78
+ timer = undefined;
79
+ state = "idle";
80
+ },
81
+ };
82
+ }
@@ -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,8 +1,32 @@
1
1
  export { Button, type ButtonProps } from "./Button.js";
2
+ export { ButtonGroup, type ButtonGroupProps } from "./ButtonGroup.js";
3
+ export { buttonGroupAttrs } from "./button-group.js";
4
+ export { IconButton, type IconButtonProps } from "./IconButton.js";
5
+ export { iconButtonAttrs, type IconButtonOptions } from "./icon-button.js";
6
+ export { HoldButton, type HoldButtonProps } from "./HoldButton.js";
7
+ export { createHoldController, type HoldController, type HoldOptions, type HoldState } from "./hold.js";
2
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";
15
+ export { StatusCard, StatusCardRows, StatusCardRow, StatusCardFooter, StatusCardEmpty, type StatusCardProps, type StatusCardRowProps, type StatusCardFooterProps, } from "./StatusCard.js";
16
+ export { attentionTone, statusCardAttrs, type StatusTone } from "./status-card.js";
17
+ export { Stepper, type StepperProps, type StepItem } from "./Stepper.js";
18
+ export { stepAttrs, stepState, type StepState } from "./stepper-state.js";
3
19
  export { NavLink, type NavLinkProps } from "./NavLink.js";
4
20
  export { Input, Textarea, Select, Label, Field, Checkbox, Radio, Switch, type InputProps, type TextareaProps, type SelectProps, type LabelProps, } from "./form.js";
5
21
  export { Badge, Alert, Progress, Spinner, type BadgeProps, type AlertProps, type ProgressProps, type SemanticVariant, } from "./feedback.js";
6
22
  export { Dialog, type DialogProps } from "./Dialog.js";
7
23
  export { Tabs, type TabsProps, type TabItem } from "./Tabs.js";
24
+ export { slideDir, type SlideDir } from "./slide-dir.js";
25
+ export { useSlideDir } from "./useSlideDir.js";
8
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,8 +1,31 @@
1
1
  export { Button } from "./Button.js";
2
+ export { ButtonGroup } from "./ButtonGroup.js";
3
+ export { buttonGroupAttrs } from "./button-group.js";
4
+ export { IconButton } from "./IconButton.js";
5
+ export { iconButtonAttrs } from "./icon-button.js";
6
+ export { HoldButton } from "./HoldButton.js";
7
+ export { createHoldController } from "./hold.js";
2
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";
15
+ export { StatusCard, StatusCardRows, StatusCardRow, StatusCardFooter, StatusCardEmpty, } from "./StatusCard.js";
16
+ export { attentionTone, statusCardAttrs } from "./status-card.js";
17
+ export { Stepper } from "./Stepper.js";
18
+ export { stepAttrs, stepState } from "./stepper-state.js";
3
19
  export { NavLink } from "./NavLink.js";
4
20
  export { Input, Textarea, Select, Label, Field, Checkbox, Radio, Switch, } from "./form.js";
5
21
  export { Badge, Alert, Progress, Spinner, } from "./feedback.js";
6
22
  export { Dialog } from "./Dialog.js";
7
23
  export { Tabs } from "./Tabs.js";
24
+ export { slideDir } from "./slide-dir.js";
25
+ export { useSlideDir } from "./useSlideDir.js";
8
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,5 @@
1
+ export type SlideDir = "forward" | "back";
2
+ /** Which way the active item moved between two child indexes. `undefined` when
3
+ * there is no travel to describe — nothing was active before or after (-1),
4
+ * or the index did not change — so the caller leaves the last direction alone. */
5
+ export declare function slideDir(prev: number, next: number): SlideDir | undefined;
@@ -0,0 +1,12 @@
1
+ // Pure direction logic behind the sliding indicator (Tabs underline,
2
+ // ButtonGroup pill), kept DOM-free so it can be tested under node:test without
3
+ // a renderer (so it imports nothing — Node runs the .ts source and cannot
4
+ // resolve the .js specifiers the build uses).
5
+ /** Which way the active item moved between two child indexes. `undefined` when
6
+ * there is no travel to describe — nothing was active before or after (-1),
7
+ * or the index did not change — so the caller leaves the last direction alone. */
8
+ export function slideDir(prev, next) {
9
+ if (prev < 0 || next < 0 || prev === next)
10
+ return undefined;
11
+ return next > prev ? "forward" : "back";
12
+ }