@haruhimemoe/ui 0.2.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 +50 -1
- package/README.md +265 -29
- 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/icons/DiscordIcon.d.ts +21 -0
- package/dist/components/icons/DiscordIcon.js +10 -0
- 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 +11 -6
- package/dist/components/shell/SiteFooter.js +18 -7
- 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 +24 -2
- package/dist/index.js +28 -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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `SiteFooter` takes `discordLabel` (default "Discord"), the Discord link's accessible name, as `githubLabel` does for GitHub.
|
|
14
|
+
- `HeadingLevel` (`2 | 3 | 4 | 5 | 6`), the type of `headingLevel` on `Card` and `FilterPanel`.
|
|
15
|
+
- `TextLink` and `linkClasses`: a text link in two looks, `accent` (`h1`, underlined, for running text) and `plain` (bold `c1`, underlined on hover, for names in lists). Like `ButtonLink`, it is `next/link` inside the app and a plain `<a>` off-site.
|
|
16
|
+
- `Badge`: a small pill for a status or tag, in `neutral`, `accent`, `warning` or `muted` (an outlined "beta" tag).
|
|
17
|
+
- Table primitives: `Table` (a sideways-scrolling wrapper, and a caption that can be for screen readers only), `THead`, `TBody`, `Th` (`scope="col"` by default, bold for `scope="row"`) and `Td`, with `numeric` for `tabular-nums` cells. They carry the look the apps' tables share.
|
|
18
|
+
- `cx`, the class merger the components use (tailwind-merge), and its `ClassValue` type.
|
|
19
|
+
- `InlineConfirm`: a two-step confirm in the page. Opening moves focus to cancel; cancel, Escape or a confirm that resolves puts it back on the trigger, so focus never falls to the page body. The open confirm is a group named by its question. A pending confirm keeps focus and ignores presses; a failed one stays open.
|
|
20
|
+
- `AsyncButton`: runs an async action and announces its result (or a failure, in rose) in an `<output>`, one run at a time, with an optional pending label.
|
|
21
|
+
- `Disclosure`: a button with `aria-expanded` and `aria-controls` that shows and hides a panel, which stays in the page while hidden. Uncontrolled, or controlled with `open` and `onOpenChange`.
|
|
22
|
+
- `RadioGroup`: a native radio fieldset on the `Checkbox` look, with a legend, per-option hints, and a group hint and error. Controlled or uncontrolled.
|
|
23
|
+
- `TypeToConfirm`: a form whose submit stays off until a name is typed exactly, for actions that can't be undone.
|
|
24
|
+
- `ChoiceChips`: single-select chips as native radios (arrow keys move and pick), with `Chip`'s look and `ChipGroup`'s `label` and `hideLabel`.
|
|
25
|
+
- `Chip` and `ChipOption` take `unavailableReason`: the chip is blocked but stays focusable (`aria-disabled`), with the reason as its description and title.
|
|
26
|
+
- `LinkTabs`: a named nav of pill links with `aria-current="page"` on the current one.
|
|
27
|
+
- `HeaderMenu`: the header's account disclosure (button, links, extra controls), closed by Escape (focus back on the button), a click outside, a link, or focus leaving it.
|
|
28
|
+
- osu! display pieces: `StarRating` (a pill on osu!'s star-rating spectrum that reads "5.23 stars"), `BeatmapStats` (CS, AR, OD, HP, BPM and length from plain numbers) and `ModBadge` (a slot pill colored by its mod bucket).
|
|
29
|
+
- `Pagination` button mode: `onPageChange` instead of `hrefFor`, for results fetched in place. `pageCount` can be `null` (the status reads "Page X", and `hasNext` says whether Next works). The ends stay in place with `aria-disabled`, and the status is a polite live region.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `Card` takes `headingLevel` `5` and `6` too, like `FilterPanel`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Hrefs that browsers read as off-site now count as external: `/\host`, `\\host`, and a URL behind leading spaces or control characters. Before, `ButtonLink` sent them through `next/link` without its `rel="noreferrer"` default, and a nav hydrated its client list for them.
|
|
38
|
+
- `Pagination` normalizes `page` and `pageCount`: `NaN` reads as page 1 (a `NaN` count as one page), a page past either end is pulled back inside, and fractions are dropped. Before, `page={NaN}` showed "Page NaN of 5" with no links, and page 99 of 5 linked to page 98.
|
|
39
|
+
- `CopyButton` reports only the latest press. A slow earlier copy that failed after a later one worked no longer replaces "Copied." with the failure.
|
|
40
|
+
- `SiteHeader` and `NavLinks` key entries by label and href, as `SiteFooter` does, so two entries with the same href no longer share a React key.
|
|
41
|
+
- `Checkbox` keeps an `aria-labelledby` you pass, after its own label. Before, it was dropped.
|
|
42
|
+
- `RangeSlider` counts a `step` of `0`, below `0` or not finite as `1`, and swaps `min` and `max` given the wrong way round. Before, `step={0}` sent `NaN` to `onChange` on every key press or drag.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
|
|
46
|
+
- The release workflow pins every GitHub Action to a full commit SHA and npm to an exact version, since that job holds the npm publish token. It also runs the coverage floor and the consumer check before publishing, as CI does. Dependabot (the `bun` and `github-actions` ecosystems) keeps the pins and the exact dependency versions current.
|
|
47
|
+
- Report vulnerabilities through GitHub's private vulnerability reporting first, or by email (SECURITY.md).
|
|
48
|
+
|
|
49
|
+
## [0.3.0] - 2026-09-25
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- `DiscordIcon`: the Discord logo as an inline SVG in the current text color, sized and hidden from screen readers like `GitHubIcon`. The path is Simple Icons' `discord.svg` (CC0 1.0). Discord's brand guidelines ask for the logo in color, black or white, so set one of those as the text color.
|
|
54
|
+
- `SiteFooter` takes `discordHref`. When set, a Discord icon link named "Discord" sits before the GitHub icon in the last row, at the same size. The logo stays white and dims on hover instead of changing color, as Discord's guidelines ask. Without `discordHref` the footer is unchanged.
|
|
55
|
+
|
|
9
56
|
## [0.2.0] - 2026-09-24
|
|
10
57
|
|
|
11
58
|
### Added
|
|
@@ -30,6 +77,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
30
77
|
- `className` on every component, and the extras passed to `buttonClasses` and `fieldClasses`, merge with tailwind-merge: a caller's class replaces a built-in one that sets the same property (`fieldClasses("w-auto")` drops `w-full`).
|
|
31
78
|
- Shell: `SiteHeader` (brand slot, nav links as data with `aria-current`, actions slot), `NavLinks`, `SiteFooter` (link columns as data, fine print, the haruhime.moe wordmark and a GitHub link) and `PageShell` (skip link, header, main, footer).
|
|
32
79
|
|
|
33
|
-
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.
|
|
80
|
+
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.4.0...HEAD
|
|
81
|
+
[0.4.0]: https://github.com/haruhimemoe/ui/compare/v0.3.0...v0.4.0
|
|
82
|
+
[0.3.0]: https://github.com/haruhimemoe/ui/compare/v0.2.0...v0.3.0
|
|
34
83
|
[0.2.0]: https://github.com/haruhimemoe/ui/compare/v0.1.0...v0.2.0
|
|
35
84
|
[0.1.0]: https://github.com/haruhimemoe/ui/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
<p align="center"><a href="https://github.com/haruhimemoe/ui"><picture><source media="(prefers-color-scheme: light)" srcset="https://www.haruhime.moe/brand/repos/ui-banner-on-light.svg"><img alt="@haruhimemoe/ui" src="https://www.haruhime.moe/brand/repos/ui-banner.svg" width="640"></picture></a></p>
|
|
2
|
+
|
|
1
3
|
# @haruhimemoe/ui
|
|
2
4
|
|
|
3
|
-
React components for the haruhime.moe osu! tools on Next.js. It ships the osu!-web-style palette as a Tailwind 4 theme, plus buttons,
|
|
5
|
+
React components for the haruhime.moe osu! tools on Next.js. It ships the osu!-web-style palette as a Tailwind 4 theme, plus buttons, links, badges, form fields and confirms, filter controls (toggle and choice chips, a two-thumb range slider, a filter panel), tables, osu! beatmap display pieces and the site header, footer, tabs, account menu and page frame. Most components are Server Components. The few that need the browser carry `"use client"` in their own files, so you import everything from one place.
|
|
4
6
|
|
|
5
7
|
See every component in its states at [haruhime.moe/ui](https://www.haruhime.moe/ui). The page names the version it runs.
|
|
6
8
|
|
|
7
|
-
This README describes version 0.
|
|
9
|
+
This README describes version 0.4.0. Anything marked "since 0.4.0" is not in 0.3.0, anything marked "since 0.3.0" is not in 0.2.0, and anything marked "since 0.2.0" is not in 0.1.0. [CHANGELOG.md](./CHANGELOG.md) lists what changed in each version.
|
|
8
10
|
|
|
9
11
|
## Requirements
|
|
10
12
|
|
|
@@ -151,17 +153,17 @@ export default function Home() {
|
|
|
151
153
|
|
|
152
154
|
Import every component from `@haruhimemoe/ui`, in Server and Client Components alike.
|
|
153
155
|
|
|
154
|
-
- **Client components:** `CopyButton`, `Chip`, `ChipGroup`, `RangeSlider` and `FilterPanel`. Each file starts with `"use client"`.
|
|
156
|
+
- **Client components:** `CopyButton`, `Chip`, `ChipGroup`, `RangeSlider` and `FilterPanel`, and since 0.4.0 `AsyncButton`, `InlineConfirm`, `Disclosure`, `ChoiceChips`, `RadioGroup`, `TypeToConfirm` and `HeaderMenu`. Each file starts with `"use client"`. They merge their classes with tailwind-merge in the browser, so a page that renders any of them loads tailwind-merge (about 9 KB gzipped), however its header renders.
|
|
155
157
|
- **`SiteHeader` and `NavLinks`** are Server Components with a small client part (since 0.2.0; in 0.1.0 `NavLinks` is a client component). When a nav link can be the current page (a path such as `/packs`), a client list reads the path to set `aria-current`. With only external or text-only links, the nav renders on the server alone and nothing in it hydrates. Relative hrefs (`#main`) skip the client list too, but they render `next/link`, which hydrates.
|
|
156
158
|
- **Everything else is server-safe:** no state, no effects, no browser APIs.
|
|
157
159
|
|
|
158
|
-
A Server Component can't pass a function to a Client Component. So callback props (`onChange`, `onPressedChange`, `onClear`) have to come from your own `"use client"` file, like the filters example below. Props that are plain data (`CopyButton`'s `text`, `Chip`'s `pressed`) work from a Server Component. `Pagination` takes a function (`hrefFor`), but it is a Server Component itself, so that is fine anywhere.
|
|
160
|
+
A Server Component can't pass a function to a Client Component. So callback props (`onChange`, `onPressedChange`, `onClear`) have to come from your own `"use client"` file, like the filters example below. Props that are plain data (`CopyButton`'s `text`, `Chip`'s `pressed`) work from a Server Component. `Pagination` takes a function (`hrefFor`), but it is a Server Component itself, so that is fine anywhere. Its button mode (`onPageChange`) is a callback, so render that from a `"use client"` file.
|
|
159
161
|
|
|
160
162
|
## Props, classes and refs
|
|
161
163
|
|
|
162
164
|
- Every component takes its element's native props and passes them through (`id`, `aria-*`, `data-*`, event handlers). Each section below names that element. The tables list only the extra props.
|
|
163
165
|
- `ref` is a normal prop (React 19). It goes where the native props go: the outer element for most components, the control (`<input>`, `<select>`, `<textarea>`) for the form fields, and the `<button>` for `CopyButton`. On `PageShell` that is the wrapper `<div>`, not `<main>`.
|
|
164
|
-
- `className` is added after the built-in classes and wins on conflict: a class that sets the same property as a built-in one replaces it (merged with [tailwind-merge](https://github.com/dcastil/tailwind-merge)). `<Select className="w-auto">` drops the built-in `w-full`. On `GitHubIcon` and `HaruhimeWordmark`, `className` replaces the default size instead.
|
|
166
|
+
- `className` is added after the built-in classes and wins on conflict: a class that sets the same property as a built-in one replaces it (merged with [tailwind-merge](https://github.com/dcastil/tailwind-merge)). `<Select className="w-auto">` drops the built-in `w-full`. On `DiscordIcon`, `GitHubIcon` and `HaruhimeWordmark`, `className` replaces the default size instead.
|
|
165
167
|
|
|
166
168
|
## Components
|
|
167
169
|
|
|
@@ -181,7 +183,7 @@ A pill button. Every native `<button>` prop.
|
|
|
181
183
|
|
|
182
184
|
A link that looks like `Button`. Every `next/link` prop (`href`, `prefetch`, `replace`, `scroll`, `target`, `rel`...), plus `variant` and `size` as on `Button`.
|
|
183
185
|
|
|
184
|
-
- A string `href` with a scheme (`https:`, `mailto:`) or starting with `//` renders a plain `<a>`, and `next/link`'s own props are dropped.
|
|
186
|
+
- A string `href` with a scheme (`https:`, `mailto:`) or starting with `//` renders a plain `<a>`, and `next/link`'s own props are dropped. The href is read the way the browser reads it: leading spaces don't count and a backslash counts as a slash, so `/\host` and `\\host` are off-site too (since 0.4.0).
|
|
185
187
|
- That plain `<a>` with `target="_blank"` and no `rel` gets `rel="noreferrer"`. A `rel` you pass always wins. Internal links get only the `rel` you pass.
|
|
186
188
|
|
|
187
189
|
#### `buttonClasses`
|
|
@@ -199,7 +201,7 @@ The osu!-web panel: rounded, `b4` background, `p-5`. Every native `<section>` pr
|
|
|
199
201
|
| Prop | Type | Default | What it does |
|
|
200
202
|
| --- | --- | --- | --- |
|
|
201
203
|
| `title` | `ReactNode` | none | Rendered as a heading at the top (`<h2>` by default). It also names the section (`aria-labelledby`), which makes the card a region landmark. |
|
|
202
|
-
| `headingLevel` | `2 \| 3 \| 4` | `2` | The title's heading level. Use `3` or
|
|
204
|
+
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `2` | The title's heading level (type `HeadingLevel`). Use `3` or lower for a card that sits under another heading, such as a card inside a titled card. Since 0.2.0; `5` and `6` since 0.4.0. |
|
|
203
205
|
|
|
204
206
|
#### `PageHeader`
|
|
205
207
|
|
|
@@ -251,6 +253,45 @@ The first element inside gets no top margin (`[&>:first-child]:mt-0`, since 0.2.
|
|
|
251
253
|
|
|
252
254
|
Later sections keep their heading margin, which spaces them apart.
|
|
253
255
|
|
|
256
|
+
#### `Disclosure` (client)
|
|
257
|
+
|
|
258
|
+
Since 0.4.0. A button that shows and hides a panel below it, with `aria-expanded` and `aria-controls` and a ▾ / ▴ arrow. The closed panel stays in the page, hidden, so fields inside keep their values. Every native `<div>` prop except `children`, for the wrapper.
|
|
259
|
+
|
|
260
|
+
| Prop | Type | Default | What it does |
|
|
261
|
+
| --- | --- | --- | --- |
|
|
262
|
+
| `summary` | `ReactNode` | required | The button's text. It can change with the state. |
|
|
263
|
+
| `children` | `ReactNode` | required | The panel. |
|
|
264
|
+
| `defaultOpen` | `boolean` | `false` | Whether it starts open. |
|
|
265
|
+
| `open`, `onOpenChange` | `boolean`, `(open: boolean) => void` | none | Control the state from outside. |
|
|
266
|
+
| `buttonClassName`, `panelClassName` | `string` | none | Classes for the button and the panel, merged last. |
|
|
267
|
+
|
|
268
|
+
#### `TextLink`
|
|
269
|
+
|
|
270
|
+
Since 0.4.0. A text link: `next/link` inside the app, a plain `<a>` off-site (with `rel="noreferrer"` in a new tab), like `ButtonLink`. Every `next/link` prop. Type: `TextLinkVariant`.
|
|
271
|
+
|
|
272
|
+
| Prop | Type | Default | What it does |
|
|
273
|
+
| --- | --- | --- | --- |
|
|
274
|
+
| `variant` | `"accent" \| "plain"` | `"accent"` | `accent` is `h1` and underlined, for links in running text (it doesn't rely on color alone). `plain` is bold `c1`, underlined on hover, for a name or title in a list or table. |
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
<p>
|
|
278
|
+
Scripts can read public packs. <TextLink href="/docs/api">Read the API docs</TextLink>.
|
|
279
|
+
</p>
|
|
280
|
+
<TextLink href={`/packs/${pack.id}`} variant="plain">{pack.name}</TextLink>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### `linkClasses`
|
|
284
|
+
|
|
285
|
+
Since 0.4.0. `linkClasses({ variant?, className? }): string` returns the `TextLink` classes, for an element that should look like one (a `<button>` that reads as a link, say). Type: `LinkClassOptions`.
|
|
286
|
+
|
|
287
|
+
#### `Badge`
|
|
288
|
+
|
|
289
|
+
Since 0.4.0. A small pill for a status or tag ("Unranked", "beta", a count). A `<span>` with no role, so screen readers read it in line with the text around it. Every native `<span>` prop. Type: `BadgeTone`.
|
|
290
|
+
|
|
291
|
+
| Prop | Type | Default | What it does |
|
|
292
|
+
| --- | --- | --- | --- |
|
|
293
|
+
| `tone` | `"neutral" \| "accent" \| "warning" \| "muted"` | `"neutral"` | `neutral` is `b3` with `c2` text, `accent` is `h1` with dark bold text, `warning` is a faint amber with bold amber text, `muted` is an outlined pill in small bold capitals (a "beta" tag). |
|
|
294
|
+
|
|
254
295
|
### Forms
|
|
255
296
|
|
|
256
297
|
The fields render a label, the control, an optional hint and an optional error, wired together for screen readers. They are Server Components: you pass the `id`, so they need no generated ids.
|
|
@@ -285,7 +326,42 @@ Every native `<select>` prop, plus the field props. Pass `<option>` elements as
|
|
|
285
326
|
|
|
286
327
|
#### `Checkbox`
|
|
287
328
|
|
|
288
|
-
Every native `<input>` prop except `type` (`checked`, `defaultChecked`, `onChange`, `name`, `disabled`...), plus the field props. The label is bold `text-c1` and the hint follows it inline after a dot. Clicking anywhere on the row toggles it. The label alone is the accessible name; the hint is the description.
|
|
329
|
+
Every native `<input>` prop except `type` (`checked`, `defaultChecked`, `onChange`, `name`, `disabled`...), plus the field props. The label is bold `text-c1` and the hint follows it inline after a dot. Clicking anywhere on the row toggles it. The label alone is the accessible name; the hint is the description. An `aria-labelledby` you pass is added after the label (since 0.4.0; 0.3.0 dropped it).
|
|
330
|
+
|
|
331
|
+
#### `RadioGroup` (client)
|
|
332
|
+
|
|
333
|
+
Since 0.4.0. A native radio group on the `Checkbox` look: a `<fieldset>` named by its `<legend>`, one radio per option with a bold label and an inline hint, then the group's hint and error. Each option's label is its accessible name and its hint its description. Arrow keys move and pick, as native radios do. Every native `<fieldset>` prop except `onChange`, `children` and `defaultValue`; `disabled` turns off every radio. Types: `RadioOption`.
|
|
334
|
+
|
|
335
|
+
| Prop | Type | Default | What it does |
|
|
336
|
+
| --- | --- | --- | --- |
|
|
337
|
+
| `label` | `ReactNode` | required | The legend. |
|
|
338
|
+
| `options` | `readonly RadioOption[]` | required | `{ value: string; label: ReactNode; hint?: ReactNode; disabled?: boolean }` for each radio. |
|
|
339
|
+
| `value` / `defaultValue` | `string` | none | The picked value, held by you (`value`, with `onChange`) or by the group (`defaultValue`). |
|
|
340
|
+
| `onChange` | `(value: string) => void` | none | Gets the picked option's value. |
|
|
341
|
+
| `name` | `string` | generated | The radios' name, for a form. |
|
|
342
|
+
| `hint`, `error` | `ReactNode` | none | Under the options, linked to the group with `aria-describedby`. An error is a `role="alert"` and marks the radios `aria-invalid`. |
|
|
343
|
+
| `required` | `boolean` | `false` | Every radio gets `required`. |
|
|
344
|
+
|
|
345
|
+
#### `TypeToConfirm` (client)
|
|
346
|
+
|
|
347
|
+
Since 0.4.0. A confirm for something that can't be undone: a `<form>` whose submit button stays off until the expected text is typed exactly (spaces around it don't count). The field has no autocomplete, autocapitalize or spellcheck. Enter submits once it matches. Children show above the field, to say what the action does. Every native `<form>` prop except `onSubmit`.
|
|
348
|
+
|
|
349
|
+
| Prop | Type | Default | What it does |
|
|
350
|
+
| --- | --- | --- | --- |
|
|
351
|
+
| `id` | `string` | required | The text field's id, as on `TextInput`. |
|
|
352
|
+
| `expected` | `string` | required | What has to be typed. |
|
|
353
|
+
| `label` | `ReactNode` | `"Type <expected> to confirm"` | The field's label. |
|
|
354
|
+
| `hint`, `error` | `ReactNode` | none | As on `TextInput`. Pass `error` when the action fails. |
|
|
355
|
+
| `submitLabel` | `ReactNode` | required | The button's text. |
|
|
356
|
+
| `pendingLabel` | `ReactNode` | `submitLabel` | The button's text while `onConfirm` runs. It runs once at a time. |
|
|
357
|
+
| `onConfirm` | `() => void \| Promise<void>` | required | Runs on submit once the text matches. If it throws or rejects, the form stays as typed. |
|
|
358
|
+
| `variant` | `"primary" \| "secondary" \| "ghost"` | `"secondary"` | The button's look. |
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
<TypeToConfirm id="delete-pool" expected={pool.name} submitLabel="Delete this pool" onConfirm={remove} error={error}>
|
|
362
|
+
<p>This deletes the pool for everyone who edits it. It can't be undone.</p>
|
|
363
|
+
</TypeToConfirm>
|
|
364
|
+
```
|
|
289
365
|
|
|
290
366
|
#### `fieldClasses`
|
|
291
367
|
|
|
@@ -299,7 +375,7 @@ Every native `<input>` prop except `type` (`checked`, `defaultChecked`, `onChang
|
|
|
299
375
|
|
|
300
376
|
#### `CopyButton` (client)
|
|
301
377
|
|
|
302
|
-
A button that copies text, with the result in an `<output>` beside it that screen readers announce. Each press clears the message first, so a second copy is announced too. If the clipboard is missing or refuses (an insecure page, say), it shows the failure message. Every `Button` prop except `onClick` and `children`; `className` and the native props go on the button.
|
|
378
|
+
A button that copies text, with the result in an `<output>` beside it that screen readers announce. Each press clears the message first, so a second copy is announced too. Only the latest press reports: an earlier copy that settles later (behind a permission prompt, say) doesn't overwrite it (since 0.4.0). If the clipboard is missing or refuses (an insecure page, say), it shows the failure message. Every `Button` prop except `onClick` and `children`; `className` and the native props go on the button.
|
|
303
379
|
|
|
304
380
|
| Prop | Type | Default | What it does |
|
|
305
381
|
| --- | --- | --- | --- |
|
|
@@ -313,29 +389,69 @@ A button that copies text, with the result in an `<output>` beside it that scree
|
|
|
313
389
|
|
|
314
390
|
#### `Pagination`
|
|
315
391
|
|
|
316
|
-
Previous and next
|
|
392
|
+
Previous and next pills around "Page X of Y": links (`hrefFor`), or since 0.4.0 buttons (`onPageChange`). Renders nothing when there is one page or none. Every native `<nav>` prop except `children`; `aria-label` defaults to `"Pages"`.
|
|
317
393
|
|
|
318
394
|
| Prop | Type | Default | What it does |
|
|
319
395
|
| --- | --- | --- | --- |
|
|
320
396
|
| `page` | `number` | required | The current page, starting at 1. |
|
|
321
397
|
| `pageCount` | `number` | required | How many pages there are. |
|
|
322
|
-
| `hrefFor` | `(page: number) => string` |
|
|
398
|
+
| `hrefFor` | `(page: number) => string` | one of the two | Link mode: builds a page's URL, e.g. `` (p) => `/packs?page=${p}` ``. |
|
|
399
|
+
| `onPageChange` | `(page: number) => void` | one of the two | Button mode (since 0.4.0): called with the page to show, for results fetched in place. `pageCount` may be `null` there. |
|
|
400
|
+
| `hasNext` | `boolean` | `false` | Button mode with `pageCount={null}`: whether a page comes after this one. |
|
|
323
401
|
| `previousLabel` | `ReactNode` | `"Previous"` | Text of the link to the page before. |
|
|
324
402
|
| `nextLabel` | `ReactNode` | `"Next"` | Text of the link to the page after. |
|
|
325
|
-
| `formatStatus` | `(page: number, pageCount: number) => ReactNode` | `"Page X of Y"` | The text in the middle. |
|
|
403
|
+
| `formatStatus` | `(page: number, pageCount: number) => ReactNode` | `"Page X of Y"` | The text in the middle. In button mode `pageCount` can be `null`, and the default reads "Page X". |
|
|
404
|
+
|
|
405
|
+
A `page` or `pageCount` straight from a URL is safe to pass (since 0.4.0): `NaN` reads as page 1 (and a `NaN` count as one page), a page past either end is pulled back inside, and fractions are dropped. `Number("abc")` shows page 1 with a Next link, and page 99 of 5 shows page 5.
|
|
326
406
|
|
|
327
407
|
The links use `next/link` with `rel="prev"` and `rel="next"`. On the first and last page one link goes away. If it had keyboard focus (Next pressed on page 4 of 5), focus moves to the "Page X of Y" text instead of falling back to the top of the page. `Pagination` stays a Server Component; that text is a small client component inside it.
|
|
328
408
|
|
|
329
|
-
|
|
409
|
+
In button mode, Previous and Next are `<button>`s. At the first or last page they stay in place, dimmed, with `aria-disabled="true"`: they keep focus and ignore presses. The status is a polite live region there, so each new page is announced.
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
<Pagination page={page} pageCount={null} hasNext={data.hasMore} onPageChange={setPage} />
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
#### `AsyncButton` (client)
|
|
330
416
|
|
|
331
|
-
|
|
417
|
+
Since 0.4.0. A button that runs an async action and reports how it went in an `<output>` beside it that screen readers announce, like `CopyButton`. While it runs, the button keeps focus but ignores presses (`aria-disabled`). Every `Button` prop except `onClick`; the children are its text.
|
|
332
418
|
|
|
333
419
|
| Prop | Type | Default | What it does |
|
|
334
420
|
| --- | --- | --- | --- |
|
|
335
|
-
| `
|
|
421
|
+
| `action` | `() => ReactNode \| Promise<ReactNode>` | required | Runs on press. What it returns is the message ("Done."). |
|
|
422
|
+
| `pendingLabel` | `ReactNode` | the children | The button's text while the action runs. |
|
|
423
|
+
| `failedMessage` | `ReactNode \| ((error: unknown) => ReactNode)` | `"Something went wrong. Try again."` | Shown in `text-rose-300` when the action throws or rejects. |
|
|
424
|
+
| `wrapperClassName` | `string` | none | Classes for the wrapper around the button and the message. |
|
|
425
|
+
|
|
426
|
+
#### `InlineConfirm` (client)
|
|
427
|
+
|
|
428
|
+
Since 0.4.0. A two-step confirm in the page, no dialog: a trigger button, then the question with a cancel and a confirm button in its place. Opening moves focus to cancel, the safe choice. Cancel or Escape closes it and puts focus back on the trigger, and so does a confirm that resolves. The open confirm is a `<fieldset>` (a group) named by the question, so screen readers hear the question when focus arrives. Every native `<div>` prop except `children`, for the wrapper.
|
|
429
|
+
|
|
430
|
+
| Prop | Type | Default | What it does |
|
|
431
|
+
| --- | --- | --- | --- |
|
|
432
|
+
| `trigger` | `ReactNode` | required | The first button's text. |
|
|
433
|
+
| `question` | `ReactNode` | required | Shown once the trigger is pressed. |
|
|
434
|
+
| `onConfirm` | `() => void \| Promise<void>` | required | Runs on confirm. While it runs, both buttons keep focus but ignore presses. If it throws or rejects, the confirm stays open: show the error yourself. |
|
|
435
|
+
| `confirmLabel`, `cancelLabel` | `ReactNode` | `"Confirm"`, `"Cancel"` | The two buttons' text. |
|
|
436
|
+
| `pendingLabel` | `ReactNode` | `confirmLabel` | The confirm button's text while `onConfirm` runs. |
|
|
437
|
+
| `onCancel` | `() => void` | none | Called when it closes without confirming. |
|
|
438
|
+
| `triggerProps` | `ButtonProps` | none | Props for the trigger (`aria-label`, `variant`, `disabled`). It is `secondary` by default. |
|
|
439
|
+
| `confirmVariant` | `"primary" \| "secondary" \| "ghost"` | `"secondary"` | The confirm button's look. Cancel is always `ghost`. |
|
|
440
|
+
|
|
441
|
+
If a confirm removes the item (and the `InlineConfirm` with it), move focus somewhere sensible yourself, such as the list's heading.
|
|
336
442
|
|
|
337
443
|
### Icons
|
|
338
444
|
|
|
445
|
+
#### `DiscordIcon`
|
|
446
|
+
|
|
447
|
+
Since 0.3.0. The Discord logo as an inline SVG in the current text color. Hidden from screen readers by default (`aria-hidden="true"`), so put a label on the link around it. Every native `<svg>` prop except `children` and `viewBox`.
|
|
448
|
+
|
|
449
|
+
The path is Simple Icons' `discord.svg` at tag 16.32.0 ([simple-icons/simple-icons](https://github.com/simple-icons/simple-icons), CC0 1.0). Discord is a trademark of Discord Inc. Its [brand guidelines](https://discord.com/branding) ask for the logo in color, black or white, and not recolored. The icon takes the current text color, so give the element around it one of those (`SiteFooter` uses white).
|
|
450
|
+
|
|
451
|
+
| Prop | Type | Default | What it does |
|
|
452
|
+
| --- | --- | --- | --- |
|
|
453
|
+
| `className` | `string` | `"size-5"` | Replaces the default size. |
|
|
454
|
+
|
|
339
455
|
#### `GitHubIcon`
|
|
340
456
|
|
|
341
457
|
The GitHub mark as an inline SVG in the current text color. Hidden from screen readers by default (`aria-hidden="true"`), so put a label on the link around it. Every native `<svg>` prop except `children` and `viewBox`.
|
|
@@ -356,7 +472,7 @@ The haruhime.moe wordmark as an inline SVG. It keeps the brand's own white and p
|
|
|
356
472
|
|
|
357
473
|
#### `HaruhimeWordmarkLink`
|
|
358
474
|
|
|
359
|
-
A plain `<a>` around a decorative `HaruhimeWordmark`, dimmed until hovered. Every native `<a>` prop
|
|
475
|
+
A plain `<a>` around a decorative `HaruhimeWordmark`, dimmed until hovered. Every native `<a>` prop except `children`.
|
|
360
476
|
|
|
361
477
|
| Prop | Type | Default | What it does |
|
|
362
478
|
| --- | --- | --- | --- |
|
|
@@ -426,13 +542,14 @@ Give the `ChipGroup` or `RangeSlider` inside a `FilterRow` `hideLabel`. The row'
|
|
|
426
542
|
|
|
427
543
|
#### `Chip` (client)
|
|
428
544
|
|
|
429
|
-
A toggle pill: a `<button>` with `aria-pressed`, `h1` when on. Every native `<button>` prop.
|
|
545
|
+
A toggle pill: a `<button>` with `aria-pressed`, `h1` when on. Every native `<button>` prop except `aria-pressed`, which `pressed` sets.
|
|
430
546
|
|
|
431
547
|
| Prop | Type | Default | What it does |
|
|
432
548
|
| --- | --- | --- | --- |
|
|
433
549
|
| `pressed` | `boolean` | required | Whether it is on. |
|
|
434
550
|
| `onPressedChange` | `(pressed: boolean) => void` | none | Called with the new state on click, Enter or Space. |
|
|
435
551
|
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Never submits a form by default. |
|
|
552
|
+
| `unavailableReason` | `ReactNode` | none | Since 0.4.0. Why the chip can't be pressed right now (EZ with HR picked). The chip is dimmed and never toggles, but stays in the tab order (`aria-disabled="true"`, not `disabled`), so keyboard and screen reader users find it and hear the reason as its description. A string reason is also its `title`. |
|
|
436
553
|
|
|
437
554
|
Your own `onClick` runs first. Call `event.preventDefault()` in it to skip the toggle.
|
|
438
555
|
|
|
@@ -444,20 +561,33 @@ A labelled row of chips for picking several values (mods, game modes). A `<field
|
|
|
444
561
|
| --- | --- | --- | --- |
|
|
445
562
|
| `label` | `ReactNode` | required | Names the group. |
|
|
446
563
|
| `hideLabel` | `boolean` | `false` | For use inside a `FilterRow`, which names the row: the label doesn't render and the fieldset isn't a group of its own (`role="none"`). `disabled` still reaches every chip. |
|
|
447
|
-
| `options` | `readonly ChipOption[]` | required | `{ value: string; label: ReactNode; disabled?: boolean }` for each chip. |
|
|
564
|
+
| `options` | `readonly ChipOption[]` | required | `{ value: string; label: ReactNode; disabled?: boolean; unavailableReason?: ReactNode }` for each chip (`unavailableReason` since 0.4.0, as on `Chip`). |
|
|
448
565
|
| `value` | `readonly string[]` | required | The picked values. |
|
|
449
566
|
| `onChange` | `(value: string[]) => void` | required | Gets the new picked values, in the options' order, without duplicates. |
|
|
450
567
|
|
|
568
|
+
#### `ChoiceChips` (client)
|
|
569
|
+
|
|
570
|
+
Since 0.4.0. One choice from a few, as a real radio group drawn as chips (a status or type filter): native radios under one name, so Tab reaches the checked chip and the arrow keys move and pick. Same look as `Chip`, with the focus ring on the chip. A `<fieldset>` like `ChipGroup`, with the same `label` and `hideLabel`; every native `<fieldset>` prop except `onChange`, `children` and `defaultValue`. Type: `ChoiceChipOption`.
|
|
571
|
+
|
|
572
|
+
| Prop | Type | Default | What it does |
|
|
573
|
+
| --- | --- | --- | --- |
|
|
574
|
+
| `label` | `ReactNode` | required | Names the group. |
|
|
575
|
+
| `hideLabel` | `boolean` | `false` | As on `ChipGroup`, inside a `FilterRow`. |
|
|
576
|
+
| `options` | `readonly ChoiceChipOption<T>[]` | required | `{ value: T; label: ReactNode; disabled?: boolean }` for each chip. |
|
|
577
|
+
| `value` | `T` | required | The picked value. |
|
|
578
|
+
| `onChange` | `(value: T) => void` | required | Gets the picked value. |
|
|
579
|
+
| `name` | `string` | generated | The radios' name, for a form. |
|
|
580
|
+
|
|
451
581
|
#### `RangeSlider` (client)
|
|
452
582
|
|
|
453
|
-
Two thumbs on one track with an editable box at each end, for star rating, length or BPM. A `<fieldset>`; every native `<fieldset>` prop except `onChange` and `
|
|
583
|
+
Two thumbs on one track with an editable box at each end, for star rating, length or BPM. A `<fieldset>`; every native `<fieldset>` prop except `onChange`, `children` and `inputMode`. Type: `RangeSliderValue` (`[number, number | null]`), so `useState<RangeSliderValue>` can pass its setter straight in.
|
|
454
584
|
|
|
455
585
|
| Prop | Type | Default | What it does |
|
|
456
586
|
| --- | --- | --- | --- |
|
|
457
587
|
| `label` | `string` | required | Names the group, and the ends as "Minimum *label*" and "Maximum *label*". |
|
|
458
588
|
| `hideLabel` | `boolean` | `false` | For use inside a `FilterRow`, which names the row: the label doesn't show and the fieldset isn't a group of its own (`role="none"`). The ends keep their "Minimum *label*" and "Maximum *label*" names. |
|
|
459
|
-
| `min`, `max` | `number` | required | The bounds. |
|
|
460
|
-
| `step` | `number` | `1` | Step between values. Typed values snap to it. |
|
|
589
|
+
| `min`, `max` | `number` | required | The bounds. Given the wrong way round (`max` below `min`), they swap (since 0.4.0). |
|
|
590
|
+
| `step` | `number` | `1` | Step between values. Typed values snap to it. A step of `0`, below `0` or not finite counts as `1` (since 0.4.0), so `onChange` never gets `NaN`. |
|
|
461
591
|
| `value` | `readonly [number, number \| null]` | required | The range. A `null` top means no upper limit. |
|
|
462
592
|
| `onChange` | `(value: [number, number \| null]) => void` | required | Gets the new range. |
|
|
463
593
|
| `openEnded` | `boolean` | `false` | The top end at `max` means "no upper limit": it shows `max+` (like `10+`) and reports `null`. |
|
|
@@ -484,13 +614,23 @@ A titled panel of `FilterRow`s with a live result count and a "Clear filters" bu
|
|
|
484
614
|
| Prop | Type | Default | What it does |
|
|
485
615
|
| --- | --- | --- | --- |
|
|
486
616
|
| `title` | `ReactNode` | required | The heading. It also names the panel and the phone toggle. |
|
|
487
|
-
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `2` | The heading's level. |
|
|
617
|
+
| `headingLevel` | `2 \| 3 \| 4 \| 5 \| 6` | `2` | The heading's level (type `HeadingLevel`). |
|
|
488
618
|
| `resultCount` | `ReactNode` | none | Shown in a polite live region, so each new count is announced. |
|
|
489
619
|
| `active` | `boolean` | `false` | Whether any filter is set. |
|
|
490
620
|
| `onClear` | `() => void` | none | The clear button's action. The button shows only when `active` is true and this is set. |
|
|
491
621
|
| `clearLabel` | `ReactNode` | `"Clear filters"` | The clear button's text. |
|
|
492
622
|
| `defaultOpen` | `boolean` | `false` | Whether the rows start open on phones. |
|
|
493
623
|
|
|
624
|
+
### Meta
|
|
625
|
+
|
|
626
|
+
#### `JsonLd`
|
|
627
|
+
|
|
628
|
+
schema.org structured data in a `<script type="application/ld+json">`. Every `<` in the output is escaped, so a string in the data can't close the tag. Every native `<script>` prop except `children`, `dangerouslySetInnerHTML`, `type` and `src` (`id` and `nonce` pass through).
|
|
629
|
+
|
|
630
|
+
| Prop | Type | Default | What it does |
|
|
631
|
+
| --- | --- | --- | --- |
|
|
632
|
+
| `data` | `Record<string, unknown>` | required | The schema.org object. `@context` defaults to `https://schema.org`; set it in `data` to change it. |
|
|
633
|
+
|
|
494
634
|
### Shell
|
|
495
635
|
|
|
496
636
|
Links in the header and footer are data (type `SiteLinkItem`):
|
|
@@ -499,11 +639,11 @@ Links in the header and footer are data (type `SiteLinkItem`):
|
|
|
499
639
|
type SiteLinkItem = { label: string; href?: string; note?: string };
|
|
500
640
|
```
|
|
501
641
|
|
|
502
|
-
Paths use `next/link`; anything with a scheme (`https:`, `mailto:`) or starting with `//` is a plain `<a
|
|
642
|
+
Paths use `next/link`; anything with a scheme (`https:`, `mailto:`) or starting with `//` is a plain `<a>` (and, since 0.4.0, `/\host`, `\\host` or a URL behind leading spaces, which browsers also read as off-site). An item without `href` shows as plain text. `note` adds a word beside it in small uppercase letters (`{ label: "Sheets", note: "soon" }`). The header dims text-only items and shows the note only on them. The footer shows a note beside a link too.
|
|
503
643
|
|
|
504
644
|
#### `SiteHeader`
|
|
505
645
|
|
|
506
|
-
The dark top bar: brand on the left, the nav, and an actions slot on the right. Every native `<header>` prop
|
|
646
|
+
The dark top bar: brand on the left, the nav, and an actions slot on the right. Every native `<header>` prop except `children`. A Server Component; the nav list inside is `NavLinks`.
|
|
507
647
|
|
|
508
648
|
| Prop | Type | Default | What it does |
|
|
509
649
|
| --- | --- | --- | --- |
|
|
@@ -523,7 +663,7 @@ Next bundles every client component a route imports, rendered or not. So a page
|
|
|
523
663
|
|
|
524
664
|
#### `NavLinks`
|
|
525
665
|
|
|
526
|
-
The `<ul>` of links `SiteHeader` uses, for building your own header. Put it inside a `<nav>`. Every native `<ul>` prop
|
|
666
|
+
The `<ul>` of links `SiteHeader` uses, for building your own header. Put it inside a `<nav>`. Every native `<ul>` prop except `children`. Type: `SiteNavAlign`. It works in Server and Client Components. Since 0.2.0 it is a Server Component with a small client part (in 0.1.0 it is a client component). From a Server Component it behaves like `SiteHeader`'s nav. Inside a Client Component (a header with a menu toggle, say), it renders in the browser with the rest of that component: it merges its classes there, so tailwind-merge ships in that page's bundle.
|
|
527
667
|
|
|
528
668
|
| Prop | Type | Default | What it does |
|
|
529
669
|
| --- | --- | --- | --- |
|
|
@@ -532,17 +672,41 @@ The `<ul>` of links `SiteHeader` uses, for building your own header. Put it insi
|
|
|
532
672
|
|
|
533
673
|
#### `SiteFooter`
|
|
534
674
|
|
|
535
|
-
Link columns, an extra slot, fine print, the haruhime.moe wordmark
|
|
675
|
+
Link columns, an extra slot, fine print, the haruhime.moe wordmark, a GitHub icon link and an optional Discord icon link. Every native `<footer>` prop except `children`. Type: `SiteFooterColumn` (`{ title: string; items: readonly SiteLinkItem[] }`).
|
|
536
676
|
|
|
537
677
|
| Prop | Type | Default | What it does |
|
|
538
678
|
| --- | --- | --- | --- |
|
|
539
679
|
| `columns` | `readonly SiteFooterColumn[]` | `[]` | Each column is a `<nav>` named by its title, which shows above the list. Up to four columns side by side from `sm` up. |
|
|
540
680
|
| `extra` | `ReactNode` | none | Shown above the fine print, e.g. a "clear local data" button. |
|
|
541
681
|
| `finePrint` | `ReactNode` | none | One line of small print, in a `<p>`. |
|
|
542
|
-
| `parentLink` | `boolean` | `true` | Show the haruhime.moe wordmark linking the parent site. With it, the last row holds the wordmark and the
|
|
682
|
+
| `parentLink` | `boolean` | `true` | Show the haruhime.moe wordmark linking the parent site. With it, the last row holds the wordmark and the icons, and the fine print sits above. Without it, the fine print shares the row with the icons. |
|
|
543
683
|
| `parentHref` | `string` | `"https://www.haruhime.moe"` | Where the wordmark links. |
|
|
544
684
|
| `githubHref` | `string \| false` | `"https://github.com/haruhimemoe"` | Where the GitHub icon links. `false` leaves it out. |
|
|
545
685
|
| `githubLabel` | `string` | `"haruhimemoe on GitHub"` | The GitHub link's accessible name. |
|
|
686
|
+
| `discordHref` | `string` | none | Where the Discord icon links, such as your server's invite (`https://discord.gg/...`). Without it there is no Discord icon. The icon sits before the GitHub icon at the same size. It stays white (`text-c1`) and dims on hover instead of changing color, since Discord's brand guidelines ask that the logo not be recolored. Since 0.3.0. |
|
|
687
|
+
| `discordLabel` | `string` | `"Discord"` | The Discord link's accessible name. Since 0.4.0. |
|
|
688
|
+
|
|
689
|
+
#### `LinkTabs`
|
|
690
|
+
|
|
691
|
+
Since 0.4.0. A row of link tabs (Pools / Maps, All / Hidden): a named `<nav>` with a list of pill links. The current one gets `aria-current="page"` and a `b3` pill (underlined in forced colors mode). These are links, not ARIA tabs, since each loads its own URL. A Server Component: you say which is current. Every native `<nav>` prop except `children`. Type: `LinkTabItem`.
|
|
692
|
+
|
|
693
|
+
| Prop | Type | Default | What it does |
|
|
694
|
+
| --- | --- | --- | --- |
|
|
695
|
+
| `label` | `string` | required | The nav landmark's accessible name. |
|
|
696
|
+
| `items` | `readonly LinkTabItem[]` | required | `{ href: string; label: ReactNode; current?: boolean }` for each tab, each with its own href. |
|
|
697
|
+
|
|
698
|
+
#### `HeaderMenu` (client)
|
|
699
|
+
|
|
700
|
+
Since 0.4.0. The header's account menu: a button (an avatar and a name, say) that shows a small panel of links and extra controls such as a sign-out button. It is a disclosure, not an ARIA menu: the button has `aria-expanded` and `aria-controls`, and Tab moves through the panel. Escape closes it and puts focus back on the button; a click outside it, a click on one of its links, or focus leaving it closes it too. Every native `<div>` prop except `children`, for the wrapper. Type: `HeaderMenuItem`.
|
|
701
|
+
|
|
702
|
+
| Prop | Type | Default | What it does |
|
|
703
|
+
| --- | --- | --- | --- |
|
|
704
|
+
| `label` | `ReactNode` | required | The button's content. |
|
|
705
|
+
| `items` | `readonly HeaderMenuItem[]` | `[]` | `{ href: string; label: ReactNode }` links, top to bottom. |
|
|
706
|
+
| `children` | `ReactNode` | none | Shown after the links, such as a sign-out button. |
|
|
707
|
+
| `align` | `"start" \| "end"` | `"end"` | Which edge of the button the panel lines up with. |
|
|
708
|
+
| `buttonLabel` | `string` | none | The button's accessible name, when its content is only an image. |
|
|
709
|
+
| `buttonClassName` | `string` | none | Classes for the button, merged last. |
|
|
546
710
|
|
|
547
711
|
#### `PageShell`
|
|
548
712
|
|
|
@@ -557,6 +721,78 @@ The page frame: a skip link, the header, `<main>` and the footer, with the foote
|
|
|
557
721
|
| `mainId` | `string` | `"main"` | `<main>`'s id, which the skip link targets. |
|
|
558
722
|
| `mainClassName` | `string` | none | Extra classes for `<main>`, e.g. `"max-w-7xl"`. |
|
|
559
723
|
|
|
724
|
+
### osu!
|
|
725
|
+
|
|
726
|
+
Since 0.4.0. Display pieces for beatmaps and mod pools. They take plain values (no osu! API types) and are Server Components.
|
|
727
|
+
|
|
728
|
+
#### `StarRating`
|
|
729
|
+
|
|
730
|
+
A star-rating pill colored on osu!'s difficulty spectrum: "★ 5.23" on the rating's color, dark text up to 6.5 and pale yellow above. The spectrum is osu!'s own, the same at every `--hue`, so the pill sets its colors inline. Screen readers hear "5.23 stars" and the `label` after it. Every native `<span>` prop except `children`; a `style` you pass merges over the colors.
|
|
731
|
+
|
|
732
|
+
| Prop | Type | Default | What it does |
|
|
733
|
+
| --- | --- | --- | --- |
|
|
734
|
+
| `value` | `number` | required | The rating, shown with two decimals. `NaN` shows as "–" on grey. |
|
|
735
|
+
| `label` | `ReactNode` | none | Read after the rating by screen readers, e.g. "with HR" (what a `title` tells mouse users). |
|
|
736
|
+
| `unit` | `string` | `"stars"` | The word read after the number. |
|
|
737
|
+
|
|
738
|
+
#### `BeatmapStats`
|
|
739
|
+
|
|
740
|
+
A beatmap's CS, AR, OD, HP, BPM and length as a compact `<dl>`, in that order. Stats you leave out (or that aren't finite) don't show. CS, AR, OD, HP and BPM are `<abbr>`s titled with their full names. Every native `<dl>` prop except `children`. Type: `BeatmapStatKey`.
|
|
741
|
+
|
|
742
|
+
| Prop | Type | Default | What it does |
|
|
743
|
+
| --- | --- | --- | --- |
|
|
744
|
+
| `cs`, `ar`, `od`, `hp` | `number \| null` | none | Shown with at most one decimal. |
|
|
745
|
+
| `bpm` | `number \| null` | none | Shown whole. |
|
|
746
|
+
| `lengthSeconds` | `number \| null` | none | Shown as `m:ss`, or `h:mm:ss` from an hour. |
|
|
747
|
+
| `labels` | `Partial<Record<BeatmapStatKey, ReactNode>>` | none | Replaces a label (`{ length: "Länge" }`). |
|
|
748
|
+
|
|
749
|
+
#### `ModBadge`
|
|
750
|
+
|
|
751
|
+
A mod pool slot's pill (`NM1`, `HD2`, `TB`), colored by the first two letters: NM sky, HD amber, HR rose, DT and NC violet, FM emerald, TB orange, with dark text. Anything else is a `b3` pill. Every native `<span>` prop; `children` replace the text, and `className` recolors it (`bg-pink-300` for a custom bucket).
|
|
752
|
+
|
|
753
|
+
| Prop | Type | Default | What it does |
|
|
754
|
+
| --- | --- | --- | --- |
|
|
755
|
+
| `mod` | `string` | required | The mod or slot label. |
|
|
756
|
+
|
|
757
|
+
### Tables
|
|
758
|
+
|
|
759
|
+
Since 0.4.0. `Table`, `THead`, `TBody`, `Th` and `Td` give a data table the apps' look: full width, small left-aligned text, muted capitals in the head and a rule above each body row. They are Server Components, and each takes its element's native props, `className` (merged last) and `ref`. Use plain `<tr>` for rows.
|
|
760
|
+
|
|
761
|
+
```tsx
|
|
762
|
+
<Table caption="The pool's maps" hideCaption>
|
|
763
|
+
<THead>
|
|
764
|
+
<tr>
|
|
765
|
+
<Th>Slot</Th>
|
|
766
|
+
<Th numeric>Stars</Th>
|
|
767
|
+
</tr>
|
|
768
|
+
</THead>
|
|
769
|
+
<TBody>
|
|
770
|
+
{slots.map((slot) => (
|
|
771
|
+
<tr key={slot.label}>
|
|
772
|
+
<Th scope="row">{slot.label}</Th>
|
|
773
|
+
<Td numeric>{slot.stars}</Td>
|
|
774
|
+
</tr>
|
|
775
|
+
))}
|
|
776
|
+
</TBody>
|
|
777
|
+
</Table>
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
| Component | Extra props | What it renders |
|
|
781
|
+
| --- | --- | --- |
|
|
782
|
+
| `Table` | `caption?: ReactNode`, `hideCaption?: boolean` (default `false`), `wrapperClassName?: string` | A `<div>` that scrolls sideways on phones, around the `<table>`. `className` and `ref` go on the `<table>`. The caption names the table; `hideCaption` keeps it for screen readers only, when a heading already shows. |
|
|
783
|
+
| `THead` | none | A `<thead>` in `c3`, `text-xs`, uppercase. |
|
|
784
|
+
| `TBody` | none | A `<tbody>` whose rows get a `b4` top border. Add `[&>tr]:align-top` for rows of mixed height. |
|
|
785
|
+
| `Th` | `numeric?: boolean` | A `<th>` with `scope="col"` by default. With `scope="row"` it is a row's heading, in bold `c1`. |
|
|
786
|
+
| `Td` | `numeric?: boolean` | A `<td>`. `numeric` lines up digits (`tabular-nums`). |
|
|
787
|
+
|
|
788
|
+
Cells get `py-2 pr-3`, and the last cell of a row no right padding.
|
|
789
|
+
|
|
790
|
+
### Utilities
|
|
791
|
+
|
|
792
|
+
#### `cx`
|
|
793
|
+
|
|
794
|
+
Since 0.4.0. `cx(...classes: ClassValue[]): string` is the class merger every component uses: it skips falsy values and resolves Tailwind conflicts with tailwind-merge, so a later class wins (`cx("w-full", cond && "w-auto")`). Type: `ClassValue` (`string | false | null | undefined | 0`). It imports tailwind-merge, so a client file that uses it sends tailwind-merge to the browser.
|
|
795
|
+
|
|
560
796
|
## Accessibility
|
|
561
797
|
|
|
562
798
|
- Every component is checked in the test suite with axe-core's WCAG 2.0, 2.1 and 2.2 A and AA rules (all but color contrast, which needs a real browser). Interactive ones also have keyboard tests. The tests also calculate the contrast figures under Setup (`c1` on `h2`, `h1` on `b4`, and the `--h1-l` and `--h2-l` values).
|
|
@@ -568,7 +804,7 @@ The page frame: a skip link, the header, `<main>` and the footer, with the foote
|
|
|
568
804
|
- `CopyButton` announces "Copied." (or the failure) through an `<output>`, on every press.
|
|
569
805
|
- `Pagination` moves focus to its "Page X of Y" text when the link you pressed goes away on the first or last page.
|
|
570
806
|
- `SiteHeader` marks the current page with `aria-current`. `PageShell` starts with a skip link to `<main>`.
|
|
571
|
-
- `GitHubIcon`
|
|
807
|
+
- `DiscordIcon` and `GitHubIcon` are hidden from screen readers by default: give the link around each one an `aria-label`, as `SiteFooter` does.
|
|
572
808
|
- You supply the text, so you also supply labels: give icon-only buttons an `aria-label`, and keep `label` props meaningful.
|
|
573
809
|
|
|
574
810
|
## Compatibility
|
|
@@ -584,7 +820,7 @@ The page frame: a skip link, the header, `<main>` and the footer, with the foote
|
|
|
584
820
|
|
|
585
821
|
## Changelog and contributing
|
|
586
822
|
|
|
587
|
-
See [CHANGELOG.md](./CHANGELOG.md) for what changed in each version and [CONTRIBUTING.md](./CONTRIBUTING.md) to work on the package. Report security issues as described in [SECURITY.md](./SECURITY.md).
|
|
823
|
+
See [CHANGELOG.md](./CHANGELOG.md) for what changed in each version and [CONTRIBUTING.md](./CONTRIBUTING.md) to work on the package. Report security issues as described in [SECURITY.md](./SECURITY.md). Bring questions and feedback to the haruhime.moe [Discord server](https://discord.gg/bKy9kjMV4y).
|
|
588
824
|
|
|
589
825
|
## License
|
|
590
826
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/actions/AsyncButton.tsx
|
|
3
|
+
* @desc A button that runs an async action and says how it went in an <output> beside it (a
|
|
4
|
+
* polite live region), like the apps' refresh and retry buttons. While the action runs the
|
|
5
|
+
* button stays focusable but ignores presses (aria-disabled), and can show a pending label.
|
|
6
|
+
* A rejected action shows the failure message in rose.
|
|
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 ButtonProps } from "../basics/Button.js";
|
|
13
|
+
/** Every Button prop except onClick, plus the action and what to say while and after it runs. */
|
|
14
|
+
export type AsyncButtonProps = Omit<ButtonProps, "onClick"> & {
|
|
15
|
+
/** Runs on press. What it returns (or resolves to) is shown as the result, e.g. "Done.". */
|
|
16
|
+
action: () => ReactNode | Promise<ReactNode>;
|
|
17
|
+
/** The button's text while the action runs. Defaults to the button's children. */
|
|
18
|
+
pendingLabel?: ReactNode;
|
|
19
|
+
/** Shown when the action throws or rejects: a message, or a function of the error. */
|
|
20
|
+
failedMessage?: ReactNode | ((error: unknown) => ReactNode);
|
|
21
|
+
/** Classes for the wrapper around the button and its status. */
|
|
22
|
+
wrapperClassName?: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @function AsyncButton
|
|
26
|
+
* @param props {AsyncButtonProps} the action, an optional pending label and failure message, plus
|
|
27
|
+
* Button props (`className` and `ref` go on the button)
|
|
28
|
+
* @returns {JSX.Element} the button and an `<output>` that announces the action's result
|
|
29
|
+
*/
|
|
30
|
+
export declare function AsyncButton({ action, pendingLabel, failedMessage, wrapperClassName, className, children, ...props }: AsyncButtonProps): import("react").JSX.Element;
|