@haruhimemoe/ui 0.3.0 → 0.5.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/CHANGELOG.md +52 -1
- package/README.md +336 -27
- package/dist/components/actions/AsyncButton.d.ts +30 -0
- package/dist/components/actions/AsyncButton.js +48 -0
- package/dist/components/actions/CopyButton.d.ts +3 -3
- package/dist/components/actions/CopyButton.js +9 -12
- package/dist/components/actions/InlineConfirm.d.ts +45 -0
- package/dist/components/actions/InlineConfirm.js +80 -0
- package/dist/components/actions/Pagination.d.ts +40 -17
- package/dist/components/actions/Pagination.js +25 -18
- package/dist/components/actions/PaginationButton.d.ts +25 -0
- package/dist/components/actions/PaginationButton.js +23 -0
- package/dist/components/actions/PaginationStatus.d.ts +7 -4
- package/dist/components/actions/PaginationStatus.js +7 -5
- package/dist/components/actions/StatusOutput.d.ts +20 -0
- package/dist/components/actions/StatusOutput.js +9 -0
- package/dist/components/actions/pages.d.ts +21 -0
- package/dist/components/actions/pages.js +21 -0
- package/dist/components/actions/useLatestStatus.d.ts +26 -0
- package/dist/components/actions/useLatestStatus.js +32 -0
- package/dist/components/basics/AutoLink.d.ts +21 -0
- package/dist/components/basics/AutoLink.js +26 -0
- package/dist/components/basics/Badge.d.ts +22 -0
- package/dist/components/basics/Badge.js +18 -0
- package/dist/components/basics/ButtonLink.d.ts +5 -6
- package/dist/components/basics/ButtonLink.js +5 -11
- package/dist/components/basics/Card.d.ts +4 -3
- package/dist/components/basics/Card.js +3 -2
- package/dist/components/basics/Disclosure.d.ts +34 -0
- package/dist/components/basics/Disclosure.js +30 -0
- package/dist/components/basics/Notice.d.ts +2 -1
- package/dist/components/basics/Tabs.d.ts +37 -0
- package/dist/components/basics/Tabs.js +47 -0
- package/dist/components/basics/TextLink.d.ts +21 -0
- package/dist/components/basics/TextLink.js +19 -0
- package/dist/components/basics/buttonStyles.d.ts +3 -1
- package/dist/components/basics/buttonStyles.js +1 -1
- package/dist/components/basics/cardStyles.d.ts +14 -0
- package/dist/components/basics/cardStyles.js +12 -0
- package/dist/components/basics/linkStyles.d.ts +23 -0
- package/dist/components/basics/linkStyles.js +23 -0
- package/dist/components/basics/tabIds.d.ts +23 -0
- package/dist/components/basics/tabIds.js +23 -0
- package/dist/components/filters/Chip.d.ts +13 -5
- package/dist/components/filters/Chip.js +20 -11
- package/dist/components/filters/ChipGroup.d.ts +11 -12
- package/dist/components/filters/ChipGroup.js +5 -10
- package/dist/components/filters/ChoiceChips.d.ts +38 -0
- package/dist/components/filters/ChoiceChips.js +31 -0
- package/dist/components/filters/FilterPanel.d.ts +5 -4
- package/dist/components/filters/FilterPanel.js +5 -4
- package/dist/components/filters/FilterRow.d.ts +1 -1
- package/dist/components/filters/FilterRow.js +3 -2
- package/dist/components/filters/GroupFrame.d.ts +27 -0
- package/dist/components/filters/GroupFrame.js +27 -0
- package/dist/components/filters/RangeBox.d.ts +28 -0
- package/dist/components/filters/RangeBox.js +41 -0
- package/dist/components/filters/RangeSlider.d.ts +13 -14
- package/dist/components/filters/RangeSlider.js +40 -103
- package/dist/components/filters/chipStyles.d.ts +17 -0
- package/dist/components/filters/chipStyles.js +17 -0
- package/dist/components/filters/rangeMath.d.ts +106 -0
- package/dist/components/filters/rangeMath.js +146 -0
- package/dist/components/forms/CharCounter.d.ts +25 -0
- package/dist/components/forms/CharCounter.js +12 -0
- package/dist/components/forms/Checkbox.d.ts +5 -4
- package/dist/components/forms/Checkbox.js +3 -3
- package/dist/components/forms/FieldFrame.d.ts +28 -2
- package/dist/components/forms/FieldFrame.js +14 -1
- package/dist/components/forms/RadioGroup.d.ts +46 -0
- package/dist/components/forms/RadioGroup.js +32 -0
- package/dist/components/forms/ReportDisclosure.d.ts +52 -0
- package/dist/components/forms/ReportDisclosure.js +54 -0
- package/dist/components/forms/Select.d.ts +1 -1
- package/dist/components/forms/Select.js +2 -2
- package/dist/components/forms/TextInput.d.ts +1 -1
- package/dist/components/forms/TextInput.js +2 -2
- package/dist/components/forms/Textarea.d.ts +1 -1
- package/dist/components/forms/Textarea.js +2 -2
- package/dist/components/forms/TypeToConfirm.d.ts +40 -0
- package/dist/components/forms/TypeToConfirm.js +46 -0
- package/dist/components/forms/VisibilitySelect.d.ts +52 -0
- package/dist/components/forms/VisibilitySelect.js +45 -0
- package/dist/components/forms/fieldStyles.d.ts +5 -2
- package/dist/components/forms/fieldStyles.js +5 -2
- package/dist/components/{actions → meta}/JsonLd.d.ts +2 -2
- package/dist/components/osu/BeatmapStats.d.ts +36 -0
- package/dist/components/osu/BeatmapStats.js +44 -0
- package/dist/components/osu/ModBadge.d.ts +23 -0
- package/dist/components/osu/ModBadge.js +22 -0
- package/dist/components/osu/StarRating.d.ts +26 -0
- package/dist/components/osu/StarRating.js +17 -0
- package/dist/components/osu/starColors.d.ts +23 -0
- package/dist/components/osu/starColors.js +48 -0
- package/dist/components/shell/HeaderMenu.d.ts +39 -0
- package/dist/components/shell/HeaderMenu.js +58 -0
- package/dist/components/shell/LinkNote.d.ts +16 -0
- package/dist/components/shell/LinkNote.js +17 -0
- package/dist/components/shell/LinkTabs.d.ts +29 -0
- package/dist/components/shell/LinkTabs.js +15 -0
- package/dist/components/shell/NavItem.d.ts +1 -7
- package/dist/components/shell/NavItem.js +5 -10
- package/dist/components/shell/NavLinks.d.ts +1 -1
- package/dist/components/shell/NavLinks.js +3 -3
- package/dist/components/shell/NavListClient.d.ts +1 -1
- package/dist/components/shell/NavListClient.js +4 -4
- package/dist/components/shell/SiteFooter.d.ts +6 -4
- package/dist/components/shell/SiteFooter.js +8 -6
- package/dist/components/shell/links.d.ts +8 -1
- package/dist/components/shell/links.js +8 -1
- package/dist/components/tables/TBody.d.ts +16 -0
- package/dist/components/tables/TBody.js +10 -0
- package/dist/components/tables/THead.d.ts +16 -0
- package/dist/components/tables/THead.js +10 -0
- package/dist/components/tables/Table.d.ts +26 -0
- package/dist/components/tables/Table.js +12 -0
- package/dist/components/tables/Td.d.ts +19 -0
- package/dist/components/tables/Td.js +11 -0
- package/dist/components/tables/Th.d.ts +20 -0
- package/dist/components/tables/Th.js +11 -0
- package/dist/components/tables/tableStyles.d.ts +12 -0
- package/dist/components/tables/tableStyles.js +12 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +32 -2
- package/dist/utils/href.d.ts +7 -4
- package/dist/utils/href.js +23 -7
- package/package.json +3 -3
- package/dist/components/shell/AutoLink.d.ts +0 -19
- package/dist/components/shell/AutoLink.js +0 -23
- /package/dist/components/{actions → meta}/JsonLd.js +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/actions/useLatestStatus.ts
|
|
3
|
+
* @desc The status that CopyButton and AsyncButton report (internal): each run empties it first,
|
|
4
|
+
* and only the latest run may fill it, so a slow earlier run can't overwrite a newer result.
|
|
5
|
+
* Each result carries its run's number, so StatusOutput mounts it as a fresh node and a
|
|
6
|
+
* repeated message is announced again.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { useRef, useState } from "react";
|
|
13
|
+
/**
|
|
14
|
+
* @function useLatestStatus
|
|
15
|
+
* @returns {{ status: LatestStatus<T>; start: () => number; settle: (run: number, result: T) =>
|
|
16
|
+
* void }} the status, `start` (empties it and returns the new run's number) and `settle`
|
|
17
|
+
* (sets a run's result, unless a later run has started since)
|
|
18
|
+
*/
|
|
19
|
+
export function useLatestStatus() {
|
|
20
|
+
const [status, setStatus] = useState(null);
|
|
21
|
+
const runs = useRef(0);
|
|
22
|
+
const start = () => {
|
|
23
|
+
setStatus(null);
|
|
24
|
+
runs.current += 1;
|
|
25
|
+
return runs.current;
|
|
26
|
+
};
|
|
27
|
+
const settle = (run, result) => {
|
|
28
|
+
if (run === runs.current)
|
|
29
|
+
setStatus({ result, run });
|
|
30
|
+
};
|
|
31
|
+
return { status, start, settle };
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/AutoLink.tsx
|
|
3
|
+
* @desc A link that picks its element from the href (internal): `next/link` for paths inside the
|
|
4
|
+
* app, a plain `<a>` for a string href that leaves it (a scheme like https: or mailto:, or
|
|
5
|
+
* //host). The plain `<a>` drops next/link's own props, and gets rel="noreferrer" when it
|
|
6
|
+
* opens in a new tab. ButtonLink, TextLink, the nav and the footer all render it.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Wed Sep 23, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
import Link from "next/link.js";
|
|
12
|
+
import type { ComponentProps } from "react";
|
|
13
|
+
/** Every next/link prop (including `ref`). */
|
|
14
|
+
export type AutoLinkProps = ComponentProps<typeof Link>;
|
|
15
|
+
/**
|
|
16
|
+
* @function AutoLink
|
|
17
|
+
* @param props {AutoLinkProps} next/link props
|
|
18
|
+
* @returns {JSX.Element} a `next/link` for internal paths (and URL objects), or a plain `<a>` for
|
|
19
|
+
* an external string href, with rel="noreferrer" when `target="_blank"` and no `rel`
|
|
20
|
+
*/
|
|
21
|
+
export declare function AutoLink({ href, target, rel, ...props }: AutoLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/basics/AutoLink.tsx
|
|
4
|
+
* @desc A link that picks its element from the href (internal): `next/link` for paths inside the
|
|
5
|
+
* app, a plain `<a>` for a string href that leaves it (a scheme like https: or mailto:, or
|
|
6
|
+
* //host). The plain `<a>` drops next/link's own props, and gets rel="noreferrer" when it
|
|
7
|
+
* opens in a new tab. ButtonLink, TextLink, the nav and the footer all render it.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Wed Sep 23, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
import Link from "next/link.js";
|
|
13
|
+
import { isExternalHref } from "../../utils/href.js";
|
|
14
|
+
/**
|
|
15
|
+
* @function AutoLink
|
|
16
|
+
* @param props {AutoLinkProps} next/link props
|
|
17
|
+
* @returns {JSX.Element} a `next/link` for internal paths (and URL objects), or a plain `<a>` for
|
|
18
|
+
* an external string href, with rel="noreferrer" when `target="_blank"` and no `rel`
|
|
19
|
+
*/
|
|
20
|
+
export function AutoLink({ href, target, rel, ...props }) {
|
|
21
|
+
if (typeof href === "string" && isExternalHref(href)) {
|
|
22
|
+
const { as: _as, replace: _replace, scroll: _scroll, shallow: _shallow, passHref: _passHref, prefetch: _prefetch, locale: _locale, legacyBehavior: _legacyBehavior, onNavigate: _onNavigate, transitionTypes: _transitionTypes, ...anchorProps } = props;
|
|
23
|
+
return (_jsx("a", { href: href, target: target, rel: rel ?? (target === "_blank" ? "noreferrer" : undefined), ...anchorProps }));
|
|
24
|
+
}
|
|
25
|
+
return _jsx(Link, { href: href, target: target, rel: rel, ...props });
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Badge.tsx
|
|
3
|
+
* @desc A small pill for a status or a tag ("Unranked", "beta", a count). Static text: no role,
|
|
4
|
+
* so screen readers read it in place with the text around it. Server-safe.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Mon Sep 28, 2026
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { ComponentProps } from "react";
|
|
10
|
+
/** The badge's colors: neutral b3, accent h1, a warning amber, or a muted outline. */
|
|
11
|
+
export type BadgeTone = "neutral" | "accent" | "warning" | "muted";
|
|
12
|
+
/** Every native `<span>` prop (including `ref`), plus a tone. */
|
|
13
|
+
export type BadgeProps = ComponentProps<"span"> & {
|
|
14
|
+
/** "neutral" (default), "accent", "warning" or "muted" (outlined, small capitals). */
|
|
15
|
+
tone?: BadgeTone | undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function Badge
|
|
19
|
+
* @param props {BadgeProps} native span props, plus a tone (default "neutral")
|
|
20
|
+
* @returns {JSX.Element} a rounded `<span>` pill in the tone's colors
|
|
21
|
+
*/
|
|
22
|
+
export declare function Badge({ tone, className, ...props }: BadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
const BASE = "inline-flex items-center whitespace-nowrap rounded-full px-2 py-0.5 text-xs";
|
|
4
|
+
// The looks the apps already use: pools' tags and warnings, and its header's beta pill.
|
|
5
|
+
const TONES = {
|
|
6
|
+
neutral: "bg-b3 text-c2",
|
|
7
|
+
accent: "bg-h1 font-bold text-b6",
|
|
8
|
+
warning: "bg-amber-300/20 font-bold text-amber-200",
|
|
9
|
+
muted: "border border-b3 bg-b5 font-bold text-c4 uppercase tracking-wide",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* @function Badge
|
|
13
|
+
* @param props {BadgeProps} native span props, plus a tone (default "neutral")
|
|
14
|
+
* @returns {JSX.Element} a rounded `<span>` pill in the tone's colors
|
|
15
|
+
*/
|
|
16
|
+
export function Badge({ tone = "neutral", className, ...props }) {
|
|
17
|
+
return _jsx("span", { className: cx(BASE, TONES[tone], className), ...props });
|
|
18
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/basics/ButtonLink.tsx
|
|
3
3
|
* @desc next/link styled as a pill button. Off-site hrefs (a scheme like https: or mailto:, or
|
|
4
|
-
* //host) render a plain <a>, with rel="noreferrer" when opened in a new tab.
|
|
4
|
+
* //host) render a plain <a>, with rel="noreferrer" when opened in a new tab (AutoLink).
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
10
|
-
import type { ComponentProps } from "react";
|
|
9
|
+
import { type AutoLinkProps } from "./AutoLink.js";
|
|
11
10
|
import { type ButtonSize, type ButtonVariant } from "./buttonStyles.js";
|
|
12
11
|
/** Every next/link prop (including `ref`), plus a button variant and size. */
|
|
13
|
-
export type ButtonLinkProps =
|
|
12
|
+
export type ButtonLinkProps = AutoLinkProps & {
|
|
14
13
|
variant?: ButtonVariant | undefined;
|
|
15
14
|
size?: ButtonSize | undefined;
|
|
16
15
|
};
|
|
@@ -20,4 +19,4 @@ export type ButtonLinkProps = ComponentProps<typeof Link> & {
|
|
|
20
19
|
* @returns {JSX.Element} a next/link styled as a pill button, or a plain `<a>` for an external
|
|
21
20
|
* href (next/link's own props are dropped there)
|
|
22
21
|
*/
|
|
23
|
-
export declare function ButtonLink({ variant, size, className,
|
|
22
|
+
export declare function ButtonLink({ variant, size, className, ...props }: ButtonLinkProps): import("react").JSX.Element;
|
|
@@ -2,13 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* @file src/components/basics/ButtonLink.tsx
|
|
4
4
|
* @desc next/link styled as a pill button. Off-site hrefs (a scheme like https: or mailto:, or
|
|
5
|
-
* //host) render a plain <a>, with rel="noreferrer" when opened in a new tab.
|
|
5
|
+
* //host) render a plain <a>, with rel="noreferrer" when opened in a new tab (AutoLink).
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
11
|
-
import { isExternalHref } from "../../utils/href.js";
|
|
10
|
+
import { AutoLink } from "./AutoLink.js";
|
|
12
11
|
import { buttonClasses } from "./buttonStyles.js";
|
|
13
12
|
/**
|
|
14
13
|
* @function ButtonLink
|
|
@@ -16,11 +15,6 @@ import { buttonClasses } from "./buttonStyles.js";
|
|
|
16
15
|
* @returns {JSX.Element} a next/link styled as a pill button, or a plain `<a>` for an external
|
|
17
16
|
* href (next/link's own props are dropped there)
|
|
18
17
|
*/
|
|
19
|
-
export function ButtonLink({ variant, size, className,
|
|
20
|
-
|
|
21
|
-
if (typeof href === "string" && isExternalHref(href)) {
|
|
22
|
-
const { as: _as, replace: _replace, scroll: _scroll, shallow: _shallow, passHref: _passHref, prefetch: _prefetch, locale: _locale, legacyBehavior: _legacyBehavior, onNavigate: _onNavigate, transitionTypes: _transitionTypes, ...anchorProps } = props;
|
|
23
|
-
return (_jsx("a", { href: href, target: target, rel: rel ?? (target === "_blank" ? "noreferrer" : undefined), className: classes, ...anchorProps }));
|
|
24
|
-
}
|
|
25
|
-
return _jsx(Link, { href: href, target: target, rel: rel, className: classes, ...props });
|
|
18
|
+
export function ButtonLink({ variant, size, className, ...props }) {
|
|
19
|
+
return _jsx(AutoLink, { className: buttonClasses({ variant, size, className }), ...props });
|
|
26
20
|
}
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* region. Server-safe (useId works in Server Components).
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
8
|
*/
|
|
9
9
|
import { type ComponentProps, type ReactNode } from "react";
|
|
10
|
+
import { type HeadingLevel } from "./cardStyles.js";
|
|
10
11
|
/** Every native `<section>` prop (including `ref`), with `title` rendered as the card's heading. */
|
|
11
12
|
export type CardProps = Omit<ComponentProps<"section">, "title"> & {
|
|
12
13
|
title?: ReactNode | undefined;
|
|
13
|
-
/** Heading level for the title (default 2). Use 3 or
|
|
14
|
-
headingLevel?:
|
|
14
|
+
/** Heading level for the title (default 2). Use 3 or lower for a card under another heading. */
|
|
15
|
+
headingLevel?: HeadingLevel | undefined;
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
18
|
* @function Card
|
|
@@ -5,10 +5,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* region. Server-safe (useId works in Server Components).
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
9
|
*/
|
|
10
10
|
import { useId } from "react";
|
|
11
11
|
import { cx } from "../../utils/cx.js";
|
|
12
|
+
import { CARD, CARD_HEADING } from "./cardStyles.js";
|
|
12
13
|
/**
|
|
13
14
|
* @function Card
|
|
14
15
|
* @param props {CardProps} an optional title that labels the region, its heading level (default
|
|
@@ -18,5 +19,5 @@ import { cx } from "../../utils/cx.js";
|
|
|
18
19
|
export function Card({ title, headingLevel = 2, className, children, ...props }) {
|
|
19
20
|
const headingId = useId();
|
|
20
21
|
const Heading = `h${headingLevel}`;
|
|
21
|
-
return (_jsxs("section", { "aria-labelledby": title ? headingId : undefined, className: cx(
|
|
22
|
+
return (_jsxs("section", { "aria-labelledby": title ? headingId : undefined, className: cx(CARD, className), ...props, children: [title ? (_jsx(Heading, { id: headingId, className: cx("mb-2", CARD_HEADING), children: title })) : null, children] }));
|
|
22
23
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Disclosure.tsx
|
|
3
|
+
* @desc A button that shows and hides a panel below it (the packs download options): the button
|
|
4
|
+
* carries aria-expanded and aria-controls, and the closed panel stays in the page, hidden,
|
|
5
|
+
* so its form fields keep their values. Uncontrolled by default, or controlled with `open`.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
11
|
+
/** Every native `<div>` prop for the wrapper, plus the button's text and the panel. */
|
|
12
|
+
export type DisclosureProps = Omit<ComponentProps<"div">, "children"> & {
|
|
13
|
+
/** The button's text. It can change with `open` ("Download options" / a summary). */
|
|
14
|
+
summary: ReactNode;
|
|
15
|
+
/** The panel, shown while open. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
/** Whether it starts open (default false). Ignored when `open` is set. */
|
|
18
|
+
defaultOpen?: boolean | undefined;
|
|
19
|
+
/** Controls the state from outside; pair it with `onOpenChange`. */
|
|
20
|
+
open?: boolean | undefined;
|
|
21
|
+
/** Called with the new state when the button is pressed. */
|
|
22
|
+
onOpenChange?: ((open: boolean) => void) | undefined;
|
|
23
|
+
/** Classes for the button, merged last. */
|
|
24
|
+
buttonClassName?: string | undefined;
|
|
25
|
+
/** Classes for the panel, merged last. */
|
|
26
|
+
panelClassName?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* @function Disclosure
|
|
30
|
+
* @param props {DisclosureProps} the summary, the panel as children, the open state, and native
|
|
31
|
+
* div props for the wrapper
|
|
32
|
+
* @returns {JSX.Element} the toggle button (with a ▾ / ▴ arrow) and the panel it controls
|
|
33
|
+
*/
|
|
34
|
+
export declare function Disclosure({ summary, children, defaultOpen, open, onOpenChange, buttonClassName, panelClassName, className, ...props }: DisclosureProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Disclosure.tsx
|
|
3
|
+
* @desc A button that shows and hides a panel below it (the packs download options): the button
|
|
4
|
+
* carries aria-expanded and aria-controls, and the closed panel stays in the page, hidden,
|
|
5
|
+
* so its form fields keep their values. Uncontrolled by default, or controlled with `open`.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
"use client";
|
|
11
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { useId, useState } from "react";
|
|
13
|
+
import { cx } from "../../utils/cx.js";
|
|
14
|
+
/**
|
|
15
|
+
* @function Disclosure
|
|
16
|
+
* @param props {DisclosureProps} the summary, the panel as children, the open state, and native
|
|
17
|
+
* div props for the wrapper
|
|
18
|
+
* @returns {JSX.Element} the toggle button (with a ▾ / ▴ arrow) and the panel it controls
|
|
19
|
+
*/
|
|
20
|
+
export function Disclosure({ summary, children, defaultOpen = false, open, onOpenChange, buttonClassName, panelClassName, className, ...props }) {
|
|
21
|
+
const panelId = useId();
|
|
22
|
+
const [inner, setInner] = useState(defaultOpen);
|
|
23
|
+
const isOpen = open ?? inner;
|
|
24
|
+
const toggle = () => {
|
|
25
|
+
if (open === undefined)
|
|
26
|
+
setInner(!isOpen);
|
|
27
|
+
onOpenChange?.(!isOpen);
|
|
28
|
+
};
|
|
29
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-2", className), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": isOpen, "aria-controls": panelId, onClick: toggle, className: cx("inline-flex items-center gap-1 self-start font-bold text-c2 text-sm transition-colors hover:text-c1", buttonClassName), children: [summary, _jsx("span", { "aria-hidden": "true", children: isOpen ? "▴" : "▾" })] }), _jsx("div", { id: panelId, hidden: !isOpen, className: panelClassName, children: children })] }));
|
|
30
|
+
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* status region that mounts with its text already inside may not be announced.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentProps } from "react";
|
|
12
|
+
/** The notice's color, and its live role: `info` is c3, `warning` amber, `error` rose. */
|
|
12
13
|
export type NoticeTone = "info" | "warning" | "error";
|
|
13
14
|
/** Every native `<p>` prop (including `ref`), plus a tone, a live flag and the element to render. */
|
|
14
15
|
export type NoticeProps = ComponentProps<"p"> & {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Tabs.tsx
|
|
3
|
+
* @desc A tab list for panels on the same page (not links; LinkTabs is for those): buttons with
|
|
4
|
+
* role="tab", the chosen one aria-selected and the only one in the Tab order. Left and Right
|
|
5
|
+
* (wrapping), Home and End pick and focus a tab. The panels are the caller's: give each
|
|
6
|
+
* `id={tabPanelId(idPrefix, tab)}`, role="tabpanel" and
|
|
7
|
+
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
13
|
+
/** One tab: its id (part of the tab's and panel's ids) and its text. */
|
|
14
|
+
export type TabItem<T extends string = string> = {
|
|
15
|
+
id: T;
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
/** Every native `<div>` prop except `onChange` and children, plus the list's parts. */
|
|
19
|
+
export type TabsProps<T extends string = string> = Omit<ComponentProps<"div">, "onChange" | "children"> & {
|
|
20
|
+
/** Names the tab list. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Prefix for the tabs' and panels' ids (tabId, tabPanelId). */
|
|
23
|
+
idPrefix: string;
|
|
24
|
+
/** The tabs, left to right. */
|
|
25
|
+
tabs: readonly TabItem<T>[];
|
|
26
|
+
/** The chosen tab's id. */
|
|
27
|
+
value: T;
|
|
28
|
+
/** Called with the picked tab's id. */
|
|
29
|
+
onChange: (id: T) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* @function Tabs
|
|
33
|
+
* @param props {TabsProps<T>} the list's name, id prefix, tabs, chosen tab and handler, and
|
|
34
|
+
* native div props
|
|
35
|
+
* @returns {JSX.Element} the tablist with one pill button per tab
|
|
36
|
+
*/
|
|
37
|
+
export declare function Tabs<T extends string>({ label, idPrefix, tabs, value, onChange, className, onKeyDown, ...props }: TabsProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Tabs.tsx
|
|
3
|
+
* @desc A tab list for panels on the same page (not links; LinkTabs is for those): buttons with
|
|
4
|
+
* role="tab", the chosen one aria-selected and the only one in the Tab order. Left and Right
|
|
5
|
+
* (wrapping), Home and End pick and focus a tab. The panels are the caller's: give each
|
|
6
|
+
* `id={tabPanelId(idPrefix, tab)}`, role="tabpanel" and
|
|
7
|
+
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import { cx } from "../../utils/cx.js";
|
|
15
|
+
import { tabId, tabPanelId } from "./tabIds.js";
|
|
16
|
+
const KEYS = {
|
|
17
|
+
ArrowRight: (at, count) => (at + 1) % count,
|
|
18
|
+
ArrowLeft: (at, count) => (at - 1 + count) % count,
|
|
19
|
+
Home: () => 0,
|
|
20
|
+
End: (_at, count) => count - 1,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @function Tabs
|
|
24
|
+
* @param props {TabsProps<T>} the list's name, id prefix, tabs, chosen tab and handler, and
|
|
25
|
+
* native div props
|
|
26
|
+
* @returns {JSX.Element} the tablist with one pill button per tab
|
|
27
|
+
*/
|
|
28
|
+
export function Tabs({ label, idPrefix, tabs, value, onChange, className, onKeyDown, ...props }) {
|
|
29
|
+
// With no tab chosen, the first one is still reachable with Tab.
|
|
30
|
+
const at = Math.max(0, tabs.findIndex((item) => item.id === value));
|
|
31
|
+
const move = (event) => {
|
|
32
|
+
onKeyDown?.(event);
|
|
33
|
+
const step = KEYS[event.key];
|
|
34
|
+
if (!step || tabs.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const tab = tabs[step(at, tabs.length)];
|
|
37
|
+
if (!tab)
|
|
38
|
+
return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
onChange(tab.id);
|
|
41
|
+
document.getElementById(tabId(idPrefix, tab.id))?.focus();
|
|
42
|
+
};
|
|
43
|
+
return (_jsx("div", { role: "tablist", "aria-label": label, onKeyDown: move, className: cx("flex gap-1 rounded-full bg-b4 p-1", className), ...props, children: tabs.map((tab, index) => {
|
|
44
|
+
const selected = tab.id === value;
|
|
45
|
+
return (_jsx("button", { type: "button", role: "tab", id: tabId(idPrefix, tab.id), "aria-selected": selected, "aria-controls": tabPanelId(idPrefix, tab.id), tabIndex: index === at ? 0 : -1, onClick: () => onChange(tab.id), className: cx("flex-1 rounded-full px-4 py-1.5 font-bold text-sm transition-colors", selected ? "bg-h2 text-c1 forced-colors:underline" : "text-c3 hover:text-c1"), children: tab.label }, tab.id));
|
|
46
|
+
}) }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/TextLink.tsx
|
|
3
|
+
* @desc A text link: next/link inside the app, a plain <a> off-site (with rel="noreferrer" in a
|
|
4
|
+
* new tab), in the `accent` look for running text or the `plain` one for names in lists.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Mon Sep 28, 2026
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { type AutoLinkProps } from "./AutoLink.js";
|
|
10
|
+
import { type TextLinkVariant } from "./linkStyles.js";
|
|
11
|
+
/** Every next/link prop (including `ref`), plus the link's look. */
|
|
12
|
+
export type TextLinkProps = AutoLinkProps & {
|
|
13
|
+
/** "accent" (default) for running text, "plain" for a name or title in a list or table. */
|
|
14
|
+
variant?: TextLinkVariant | undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @function TextLink
|
|
18
|
+
* @param props {TextLinkProps} next/link props, plus an optional variant
|
|
19
|
+
* @returns {JSX.Element} a next/link, or a plain `<a>` for an external href, styled as a text link
|
|
20
|
+
*/
|
|
21
|
+
export declare function TextLink({ variant, className, ...props }: TextLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/basics/TextLink.tsx
|
|
4
|
+
* @desc A text link: next/link inside the app, a plain <a> off-site (with rel="noreferrer" in a
|
|
5
|
+
* new tab), in the `accent` look for running text or the `plain` one for names in lists.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
import { AutoLink } from "./AutoLink.js";
|
|
11
|
+
import { linkClasses } from "./linkStyles.js";
|
|
12
|
+
/**
|
|
13
|
+
* @function TextLink
|
|
14
|
+
* @param props {TextLinkProps} next/link props, plus an optional variant
|
|
15
|
+
* @returns {JSX.Element} a next/link, or a plain `<a>` for an external href, styled as a text link
|
|
16
|
+
*/
|
|
17
|
+
export function TextLink({ variant, className, ...props }) {
|
|
18
|
+
return _jsx(AutoLink, { className: linkClasses({ variant, className }), ...props });
|
|
19
|
+
}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* @desc Shared class builder for Button and ButtonLink (osu!-web pill buttons).
|
|
4
4
|
* @author David @dvhsh (https://dvh.sh)
|
|
5
5
|
* @created Wed Sep 23, 2026
|
|
6
|
-
* @modified
|
|
6
|
+
* @modified Mon Sep 28, 2026
|
|
7
7
|
*/
|
|
8
|
+
/** The pill's colors: `primary` is h2 (h1 on hover), `secondary` b3, `ghost` transparent. */
|
|
8
9
|
export type ButtonVariant = "primary" | "secondary" | "ghost";
|
|
10
|
+
/** The pill's height, padding and text size: `md` is h-9 and text-sm, `lg` h-11 and text-base. */
|
|
9
11
|
export type ButtonSize = "md" | "lg";
|
|
10
12
|
/** Options for {@link buttonClasses}. */
|
|
11
13
|
export type ButtonClassOptions = {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @desc Shared class builder for Button and ButtonLink (osu!-web pill buttons).
|
|
4
4
|
* @author David @dvhsh (https://dvh.sh)
|
|
5
5
|
* @created Wed Sep 23, 2026
|
|
6
|
-
* @modified
|
|
6
|
+
* @modified Mon Sep 28, 2026
|
|
7
7
|
*/
|
|
8
8
|
import { cx } from "../../utils/cx.js";
|
|
9
9
|
const BASE = "inline-flex items-center justify-center gap-2 rounded-full font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-h1 disabled:cursor-not-allowed disabled:opacity-50";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/cardStyles.ts
|
|
3
|
+
* @desc The osu!-web panel look that Card and FilterPanel share: the rounded b4 surface and its
|
|
4
|
+
* title, plus the heading levels either one takes for that title.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Mon Sep 28, 2026
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
|
+
*/
|
|
9
|
+
/** A heading level for a component's title: `h2` to `h6` (a page has one `h1`). */
|
|
10
|
+
export type HeadingLevel = 2 | 3 | 4 | 5 | 6;
|
|
11
|
+
/** The panel: rounded, b4 background, p-5, c2 text. */
|
|
12
|
+
export declare const CARD = "rounded-[10px] bg-b4 p-5 text-c2";
|
|
13
|
+
/** The panel's title: bold c1 at text-lg. */
|
|
14
|
+
export declare const CARD_HEADING = "font-bold text-c1 text-lg";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/cardStyles.ts
|
|
3
|
+
* @desc The osu!-web panel look that Card and FilterPanel share: the rounded b4 surface and its
|
|
4
|
+
* title, plus the heading levels either one takes for that title.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Mon Sep 28, 2026
|
|
7
|
+
* @modified Mon Sep 28, 2026
|
|
8
|
+
*/
|
|
9
|
+
/** The panel: rounded, b4 background, p-5, c2 text. */
|
|
10
|
+
export const CARD = "rounded-[10px] bg-b4 p-5 text-c2";
|
|
11
|
+
/** The panel's title: bold c1 at text-lg. */
|
|
12
|
+
export const CARD_HEADING = "font-bold text-c1 text-lg";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/linkStyles.ts
|
|
3
|
+
* @desc Class builder for text links, shared by TextLink and any element that should look like
|
|
4
|
+
* one: `accent` for links in running text (h1, underlined, like Prose), `plain` for names
|
|
5
|
+
* and titles in lists and tables (bold c1, underlined on hover).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
/** A text link's look: `accent` in running text, `plain` for a name or title in a list. */
|
|
11
|
+
export type TextLinkVariant = "accent" | "plain";
|
|
12
|
+
/** Options for {@link linkClasses}. */
|
|
13
|
+
export type LinkClassOptions = {
|
|
14
|
+
variant?: TextLinkVariant | undefined;
|
|
15
|
+
className?: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function linkClasses
|
|
19
|
+
* @param opts {LinkClassOptions} variant (default "accent") and extra classes
|
|
20
|
+
* @returns {string} the text link classes, with the caller's className appended last (it
|
|
21
|
+
* replaces a built-in class that sets the same property)
|
|
22
|
+
*/
|
|
23
|
+
export declare const linkClasses: ({ variant, className }?: LinkClassOptions) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/linkStyles.ts
|
|
3
|
+
* @desc Class builder for text links, shared by TextLink and any element that should look like
|
|
4
|
+
* one: `accent` for links in running text (h1, underlined, like Prose), `plain` for names
|
|
5
|
+
* and titles in lists and tables (bold c1, underlined on hover).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
import { cx } from "../../utils/cx.js";
|
|
11
|
+
// The accent link is underlined at rest, so it doesn't rely on color alone (WCAG 1.4.1). A plain
|
|
12
|
+
// link sits where the context says it's a link (a list of names), and underlines on hover.
|
|
13
|
+
const VARIANTS = {
|
|
14
|
+
accent: "text-h1 underline underline-offset-2 transition-colors hover:text-c1",
|
|
15
|
+
plain: "font-bold text-c1 underline-offset-2 hover:underline",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function linkClasses
|
|
19
|
+
* @param opts {LinkClassOptions} variant (default "accent") and extra classes
|
|
20
|
+
* @returns {string} the text link classes, with the caller's className appended last (it
|
|
21
|
+
* replaces a built-in class that sets the same property)
|
|
22
|
+
*/
|
|
23
|
+
export const linkClasses = ({ variant = "accent", className } = {}) => cx(VARIANTS[variant], className);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/tabIds.ts
|
|
3
|
+
* @desc The ids that tie a Tabs tab to its panel: the tab button is `<prefix>-tab-<tab>` and its
|
|
4
|
+
* panel `<prefix>-panel-<tab>`. Pure and server-safe, so a server page can give its panels
|
|
5
|
+
* the ids. Moved from bb.haruhime.moe (src/utils/tabs.ts).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function tabId
|
|
12
|
+
* @param prefix {string} the tab list's id prefix
|
|
13
|
+
* @param tab {string} the tab
|
|
14
|
+
* @returns {string} the tab button's id
|
|
15
|
+
*/
|
|
16
|
+
export declare const tabId: (prefix: string, tab: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* @function tabPanelId
|
|
19
|
+
* @param prefix {string} the tab list's id prefix
|
|
20
|
+
* @param tab {string} the tab
|
|
21
|
+
* @returns {string} its panel's id
|
|
22
|
+
*/
|
|
23
|
+
export declare const tabPanelId: (prefix: string, tab: string) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/tabIds.ts
|
|
3
|
+
* @desc The ids that tie a Tabs tab to its panel: the tab button is `<prefix>-tab-<tab>` and its
|
|
4
|
+
* panel `<prefix>-panel-<tab>`. Pure and server-safe, so a server page can give its panels
|
|
5
|
+
* the ids. Moved from bb.haruhime.moe (src/utils/tabs.ts).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function tabId
|
|
12
|
+
* @param prefix {string} the tab list's id prefix
|
|
13
|
+
* @param tab {string} the tab
|
|
14
|
+
* @returns {string} the tab button's id
|
|
15
|
+
*/
|
|
16
|
+
export const tabId = (prefix, tab) => `${prefix}-tab-${tab}`;
|
|
17
|
+
/**
|
|
18
|
+
* @function tabPanelId
|
|
19
|
+
* @param prefix {string} the tab list's id prefix
|
|
20
|
+
* @param tab {string} the tab
|
|
21
|
+
* @returns {string} its panel's id
|
|
22
|
+
*/
|
|
23
|
+
export const tabPanelId = (prefix, tab) => `${prefix}-panel-${tab}`;
|
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
* @file src/components/filters/Chip.tsx
|
|
3
3
|
* @desc Toggle pill (osu! beatmap listing style): a `<button>` with `aria-pressed`, pink when on.
|
|
4
4
|
* Same look as the mod chips on packs.haruhime.moe. In forced-colors mode a pressed chip
|
|
5
|
-
* takes the system highlight colors, so on and off still look different.
|
|
5
|
+
* takes the system highlight colors, so on and off still look different. With
|
|
6
|
+
* `unavailableReason` it is blocked but stays focusable (aria-disabled), and screen readers
|
|
7
|
+
* hear the reason as its description.
|
|
6
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
9
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
9
11
|
*/
|
|
10
|
-
import type
|
|
12
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
11
13
|
/** Every native `<button>` prop (including `ref`), plus the pressed state and its handler. */
|
|
12
14
|
export type ChipProps = Omit<ComponentProps<"button">, "aria-pressed"> & {
|
|
13
15
|
/** Whether the chip is on. */
|
|
14
16
|
pressed: boolean;
|
|
15
17
|
/** Called with the new state when the chip is clicked (or toggled with Enter or Space). */
|
|
16
18
|
onPressedChange?: ((pressed: boolean) => void) | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Why the chip can't be pressed right now (EZ with HR picked). The chip is blocked but stays
|
|
21
|
+
* in the tab order (aria-disabled), and the reason is its description (and its title).
|
|
22
|
+
*/
|
|
23
|
+
unavailableReason?: ReactNode;
|
|
17
24
|
};
|
|
18
25
|
/**
|
|
19
26
|
* @function Chip
|
|
20
27
|
* @param props {ChipProps} native button props, plus `pressed` and an optional `onPressedChange`.
|
|
21
28
|
* A caller's `onClick` runs first; calling `event.preventDefault()` in it skips the toggle.
|
|
22
29
|
* Callbacks can only be passed from client code.
|
|
23
|
-
* @returns {JSX.Element} a `<button type="button" aria-pressed>` styled as a pill
|
|
30
|
+
* @returns {JSX.Element} a `<button type="button" aria-pressed>` styled as a pill, and the
|
|
31
|
+
* screen-reader-only reason beside it when it is unavailable
|
|
24
32
|
*/
|
|
25
|
-
export declare function Chip({ pressed, onPressedChange, onClick, className, type, ...props }: ChipProps): import("react").JSX.Element;
|
|
33
|
+
export declare function Chip({ pressed, onPressedChange, unavailableReason, onClick, className, type, title, "aria-describedby": describedBy, ...props }: ChipProps): import("react").JSX.Element;
|