@haruhimemoe/ui 0.3.0 → 0.4.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 +42 -1
- package/README.md +252 -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/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/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/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/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/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 +23 -2
- package/dist/index.js +27 -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,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,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);
|
|
@@ -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;
|
|
@@ -2,28 +2,37 @@
|
|
|
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
12
|
"use client";
|
|
11
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { useId } from "react";
|
|
12
15
|
import { cx } from "../../utils/cx.js";
|
|
13
|
-
|
|
14
|
-
const PRESSED = "bg-h1 text-b6 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]";
|
|
15
|
-
const UNPRESSED = "bg-b3 text-c2 not-disabled:hover:bg-b2";
|
|
16
|
+
import { CHIP, CHIP_OFF, CHIP_ON, CHIP_UNAVAILABLE } from "./chipStyles.js";
|
|
16
17
|
/**
|
|
17
18
|
* @function Chip
|
|
18
19
|
* @param props {ChipProps} native button props, plus `pressed` and an optional `onPressedChange`.
|
|
19
20
|
* A caller's `onClick` runs first; calling `event.preventDefault()` in it skips the toggle.
|
|
20
21
|
* Callbacks can only be passed from client code.
|
|
21
|
-
* @returns {JSX.Element} a `<button type="button" aria-pressed>` styled as a pill
|
|
22
|
+
* @returns {JSX.Element} a `<button type="button" aria-pressed>` styled as a pill, and the
|
|
23
|
+
* screen-reader-only reason beside it when it is unavailable
|
|
22
24
|
*/
|
|
23
|
-
export function Chip({ pressed, onPressedChange, onClick, className, type = "button", ...props }) {
|
|
24
|
-
|
|
25
|
+
export function Chip({ pressed, onPressedChange, unavailableReason, onClick, className, type = "button", title, "aria-describedby": describedBy, ...props }) {
|
|
26
|
+
const reasonId = useId();
|
|
27
|
+
const blocked = unavailableReason !== undefined && unavailableReason !== null;
|
|
28
|
+
const reasonTitle = blocked && typeof unavailableReason === "string" ? unavailableReason : undefined;
|
|
29
|
+
const chip = (_jsx("button", { type: type, "aria-pressed": pressed, "aria-disabled": blocked || undefined, "aria-describedby": blocked ? [reasonId, describedBy].filter(Boolean).join(" ") : describedBy, title: title ?? reasonTitle, onClick: (event) => {
|
|
25
30
|
onClick?.(event);
|
|
26
|
-
if (!event.defaultPrevented)
|
|
31
|
+
if (!event.defaultPrevented && !blocked)
|
|
27
32
|
onPressedChange?.(!pressed);
|
|
28
|
-
}, className: cx(
|
|
33
|
+
}, className: cx(CHIP, "disabled:cursor-not-allowed disabled:opacity-40", pressed ? CHIP_ON : CHIP_OFF, blocked ? CHIP_UNAVAILABLE : !pressed && "not-disabled:hover:bg-b2", className), ...props }));
|
|
34
|
+
if (!blocked)
|
|
35
|
+
return chip;
|
|
36
|
+
// The reason sits beside the button, not inside it, where it would join the button's name.
|
|
37
|
+
return (_jsxs(_Fragment, { children: [chip, _jsx("span", { id: reasonId, className: "sr-only", children: unavailableReason })] }));
|
|
29
38
|
}
|
|
@@ -4,24 +4,23 @@
|
|
|
4
4
|
* in the tab order; the group reports the picked values in the options' order.
|
|
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
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
|
+
import { type GroupFrameProps } from "./GroupFrame.js";
|
|
10
11
|
/** One chip: the value it stands for, what it shows, and whether it can be toggled. */
|
|
11
12
|
export type ChipOption = {
|
|
12
13
|
value: string;
|
|
13
14
|
label: ReactNode;
|
|
14
15
|
disabled?: boolean | undefined;
|
|
16
|
+
/** Why the chip can't be picked right now; it stays focusable. See Chip. */
|
|
17
|
+
unavailableReason?: ReactNode;
|
|
15
18
|
};
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Leave the name to a surrounding FilterRow: no label shows, and the fieldset is not a group
|
|
22
|
-
* of its own (role none), so screen readers hear the row's name once.
|
|
23
|
-
*/
|
|
24
|
-
hideLabel?: boolean | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Every native `<fieldset>` prop except `onChange` and `children`, plus `label` (names the group)
|
|
21
|
+
* and `hideLabel` (leave the name to a surrounding FilterRow), the options and the picked values.
|
|
22
|
+
*/
|
|
23
|
+
export type ChipGroupProps = Omit<GroupFrameProps, "onChange" | "children"> & {
|
|
25
24
|
options: readonly ChipOption[];
|
|
26
25
|
/** The picked values. */
|
|
27
26
|
value: readonly string[];
|
|
@@ -36,4 +35,4 @@ export type ChipGroupProps = Omit<ComponentProps<"fieldset">, "onChange" | "chil
|
|
|
36
35
|
* @returns {JSX.Element} a `<fieldset>` (role group) labeled by its label, one Chip per option.
|
|
37
36
|
* With `hideLabel`, the fieldset has role none and no label.
|
|
38
37
|
*/
|
|
39
|
-
export declare function ChipGroup({
|
|
38
|
+
export declare function ChipGroup({ options, value, onChange, ...props }: ChipGroupProps): import("react").JSX.Element;
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
* in the tab order; the group reports the picked values in the options' order.
|
|
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
|
"use client";
|
|
10
|
-
import { jsx as _jsx
|
|
11
|
-
import { useId } from "react";
|
|
12
|
-
import { cx } from "../../utils/cx.js";
|
|
10
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
11
|
import { Chip } from "./Chip.js";
|
|
12
|
+
import { GroupFrame } from "./GroupFrame.js";
|
|
14
13
|
/**
|
|
15
14
|
* @function toggleValue
|
|
16
15
|
* @param value {readonly string[]} the picked values
|
|
@@ -35,10 +34,6 @@ const toggleValue = (value, option, on, options) => {
|
|
|
35
34
|
* @returns {JSX.Element} a `<fieldset>` (role group) labeled by its label, one Chip per option.
|
|
36
35
|
* With `hideLabel`, the fieldset has role none and no label.
|
|
37
36
|
*/
|
|
38
|
-
export function ChipGroup({
|
|
39
|
-
|
|
40
|
-
return (_jsxs("fieldset", {
|
|
41
|
-
// Inside a FilterRow (hideLabel), the row's fieldset is the group. A second group with the
|
|
42
|
-
// same name would be read twice. The fieldset stays, so `disabled` still reaches every chip.
|
|
43
|
-
role: hideLabel ? "none" : undefined, "aria-labelledby": hideLabel ? undefined : labelId, className: cx("flex flex-col gap-2", className), ...props, children: [hideLabel ? null : (_jsx("span", { id: labelId, className: "font-bold text-c3 text-sm", children: label })), _jsx("div", { className: "flex flex-wrap items-center gap-1", children: options.map((option) => (_jsx(Chip, { pressed: value.includes(option.value), disabled: option.disabled, onPressedChange: (on) => onChange(toggleValue(value, option.value, on, options)), children: option.label }, option.value))) })] }));
|
|
37
|
+
export function ChipGroup({ options, value, onChange, ...props }) {
|
|
38
|
+
return (_jsx(GroupFrame, { ...props, children: _jsx("div", { className: "flex flex-wrap items-center gap-1", children: options.map((option) => (_jsx(Chip, { pressed: value.includes(option.value), disabled: option.disabled, unavailableReason: option.unavailableReason, onPressedChange: (on) => onChange(toggleValue(value, option.value, on, options)), children: option.label }, option.value))) }) }));
|
|
44
39
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/filters/ChoiceChips.tsx
|
|
3
|
+
* @desc One choice from a few, as a real radio group drawn as chips (the pools status and type
|
|
4
|
+
* filters): native radio inputs under one name, so Tab reaches the checked chip, the arrow
|
|
5
|
+
* keys move and pick, and screen readers hear "radio, 1 of 3, checked". Same look as Chip,
|
|
6
|
+
* with the focus ring on the chip, not the hidden input. A labelled fieldset like ChipGroup.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode } from "react";
|
|
12
|
+
import { type GroupFrameProps } from "./GroupFrame.js";
|
|
13
|
+
/** One choice: the value it stands for, what it shows, and whether it can be picked. */
|
|
14
|
+
export type ChoiceChipOption<T extends string = string> = {
|
|
15
|
+
value: T;
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
disabled?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Every native `<fieldset>` prop except `onChange`, `children` and `defaultValue`, plus `label`
|
|
21
|
+
* and `hideLabel` (as on ChipGroup), the options, the picked value and the radios' name.
|
|
22
|
+
*/
|
|
23
|
+
export type ChoiceChipsProps<T extends string = string> = Omit<GroupFrameProps, "onChange" | "children" | "defaultValue"> & {
|
|
24
|
+
options: readonly ChoiceChipOption<T>[];
|
|
25
|
+
/** The picked value. */
|
|
26
|
+
value: T;
|
|
27
|
+
/** Called with the value of the chip that was picked. */
|
|
28
|
+
onChange: (value: T) => void;
|
|
29
|
+
/** The radios' name, for a form (default: a generated one). */
|
|
30
|
+
name?: string | undefined;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* @function ChoiceChips
|
|
34
|
+
* @param props {ChoiceChipsProps} label, options, the picked value and a change handler, plus
|
|
35
|
+
* native fieldset props (`disabled` turns off every chip). Render it from client code.
|
|
36
|
+
* @returns {JSX.Element} a `<fieldset>` named by its label, with one radio chip per option
|
|
37
|
+
*/
|
|
38
|
+
export declare function ChoiceChips<T extends string = string>({ options, value, onChange, name, ...props }: ChoiceChipsProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/filters/ChoiceChips.tsx
|
|
3
|
+
* @desc One choice from a few, as a real radio group drawn as chips (the pools status and type
|
|
4
|
+
* filters): native radio inputs under one name, so Tab reaches the checked chip, the arrow
|
|
5
|
+
* keys move and pick, and screen readers hear "radio, 1 of 3, checked". Same look as Chip,
|
|
6
|
+
* with the focus ring on the chip, not the hidden input. A labelled fieldset like ChipGroup.
|
|
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useId } from "react";
|
|
14
|
+
import { cx } from "../../utils/cx.js";
|
|
15
|
+
import { CHIP, CHIP_OFF, CHIP_ON, CHIP_UNAVAILABLE } from "./chipStyles.js";
|
|
16
|
+
import { GroupFrame } from "./GroupFrame.js";
|
|
17
|
+
// A label around a hidden radio: it shows the input's focus and disabled state itself.
|
|
18
|
+
const LABEL = "cursor-pointer has-focus-visible:outline-2 has-focus-visible:outline-h1 has-focus-visible:outline-offset-2 has-disabled:cursor-not-allowed has-disabled:opacity-40";
|
|
19
|
+
/**
|
|
20
|
+
* @function ChoiceChips
|
|
21
|
+
* @param props {ChoiceChipsProps} label, options, the picked value and a change handler, plus
|
|
22
|
+
* native fieldset props (`disabled` turns off every chip). Render it from client code.
|
|
23
|
+
* @returns {JSX.Element} a `<fieldset>` named by its label, with one radio chip per option
|
|
24
|
+
*/
|
|
25
|
+
export function ChoiceChips({ options, value, onChange, name, ...props }) {
|
|
26
|
+
const generated = useId();
|
|
27
|
+
return (_jsx(GroupFrame, { ...props, children: _jsx("div", { className: "flex flex-wrap items-center gap-1", children: options.map((option) => {
|
|
28
|
+
const on = option.value === value;
|
|
29
|
+
return (_jsxs("label", { className: cx(CHIP, LABEL, on ? CHIP_ON : CHIP_OFF, !on && !option.disabled && "hover:bg-b2", option.disabled && CHIP_UNAVAILABLE), children: [_jsx("input", { type: "radio", name: name ?? generated, value: option.value, checked: on, disabled: option.disabled, onChange: () => onChange(option.value), className: "sr-only" }), option.label] }, option.value));
|
|
30
|
+
}) }) }));
|
|
31
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/filters/FilterPanel.tsx
|
|
3
3
|
* @desc Filter panel (osu! beatmap listing style): a titled card of FilterRows with a live result
|
|
4
|
-
* count and a "Clear filters"
|
|
5
|
-
* from `sm` up they are always shown.
|
|
4
|
+
* count and a "Clear filters" button. On phones the rows fold away behind a disclosure
|
|
5
|
+
* button; from `sm` up they are always shown.
|
|
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 { type ComponentProps, type ReactNode } from "react";
|
|
11
|
+
import { type HeadingLevel } from "../basics/cardStyles.js";
|
|
11
12
|
/** Every native `<section>` prop except `title`, plus the panel's title, count and clear action. */
|
|
12
13
|
export type FilterPanelProps = Omit<ComponentProps<"section">, "title"> & {
|
|
13
14
|
/** The panel heading; it also names the panel and its phone disclosure button. */
|
|
14
15
|
title: ReactNode;
|
|
15
16
|
/** Heading level for the title (default 2). */
|
|
16
|
-
headingLevel?:
|
|
17
|
+
headingLevel?: HeadingLevel | undefined;
|
|
17
18
|
/** Shown in an `<output aria-live="polite">`, so screen readers hear each new count. */
|
|
18
19
|
resultCount?: ReactNode;
|
|
19
20
|
/** True while any filter is set; shows the clear button. */
|