@interstellarforge/matter-react 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/dist/Button.d.ts +15 -0
- package/dist/Button.js +12 -0
- package/dist/Controls.d.ts +14 -0
- package/dist/Controls.js +25 -0
- package/dist/Dialog.d.ts +13 -0
- package/dist/Dialog.js +29 -0
- package/dist/Display.d.ts +12 -0
- package/dist/Display.js +9 -0
- package/dist/Feedback.d.ts +29 -0
- package/dist/Feedback.js +31 -0
- package/dist/Fields.d.ts +18 -0
- package/dist/Fields.js +13 -0
- package/dist/Icon.d.ts +10 -0
- package/dist/Icon.js +7 -0
- package/dist/Menu.d.ts +12 -0
- package/dist/Menu.js +84 -0
- package/dist/Tabs.d.ts +16 -0
- package/dist/Tabs.js +37 -0
- package/dist/Tooltip.d.ts +10 -0
- package/dist/Tooltip.js +21 -0
- package/dist/cx.d.ts +2 -0
- package/dist/cx.js +4 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +10 -0
- package/package.json +45 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Interstellarforge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
3
|
+
export type ControlSize = "sm" | "md" | "lg";
|
|
4
|
+
export interface ButtonProps extends ComponentProps<"button"> {
|
|
5
|
+
variant?: ButtonVariant;
|
|
6
|
+
size?: ControlSize;
|
|
7
|
+
/** Keeps the button focusable but blocks clicks while pending. */
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function Button({ variant, size, type, loading, className, onClick, ...rest }: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export interface IconButtonProps extends Omit<ButtonProps, "aria-label"> {
|
|
12
|
+
/** Accessible name; required because the button has no visible text. */
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function IconButton({ label, ...rest }: IconButtonProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
export function Button({ variant = "secondary", size = "md", type = "button", loading = false, className, onClick, ...rest }) {
|
|
4
|
+
return (_jsx("button", { ...rest, type: type, className: cx("m-button", className), "data-variant": variant, "data-size": size, "aria-busy": loading || undefined, "aria-disabled": loading || undefined, onClick: loading
|
|
5
|
+
? (event) => {
|
|
6
|
+
event.preventDefault();
|
|
7
|
+
}
|
|
8
|
+
: onClick }));
|
|
9
|
+
}
|
|
10
|
+
export function IconButton({ label, ...rest }) {
|
|
11
|
+
return _jsx(Button, { ...rest, "aria-label": label, "data-icon-only": "" });
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
export interface CheckboxProps extends Omit<ComponentProps<"input">, "type"> {
|
|
3
|
+
invalid?: boolean;
|
|
4
|
+
indeterminate?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Client component: `indeterminate` is a DOM property that needs a ref. */
|
|
7
|
+
export declare function Checkbox({ invalid, indeterminate, className, children, ...rest }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface SwitchProps extends Omit<ComponentProps<"button">, "onChange" | "role" | "value"> {
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
defaultChecked?: boolean;
|
|
11
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Needs an accessible name via `aria-label` or `aria-labelledby`. */
|
|
14
|
+
export declare function Switch({ checked, defaultChecked, onCheckedChange, className, onClick, ...rest }: SwitchProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Controls.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
/** Client component: `indeterminate` is a DOM property that needs a ref. */
|
|
6
|
+
export function Checkbox({ invalid, indeterminate = false, className, children, ...rest }) {
|
|
7
|
+
return (_jsxs("label", { className: cx("m-checkbox", className), children: [_jsx("input", { ...rest, type: "checkbox", "aria-invalid": invalid ? true : undefined, ref: (element) => {
|
|
8
|
+
if (element) {
|
|
9
|
+
element.indeterminate = indeterminate;
|
|
10
|
+
}
|
|
11
|
+
} }), _jsx("span", { children: children })] }));
|
|
12
|
+
}
|
|
13
|
+
/** Needs an accessible name via `aria-label` or `aria-labelledby`. */
|
|
14
|
+
export function Switch({ checked, defaultChecked = false, onCheckedChange, className, onClick, ...rest }) {
|
|
15
|
+
const [internal, setInternal] = useState(defaultChecked);
|
|
16
|
+
const on = checked ?? internal;
|
|
17
|
+
return (_jsx("button", { ...rest, type: "button", role: "switch", "aria-checked": on, className: cx("m-switch", className), onClick: (event) => {
|
|
18
|
+
onClick?.(event);
|
|
19
|
+
if (event.defaultPrevented) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setInternal(!on);
|
|
23
|
+
onCheckedChange?.(!on);
|
|
24
|
+
} }));
|
|
25
|
+
}
|
package/dist/Dialog.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface DialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
/** Visible heading; also the accessible name. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Accessible name of the close button. */
|
|
8
|
+
closeLabel: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
/** Modal dialog on the native `<dialog>`: focus trap, Esc and inert background come from the platform. */
|
|
13
|
+
export declare function Dialog({ open, onOpenChange, title, closeLabel, children, footer, }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Dialog.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef } from "react";
|
|
4
|
+
/** Modal dialog on the native `<dialog>`: focus trap, Esc and inert background come from the platform. */
|
|
5
|
+
export function Dialog({ open, onOpenChange, title, closeLabel, children, footer, }) {
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
const titleId = useId();
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const dialog = ref.current;
|
|
10
|
+
if (!dialog) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (open && !dialog.open) {
|
|
14
|
+
dialog.showModal();
|
|
15
|
+
}
|
|
16
|
+
else if (!open && dialog.open) {
|
|
17
|
+
dialog.close();
|
|
18
|
+
}
|
|
19
|
+
}, [open]);
|
|
20
|
+
return (_jsxs("dialog", { ref: ref, className: "m-dialog", "aria-labelledby": titleId, onClose: () => {
|
|
21
|
+
onOpenChange(false);
|
|
22
|
+
}, onClick: (event) => {
|
|
23
|
+
if (event.target === event.currentTarget) {
|
|
24
|
+
onOpenChange(false);
|
|
25
|
+
}
|
|
26
|
+
}, children: [_jsxs("div", { className: "m-dialog__header", children: [_jsx("h2", { id: titleId, className: "m-dialog__title", children: title }), _jsx("button", { type: "button", className: "m-button", "data-variant": "ghost", "data-icon-only": "", "aria-label": closeLabel, onClick: () => {
|
|
27
|
+
onOpenChange(false);
|
|
28
|
+
}, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] }), _jsx("div", { className: "m-dialog__body", children: children }), footer ? _jsx("div", { className: "m-dialog__footer", children: footer }) : undefined] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComponentProps, ElementType } from "react";
|
|
2
|
+
export type BadgeTone = "neutral" | "accent" | "info" | "success" | "warning" | "danger";
|
|
3
|
+
export interface BadgeProps extends ComponentProps<"span"> {
|
|
4
|
+
tone?: BadgeTone;
|
|
5
|
+
}
|
|
6
|
+
/** Status must also be readable as text; never rely on the tone colour alone. */
|
|
7
|
+
export declare function Badge({ tone, className, ...rest }: BadgeProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface CardProps extends ComponentProps<"div"> {
|
|
9
|
+
as?: ElementType;
|
|
10
|
+
padded?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function Card({ as: Tag, padded, className, ...rest }: CardProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Display.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Status must also be readable as text; never rely on the tone colour alone. */
|
|
4
|
+
export function Badge({ tone = "neutral", className, ...rest }) {
|
|
5
|
+
return (_jsx("span", { ...rest, className: cx("m-badge", className), "data-tone": tone }));
|
|
6
|
+
}
|
|
7
|
+
export function Card({ as: Tag = "div", padded = false, className, ...rest }) {
|
|
8
|
+
return (_jsx(Tag, { ...rest, className: cx("m-card", className), "data-padded": padded ? "" : undefined }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
export interface AvatarProps extends Omit<ComponentProps<"span">, "children"> {
|
|
3
|
+
/** Full name: used as the accessible name and for the initials fallback. */
|
|
4
|
+
name: string;
|
|
5
|
+
src?: string;
|
|
6
|
+
size?: "sm" | "md" | "lg";
|
|
7
|
+
}
|
|
8
|
+
export declare function Avatar({ name, src, size, className, ...rest }: AvatarProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export type BannerTone = "info" | "success" | "warning" | "danger";
|
|
10
|
+
export interface BannerProps extends Omit<ComponentProps<"div">, "title"> {
|
|
11
|
+
tone?: BannerTone;
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** Warnings and errors are announced immediately (`alert`); the rest politely (`status`). */
|
|
15
|
+
export declare function Banner({ tone, title, className, children, ...rest }: BannerProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export interface ProgressBarProps extends Omit<ComponentProps<"div">, "children"> {
|
|
17
|
+
/** Accessible name (required). */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Omit for an indeterminate bar. */
|
|
20
|
+
value?: number;
|
|
21
|
+
max?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function ProgressBar({ label, value, max, className, style, ...rest }: ProgressBarProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export interface EmptyStateProps extends Omit<ComponentProps<"div">, "title"> {
|
|
25
|
+
title: ReactNode;
|
|
26
|
+
/** Optional call to action, e.g. a `Button`. */
|
|
27
|
+
action?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export declare function EmptyState({ title, action, className, children, ...rest }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Feedback.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
function initials(name) {
|
|
4
|
+
return name
|
|
5
|
+
.split(/\s+/)
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.slice(0, 2)
|
|
8
|
+
.map((part) => part.charAt(0))
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
export function Avatar({ name, src, size = "md", className, ...rest }) {
|
|
12
|
+
return (_jsx("span", { ...rest, className: cx("m-avatar", className), "data-size": size, role: "img", "aria-label": name, children: src ? _jsx("img", { src: src, alt: "" }) : initials(name) }));
|
|
13
|
+
}
|
|
14
|
+
/** Warnings and errors are announced immediately (`alert`); the rest politely (`status`). */
|
|
15
|
+
export function Banner({ tone = "info", title, className, children, ...rest }) {
|
|
16
|
+
return (_jsxs("div", { role: tone === "danger" || tone === "warning" ? "alert" : "status", ...rest, className: cx("m-banner", className), "data-tone": tone, children: [title ? _jsx("div", { className: "m-banner__title", children: title }) : null, children] }));
|
|
17
|
+
}
|
|
18
|
+
export function ProgressBar({ label, value, max = 100, className, style, ...rest }) {
|
|
19
|
+
const percent = value === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: Math.min(100, Math.max(0, (value / max) * 100));
|
|
22
|
+
return (_jsx("div", { ...rest, className: cx("m-progress", className), role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": value, style: percent === undefined
|
|
23
|
+
? style
|
|
24
|
+
: {
|
|
25
|
+
...style,
|
|
26
|
+
"--m-progress": `${String(percent)}%`,
|
|
27
|
+
}, children: _jsx("div", { className: "m-progress__bar" }) }));
|
|
28
|
+
}
|
|
29
|
+
export function EmptyState({ title, action, className, children, ...rest }) {
|
|
30
|
+
return (_jsxs("div", { ...rest, className: cx("m-empty", className), children: [_jsx("div", { className: "m-empty__title", children: title }), children ? _jsx("div", { children: children }) : null, action] }));
|
|
31
|
+
}
|
package/dist/Fields.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IconDefinition } from "@interstellarforge/matter-icons";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
interface FieldProps {
|
|
4
|
+
/** Sets `aria-invalid` and the error border. */
|
|
5
|
+
invalid?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface InputProps extends ComponentProps<"input">, FieldProps {
|
|
8
|
+
/** Decorative leading icon, e.g. `search`. The input still needs its own accessible name. */
|
|
9
|
+
icon?: IconDefinition;
|
|
10
|
+
}
|
|
11
|
+
export declare function Input({ invalid, icon, className, ...rest }: InputProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export interface TextareaProps extends ComponentProps<"textarea">, FieldProps {
|
|
13
|
+
}
|
|
14
|
+
export declare function Textarea({ invalid, className, ...rest }: TextareaProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export interface SelectProps extends ComponentProps<"select">, FieldProps {
|
|
16
|
+
}
|
|
17
|
+
export declare function Select({ invalid, className, ...rest }: SelectProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
package/dist/Fields.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { Icon } from "./Icon.js";
|
|
4
|
+
export function Input({ invalid, icon, className, ...rest }) {
|
|
5
|
+
const input = (_jsx("input", { ...rest, className: cx("m-input", icon ? undefined : className), "aria-invalid": invalid ? true : undefined }));
|
|
6
|
+
return icon ? (_jsxs("span", { className: cx("m-input-icon", className), children: [_jsx(Icon, { icon: icon }), input] })) : (input);
|
|
7
|
+
}
|
|
8
|
+
export function Textarea({ invalid, className, ...rest }) {
|
|
9
|
+
return (_jsx("textarea", { ...rest, className: cx("m-input m-textarea", className), "aria-invalid": invalid ? true : undefined }));
|
|
10
|
+
}
|
|
11
|
+
export function Select({ invalid, className, ...rest }) {
|
|
12
|
+
return (_jsx("select", { ...rest, className: cx("m-input m-select", className), "aria-invalid": invalid ? true : undefined }));
|
|
13
|
+
}
|
package/dist/Icon.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IconDefinition } from "@interstellarforge/matter-icons";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
export interface IconProps extends Omit<ComponentProps<"svg">, "children" | "dangerouslySetInnerHTML"> {
|
|
4
|
+
/** A named export of `@interstellarforge/matter-icons`, e.g. `search` (tree-shakeable). */
|
|
5
|
+
icon: IconDefinition;
|
|
6
|
+
size?: number;
|
|
7
|
+
/** Accessible name. Without it the icon is decorative and hidden from assistive technology. */
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function Icon({ icon, size, label, className, ...rest }: IconProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Icon.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
export function Icon({ icon, size = 18, label, className, ...rest }) {
|
|
4
|
+
return (_jsx("svg", { ...rest, className: cx("m-icon", className), width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", focusable: "false", role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true,
|
|
5
|
+
// The body is static markup from our own, SVGO-optimised icon sources.
|
|
6
|
+
dangerouslySetInnerHTML: { __html: icon.body } }));
|
|
7
|
+
}
|
package/dist/Menu.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface MenuItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface MenuProps {
|
|
7
|
+
/** Visible text of the trigger button. */
|
|
8
|
+
triggerLabel: string;
|
|
9
|
+
items: MenuItem[];
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function Menu({ triggerLabel, items, onSelect }: MenuProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Menu.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
export function Menu({ triggerLabel, items, onSelect }) {
|
|
5
|
+
const menuId = useId();
|
|
6
|
+
const root = useRef(null);
|
|
7
|
+
const trigger = useRef(null);
|
|
8
|
+
const itemRefs = useRef([]);
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const enabled = items.flatMap((item, index) => item.disabled ? [] : [index]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!open) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
function onPointerDown(event) {
|
|
16
|
+
if (!root.current?.contains(event.target)) {
|
|
17
|
+
setOpen(false);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
21
|
+
return () => {
|
|
22
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
23
|
+
};
|
|
24
|
+
}, [open]);
|
|
25
|
+
function focusItem(index) {
|
|
26
|
+
if (index !== undefined) {
|
|
27
|
+
itemRefs.current[index]?.focus();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function close() {
|
|
31
|
+
setOpen(false);
|
|
32
|
+
trigger.current?.focus();
|
|
33
|
+
}
|
|
34
|
+
function openAt(position) {
|
|
35
|
+
setOpen(true);
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
focusItem(position === "first" ? enabled[0] : enabled.at(-1));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function onMenuKeyDown(event) {
|
|
41
|
+
const current = itemRefs.current.findIndex((element) => element === document.activeElement);
|
|
42
|
+
const position = enabled.indexOf(current);
|
|
43
|
+
if (event.key === "Escape") {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
close();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const next = {
|
|
49
|
+
ArrowDown: enabled[(position + 1) % enabled.length],
|
|
50
|
+
ArrowUp: enabled[(position - 1 + enabled.length) % enabled.length],
|
|
51
|
+
Home: enabled[0],
|
|
52
|
+
End: enabled.at(-1),
|
|
53
|
+
}[event.key];
|
|
54
|
+
if (next !== undefined) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
focusItem(next);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.key.length === 1) {
|
|
60
|
+
const typed = event.key.toLowerCase();
|
|
61
|
+
focusItem(enabled.find((index) => index > current &&
|
|
62
|
+
items[index]?.label.toLowerCase().startsWith(typed)) ??
|
|
63
|
+
enabled.find((index) => items[index]?.label.toLowerCase().startsWith(typed)));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs("div", { ref: root, className: "m-menu-wrap", children: [_jsx("button", { ref: trigger, type: "button", className: "m-button", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": menuId, onClick: () => {
|
|
67
|
+
if (open) {
|
|
68
|
+
setOpen(false);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
openAt("first");
|
|
72
|
+
}
|
|
73
|
+
}, onKeyDown: (event) => {
|
|
74
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
openAt(event.key === "ArrowDown" ? "first" : "last");
|
|
77
|
+
}
|
|
78
|
+
}, children: triggerLabel }), _jsx("div", { id: menuId, role: "menu", className: "m-menu", hidden: !open, onKeyDown: onMenuKeyDown, children: items.map((item, index) => (_jsx("button", { ref: (element) => {
|
|
79
|
+
itemRefs.current[index] = element;
|
|
80
|
+
}, type: "button", role: "menuitem", className: "m-menu__item", tabIndex: -1, disabled: item.disabled, onClick: () => {
|
|
81
|
+
onSelect(item.id);
|
|
82
|
+
close();
|
|
83
|
+
}, children: item.label }, item.id))) })] }));
|
|
84
|
+
}
|
package/dist/Tabs.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface TabItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
content: ReactNode;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface TabsProps {
|
|
9
|
+
items: TabItem[];
|
|
10
|
+
/** Accessible name of the tab list. */
|
|
11
|
+
label: string;
|
|
12
|
+
value?: string;
|
|
13
|
+
defaultValue?: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function Tabs({ items, label, value, defaultValue, onValueChange, }: TabsProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Tabs.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState } from "react";
|
|
4
|
+
export function Tabs({ items, label, value, defaultValue, onValueChange, }) {
|
|
5
|
+
const base = useId();
|
|
6
|
+
const refs = useRef(new Map());
|
|
7
|
+
const firstEnabled = items.find((item) => !item.disabled)?.id;
|
|
8
|
+
const [internal, setInternal] = useState(defaultValue ?? firstEnabled);
|
|
9
|
+
const selected = value ?? internal;
|
|
10
|
+
const enabled = items.filter((item) => !item.disabled);
|
|
11
|
+
function select(id) {
|
|
12
|
+
setInternal(id);
|
|
13
|
+
onValueChange?.(id);
|
|
14
|
+
refs.current.get(id)?.focus();
|
|
15
|
+
}
|
|
16
|
+
function onKeyDown(event) {
|
|
17
|
+
const index = enabled.findIndex((item) => item.id === selected);
|
|
18
|
+
const last = enabled.length - 1;
|
|
19
|
+
const target = {
|
|
20
|
+
ArrowRight: enabled[index === last ? 0 : index + 1],
|
|
21
|
+
ArrowLeft: enabled[index <= 0 ? last : index - 1],
|
|
22
|
+
Home: enabled[0],
|
|
23
|
+
End: enabled[last],
|
|
24
|
+
}[event.key];
|
|
25
|
+
if (target) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
select(target.id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return (_jsxs("div", { className: "m-tabs", children: [_jsx("div", { role: "tablist", "aria-label": label, className: "m-tablist", onKeyDown: onKeyDown, children: items.map((item) => (_jsx("button", { ref: (element) => {
|
|
31
|
+
if (element) {
|
|
32
|
+
refs.current.set(item.id, element);
|
|
33
|
+
}
|
|
34
|
+
}, type: "button", role: "tab", id: `${base}-tab-${item.id}`, className: "m-tab", "aria-selected": item.id === selected, "aria-controls": `${base}-panel-${item.id}`, tabIndex: item.id === selected ? 0 : -1, disabled: item.disabled, onClick: () => {
|
|
35
|
+
select(item.id);
|
|
36
|
+
}, children: item.label }, item.id))) }), items.map((item) => (_jsx("div", { role: "tabpanel", id: `${base}-panel-${item.id}`, className: "m-tabpanel", "aria-labelledby": `${base}-tab-${item.id}`, hidden: item.id !== selected, children: item.id === selected ? item.content : undefined }, item.id)))] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface TooltipProps {
|
|
3
|
+
content: string;
|
|
4
|
+
/** The single element that triggers the tooltip; it receives `aria-describedby`. */
|
|
5
|
+
children: ReactElement<{
|
|
6
|
+
"aria-describedby"?: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
/** Opens on hover and focus; Esc dismisses. Placement is fixed to the top in the MVP (no collision handling). */
|
|
10
|
+
export declare function Tooltip({ content, children }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Tooltip.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cloneElement, useId, useState } from "react";
|
|
4
|
+
/** Opens on hover and focus; Esc dismisses. Placement is fixed to the top in the MVP (no collision handling). */
|
|
5
|
+
export function Tooltip({ content, children }) {
|
|
6
|
+
const id = useId();
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
return (_jsxs("span", { className: "m-tooltip-wrap", onMouseEnter: () => {
|
|
9
|
+
setOpen(true);
|
|
10
|
+
}, onMouseLeave: () => {
|
|
11
|
+
setOpen(false);
|
|
12
|
+
}, onFocus: () => {
|
|
13
|
+
setOpen(true);
|
|
14
|
+
}, onBlur: () => {
|
|
15
|
+
setOpen(false);
|
|
16
|
+
}, onKeyDown: (event) => {
|
|
17
|
+
if (event.key === "Escape") {
|
|
18
|
+
setOpen(false);
|
|
19
|
+
}
|
|
20
|
+
}, children: [cloneElement(children, { "aria-describedby": id }), _jsx("span", { id: id, role: "tooltip", className: "m-tooltip", hidden: !open, children: content })] }));
|
|
21
|
+
}
|
package/dist/cx.d.ts
ADDED
package/dist/cx.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { ButtonProps, ButtonVariant, ControlSize, IconButtonProps, } from "./Button.tsx";
|
|
2
|
+
export { Button, IconButton } from "./Button.tsx";
|
|
3
|
+
export type { DialogProps } from "./Dialog.tsx";
|
|
4
|
+
export { Dialog } from "./Dialog.tsx";
|
|
5
|
+
export type { BadgeProps, BadgeTone, CardProps } from "./Display.tsx";
|
|
6
|
+
export { Badge, Card } from "./Display.tsx";
|
|
7
|
+
export type { CheckboxProps, SwitchProps } from "./Controls.tsx";
|
|
8
|
+
export { Checkbox, Switch } from "./Controls.tsx";
|
|
9
|
+
export type { InputProps, SelectProps, TextareaProps } from "./Fields.tsx";
|
|
10
|
+
export { Input, Select, Textarea } from "./Fields.tsx";
|
|
11
|
+
export type { MenuItem, MenuProps } from "./Menu.tsx";
|
|
12
|
+
export { Menu } from "./Menu.tsx";
|
|
13
|
+
export type { TabItem, TabsProps } from "./Tabs.tsx";
|
|
14
|
+
export { Tabs } from "./Tabs.tsx";
|
|
15
|
+
export type { TooltipProps } from "./Tooltip.tsx";
|
|
16
|
+
export { Tooltip } from "./Tooltip.tsx";
|
|
17
|
+
export type { IconProps } from "./Icon.tsx";
|
|
18
|
+
export { Icon } from "./Icon.tsx";
|
|
19
|
+
export type { AvatarProps, BannerProps, BannerTone, EmptyStateProps, ProgressBarProps, } from "./Feedback.tsx";
|
|
20
|
+
export { Avatar, Banner, EmptyState, ProgressBar } from "./Feedback.tsx";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Button, IconButton } from "./Button.js";
|
|
2
|
+
export { Dialog } from "./Dialog.js";
|
|
3
|
+
export { Badge, Card } from "./Display.js";
|
|
4
|
+
export { Checkbox, Switch } from "./Controls.js";
|
|
5
|
+
export { Input, Select, Textarea } from "./Fields.js";
|
|
6
|
+
export { Menu } from "./Menu.js";
|
|
7
|
+
export { Tabs } from "./Tabs.js";
|
|
8
|
+
export { Tooltip } from "./Tooltip.js";
|
|
9
|
+
export { Icon } from "./Icon.js";
|
|
10
|
+
export { Avatar, Banner, EmptyState, ProgressBar } from "./Feedback.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interstellarforge/matter-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Matter components for React 19 / Next.js. Styling comes from @interstellarforge/matter-css.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/interstellarforge/matter.git",
|
|
9
|
+
"directory": "packages/react"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@testing-library/dom": "10.4.2",
|
|
31
|
+
"@testing-library/react": "16.3.3",
|
|
32
|
+
"@testing-library/user-event": "14.6.8",
|
|
33
|
+
"@types/react": "19.2.14",
|
|
34
|
+
"@types/react-dom": "19.2.3",
|
|
35
|
+
"axe-core": "4.14.0",
|
|
36
|
+
"jsdom": "30.1.3",
|
|
37
|
+
"react": "19.2.4",
|
|
38
|
+
"react-dom": "19.2.4",
|
|
39
|
+
"vitest": "4.1.6"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@interstellarforge/matter-icons": "0.2.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
}
|
|
6
48
|
}
|
package/README.md
DELETED