@haruhimemoe/ui 0.11.1 → 0.12.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 +36 -1
- package/README.md +64 -6
- package/dist/components/actions/AsyncButton.d.ts +1 -1
- package/dist/components/actions/AsyncButton.js +3 -2
- package/dist/components/basics/AutoLink.d.ts +8 -4
- package/dist/components/basics/AutoLink.js +12 -5
- package/dist/components/basics/Badge.d.ts +1 -1
- package/dist/components/basics/Badge.js +3 -3
- package/dist/components/basics/Disclosure.d.ts +1 -1
- package/dist/components/basics/Disclosure.js +2 -2
- package/dist/components/basics/Notice.d.ts +1 -1
- package/dist/components/basics/Notice.js +6 -6
- package/dist/components/basics/Tabs.d.ts +1 -1
- package/dist/components/basics/Tabs.js +3 -3
- package/dist/components/basics/Text.d.ts +26 -0
- package/dist/components/basics/Text.js +17 -0
- package/dist/components/basics/buttonStyles.d.ts +2 -2
- package/dist/components/basics/buttonStyles.js +9 -6
- package/dist/components/basics/cardStyles.d.ts +4 -3
- package/dist/components/basics/cardStyles.js +4 -3
- package/dist/components/basics/textStyles.d.ts +28 -0
- package/dist/components/basics/textStyles.js +18 -0
- package/dist/components/basics/textTones.d.ts +17 -0
- package/dist/components/basics/textTones.js +24 -0
- package/dist/components/basics/useMotionAllowed.d.ts +16 -0
- package/dist/components/basics/useMotionAllowed.js +41 -0
- package/dist/components/brand/BrandPage.js +2 -1
- package/dist/components/content/ContentNav.js +2 -2
- package/dist/components/content/CopyMarkdownButton.d.ts +3 -1
- package/dist/components/content/CopyMarkdownButton.js +23 -8
- package/dist/components/filters/FilterPanel.d.ts +1 -1
- package/dist/components/filters/FilterPanel.js +2 -2
- package/dist/components/filters/GroupFrame.d.ts +4 -2
- package/dist/components/filters/GroupFrame.js +1 -1
- package/dist/components/filters/chipStyles.d.ts +7 -6
- package/dist/components/filters/chipStyles.js +8 -6
- package/dist/components/forms/CharCounter.d.ts +1 -1
- package/dist/components/forms/CharCounter.js +6 -1
- package/dist/components/forms/Checkbox.d.ts +4 -3
- package/dist/components/forms/Checkbox.js +5 -4
- package/dist/components/forms/FieldFrame.d.ts +9 -4
- package/dist/components/forms/FieldFrame.js +6 -5
- package/dist/components/forms/RadioGroup.d.ts +4 -2
- package/dist/components/forms/RadioGroup.js +4 -4
- package/dist/components/forms/ReportDisclosure.d.ts +1 -1
- package/dist/components/forms/ReportDisclosure.js +2 -2
- package/dist/components/forms/Select.d.ts +4 -4
- package/dist/components/forms/Select.js +4 -4
- package/dist/components/forms/TextInput.d.ts +3 -3
- package/dist/components/forms/TextInput.js +3 -3
- package/dist/components/forms/Textarea.d.ts +6 -4
- package/dist/components/forms/Textarea.js +3 -3
- package/dist/components/forms/TypeToConfirm.d.ts +1 -1
- package/dist/components/forms/TypeToConfirm.js +2 -2
- package/dist/components/forms/fieldStyles.d.ts +5 -3
- package/dist/components/forms/fieldStyles.js +6 -4
- package/dist/components/mdx/CodeBlock.d.ts +1 -1
- package/dist/components/mdx/CodeBlock.js +1 -1
- package/dist/components/mdx/CodeCopyButton.d.ts +1 -1
- package/dist/components/mdx/CodeCopyButton.js +2 -2
- package/dist/components/osu/ModBadge.d.ts +14 -9
- package/dist/components/osu/ModBadge.js +42 -14
- package/dist/components/palette/CommandPalette.d.ts +1 -1
- package/dist/components/palette/CommandPalette.js +2 -2
- package/dist/components/palette/PaletteInput.d.ts +1 -1
- package/dist/components/palette/PaletteInput.js +3 -2
- package/dist/components/palette/PaletteRow.d.ts +1 -1
- package/dist/components/palette/PaletteRow.js +3 -3
- package/dist/components/shell/HeaderMenu.d.ts +1 -1
- package/dist/components/shell/HeaderMenu.js +3 -3
- package/dist/components/shell/LinkTabs.d.ts +1 -1
- package/dist/components/shell/LinkTabs.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/theme.css +56 -6
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/textStyles.ts
|
|
3
|
+
* @desc textClasses: Text's look as a class string, for elements Text can't be (an <output>, a
|
|
4
|
+
* <time>, a <li>, a class map). Server-safe.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Sun Oct 4, 2026
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { cx } from "../../utils/cx.js";
|
|
10
|
+
import { TEXT_SIZES, TEXT_TONES } from "./textTones.js";
|
|
11
|
+
/**
|
|
12
|
+
* @function textClasses
|
|
13
|
+
* @param opts {TextClassOptions} tone (default "default"), size (default "sm"), bold and extra
|
|
14
|
+
* classes
|
|
15
|
+
* @returns {string} the text classes, with the caller's className last (it replaces a built-in
|
|
16
|
+
* class that sets the same property)
|
|
17
|
+
*/
|
|
18
|
+
export const textClasses = ({ tone = "default", size = "sm", bold = false, className, } = {}) => cx(TEXT_TONES[tone], TEXT_SIZES[size], bold && "font-bold", className);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/textTones.ts
|
|
3
|
+
* @desc Text's tones and sizes as finished class strings (internal, no cx), so a client file
|
|
4
|
+
* that must not load tailwind-merge (PaletteInput) can still use the error tone. Status
|
|
5
|
+
* tones get one lighter step under prefers-contrast: more.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
/** Text's color: c2 body, c3 muted, c4 subtle, or a status color. */
|
|
11
|
+
export type TextTone = "default" | "muted" | "subtle" | "error" | "warning" | "success";
|
|
12
|
+
/** Text's size: text-xs, text-sm (the default) or text-base. */
|
|
13
|
+
export type TextSize = "xs" | "sm" | "base";
|
|
14
|
+
/** The class for each tone. Every tone clears 4.5:1 on b4, b5 and b6 at text-sm. */
|
|
15
|
+
export declare const TEXT_TONES: Readonly<Record<TextTone, string>>;
|
|
16
|
+
/** The class for each size. */
|
|
17
|
+
export declare const TEXT_SIZES: Readonly<Record<TextSize, string>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/textTones.ts
|
|
3
|
+
* @desc Text's tones and sizes as finished class strings (internal, no cx), so a client file
|
|
4
|
+
* that must not load tailwind-merge (PaletteInput) can still use the error tone. Status
|
|
5
|
+
* tones get one lighter step under prefers-contrast: more.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
/** The class for each tone. Every tone clears 4.5:1 on b4, b5 and b6 at text-sm. */
|
|
11
|
+
export const TEXT_TONES = {
|
|
12
|
+
default: "text-c2",
|
|
13
|
+
muted: "text-c3",
|
|
14
|
+
subtle: "text-c4",
|
|
15
|
+
error: "text-rose-300 contrast-more:text-rose-200",
|
|
16
|
+
warning: "text-amber-300 contrast-more:text-amber-200",
|
|
17
|
+
success: "text-emerald-300 contrast-more:text-emerald-200",
|
|
18
|
+
};
|
|
19
|
+
/** The class for each size. */
|
|
20
|
+
export const TEXT_SIZES = {
|
|
21
|
+
xs: "text-xs",
|
|
22
|
+
sm: "text-sm",
|
|
23
|
+
base: "text-base",
|
|
24
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/useMotionAllowed.ts
|
|
3
|
+
* @desc Whether the visitor allows motion, for motion JavaScript drives (an autoplay video, a
|
|
4
|
+
* canvas, element.animate()). CSS motion is already covered by theme.css's reduced-motion
|
|
5
|
+
* rule. False on the server, during hydration, without matchMedia, and while the visitor
|
|
6
|
+
* asks for reduced motion; it follows the setting live.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sun Oct 4, 2026
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @function useMotionAllowed
|
|
13
|
+
* @returns {boolean} true when the visitor allows motion; false on the server, during hydration,
|
|
14
|
+
* without matchMedia, and under prefers-reduced-motion: reduce
|
|
15
|
+
*/
|
|
16
|
+
export declare function useMotionAllowed(): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/useMotionAllowed.ts
|
|
3
|
+
* @desc Whether the visitor allows motion, for motion JavaScript drives (an autoplay video, a
|
|
4
|
+
* canvas, element.animate()). CSS motion is already covered by theme.css's reduced-motion
|
|
5
|
+
* rule. False on the server, during hydration, without matchMedia, and while the visitor
|
|
6
|
+
* asks for reduced motion; it follows the setting live.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sun Oct 4, 2026
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { useSyncExternalStore } from "react";
|
|
13
|
+
const QUERY = "(prefers-reduced-motion: reduce)";
|
|
14
|
+
const reducedMotionQuery = () => typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
15
|
+
? window.matchMedia(QUERY)
|
|
16
|
+
: null;
|
|
17
|
+
const subscribe = (onChange) => {
|
|
18
|
+
const list = reducedMotionQuery();
|
|
19
|
+
if (!list)
|
|
20
|
+
return () => { };
|
|
21
|
+
if (typeof list.addEventListener === "function") {
|
|
22
|
+
list.addEventListener("change", onChange);
|
|
23
|
+
return () => list.removeEventListener("change", onChange);
|
|
24
|
+
}
|
|
25
|
+
// Safari before 14 has only the older addListener on a MediaQueryList.
|
|
26
|
+
list.addListener(onChange);
|
|
27
|
+
return () => list.removeListener(onChange);
|
|
28
|
+
};
|
|
29
|
+
const getSnapshot = () => {
|
|
30
|
+
const list = reducedMotionQuery();
|
|
31
|
+
return list ? !list.matches : false;
|
|
32
|
+
};
|
|
33
|
+
const getServerSnapshot = () => false;
|
|
34
|
+
/**
|
|
35
|
+
* @function useMotionAllowed
|
|
36
|
+
* @returns {boolean} true when the visitor allows motion; false on the server, during hydration,
|
|
37
|
+
* without matchMedia, and under prefers-reduced-motion: reduce
|
|
38
|
+
*/
|
|
39
|
+
export function useMotionAllowed() {
|
|
40
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
41
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Card } from "../basics/Card.js";
|
|
3
3
|
import { linkClasses } from "../basics/linkStyles.js";
|
|
4
|
+
import { TextLink } from "../basics/TextLink.js";
|
|
4
5
|
import { BrandSwatch } from "./BrandSwatch.js";
|
|
5
6
|
const DEFAULT_FONTS = [
|
|
6
7
|
{ name: "Nunito", usage: "Wordmarks and headings" },
|
|
@@ -13,5 +14,5 @@ const IMAGE = /\.(svg|png|jpe?g|webp)$/i;
|
|
|
13
14
|
* @returns {JSX.Element} the brand page's sections, in order
|
|
14
15
|
*/
|
|
15
16
|
export function BrandPage({ name, tagline, url, writing, dos, donts, palette, assets, contact, familyHref, fonts = DEFAULT_FONTS, slots, }) {
|
|
16
|
-
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { title: "Name", children: [_jsx("p", { className: "font-bold text-c1 text-xl", children: name }), _jsx("p", { children: tagline }), _jsx("p", { children: writing }), _jsx("p", { children: _jsx("a", { className: LINK, href: url, children: url }) })] }), _jsx(Card, { title: "Logo", children: _jsx("ul", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: assets.map((asset) => (_jsxs("li", { className: "flex flex-col gap-2", children: [IMAGE.test(asset.href) ? (_jsx("div", { className: `flex h-32 items-center justify-center rounded-md p-4 ${asset.dark ? "bg-b6" : "bg-c1"}`, children: _jsx("img", { src: asset.href, alt: "", className: "max-h-full max-w-full" }) })) : null, _jsx(
|
|
17
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { title: "Name", children: [_jsx("p", { className: "font-bold text-c1 text-xl", children: name }), _jsx("p", { children: tagline }), _jsx("p", { children: writing }), _jsx("p", { children: _jsx("a", { className: LINK, href: url, children: url }) })] }), _jsx(Card, { title: "Logo", children: _jsx("ul", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: assets.map((asset) => (_jsxs("li", { className: "flex flex-col gap-2", children: [IMAGE.test(asset.href) ? (_jsx("div", { className: `flex h-32 items-center justify-center rounded-md p-4 ${asset.dark ? "bg-b6" : "bg-c1"}`, children: _jsx("img", { src: asset.href, alt: "", className: "max-h-full max-w-full" }) })) : null, _jsx(TextLink, { href: asset.href, download: true, children: asset.label })] }, asset.href))) }) }), slots?.afterLogo, _jsx(Card, { title: "Colors", children: _jsx("ul", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", children: Object.entries(palette).map(([token, hex]) => (_jsx("li", { children: _jsx(BrandSwatch, { token: token, hex: hex }) }, token))) }) }), slots?.afterColors, _jsx(Card, { title: "Type", children: _jsx("ul", { className: "flex flex-col gap-1", children: fonts.map((font) => (_jsxs("li", { children: [_jsx("span", { className: "font-bold text-c1", children: font.name }), ": ", font.usage] }, font.name))) }) }), _jsx(Card, { title: "Do's and don'ts", children: _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("h3", { className: "mb-1 font-bold text-c1", children: "Do" }), _jsx("ul", { className: "list-disc pl-5", children: dos.map((item) => (_jsx("li", { children: item }, item))) })] }), _jsxs("div", { children: [_jsx("h3", { className: "mb-1 font-bold text-c1", children: "Don't" }), _jsx("ul", { className: "list-disc pl-5", children: donts.map((item) => (_jsx("li", { children: item }, item))) })] })] }) }), _jsx(Card, { title: "osu!", children: _jsx("p", { children: "Not affiliated with osu! or ppy. osu! is a trademark of ppy Pty Ltd." }) }), familyHref ? (_jsx(Card, { title: "Family", children: _jsx("a", { className: LINK, href: familyHref, children: "Part of the haruhime.moe family." }) })) : null, _jsx(Card, { title: "Contact", children: _jsx("a", { className: LINK, href: `mailto:${contact}`, children: contact }) }), slots?.end] }));
|
|
17
18
|
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
14
|
import Link from "next/link.js";
|
|
15
15
|
import { usePathname } from "next/navigation.js";
|
|
16
|
-
const LINK_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 text-c3";
|
|
17
|
-
const CURRENT_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 bg-b4 font-bold text-c1";
|
|
16
|
+
const LINK_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 text-c3 coarse:py-2.5";
|
|
17
|
+
const CURRENT_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 bg-b4 font-bold text-c1 coarse:py-2.5";
|
|
18
18
|
/**
|
|
19
19
|
* @function ContentNav
|
|
20
20
|
* @param props {ContentNavProps} the nav's accessible name, the index link, and the groups
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* @file src/components/content/CopyMarkdownButton.tsx
|
|
3
3
|
* @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
|
|
4
4
|
* result in an `<output>` beside it (a polite live region), for a doc page's "Copy as
|
|
5
|
-
* Markdown" action.
|
|
5
|
+
* Markdown" action. Where `ClipboardItem` exists, the clipboard write starts inside the click
|
|
6
|
+
* with the fetch as a promised Blob, since Safari rejects a write that starts after an
|
|
7
|
+
* awaited fetch; elsewhere it fetches, then calls `writeText`. Any failure (the fetch rejects, the response isn't ok, or the clipboard
|
|
6
8
|
* refuses) announces the same failure message; the button never throws. Its own client file
|
|
7
9
|
* with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
|
|
8
10
|
* for `ContentPage`, the Server Component that renders it. Not a wrapper around
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* @file src/components/content/CopyMarkdownButton.tsx
|
|
3
3
|
* @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
|
|
4
4
|
* result in an `<output>` beside it (a polite live region), for a doc page's "Copy as
|
|
5
|
-
* Markdown" action.
|
|
5
|
+
* Markdown" action. Where `ClipboardItem` exists, the clipboard write starts inside the click
|
|
6
|
+
* with the fetch as a promised Blob, since Safari rejects a write that starts after an
|
|
7
|
+
* awaited fetch; elsewhere it fetches, then calls `writeText`. Any failure (the fetch rejects, the response isn't ok, or the clipboard
|
|
6
8
|
* refuses) announces the same failure message; the button never throws. Its own client file
|
|
7
9
|
* with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
|
|
8
10
|
* for `ContentPage`, the Server Component that renders it. Not a wrapper around
|
|
@@ -15,8 +17,9 @@
|
|
|
15
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
18
|
import { StatusOutput } from "../actions/StatusOutput.js";
|
|
17
19
|
import { useLatestStatus } from "../actions/useLatestStatus.js";
|
|
18
|
-
// buttonClasses({ variant: "secondary" }) copied as a literal, so this file imports no cx.
|
|
19
|
-
|
|
20
|
+
// buttonClasses({ variant: "secondary" }) copied as a literal, so this file imports no cx. The
|
|
21
|
+
// test pins it to buttonClasses.
|
|
22
|
+
const BUTTON_CLASS_NAME = "inline-flex w-fit 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 forced-colors:border forced-colors:disabled:text-[GrayText] bg-b3 text-c1 not-disabled:hover:bg-b2 contrast-more:inset-ring contrast-more:inset-ring-c4 h-9 px-4 text-sm coarse:h-11";
|
|
20
23
|
/**
|
|
21
24
|
* @function CopyMarkdownButton
|
|
22
25
|
* @param props {CopyMarkdownButtonProps} the Markdown source's URL, plus optional label and
|
|
@@ -28,11 +31,23 @@ export function CopyMarkdownButton({ href, label = "Copy as Markdown", copiedLab
|
|
|
28
31
|
const copy = async () => {
|
|
29
32
|
const run = start();
|
|
30
33
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
35
|
+
// Started synchronously in the click so Safari keeps the user activation.
|
|
36
|
+
const blob = fetch(href).then(async (response) => {
|
|
37
|
+
if (!response.ok)
|
|
38
|
+
throw new Error(String(response.status));
|
|
39
|
+
return new Blob([await response.text()], { type: "text/plain" });
|
|
40
|
+
});
|
|
41
|
+
// If the write is refused before reading the blob, its own rejection stays handled.
|
|
42
|
+
blob.catch(() => undefined);
|
|
43
|
+
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const response = await fetch(href);
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(String(response.status));
|
|
49
|
+
await navigator.clipboard.writeText(await response.text());
|
|
50
|
+
}
|
|
36
51
|
settle(run, "copied");
|
|
37
52
|
}
|
|
38
53
|
catch {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
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 Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
import { type ComponentProps, type ReactNode } from "react";
|
|
11
11
|
import { type HeadingLevel } from "../basics/cardStyles.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
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 Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
"use client";
|
|
11
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -39,7 +39,7 @@ export function FilterPanel({ title, headingLevel = 2, resultCount, active = fal
|
|
|
39
39
|
cleared.current = false;
|
|
40
40
|
}, [showClear]);
|
|
41
41
|
const Heading = `h${headingLevel}`;
|
|
42
|
-
return (_jsxs("section", { "aria-labelledby": titleId, className: cx("flex flex-col gap-4", CARD, className), ...props, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Heading, { id: titleId, ref: headingRef, tabIndex: -1, className: CARD_HEADING, children: title }), _jsx("button", { type: "button", "aria-labelledby": titleId, "aria-expanded": open, "aria-controls": bodyId, onClick: () => setOpen(!open), className: "inline-flex size-8 items-center justify-center rounded-full text-c2 transition-colors hover:bg-b3 hover:text-c1 sm:hidden", children: _jsx("span", { "aria-hidden": "true", children: open ? "▴" : "▾" }) })] }), _jsx("output", { "aria-live": "polite", className: "text-c3 text-sm", children: resultCount }), showClear ? (_jsx("button", { type: "button", onClick: () => {
|
|
42
|
+
return (_jsxs("section", { "aria-labelledby": titleId, className: cx("flex flex-col gap-4", CARD, className), ...props, children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Heading, { id: titleId, ref: headingRef, tabIndex: -1, className: CARD_HEADING, children: title }), _jsx("button", { type: "button", "aria-labelledby": titleId, "aria-expanded": open, "aria-controls": bodyId, onClick: () => setOpen(!open), className: "inline-flex coarse:size-11 size-8 items-center justify-center rounded-full text-c2 transition-colors hover:bg-b3 hover:text-c1 sm:hidden", children: _jsx("span", { "aria-hidden": "true", children: open ? "▴" : "▾" }) })] }), _jsx("output", { "aria-live": "polite", className: "text-c3 text-sm", children: resultCount }), showClear ? (_jsx("button", { type: "button", onClick: () => {
|
|
43
43
|
cleared.current = true;
|
|
44
44
|
onClear();
|
|
45
45
|
}, className: "ml-auto font-bold text-h1 text-sm transition-colors hover:text-c1", children: clearLabel })) : null] }), _jsx("div", { id: bodyId, className: cx("flex-col gap-3", open ? "flex" : "hidden sm:flex"), children: children })] }));
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* that leaves the name to a surrounding FilterRow. Server-safe.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Mon Sep 28, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
import { type ComponentProps, type ReactNode } from "react";
|
|
11
11
|
/** Every native `<fieldset>` prop, plus the group's label and whether a FilterRow names it. */
|
|
@@ -14,7 +14,9 @@ export type GroupFrameProps = ComponentProps<"fieldset"> & {
|
|
|
14
14
|
label: ReactNode;
|
|
15
15
|
/**
|
|
16
16
|
* Leave the name to a surrounding FilterRow: no label shows, and the fieldset is not a group
|
|
17
|
-
* of its own (role none), so screen readers hear the row's name once.
|
|
17
|
+
* of its own (role none), so screen readers hear the row's name once. Unlike the fields'
|
|
18
|
+
* hideLabel (TextInput, RadioGroup...), which keeps the label for screen readers, this one
|
|
19
|
+
* drops it.
|
|
18
20
|
*/
|
|
19
21
|
hideLabel?: boolean | undefined;
|
|
20
22
|
};
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* that leaves the name to a surrounding FilterRow. Server-safe.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import { useId } from "react";
|
|
12
12
|
import { cx } from "../../utils/cx.js";
|
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* colors, so on and off still look different.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Mon Sep 28, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
|
-
/** The pill: rounded, px-2.5 py-
|
|
11
|
-
export declare const CHIP = "rounded-full px-2.5 py-1 font-bold text-xs transition-colors";
|
|
10
|
+
/** The pill: rounded, px-2.5 py-1, bold text-xs; 44px tall on a coarse pointer. */
|
|
11
|
+
export declare const CHIP = "inline-flex items-center rounded-full px-2.5 py-1 font-bold text-xs transition-colors coarse:min-h-11 coarse:px-3.5";
|
|
12
12
|
/** A chip that is on (pressed or checked). */
|
|
13
13
|
export declare const CHIP_ON = "bg-h1 text-b6 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]";
|
|
14
14
|
/** A chip that is off. */
|
|
15
|
-
export declare const CHIP_OFF = "bg-b3 text-c2";
|
|
16
|
-
/** A chip that can't be used right now: dimmed, with a not-allowed cursor
|
|
17
|
-
|
|
15
|
+
export declare const CHIP_OFF = "bg-b3 text-c2 contrast-more:inset-ring contrast-more:inset-ring-c4 forced-colors:border";
|
|
16
|
+
/** A chip that can't be used right now: dimmed, with a not-allowed cursor (GrayText in forced
|
|
17
|
+
* colors). */
|
|
18
|
+
export declare const CHIP_UNAVAILABLE = "cursor-not-allowed opacity-40 forced-colors:text-[GrayText]";
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* colors, so on and off still look different.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Mon Sep 28, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
|
-
/** The pill: rounded, px-2.5 py-
|
|
10
|
+
/** The pill: rounded, px-2.5 py-1, bold text-xs; 44px tall on a coarse pointer. */
|
|
11
11
|
// py-1 with the text-xs line height makes a 24px pill: WCAG 2.2's minimum target size.
|
|
12
|
-
|
|
12
|
+
// inline-flex so a ChoiceChips <label> (inline) can take a min-height.
|
|
13
|
+
export const CHIP = "inline-flex items-center rounded-full px-2.5 py-1 font-bold text-xs transition-colors coarse:min-h-11 coarse:px-3.5";
|
|
13
14
|
/** A chip that is on (pressed or checked). */
|
|
14
15
|
export const CHIP_ON = "bg-h1 text-b6 forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]";
|
|
15
16
|
/** A chip that is off. */
|
|
16
|
-
export const CHIP_OFF = "bg-b3 text-c2";
|
|
17
|
-
/** A chip that can't be used right now: dimmed, with a not-allowed cursor
|
|
18
|
-
|
|
17
|
+
export const CHIP_OFF = "bg-b3 text-c2 contrast-more:inset-ring contrast-more:inset-ring-c4 forced-colors:border";
|
|
18
|
+
/** A chip that can't be used right now: dimmed, with a not-allowed cursor (GrayText in forced
|
|
19
|
+
* colors). */
|
|
20
|
+
export const CHIP_UNAVAILABLE = "cursor-not-allowed opacity-40 forced-colors:text-[GrayText]";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* length), so it fits any rule. Server-safe. Moved from bb.haruhime.moe.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Mon Sep 28, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
import type { ComponentProps } from "react";
|
|
11
11
|
/** Every native `<p>` prop except children, plus the count, the limit and the unit. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { textClasses } from "../basics/textStyles.js";
|
|
3
4
|
const format = (value) => value.toLocaleString("en-US");
|
|
4
5
|
/**
|
|
5
6
|
* @function CharCounter
|
|
@@ -10,5 +11,9 @@ export function CharCounter({ count, limit, unit = "characters", live = false, c
|
|
|
10
11
|
const over = count - limit;
|
|
11
12
|
const overText = over > 0 ? `${format(over)} over the limit` : "";
|
|
12
13
|
const shown = `${format(count)} / ${format(limit)} ${unit}${over > 0 ? `: ${overText}` : ""}`;
|
|
13
|
-
return (_jsx("p", { className:
|
|
14
|
+
return (_jsx("p", { className: textClasses({
|
|
15
|
+
tone: over > 0 ? "error" : "muted",
|
|
16
|
+
bold: over > 0,
|
|
17
|
+
className: cx("tabular-nums", className),
|
|
18
|
+
}), ...props, children: live ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: shown }), _jsx("output", { "aria-live": "polite", className: "sr-only", children: overText })] })) : (shown) }));
|
|
14
19
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* hint and error.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentProps } from "react";
|
|
12
12
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -15,7 +15,8 @@ export type CheckboxProps = Omit<ComponentProps<"input">, "id" | "type"> & Field
|
|
|
15
15
|
/**
|
|
16
16
|
* @function Checkbox
|
|
17
17
|
* @param props {CheckboxProps} native input props (`checked`, `defaultChecked`, `onChange`...),
|
|
18
|
-
* plus id, label, hint, error and wrapperClassName; `className` goes on the
|
|
18
|
+
* plus id, label, hideLabel, hint, error and wrapperClassName; `className` goes on the
|
|
19
|
+
* `<input>`
|
|
19
20
|
* @returns {JSX.Element} the checkbox row, and the error under it when given
|
|
20
21
|
*/
|
|
21
|
-
export declare function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }: CheckboxProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Checkbox({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }: CheckboxProps): import("react").JSX.Element;
|
|
@@ -4,12 +4,13 @@ import { FieldError, fieldControlProps, hintId } from "./FieldFrame.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* @function Checkbox
|
|
6
6
|
* @param props {CheckboxProps} native input props (`checked`, `defaultChecked`, `onChange`...),
|
|
7
|
-
* plus id, label, hint, error and wrapperClassName; `className` goes on the
|
|
7
|
+
* plus id, label, hideLabel, hint, error and wrapperClassName; `className` goes on the
|
|
8
|
+
* `<input>`
|
|
8
9
|
* @returns {JSX.Element} the checkbox row, and the error under it when given
|
|
9
10
|
*/
|
|
10
|
-
export function Checkbox({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }) {
|
|
11
|
+
export function Checkbox({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, "aria-labelledby": labelledBy, ...props }) {
|
|
11
12
|
const labelId = `${id}-label`;
|
|
12
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-1", wrapperClassName), children: [_jsxs("label", { htmlFor: id, className: "flex items-start gap-2 text-sm", children: [_jsx("input", { ...props, type: "checkbox", ...fieldControlProps({ id, hint, error, describedBy, invalid }), "aria-labelledby": labelledBy ? `${labelId} ${labelledBy}` : labelId,
|
|
13
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-1", wrapperClassName), children: [_jsxs("label", { htmlFor: id, className: "flex items-start gap-2 coarse:py-2.5 text-sm", children: [_jsx("input", { ...props, type: "checkbox", ...fieldControlProps({ id, hint, error, describedBy, invalid }), "aria-labelledby": labelledBy ? `${labelId} ${labelledBy}` : labelId,
|
|
13
14
|
// 24px, the WCAG 2.2 target size; -mt-0.5 centers it on the first text-sm line.
|
|
14
|
-
className: cx("-mt-0.5 size-6 shrink-0 accent-h1", className) }), _jsxs("span", { children: [_jsx("span", { id: labelId, className: "font-bold text-c1", children: label }), hint ? (_jsxs("span", { className: "text-c3", children: [" · ", _jsx("span", { id: hintId(id), children: hint })] })) : null] })] }), _jsx(FieldError, { id: id, error: error })] }));
|
|
15
|
+
className: cx("-mt-0.5 size-6 shrink-0 accent-h1", className) }), _jsxs("span", { children: [_jsx("span", { id: labelId, className: cx("font-bold text-c1", hideLabel && "sr-only"), children: label }), hint ? (_jsxs("span", { className: "text-c3", children: [hideLabel ? null : " · ", _jsx("span", { id: hintId(id), children: hint })] })) : null] })] }), _jsx(FieldError, { id: id, error: error })] }));
|
|
15
16
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* aria-invalid.
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Wed Sep 23, 2026
|
|
10
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
11
|
*/
|
|
12
12
|
import type { ComponentProps, ReactNode } from "react";
|
|
13
13
|
/** The label, hint and error every form field takes, keyed on the control's required id. */
|
|
@@ -20,6 +20,11 @@ export type FieldProps = {
|
|
|
20
20
|
hint?: ReactNode | undefined;
|
|
21
21
|
/** Error text under the control (a list is fine). Marks the control aria-invalid and links it. */
|
|
22
22
|
error?: ReactNode | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Hide the label visually. It still names the control (screen readers, voice control). Unlike
|
|
25
|
+
* a filter group's hideLabel, the label stays: only its look goes.
|
|
26
|
+
*/
|
|
27
|
+
hideLabel?: boolean | undefined;
|
|
23
28
|
/** Classes for the wrapper around the label, control, hint and error (layout, width). */
|
|
24
29
|
wrapperClassName?: string | undefined;
|
|
25
30
|
};
|
|
@@ -84,9 +89,9 @@ type FieldFrameProps = Omit<FieldProps, "wrapperClassName"> & {
|
|
|
84
89
|
};
|
|
85
90
|
/**
|
|
86
91
|
* @function FieldFrame
|
|
87
|
-
* @param props {FieldFrameProps} the control's id, label, hint, error, wrapper classes
|
|
88
|
-
* control itself as children
|
|
92
|
+
* @param props {FieldFrameProps} the control's id, label, hideLabel, hint, error, wrapper classes
|
|
93
|
+
* and the control itself as children
|
|
89
94
|
* @returns {JSX.Element} the label, control, hint and error stacked in a column
|
|
90
95
|
*/
|
|
91
|
-
export declare function FieldFrame({ id, label, hint, error, className, children }: FieldFrameProps): import("react").JSX.Element;
|
|
96
|
+
export declare function FieldFrame({ id, label, hideLabel, hint, error, className, children, }: FieldFrameProps): import("react").JSX.Element;
|
|
92
97
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { textClasses } from "../basics/textStyles.js";
|
|
3
4
|
import { FIELD_LABEL } from "./fieldStyles.js";
|
|
4
5
|
/**
|
|
5
6
|
* @function hintId
|
|
@@ -44,14 +45,14 @@ export const fieldControlProps = ({ id, hint, error, describedBy, invalid, }) =>
|
|
|
44
45
|
* nothing without an error
|
|
45
46
|
*/
|
|
46
47
|
export function FieldError({ id, error }) {
|
|
47
|
-
return error ? (_jsx("div", { id: errorId(id), role: "status", className: "
|
|
48
|
+
return error ? (_jsx("div", { id: errorId(id), role: "status", className: textClasses({ tone: "error" }), children: error })) : null;
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
50
51
|
* @function FieldFrame
|
|
51
|
-
* @param props {FieldFrameProps} the control's id, label, hint, error, wrapper classes
|
|
52
|
-
* control itself as children
|
|
52
|
+
* @param props {FieldFrameProps} the control's id, label, hideLabel, hint, error, wrapper classes
|
|
53
|
+
* and the control itself as children
|
|
53
54
|
* @returns {JSX.Element} the label, control, hint and error stacked in a column
|
|
54
55
|
*/
|
|
55
|
-
export function FieldFrame({ id, label, hint, error, className, children }) {
|
|
56
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-1", className), children: [_jsx("label", { htmlFor: id, className: FIELD_LABEL, children: label }), children, hint ? (_jsx("div", { id: hintId(id), className: "text-c4 text-xs", children: hint })) : null, _jsx(FieldError, { id: id, error: error })] }));
|
|
56
|
+
export function FieldFrame({ id, label, hideLabel, hint, error, className, children, }) {
|
|
57
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-1", className), children: [_jsx("label", { htmlFor: id, className: cx(FIELD_LABEL, hideLabel && "sr-only"), children: label }), children, hint ? (_jsx("div", { id: hintId(id), className: "text-c4 text-xs", children: hint })) : null, _jsx(FieldError, { id: id, error: error })] }));
|
|
57
58
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* native radios do. Controlled (`value`) or uncontrolled (`defaultValue`).
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
11
|
*/
|
|
12
12
|
import { type ComponentProps, type ReactNode } from "react";
|
|
13
13
|
/** One radio: its value, its label, an optional inline hint, and whether it can be picked. */
|
|
@@ -21,6 +21,8 @@ export type RadioOption = {
|
|
|
21
21
|
export type RadioGroupProps = Omit<ComponentProps<"fieldset">, "onChange" | "children" | "defaultValue"> & {
|
|
22
22
|
/** The legend, which names the group. */
|
|
23
23
|
label: ReactNode;
|
|
24
|
+
/** Hide the legend visually; it still names the group. */
|
|
25
|
+
hideLabel?: boolean | undefined;
|
|
24
26
|
options: readonly RadioOption[];
|
|
25
27
|
/** The radios' name, for a form (default: a generated one). */
|
|
26
28
|
name?: string | undefined;
|
|
@@ -43,4 +45,4 @@ export type RadioGroupProps = Omit<ComponentProps<"fieldset">, "onChange" | "chi
|
|
|
43
45
|
* the group's hint and error, plus native fieldset props (`disabled` turns off every radio)
|
|
44
46
|
* @returns {JSX.Element} a `<fieldset>` with its `<legend>`, the radios, and the hint and error
|
|
45
47
|
*/
|
|
46
|
-
export declare function RadioGroup({ label, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }: RadioGroupProps): import("react").JSX.Element;
|
|
48
|
+
export declare function RadioGroup({ label, hideLabel, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }: RadioGroupProps): import("react").JSX.Element;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* native radios do. Controlled (`value`) or uncontrolled (`defaultValue`).
|
|
8
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
9
|
* @created Mon Sep 28, 2026
|
|
10
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
11
|
*/
|
|
12
12
|
"use client";
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -21,11 +21,11 @@ import { FIELD_LABEL } from "./fieldStyles.js";
|
|
|
21
21
|
* the group's hint and error, plus native fieldset props (`disabled` turns off every radio)
|
|
22
22
|
* @returns {JSX.Element} a `<fieldset>` with its `<legend>`, the radios, and the hint and error
|
|
23
23
|
*/
|
|
24
|
-
export function RadioGroup({ label, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }) {
|
|
24
|
+
export function RadioGroup({ label, hideLabel, options, name, value, defaultValue, onChange, hint, error, required, className, "aria-describedby": describedBy, ...props }) {
|
|
25
25
|
const id = useId();
|
|
26
|
-
return (_jsxs("fieldset", { "aria-describedby": fieldDescribedBy(id, hint, error, describedBy), className: cx("flex flex-col gap-2", className), ...props, children: [_jsx("legend", { className: cx("mb-1", FIELD_LABEL), children: label }), options.map((option, i) => {
|
|
26
|
+
return (_jsxs("fieldset", { "aria-describedby": fieldDescribedBy(id, hint, error, describedBy), className: cx("flex flex-col gap-2", className), ...props, children: [_jsx("legend", { className: cx("mb-1", FIELD_LABEL, hideLabel && "sr-only"), children: label }), options.map((option, i) => {
|
|
27
27
|
const optionId = `${id}-${i}`;
|
|
28
|
-
return (_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx("input", { type: "radio", id: optionId, name: name ?? id, value: option.value, ...(value === undefined
|
|
28
|
+
return (_jsxs("label", { className: "flex items-start gap-2 coarse:py-2.5 text-sm", children: [_jsx("input", { type: "radio", id: optionId, name: name ?? id, value: option.value, ...(value === undefined
|
|
29
29
|
? { defaultChecked: option.value === defaultValue }
|
|
30
30
|
: { checked: option.value === value }), disabled: option.disabled, required: required, "aria-labelledby": `${optionId}-label`, "aria-describedby": option.hint ? hintId(optionId) : undefined, onChange: () => onChange?.(option.value),
|
|
31
31
|
// 24px, the WCAG 2.2 target size; -mt-0.5 centers it on the first text-sm line.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* at a time; a throw reads as `failedMessage`. Moved from bb.haruhime.moe (ReportForm).
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import { type ReactNode } from "react";
|
|
12
12
|
/** What `onSubmit` says: done (with what to say) or not (with the error). */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* at a time; a throw reads as `failedMessage`. Moved from bb.haruhime.moe (ReportForm).
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Mon Sep 28, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
"use client";
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -54,5 +54,5 @@ export function ReportDisclosure({ onSubmit, summary = "Report", label = "What's
|
|
|
54
54
|
running.current = false;
|
|
55
55
|
setPending(false);
|
|
56
56
|
};
|
|
57
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-2", className), children: [done === null ? (_jsx(Disclosure, { summary: summary, children: _jsxs("form", { className: "flex flex-col gap-2", onSubmit: send, children: [_jsx(Textarea, { id: `${id}-reason`, label: label, hint: hint, error: error ?? undefined, value: reason, rows: rows, minLength: minLength, maxLength: maxLength, required: true, onChange: (event) => setReason(event.currentTarget.value) }), _jsx(Button, { type: "submit", variant: "secondary", disabled: pending,
|
|
57
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-2", className), children: [done === null ? (_jsx(Disclosure, { summary: summary, children: _jsxs("form", { className: "flex flex-col gap-2", onSubmit: send, children: [_jsx(Textarea, { id: `${id}-reason`, label: label, hint: hint, error: error ?? undefined, value: reason, rows: rows, minLength: minLength, maxLength: maxLength, required: true, onChange: (event) => setReason(event.currentTarget.value) }), _jsx(Button, { type: "submit", variant: "secondary", disabled: pending, children: pending ? pendingLabel : submitLabel })] }) })) : null, _jsx("p", { ref: status, role: "status", tabIndex: -1, className: done === null ? "sr-only" : "text-c2 text-sm", children: done })] }));
|
|
58
58
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* through aria-describedby and aria-invalid. Options come in as children. Server-safe.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -12,8 +12,8 @@ import { type FieldProps } from "./FieldFrame.js";
|
|
|
12
12
|
export type SelectProps = Omit<ComponentProps<"select">, "id"> & FieldProps;
|
|
13
13
|
/**
|
|
14
14
|
* @function Select
|
|
15
|
-
* @param props {SelectProps} native select props with `<option>` children, plus id, label,
|
|
16
|
-
* error and wrapperClassName; `className` goes on the `<select>`
|
|
15
|
+
* @param props {SelectProps} native select props with `<option>` children, plus id, label,
|
|
16
|
+
* hideLabel, hint, error and wrapperClassName; `className` goes on the `<select>`
|
|
17
17
|
* @returns {JSX.Element} a label, the select, and the hint and error when given
|
|
18
18
|
*/
|
|
19
|
-
export declare function Select({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: SelectProps): import("react").JSX.Element;
|
|
19
|
+
export declare function Select({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: SelectProps): import("react").JSX.Element;
|
|
@@ -3,10 +3,10 @@ import { FieldFrame, fieldControlProps } from "./FieldFrame.js";
|
|
|
3
3
|
import { fieldClasses } from "./fieldStyles.js";
|
|
4
4
|
/**
|
|
5
5
|
* @function Select
|
|
6
|
-
* @param props {SelectProps} native select props with `<option>` children, plus id, label,
|
|
7
|
-
* error and wrapperClassName; `className` goes on the `<select>`
|
|
6
|
+
* @param props {SelectProps} native select props with `<option>` children, plus id, label,
|
|
7
|
+
* hideLabel, hint, error and wrapperClassName; `className` goes on the `<select>`
|
|
8
8
|
* @returns {JSX.Element} a label, the select, and the hint and error when given
|
|
9
9
|
*/
|
|
10
|
-
export function Select({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
11
|
-
return (_jsx(FieldFrame, { id: id, label: label, hint: hint, error: error, className: wrapperClassName, children: _jsx("select", { ...props, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(className) }) }));
|
|
10
|
+
export function Select({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
|
|
11
|
+
return (_jsx(FieldFrame, { id: id, label: label, hideLabel: hideLabel, hint: hint, error: error, className: wrapperClassName, children: _jsx("select", { ...props, ...fieldControlProps({ id, hint, error, describedBy, invalid }), className: fieldClasses(className) }) }));
|
|
12
12
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* aria-describedby and aria-invalid. Server-safe: the required id names the hint and error.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Wed Sep 23, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
import { type FieldProps } from "./FieldFrame.js";
|
|
@@ -12,8 +12,8 @@ import { type FieldProps } from "./FieldFrame.js";
|
|
|
12
12
|
export type TextInputProps = Omit<ComponentProps<"input">, "id"> & FieldProps;
|
|
13
13
|
/**
|
|
14
14
|
* @function TextInput
|
|
15
|
-
* @param props {TextInputProps} native input props, plus id, label, hint, error and
|
|
15
|
+
* @param props {TextInputProps} native input props, plus id, label, hideLabel, hint, error and
|
|
16
16
|
* wrapperClassName; `className` goes on the `<input>`
|
|
17
17
|
* @returns {JSX.Element} a label, the input, and the hint and error when given
|
|
18
18
|
*/
|
|
19
|
-
export declare function TextInput({ id, label, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: TextInputProps): import("react").JSX.Element;
|
|
19
|
+
export declare function TextInput({ id, label, hideLabel, hint, error, wrapperClassName, className, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: TextInputProps): import("react").JSX.Element;
|