@haruhimemoe/ui 0.11.2 → 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 +29 -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.js +3 -2
- 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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.12.0] - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Motion tokens in `theme.css`: `duration-short` (150ms), `duration-medium` (250ms), `duration-long` (400ms), `ease-standard`, `ease-enter` and `ease-exit`. Every `transition-*` now reads `duration-short` and `ease-standard`, the values it had.
|
|
14
|
+
- One reduced-motion rule: under `prefers-reduced-motion: reduce` every animation and transition finishes in 0.01ms and smooth scrolling turns off, in the kit and in the app's own markup. `data-motion="essential"` keeps an element's motion.
|
|
15
|
+
- High contrast: under `prefers-contrast: more`, `c2`, `c3`, `c4` and `h1` get 8 points lighter and `h2` 4 points darker, through `--contrast-lift` (`0%` opts out).
|
|
16
|
+
- The `coarse:` variant (`@media (pointer: coarse)`), for a touchscreen as the main pointer.
|
|
17
|
+
- `useMotionAllowed()` (client): true when the visitor allows motion, false on the server, during hydration and under reduced motion, live. Lifted from haruhime.moe's homepage banner.
|
|
18
|
+
- `Text` and `textClasses`: a line of text in six tones (`default` c2, `muted` c3, `subtle` c4, `error`, `warning` amber-300, `success`) and three sizes, for the error, warning and muted text apps wrote by hand.
|
|
19
|
+
- `download` on `ButtonLink` and `TextLink` renders a plain `<a download>` (no prefetch, no client routing), for any value but `false`.
|
|
20
|
+
- `hideLabel` on `TextInput`, `Select`, `Textarea`, `Checkbox` and `RadioGroup`: the label (legend for RadioGroup) is hidden visually but still names the control, with no gap left above it.
|
|
21
|
+
- `ModBadge` `color` (`ModBadgeColor`): any of the six bucket colors, the ten @haruhimemoe/pool palette colors or `neutral`, overriding the color the mod's letters pick. A value outside the list falls back to the bucket.
|
|
22
|
+
- 44px touch targets on a coarse pointer: chips and choice chips, checkbox and radio rows, Disclosure and HeaderMenu buttons and menu items, Tabs and LinkTabs, palette rows, FilterPanel's toggle, code copy buttons and ContentNav links.
|
|
23
|
+
- Under more contrast, cards, chips, neutral badges and the tabs track get a `c4` edge, and code blocks, menus, the palette and its key hints a `c4` border. In forced colors, cards, chips, badges, mod badges and the tabs track keep a 1px border; an unavailable chip is GrayText.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- `c2`, `c3`, `c4`, `h1` and `h2` are defined through lightness variables (`--c2-l`, `--c3-l` and `--c4-l` join `--h1-l` and `--h2-l`) plus the contrast lift. The default colors are unchanged.
|
|
28
|
+
- `Notice`, field errors, `CharCounter`, `AsyncButton`'s failure text and the palette's input error use Text's tones, so they get one step lighter under more contrast.
|
|
29
|
+
- **Button no longer stretches** in a flex column or a grid cell: `buttonClasses()` adds `w-fit` (Button, ButtonLink, AsyncButton, CopyButton, Pagination). Pass `className="w-full"` where a full-width button is wanted. `self-start` on buttons is now a no-op.
|
|
30
|
+
- Buttons at `md` are 44px tall on touch screens (`coarse:h-11`). Secondary and ghost buttons get a `c4` edge under more contrast; every button keeps a 1px border in forced colors, and a disabled one uses the system's GrayText there.
|
|
31
|
+
- `CopyMarkdownButton` follows Button's classes again (a test now pins its copy).
|
|
32
|
+
- `BrandPage`'s file links are `TextLink download`.
|
|
33
|
+
- Fields are 44px tall with 16px text on touch screens (no iOS zoom), checkbox and radio rows 44px tall, and a field's border is `c4` under more contrast.
|
|
34
|
+
- `RangeSlider`'s value boxes also become 44px tall with 16px text on touch screens: they use `fieldClasses`, the same field look.
|
|
35
|
+
|
|
9
36
|
## [0.11.2] - 2026-10-04
|
|
10
37
|
|
|
11
38
|
### Fixed
|
|
@@ -165,7 +192,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
165
192
|
- `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`).
|
|
166
193
|
- 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).
|
|
167
194
|
|
|
168
|
-
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.
|
|
195
|
+
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.12.0...HEAD
|
|
196
|
+
[0.12.0]: https://github.com/haruhimemoe/ui/compare/v0.11.2...v0.12.0
|
|
169
197
|
[0.11.2]: https://github.com/haruhimemoe/ui/compare/v0.11.1...v0.11.2
|
|
170
198
|
[0.11.1]: https://github.com/haruhimemoe/ui/compare/v0.11.0...v0.11.1
|
|
171
199
|
[0.11.0]: https://github.com/haruhimemoe/ui/compare/v0.10.0...v0.11.0
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ React components for the haruhime.moe osu! tools on Next.js. It ships the osu!-w
|
|
|
6
6
|
|
|
7
7
|
See every component in its states at [haruhime.moe/ui](https://www.haruhime.moe/ui). The page names the version it runs.
|
|
8
8
|
|
|
9
|
-
This README describes version 0.
|
|
9
|
+
This README describes version 0.12.0. Anything marked "since 0.12.0" is not in 0.11.x, anything marked "since 0.11.0" is not in 0.10.0, anything marked "since 0.10.0" is not in 0.9.0, anything marked "since 0.9.0" is not in 0.8.0, anything marked "since 0.8.0" is not in 0.7.0, anything marked "since 0.7.0" is not in 0.6.0, anything marked "since 0.6.0" is not in 0.5.0, anything marked "since 0.5.0" is not in 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.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -77,13 +77,19 @@ const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito", display:
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
At some hues the defaults drop below 4.5:1 contrast, so check yours. Two variables fix it:
|
|
80
|
+
At some hues the defaults drop below 4.5:1 contrast, so check yours. Two variables fix it (and `--c2-l`, `--c3-l`, `--c4-l` set the content steps' lightness, default `90%`, `80%`, `70%`):
|
|
81
81
|
|
|
82
82
|
- `--h2-l` sets the lightness of `h2` (default `45%`). White text on `h2` (primary buttons, the skip link) is under 4.5:1 for hues from about 23 to 205. Use `42%` at hue 200, `35%` at hue 150, or `31%` for any hue.
|
|
83
83
|
- `--h1-l` sets the lightness of `h1` (default `76%` since 0.7.0, `70%` before). At `76%`, `h1` text on `b5` (links in cards and prose) is at or above 4.5:1 at every hue. On `b4` ("Clear filters" in a filter panel) it dips under for hues from about 238 to 248. Use `77%` there.
|
|
84
84
|
|
|
85
85
|
The theme is dark only (`color-scheme: dark`).
|
|
86
86
|
|
|
87
|
+
**5. Motion, contrast and touch (since 0.12.0).** The theme follows three browser settings. There is no in-app toggle.
|
|
88
|
+
|
|
89
|
+
- **Reduced motion.** When the visitor asks for reduced motion (`prefers-reduced-motion: reduce`), every animation and transition finishes in 0.01ms and smooth scrolling turns off, in the kit and in your own markup. Put `data-motion="essential"` on an element whose movement is the message (a progress indicator): it and its children keep their motion. Tokens: `duration-short` (150ms: hovers, presses, color changes, and the default for every `transition-*`), `duration-medium` (250ms: panels, dialogs), `duration-long` (400ms: large surfaces), with `ease-standard` (the default), `ease-enter` and `ease-exit`. For motion JavaScript drives (an autoplay video, a canvas), read `useMotionAllowed()`.
|
|
90
|
+
- **More contrast.** Under `prefers-contrast: more`, `c2`, `c3`, `c4` and `h1` get 8 points lighter and `h2` 4 points darker. Cards, chips, secondary and ghost buttons, neutral badges and the tabs track get a `c4` edge; fields, code blocks and menus swap their `b3` border for `c4`. The lift adds to your `--h1-l` and `--h2-l`. Set `--contrast-lift: 0%` on `:root` to opt out.
|
|
91
|
+
- **Touch.** `coarse:` is a variant for a touchscreen as the main pointer (`@media (pointer: coarse)`, the same query as Tailwind's `pointer-coarse:`). Buttons at `md`, chips, checkbox and radio rows, tabs, menu items, palette rows and fields grow to 44px there, and fields use 16px text so iOS Safari doesn't zoom in. Undo it for one control with a class like `coarse:h-9`. A touchscreen laptop driven by its trackpad keeps the dense layout.
|
|
92
|
+
|
|
87
93
|
## Example
|
|
88
94
|
|
|
89
95
|
```tsx
|
|
@@ -180,6 +186,8 @@ A pill button. Every native `<button>` prop.
|
|
|
180
186
|
| `size` | `"md" \| "lg"` | `"md"` | Height, padding and text size. |
|
|
181
187
|
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Never submits a form unless you ask for `"submit"`. |
|
|
182
188
|
|
|
189
|
+
Since 0.12.0 a button sits at its content width (`w-fit`), also in a flex column or a grid cell; pass `className="w-full"` for a full-width one. At `md` it is 44px tall on a coarse pointer.
|
|
190
|
+
|
|
183
191
|
#### `ButtonLink`
|
|
184
192
|
|
|
185
193
|
A link that looks like `Button`. Every `next/link` prop (`href`, `prefetch`, `replace`, `scroll`, `target`, `rel`...), plus `variant` and `size` as on `Button`.
|
|
@@ -187,6 +195,13 @@ A link that looks like `Button`. Every `next/link` prop (`href`, `prefetch`, `re
|
|
|
187
195
|
- 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).
|
|
188
196
|
- 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.
|
|
189
197
|
|
|
198
|
+
Since 0.12.0, `download` (any value but `false`) renders a plain `<a download>`: no prefetch of an API route or a large file, no client routing. An external href also renders a plain `<a>`, with `rel="noreferrer"` in a new tab.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<ButtonLink href="/api/me/export" download variant="secondary">Download my data</ButtonLink>
|
|
202
|
+
<ButtonLink href="https://osu.ppy.sh" target="_blank">osu!</ButtonLink>
|
|
203
|
+
```
|
|
204
|
+
|
|
190
205
|
#### `buttonClasses`
|
|
191
206
|
|
|
192
207
|
`buttonClasses({ variant?, size?, className? }): string` returns the `Button` classes, for elements the components don't cover. Types: `ButtonVariant`, `ButtonSize`, `ButtonClassOptions`.
|
|
@@ -233,6 +248,25 @@ A `status` region is only reliably announced when its content changes while it i
|
|
|
233
248
|
|
|
234
249
|
An error notice (`role="alert"`) is announced either way.
|
|
235
250
|
|
|
251
|
+
#### `Text` and `textClasses` (since 0.12.0)
|
|
252
|
+
|
|
253
|
+
A line of text in a tone and a size, instead of hand-written color classes. Every native `<p>` prop. Types: `TextTone`, `TextSize`, `TextClassOptions`.
|
|
254
|
+
|
|
255
|
+
| Prop | Type | Default | What it does |
|
|
256
|
+
| --- | --- | --- | --- |
|
|
257
|
+
| `tone` | `"default" \| "muted" \| "subtle" \| "error" \| "warning" \| "success"` | `"default"` | `c2`, `c3`, `c4`, rose, amber or emerald. The status tones get one step lighter under more contrast. |
|
|
258
|
+
| `size` | `"xs" \| "sm" \| "base"` | `"sm"` | The text size. |
|
|
259
|
+
| `bold` | `boolean` | `false` | `font-bold`. |
|
|
260
|
+
| `as` | `"p" \| "span" \| "div"` | `"p"` | The element. |
|
|
261
|
+
|
|
262
|
+
`textClasses({ tone, size, bold, className })` returns the same classes for an element `Text` can't be (an `<output>`, a `<time>`, a class map). `Text` is styling only, not a live region: a message that appears after an action still goes in `Notice live` or a mounted `role="status"`. Error and warning text must say what is wrong in words.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<Text tone="muted">Last updated Oct 4.</Text>
|
|
266
|
+
<Text role="alert" tone="error" bold>That pack key is not valid.</Text>
|
|
267
|
+
<output className={textClasses({ tone: "success" })}>Saved.</output>
|
|
268
|
+
```
|
|
269
|
+
|
|
236
270
|
#### `Prose`
|
|
237
271
|
|
|
238
272
|
Long-form typography for MDX, docs and legal pages. A `max-w-3xl` `<div>` that styles the `h2`, `h3`, `p`, `a`, `strong`, `ul`, `ol`, `li`, `code`, `pre`, `hr` and `table` elements inside it. Every native `<div>` prop. A `pre` scrolls sideways, so give it `tabIndex={0}` (through your Markdown renderer's `components` map) so keyboard users can reach the scroll; CSS can't add that.
|
|
@@ -281,9 +315,11 @@ Since 0.4.0. A text link: `next/link` inside the app, a plain `<a>` off-site (wi
|
|
|
281
315
|
<TextLink href={`/packs/${pack.id}`} variant="plain">{pack.name}</TextLink>
|
|
282
316
|
```
|
|
283
317
|
|
|
318
|
+
`download` works the same way (since 0.12.0): `<TextLink href="/brand/haruhime-palette.json" download>Download palette (JSON)</TextLink>`.
|
|
319
|
+
|
|
284
320
|
#### `linkClasses`
|
|
285
321
|
|
|
286
|
-
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`.
|
|
322
|
+
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`. Downloads use `TextLink download` since 0.12.0.
|
|
287
323
|
|
|
288
324
|
#### `Badge`
|
|
289
325
|
|
|
@@ -312,6 +348,21 @@ The panels are yours. `tabId(prefix, tab)` and `tabPanelId(prefix, tab)` (server
|
|
|
312
348
|
<div role="tabpanel" id={tabPanelId("ed", tab)} aria-labelledby={tabId("ed", tab)}>...</div>
|
|
313
349
|
```
|
|
314
350
|
|
|
351
|
+
#### `useMotionAllowed` (client, since 0.12.0)
|
|
352
|
+
|
|
353
|
+
`useMotionAllowed()` is `true` when the visitor allows motion. It is `false` on the server, during hydration, without `matchMedia`, and while the visitor asks for reduced motion, and it follows the setting live. Use it for motion JavaScript drives (an autoplay video, a canvas, `element.animate()`). CSS transitions and animations already stop through the theme, so components don't need it for those.
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
"use client";
|
|
357
|
+
|
|
358
|
+
import { useMotionAllowed } from "@haruhimemoe/ui";
|
|
359
|
+
|
|
360
|
+
export function Hero() {
|
|
361
|
+
const motion = useMotionAllowed();
|
|
362
|
+
return motion ? <video autoPlay loop muted playsInline src="/loop.webm" /> : null;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
315
366
|
### Forms
|
|
316
367
|
|
|
317
368
|
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.
|
|
@@ -322,11 +373,12 @@ Shared props (type `FieldProps`), taken by `TextInput`, `Textarea`, `Select` and
|
|
|
322
373
|
| --- | --- | --- | --- |
|
|
323
374
|
| `id` | `string` | required | The control's id. The label points at it. The hint gets `<id>-hint` and the error `<id>-error`. |
|
|
324
375
|
| `label` | `ReactNode` | required | The visible label. |
|
|
376
|
+
| `hideLabel` | `boolean` | `false` | Since 0.12.0. Hides the label visually; it still names the control. No gap is left above the control. |
|
|
325
377
|
| `hint` | `ReactNode` | none | Help text in a `<div>`, linked with `aria-describedby`. On `Checkbox` the hint sits inline inside the label, so keep it to text there. |
|
|
326
378
|
| `error` | `ReactNode` | none | Error text in a `role="status"` `<div>` (`text-rose-300`; `role="alert"` before 0.7.0), so a list of errors is fine. Sets `aria-invalid` and links the text with `aria-describedby`. |
|
|
327
379
|
| `wrapperClassName` | `string` | none | Classes for the wrapper around the label, control, hint and error, for layout (`min-w-48 flex-1`). |
|
|
328
380
|
|
|
329
|
-
`className` goes on the control itself. Your own `aria-describedby` is kept after the hint and error ids.
|
|
381
|
+
`className` goes on the control itself. Your own `aria-describedby` is kept after the hint and error ids. Fields are 44px tall with 16px text on a coarse pointer (since 0.12.0).
|
|
330
382
|
|
|
331
383
|
#### `TextInput`
|
|
332
384
|
|
|
@@ -355,6 +407,7 @@ Since 0.4.0. A native radio group on the `Checkbox` look: a `<fieldset>` named b
|
|
|
355
407
|
| Prop | Type | Default | What it does |
|
|
356
408
|
| --- | --- | --- | --- |
|
|
357
409
|
| `label` | `ReactNode` | required | The legend. |
|
|
410
|
+
| `hideLabel` | `boolean` | `false` | Since 0.12.0. Hides the legend visually; it still names the group. |
|
|
358
411
|
| `options` | `readonly RadioOption[]` | required | `{ value: string; label: ReactNode; hint?: ReactNode; disabled?: boolean }` for each radio. |
|
|
359
412
|
| `value` / `defaultValue` | `string` | none | The picked value, held by you (`value`, with `onChange`) or by the group (`defaultValue`). |
|
|
360
413
|
| `onChange` | `(value: string) => void` | none | Gets the picked option's value. |
|
|
@@ -694,6 +747,8 @@ One labelled row: the label above the controls on phones, in a `w-28` column on
|
|
|
694
747
|
| --- | --- | --- | --- |
|
|
695
748
|
| `label` | `ReactNode` | required | The row's label. Also names the group. |
|
|
696
749
|
|
|
750
|
+
Not the same as the fields' `hideLabel`: inside a `FilterRow`, a `ChipGroup`, `ChoiceChips` or `RangeSlider`'s own `hideLabel` drops its label entirely, because the row names it instead.
|
|
751
|
+
|
|
697
752
|
#### `FilterPanel` (client)
|
|
698
753
|
|
|
699
754
|
A titled panel of `FilterRow`s with a live result count and a "Clear filters" button. On phones the rows fold behind a button next to the title; from `sm` up they always show. Every native `<section>` prop except `title`.
|
|
@@ -963,11 +1018,12 @@ A beatmap's CS, AR, OD, HP, BPM and length as a compact `<dl>`, in that order. S
|
|
|
963
1018
|
|
|
964
1019
|
#### `ModBadge`
|
|
965
1020
|
|
|
966
|
-
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
|
|
1021
|
+
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. A custom bucket picks its color with `color`: `<ModBadge mod={code} color={PALETTE[entry.color].toLowerCase() as ModBadgeColor} />`. `className` still recolors it (`bg-pink-300`) when `color`'s fixed list doesn't fit.
|
|
967
1022
|
|
|
968
1023
|
| Prop | Type | Default | What it does |
|
|
969
1024
|
| --- | --- | --- | --- |
|
|
970
1025
|
| `mod` | `string` | required | The mod or slot label. |
|
|
1026
|
+
| `color` | `ModBadgeColor` | from `mod` | Since 0.12.0. Overrides the bucket color: `sky`, `amber`, `rose`, `violet`, `emerald`, `orange` (the buckets), `green`, `teal`, `pink`, `lime`, `cyan`, `fuchsia`, `yellow`, `red`, `indigo`, `stone` (@haruhimemoe/pool's PALETTE, lowercased), or `neutral`. |
|
|
971
1027
|
|
|
972
1028
|
#### `PlayerCard`
|
|
973
1029
|
|
|
@@ -1283,8 +1339,10 @@ Since 0.4.0. `cx(...classes: ClassValue[]): string` is the class merger every co
|
|
|
1283
1339
|
The target is WCAG 2.2 AA. House rules, which every component follows and your own code around them should too:
|
|
1284
1340
|
|
|
1285
1341
|
- **Focus is always visible, and always the same.** The theme draws a 2px `h1` outline, offset 2px, on `:focus-visible`. Fields keep it (since 0.7.0; before, they swapped it for a 1px border change) and add an `h1` border. `RangeSlider` thumbs show a solid `h1` ring instead and keep a transparent outline, so Windows high contrast mode (forced colors) still paints one. Never `outline: none`.
|
|
1286
|
-
- **Targets are 24px or more** (WCAG 2.2 2.5.8): buttons are `h-9
|
|
1342
|
+
- **Targets are 24px or more, 44px on coarse pointers** (WCAG 2.2 2.5.8, and 2.5.5 on touch): buttons are `h-9` (`h-11` on a touchscreen since 0.12.0), chips and tabs 24px tall, slider thumbs 24px (since 0.7.0), checkboxes and radios 24px and the `Disclosure` and `HeaderMenu` buttons at least 24px tall (since 0.8.0). On a coarse pointer (since 0.12.0) buttons, chips, checkbox and radio rows, tabs, menu items, palette rows, the filter panel's toggle, code copy buttons, content nav links and fields grow to 44px. Text links in running text stay as they are (2.5.8 exempts inline targets).
|
|
1287
1343
|
- **Contrast is computed, not eyeballed.** The test suite checks `c1` on `h2`, `h1` on `b4` and `b5`, and the hue overrides under Setup; `StarRating` picks its text color by contrast. Text is never dimmed with `opacity` (a `text-c4` line at 70% opacity drops under 4.5:1): use a lighter palette step instead.
|
|
1344
|
+
- **Motion follows the browser.** Under `prefers-reduced-motion: reduce` every transition and animation finishes in 0.01ms (since 0.12.0); `data-motion="essential"` is the one way out. `useMotionAllowed` covers motion JavaScript drives.
|
|
1345
|
+
- **High contrast.** Under `prefers-contrast: more` the text steps lighten, `h2` darkens and shapes that rely on a background shade get a `c4` edge (since 0.12.0). Forced colors (Windows high contrast) keep a 1px border on buttons, cards, chips and badges.
|
|
1288
1346
|
- **Color never carries meaning alone.** Accent links are underlined, a pressed `Chip` is `aria-pressed`, the current nav link is `aria-current`, `CharCounter` says "over the limit" in words, errors are text.
|
|
1289
1347
|
- **Live regions exist before they speak.** `CopyButton`, `AsyncButton`, `CharCounter live`, `FilterPanel`'s count and `ReportDisclosure`'s outcome render their `<output>` or `role="status"` node up front, empty, and swap the text in. Field errors are `role="status"` (polite); `Notice live tone="error"` is the one `role="alert"`.
|
|
1290
1348
|
- **Focus never falls to the body.** When the control you pressed goes away, focus moves somewhere sensible: `FilterPanel` to its heading, `Pagination` to "Page X of Y", `InlineConfirm` back to its trigger, `ReportDisclosure` to its outcome line. Pending buttons use `aria-disabled`, not `disabled`, so focus stays.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* A rejected action shows the failure message in rose.
|
|
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
|
import { type ButtonProps } from "../basics/Button.js";
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* A rejected action shows the failure message in rose.
|
|
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";
|
|
13
13
|
import { useRef, useState } from "react";
|
|
14
14
|
import { cx } from "../../utils/cx.js";
|
|
15
15
|
import { Button } from "../basics/Button.js";
|
|
16
|
+
import { TEXT_TONES } from "../basics/textTones.js";
|
|
16
17
|
import { StatusOutput } from "./StatusOutput.js";
|
|
17
18
|
import { useLatestStatus } from "./useLatestStatus.js";
|
|
18
19
|
/**
|
|
@@ -44,5 +45,5 @@ export function AsyncButton({ action, pendingLabel, failedMessage = "Something w
|
|
|
44
45
|
setPending(false);
|
|
45
46
|
}
|
|
46
47
|
};
|
|
47
|
-
return (_jsxs("div", { className: cx("flex flex-wrap items-center gap-3", wrapperClassName), children: [_jsx(Button, { "aria-disabled": pending || undefined, onClick: run, className: cx("aria-disabled:cursor-wait aria-disabled:opacity-70", className), ...props, children: pending && pendingLabel !== undefined ? pendingLabel : children }), _jsx(StatusOutput, { run: status?.run ?? null, children: status?.result.failed ? (_jsx("span", { className:
|
|
48
|
+
return (_jsxs("div", { className: cx("flex flex-wrap items-center gap-3", wrapperClassName), children: [_jsx(Button, { "aria-disabled": pending || undefined, onClick: run, className: cx("aria-disabled:cursor-wait aria-disabled:opacity-70", className), ...props, children: pending && pendingLabel !== undefined ? pendingLabel : children }), _jsx(StatusOutput, { run: status?.run ?? null, children: status?.result.failed ? (_jsx("span", { className: TEXT_TONES.error, children: status.result.message })) : (status?.result.message) })] }));
|
|
48
49
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* @file src/components/basics/AutoLink.tsx
|
|
3
3
|
* @desc A link that picks its element from the href (internal): `next/link` for paths inside the
|
|
4
4
|
* app, a plain `<a>` for a string href that leaves it (a scheme like https: or mailto:, or
|
|
5
|
-
* //host)
|
|
6
|
-
*
|
|
5
|
+
* //host) or that is a download (`download` set, even to `""`): no prefetch and no client
|
|
6
|
+
* routing for a file. The plain `<a>` drops next/link's own props, and gets rel="noreferrer"
|
|
7
|
+
* when it opens in a new tab. ButtonLink, TextLink, the nav and the footer all render it.
|
|
7
8
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
9
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
10
11
|
*/
|
|
11
12
|
import Link from "next/link.js";
|
|
12
13
|
import type { ComponentProps } from "react";
|
|
@@ -16,6 +17,9 @@ export type AutoLinkProps = ComponentProps<typeof Link>;
|
|
|
16
17
|
* @function AutoLink
|
|
17
18
|
* @param props {AutoLinkProps} next/link props
|
|
18
19
|
* @returns {JSX.Element} a `next/link` for internal paths (and URL objects), or a plain `<a>` for
|
|
19
|
-
* an external string href, with rel="noreferrer" when `target="_blank"`
|
|
20
|
+
* an external string href or a download, with rel="noreferrer" when `target="_blank"`
|
|
21
|
+
* and no `rel`. `download` only forces the plain `<a>` for a string `href`: a URL-object
|
|
22
|
+
* href still renders `next/link` (next/link's own typed routes don't take `download`
|
|
23
|
+
* either, so that pairing isn't one this component needs to special-case).
|
|
20
24
|
*/
|
|
21
25
|
export declare function AutoLink({ href, target, rel, ...props }: AutoLinkProps): import("react").JSX.Element;
|
|
@@ -3,11 +3,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
* @file src/components/basics/AutoLink.tsx
|
|
4
4
|
* @desc A link that picks its element from the href (internal): `next/link` for paths inside the
|
|
5
5
|
* app, a plain `<a>` for a string href that leaves it (a scheme like https: or mailto:, or
|
|
6
|
-
* //host)
|
|
7
|
-
*
|
|
6
|
+
* //host) or that is a download (`download` set, even to `""`): no prefetch and no client
|
|
7
|
+
* routing for a file. The plain `<a>` drops next/link's own props, and gets rel="noreferrer"
|
|
8
|
+
* when it opens in a new tab. ButtonLink, TextLink, the nav and the footer all render it.
|
|
8
9
|
* @author David @dvhsh (https://dvh.sh)
|
|
9
10
|
* @created Wed Sep 23, 2026
|
|
10
|
-
* @modified
|
|
11
|
+
* @modified Sun Oct 4, 2026
|
|
11
12
|
*/
|
|
12
13
|
import Link from "next/link.js";
|
|
13
14
|
import { isExternalHref } from "../../utils/href.js";
|
|
@@ -15,10 +16,16 @@ import { isExternalHref } from "../../utils/href.js";
|
|
|
15
16
|
* @function AutoLink
|
|
16
17
|
* @param props {AutoLinkProps} next/link props
|
|
17
18
|
* @returns {JSX.Element} a `next/link` for internal paths (and URL objects), or a plain `<a>` for
|
|
18
|
-
* an external string href, with rel="noreferrer" when `target="_blank"`
|
|
19
|
+
* an external string href or a download, with rel="noreferrer" when `target="_blank"`
|
|
20
|
+
* and no `rel`. `download` only forces the plain `<a>` for a string `href`: a URL-object
|
|
21
|
+
* href still renders `next/link` (next/link's own typed routes don't take `download`
|
|
22
|
+
* either, so that pairing isn't one this component needs to special-case).
|
|
19
23
|
*/
|
|
20
24
|
export function AutoLink({ href, target, rel, ...props }) {
|
|
21
|
-
|
|
25
|
+
// `download` counts when set to anything but undefined or false: "" is the attribute's own
|
|
26
|
+
// boolean form. A download is a file, never a page to prefetch or route to.
|
|
27
|
+
const download = props.download !== undefined && props.download !== false;
|
|
28
|
+
if (typeof href === "string" && (isExternalHref(href) || download)) {
|
|
22
29
|
const { as: _as, replace: _replace, scroll: _scroll, shallow: _shallow, passHref: _passHref, prefetch: _prefetch, locale: _locale, legacyBehavior: _legacyBehavior, onNavigate: _onNavigate, transitionTypes: _transitionTypes, ...anchorProps } = props;
|
|
23
30
|
return (_jsx("a", { href: href, target: target, rel: rel ?? (target === "_blank" ? "noreferrer" : undefined), ...anchorProps }));
|
|
24
31
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* so screen readers read it in place with the text around it. Server-safe.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Mon Sep 28, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
8
|
*/
|
|
9
9
|
import type { ComponentProps } from "react";
|
|
10
10
|
/** The badge's colors: neutral b3, accent h1, a warning amber, or a muted outline. */
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from "../../utils/cx.js";
|
|
3
|
-
const BASE = "inline-flex items-center whitespace-nowrap rounded-full px-2 py-0.5 text-xs";
|
|
3
|
+
const BASE = "inline-flex items-center whitespace-nowrap rounded-full px-2 py-0.5 text-xs forced-colors:border";
|
|
4
4
|
// The looks the apps already use: pools' tags and warnings, and its header's beta pill.
|
|
5
5
|
const TONES = {
|
|
6
|
-
neutral: "bg-b3 text-c2",
|
|
6
|
+
neutral: "bg-b3 text-c2 contrast-more:inset-ring contrast-more:inset-ring-c4",
|
|
7
7
|
accent: "bg-h1 font-bold text-b6",
|
|
8
8
|
warning: "bg-amber-300/20 font-bold text-amber-200",
|
|
9
|
-
muted: "border border-b3 bg-b5 font-bold text-c4 uppercase tracking-wide",
|
|
9
|
+
muted: "border border-b3 bg-b5 font-bold text-c4 uppercase tracking-wide contrast-more:border-c4",
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* @function Badge
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so its form fields keep their values. Uncontrolled by default, or controlled with `open`.
|
|
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 `<div>` prop for the wrapper, plus the button's text and the panel. */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so its form fields keep their values. Uncontrolled by default, or controlled with `open`.
|
|
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
|
"use client";
|
|
11
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -26,5 +26,5 @@ export function Disclosure({ summary, children, defaultOpen = false, open, onOpe
|
|
|
26
26
|
setInner(!isOpen);
|
|
27
27
|
onOpenChange?.(!isOpen);
|
|
28
28
|
};
|
|
29
|
-
return (_jsxs("div", { className: cx("flex flex-col gap-2", className), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": isOpen, "aria-controls": panelId, onClick: toggle, className: cx("inline-flex min-h-6 items-center gap-1 self-start font-bold text-c2 text-sm transition-colors hover:text-c1", buttonClassName), children: [summary, _jsx("span", { "aria-hidden": "true", children: isOpen ? "▴" : "▾" })] }), _jsx("div", { id: panelId, hidden: !isOpen, className: panelClassName, children: children })] }));
|
|
29
|
+
return (_jsxs("div", { className: cx("flex flex-col gap-2", className), ...props, children: [_jsxs("button", { type: "button", "aria-expanded": isOpen, "aria-controls": panelId, onClick: toggle, className: cx("inline-flex coarse:min-h-11 min-h-6 items-center gap-1 self-start font-bold text-c2 text-sm transition-colors hover:text-c1", buttonClassName), children: [summary, _jsx("span", { "aria-hidden": "true", children: isOpen ? "▴" : "▾" })] }), _jsx("div", { id: panelId, hidden: !isOpen, className: panelClassName, children: children })] }));
|
|
30
30
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* status region that mounts with its text already inside may not be announced.
|
|
7
7
|
* @author David @dvhsh (https://dvh.sh)
|
|
8
8
|
* @created Wed Sep 23, 2026
|
|
9
|
-
* @modified
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { ComponentProps } from "react";
|
|
12
12
|
/** The notice's color, and its live role: `info` is c3, `warning` amber, `error` rose. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
const
|
|
4
|
-
info: "
|
|
5
|
-
warning: "
|
|
6
|
-
error: "
|
|
2
|
+
import { textClasses } from "./textStyles.js";
|
|
3
|
+
const NOTICE_TONES = {
|
|
4
|
+
info: "muted",
|
|
5
|
+
warning: "warning",
|
|
6
|
+
error: "error",
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* @function Notice
|
|
@@ -13,7 +13,7 @@ const TONES = {
|
|
|
13
13
|
*/
|
|
14
14
|
export function Notice({ tone = "info", live = false, as = "p", className, ...props }) {
|
|
15
15
|
const role = live ? (tone === "error" ? "alert" : "status") : undefined;
|
|
16
|
-
const classes =
|
|
16
|
+
const classes = textClasses({ tone: NOTICE_TONES[tone], className });
|
|
17
17
|
if (as === "div") {
|
|
18
18
|
// Same attributes either way; only the ref's element type differs.
|
|
19
19
|
return _jsx("div", { role: role, className: classes, ...props });
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
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, ReactNode } from "react";
|
|
13
13
|
/** One tab: its id (part of the tab's and panel's ids) and its text. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
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 } from "react/jsx-runtime";
|
|
@@ -40,8 +40,8 @@ export function Tabs({ label, idPrefix, tabs, value, onChange, className, onKeyD
|
|
|
40
40
|
onChange(tab.id);
|
|
41
41
|
document.getElementById(tabId(idPrefix, tab.id))?.focus();
|
|
42
42
|
};
|
|
43
|
-
return (_jsx("div", { role: "tablist", "aria-label": label, onKeyDown: move, className: cx("flex gap-1 rounded-full bg-b4 p-1", className), ...props, children: tabs.map((tab, index) => {
|
|
43
|
+
return (_jsx("div", { role: "tablist", "aria-label": label, onKeyDown: move, className: cx("flex gap-1 rounded-full bg-b4 p-1 contrast-more:inset-ring contrast-more:inset-ring-c4 forced-colors:border", className), ...props, children: tabs.map((tab, index) => {
|
|
44
44
|
const selected = tab.id === value;
|
|
45
|
-
return (_jsx("button", { type: "button", role: "tab", id: tabId(idPrefix, tab.id), "aria-selected": selected, "aria-controls": tabPanelId(idPrefix, tab.id), tabIndex: index === at ? 0 : -1, onClick: () => onChange(tab.id), className: cx("flex-1 rounded-full px-4 py-1.5 font-bold text-sm transition-colors", selected ? "bg-h2 text-c1 forced-colors:underline" : "text-c3 hover:text-c1"), children: tab.label }, tab.id));
|
|
45
|
+
return (_jsx("button", { type: "button", role: "tab", id: tabId(idPrefix, tab.id), "aria-selected": selected, "aria-controls": tabPanelId(idPrefix, tab.id), tabIndex: index === at ? 0 : -1, onClick: () => onChange(tab.id), className: cx("coarse:min-h-11 flex-1 rounded-full px-4 py-1.5 font-bold text-sm transition-colors", selected ? "bg-h2 text-c1 forced-colors:underline" : "text-c3 hover:text-c1"), children: tab.label }, tab.id));
|
|
46
46
|
}) }));
|
|
47
47
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Text.tsx
|
|
3
|
+
* @desc A line of text in one of six tones (body, muted, subtle, error, warning, success) and
|
|
4
|
+
* three sizes. Styling only: not a live region. A message that appears after an action
|
|
5
|
+
* still goes in Notice live, StatusOutput or a mounted role="status". Server-safe.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
11
|
+
import { type TextSize, type TextTone } from "./textStyles.js";
|
|
12
|
+
/** Every native `<p>` prop (including `ref`), plus the element, tone, size and weight. */
|
|
13
|
+
export type TextProps = ComponentProps<"p"> & {
|
|
14
|
+
/** The element: "p" (default), "span" or "div". */
|
|
15
|
+
as?: "p" | "span" | "div" | undefined;
|
|
16
|
+
tone?: TextTone | undefined;
|
|
17
|
+
size?: TextSize | undefined;
|
|
18
|
+
bold?: boolean | undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* @function Text
|
|
22
|
+
* @param props {TextProps} native paragraph props, plus as, tone (default "default"), size
|
|
23
|
+
* (default "sm") and bold
|
|
24
|
+
* @returns {JSX.Element} a `<p>`, `<span>` or `<div>` in the tone's color and the size
|
|
25
|
+
*/
|
|
26
|
+
export declare function Text({ as, tone, size, bold, className, ...props }: TextProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { textClasses } from "./textStyles.js";
|
|
3
|
+
/**
|
|
4
|
+
* @function Text
|
|
5
|
+
* @param props {TextProps} native paragraph props, plus as, tone (default "default"), size
|
|
6
|
+
* (default "sm") and bold
|
|
7
|
+
* @returns {JSX.Element} a `<p>`, `<span>` or `<div>` in the tone's color and the size
|
|
8
|
+
*/
|
|
9
|
+
export function Text({ as = "p", tone, size, bold, className, ...props }) {
|
|
10
|
+
const classes = textClasses({ tone, size, bold, className });
|
|
11
|
+
// Same attributes for every element; only the ref's element type differs.
|
|
12
|
+
if (as === "span")
|
|
13
|
+
return _jsx("span", { className: classes, ...props });
|
|
14
|
+
if (as === "div")
|
|
15
|
+
return _jsx("div", { className: classes, ...props });
|
|
16
|
+
return _jsx("p", { className: classes, ...props });
|
|
17
|
+
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* @desc Shared class builder for Button and ButtonLink (osu!-web pill buttons).
|
|
4
4
|
* @author David @dvhsh (https://dvh.sh)
|
|
5
5
|
* @created Wed Sep 23, 2026
|
|
6
|
-
* @modified
|
|
6
|
+
* @modified Sun Oct 4, 2026
|
|
7
7
|
*/
|
|
8
8
|
/** The pill's colors: `primary` is h2 (h1 on hover), `secondary` b3, `ghost` transparent. */
|
|
9
9
|
export type ButtonVariant = "primary" | "secondary" | "ghost";
|
|
10
|
-
/** The pill's height, padding and text size: `md` is h-9 and text-sm, `lg` h-11 and text-base. */
|
|
10
|
+
/** The pill's height, padding and text size: `md` is h-9 (h-11 on a coarse pointer) and text-sm, `lg` h-11 and text-base. */
|
|
11
11
|
export type ButtonSize = "md" | "lg";
|
|
12
12
|
/** Options for {@link buttonClasses}. */
|
|
13
13
|
export type ButtonClassOptions = {
|
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
* @desc Shared class builder for Button and ButtonLink (osu!-web pill buttons).
|
|
4
4
|
* @author David @dvhsh (https://dvh.sh)
|
|
5
5
|
* @created Wed Sep 23, 2026
|
|
6
|
-
* @modified
|
|
6
|
+
* @modified Sun Oct 4, 2026
|
|
7
7
|
*/
|
|
8
8
|
import { cx } from "../../utils/cx.js";
|
|
9
|
-
|
|
9
|
+
// w-fit: a flex-column or grid item keeps its content width instead of stretching. The 1px
|
|
10
|
+
// border shows only in forced colors, where the pill's background is dropped.
|
|
11
|
+
const BASE = "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]";
|
|
10
12
|
// Hover only when not disabled. `not-disabled:` (not `enabled:`) so links, which are never
|
|
11
|
-
// :disabled, keep their hover colors.
|
|
13
|
+
// :disabled, keep their hover colors. Under more contrast, b3 and a transparent pill get a c4
|
|
14
|
+
// edge: b3 on b5 is under 1.5:1.
|
|
12
15
|
const VARIANTS = {
|
|
13
16
|
primary: "bg-h2 text-c1 not-disabled:hover:bg-h1 not-disabled:hover:text-b6",
|
|
14
|
-
secondary: "bg-b3 text-c1 not-disabled:hover:bg-b2",
|
|
15
|
-
ghost: "bg-transparent text-c2 not-disabled:hover:bg-b4 not-disabled:hover:text-c1",
|
|
17
|
+
secondary: "bg-b3 text-c1 not-disabled:hover:bg-b2 contrast-more:inset-ring contrast-more:inset-ring-c4",
|
|
18
|
+
ghost: "bg-transparent text-c2 not-disabled:hover:bg-b4 not-disabled:hover:text-c1 contrast-more:inset-ring contrast-more:inset-ring-c4",
|
|
16
19
|
};
|
|
17
20
|
const SIZES = {
|
|
18
|
-
md: "h-9 px-4 text-sm",
|
|
21
|
+
md: "h-9 px-4 text-sm coarse:h-11",
|
|
19
22
|
lg: "h-11 px-6 text-base",
|
|
20
23
|
};
|
|
21
24
|
/**
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* title, plus the heading levels either one takes for that title.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Mon Sep 28, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
8
|
*/
|
|
9
9
|
/** A heading level for a component's title: `h2` to `h6` (a page has one `h1`). */
|
|
10
10
|
export type HeadingLevel = 2 | 3 | 4 | 5 | 6;
|
|
11
|
-
/** The panel: rounded, b4 background, p-5, c2 text
|
|
12
|
-
|
|
11
|
+
/** The panel: rounded, b4 background, p-5, c2 text; a c4 edge under more contrast, a 1px border
|
|
12
|
+
* in forced colors. */
|
|
13
|
+
export declare const CARD = "rounded-[10px] bg-b4 p-5 text-c2 contrast-more:inset-ring contrast-more:inset-ring-c4 forced-colors:border";
|
|
13
14
|
/** The panel's title: bold c1 at text-lg. */
|
|
14
15
|
export declare const CARD_HEADING = "font-bold text-c1 text-lg";
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* title, plus the heading levels either one takes for that title.
|
|
5
5
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
6
|
* @created Mon Sep 28, 2026
|
|
7
|
-
* @modified
|
|
7
|
+
* @modified Sun Oct 4, 2026
|
|
8
8
|
*/
|
|
9
|
-
/** The panel: rounded, b4 background, p-5, c2 text
|
|
10
|
-
|
|
9
|
+
/** The panel: rounded, b4 background, p-5, c2 text; a c4 edge under more contrast, a 1px border
|
|
10
|
+
* in forced colors. */
|
|
11
|
+
export const CARD = "rounded-[10px] bg-b4 p-5 text-c2 contrast-more:inset-ring contrast-more:inset-ring-c4 forced-colors:border";
|
|
11
12
|
/** The panel's title: bold c1 at text-lg. */
|
|
12
13
|
export const CARD_HEADING = "font-bold text-c1 text-lg";
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { type TextSize, type TextTone } from "./textTones.js";
|
|
10
|
+
export type { TextSize, TextTone } from "./textTones.js";
|
|
11
|
+
/** Options for {@link textClasses}. */
|
|
12
|
+
export type TextClassOptions = {
|
|
13
|
+
/** "default" (c2), "muted" (c3), "subtle" (c4), "error", "warning" or "success". */
|
|
14
|
+
tone?: TextTone | undefined;
|
|
15
|
+
/** "xs", "sm" (default) or "base". */
|
|
16
|
+
size?: TextSize | undefined;
|
|
17
|
+
/** font-bold. */
|
|
18
|
+
bold?: boolean | undefined;
|
|
19
|
+
className?: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* @function textClasses
|
|
23
|
+
* @param opts {TextClassOptions} tone (default "default"), size (default "sm"), bold and extra
|
|
24
|
+
* classes
|
|
25
|
+
* @returns {string} the text classes, with the caller's className last (it replaces a built-in
|
|
26
|
+
* class that sets the same property)
|
|
27
|
+
*/
|
|
28
|
+
export declare const textClasses: ({ tone, size, bold, className, }?: TextClassOptions) => string;
|