@haruhimemoe/ui 0.4.0 → 0.5.1
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 +18 -1
- package/README.md +86 -2
- package/dist/components/basics/Tabs.d.ts +37 -0
- package/dist/components/basics/Tabs.js +47 -0
- package/dist/components/basics/tabIds.d.ts +23 -0
- package/dist/components/basics/tabIds.js +23 -0
- package/dist/components/forms/CharCounter.d.ts +25 -0
- package/dist/components/forms/CharCounter.js +12 -0
- package/dist/components/forms/ReportDisclosure.d.ts +52 -0
- package/dist/components/forms/ReportDisclosure.js +54 -0
- package/dist/components/forms/VisibilitySelect.d.ts +52 -0
- package/dist/components/forms/VisibilitySelect.js +45 -0
- package/dist/components/meta/JsonLd.d.ts +3 -2
- package/dist/components/meta/JsonLd.js +6 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.1] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- `JsonLd` also escapes `>`, `&`, U+2028 and U+2029 as JSON unicode escapes, not only `<`. The data parses back unchanged.
|
|
14
|
+
|
|
15
|
+
## [0.5.0] - 2026-09-28
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `Tabs`: an ARIA tab list for panels on the same page. The chosen tab is the only one in the Tab order; Left and Right (wrapping), Home and End pick and focus a tab. `tabId` and `tabPanelId` give the ids that tie a tab to its panel. Moved from bb.haruhime.moe.
|
|
20
|
+
- `CharCounter`: "1,234 / 60,000 characters", bold rose with how many to cut once over the limit. The caller counts. Moved from bb.haruhime.moe.
|
|
21
|
+
- `VisibilitySelect`: private, unlisted or public with a line each saying who sees it, as radios or a select, with the words overridable. Also `VISIBILITIES`, `VISIBILITY_TEXT` and the `Visibility` and `VisibilityText` types. It replaces the pools pool editor's radios and bb's template selects.
|
|
22
|
+
- `ReportDisclosure`: a report reason in a disclosure, sent through `onSubmit`, which returns a `ReportResult`; done replaces the form with a status line, an error stays on the field for a retry. Moved from bb.haruhime.moe (ReportForm).
|
|
23
|
+
|
|
9
24
|
## [0.4.0] - 2026-09-28
|
|
10
25
|
|
|
11
26
|
### Added
|
|
@@ -77,7 +92,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
77
92
|
- `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`).
|
|
78
93
|
- 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).
|
|
79
94
|
|
|
80
|
-
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.
|
|
95
|
+
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.5.1...HEAD
|
|
96
|
+
[0.5.1]: https://github.com/haruhimemoe/ui/compare/v0.5.0...v0.5.1
|
|
97
|
+
[0.5.0]: https://github.com/haruhimemoe/ui/compare/v0.4.0...v0.5.0
|
|
81
98
|
[0.4.0]: https://github.com/haruhimemoe/ui/compare/v0.3.0...v0.4.0
|
|
82
99
|
[0.3.0]: https://github.com/haruhimemoe/ui/compare/v0.2.0...v0.3.0
|
|
83
100
|
[0.2.0]: https://github.com/haruhimemoe/ui/compare/v0.1.0...v0.2.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.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
|
|
|
@@ -153,7 +153,7 @@ export default function Home() {
|
|
|
153
153
|
|
|
154
154
|
Import every component from `@haruhimemoe/ui`, in Server and Client Components alike.
|
|
155
155
|
|
|
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.
|
|
156
|
+
- **Client components:** `CopyButton`, `Chip`, `ChipGroup`, `RangeSlider` and `FilterPanel`, and since 0.4.0 `AsyncButton`, `InlineConfirm`, `Disclosure`, `ChoiceChips`, `RadioGroup`, `TypeToConfirm` and `HeaderMenu`, and since 0.5.0 `Tabs`, `VisibilitySelect` and `ReportDisclosure`. 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.
|
|
157
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.
|
|
158
158
|
- **Everything else is server-safe:** no state, no effects, no browser APIs.
|
|
159
159
|
|
|
@@ -292,6 +292,25 @@ Since 0.4.0. A small pill for a status or tag ("Unranked", "beta", a count). A `
|
|
|
292
292
|
| --- | --- | --- | --- |
|
|
293
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
294
|
|
|
295
|
+
#### `Tabs` (client)
|
|
296
|
+
|
|
297
|
+
Since 0.5.0. A tab list for panels on the same page (use `LinkTabs` when each tab is its own URL): pill buttons with `role="tab"` in a `role="tablist"` named by `label`. The chosen tab is `aria-selected` and the only one in the Tab order (the first one when none is chosen). Left and Right move and wrap, Home and End jump to the ends; each picks the tab and focuses it. Controlled. Every native `<div>` prop except `onChange` and `children`, for the tablist. Types: `TabItem<T>`, `TabsProps<T>`.
|
|
298
|
+
|
|
299
|
+
| Prop | Type | Default | What it does |
|
|
300
|
+
| --- | --- | --- | --- |
|
|
301
|
+
| `label` | `string` | required | The tablist's accessible name. |
|
|
302
|
+
| `idPrefix` | `string` | required | Prefix for the tabs' and panels' ids. |
|
|
303
|
+
| `tabs` | `readonly { id: T; label: ReactNode }[]` | required | The tabs, left to right. |
|
|
304
|
+
| `value` | `T` | required | The chosen tab's id. |
|
|
305
|
+
| `onChange` | `(id: T) => void` | required | Gets the picked tab's id. |
|
|
306
|
+
|
|
307
|
+
The panels are yours. `tabId(prefix, tab)` and `tabPanelId(prefix, tab)` (server-safe) give the ids that tie them together (`<prefix>-tab-<tab>`, `<prefix>-panel-<tab>`):
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
<Tabs label="Editor view" idPrefix="ed" tabs={TABS} value={tab} onChange={setTab} />
|
|
311
|
+
<div role="tabpanel" id={tabPanelId("ed", tab)} aria-labelledby={tabId("ed", tab)}>...</div>
|
|
312
|
+
```
|
|
313
|
+
|
|
295
314
|
### Forms
|
|
296
315
|
|
|
297
316
|
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.
|
|
@@ -363,6 +382,70 @@ Since 0.4.0. A confirm for something that can't be undone: a `<form>` whose subm
|
|
|
363
382
|
</TypeToConfirm>
|
|
364
383
|
```
|
|
365
384
|
|
|
385
|
+
#### `CharCounter`
|
|
386
|
+
|
|
387
|
+
Since 0.5.0. How much of a length limit a text uses: "1,234 / 60,000 characters" in `c3`, then bold rose with ": 1,500 over the limit" once past it (at the limit is not over). You count, so any rule works (a string's length, a BBCode counter). Every native `<p>` prop except `children`.
|
|
388
|
+
|
|
389
|
+
| Prop | Type | Default | What it does |
|
|
390
|
+
| --- | --- | --- | --- |
|
|
391
|
+
| `count` | `number` | required | How many are used. |
|
|
392
|
+
| `limit` | `number` | required | The most allowed. |
|
|
393
|
+
| `unit` | `string` | `"characters"` | What is counted. |
|
|
394
|
+
|
|
395
|
+
#### `VisibilitySelect` (client)
|
|
396
|
+
|
|
397
|
+
Since 0.5.0. Who can see something: private, unlisted or public, each with a line that says who that is. As radios (a `RadioGroup`, every line shown as its option's description) or as a native select (a `Select`, the picked option's line as the hint). Controlled. Also exported: `VISIBILITIES` (`["private", "unlisted", "public"]`), the `Visibility` type, `VisibilityText` (`{ label: string; hint?: ReactNode }`) and `VISIBILITY_TEXT`, the default words ("Only you can see it.", "Anyone with the link can see it. It isn't listed.", "Anyone can see it, and it's listed.").
|
|
398
|
+
|
|
399
|
+
| Prop | Type | Default | What it does |
|
|
400
|
+
| --- | --- | --- | --- |
|
|
401
|
+
| `value` | `Visibility` | required | The picked visibility. |
|
|
402
|
+
| `onChange` | `(value: Visibility) => void` | required | Gets the new one. |
|
|
403
|
+
| `label` | `ReactNode` | `"Who can see it"` | The legend, or the select's label. |
|
|
404
|
+
| `as` | `"radio" \| "select"` | `"radio"` | Radios, or a dropdown. |
|
|
405
|
+
| `text` | `Partial<Record<Visibility, Partial<VisibilityText>>>` | none | Your words, merged over `VISIBILITY_TEXT` per visibility. |
|
|
406
|
+
| `id` | `string` | generated | The select's id, or the radios' name. |
|
|
407
|
+
| `hint` | `ReactNode` | none | Under the radios. On a select it replaces the picked option's line. |
|
|
408
|
+
| `error` | `ReactNode` | none | As on the fields. |
|
|
409
|
+
| `disabled` | `boolean` | `false` | Turns it off. |
|
|
410
|
+
| `className` | `string` | none | Classes for the fieldset, or the select's wrapper. |
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
<VisibilitySelect
|
|
414
|
+
label="Who can see this pool"
|
|
415
|
+
value={visibility}
|
|
416
|
+
onChange={setVisibility}
|
|
417
|
+
text={{ private: { hint: "Only you and your editors." } }}
|
|
418
|
+
/>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### `ReportDisclosure` (client)
|
|
422
|
+
|
|
423
|
+
Since 0.5.0. "Report this": a `Disclosure` holding a reason `Textarea` (required) and a submit button. `onSubmit` gets the trimmed reason and says how it went: `{ ok: true, message? }` replaces the form with a `role="status"` line (your message, like "You already reported it.", or `sentMessage`); `{ ok: false, message }` shows the message as the field's error and keeps what was typed for a retry. A throw reads as `failedMessage`. One send at a time. Type: `ReportResult`.
|
|
424
|
+
|
|
425
|
+
| Prop | Type | Default | What it does |
|
|
426
|
+
| --- | --- | --- | --- |
|
|
427
|
+
| `onSubmit` | `(reason: string) => Promise<ReportResult>` | required | Sends the report. |
|
|
428
|
+
| `summary` | `ReactNode` | `"Report"` | The disclosure button's text. |
|
|
429
|
+
| `label` | `ReactNode` | `"What's wrong with it?"` | The reason field's label. |
|
|
430
|
+
| `hint` | `ReactNode` | none | Help under the field. |
|
|
431
|
+
| `submitLabel`, `pendingLabel` | `ReactNode` | `"Send report"`, `"Sending…"` | The button's text, and while sending. |
|
|
432
|
+
| `sentMessage` | `ReactNode` | `"Thanks. Your report was sent."` | Said when the result has no message. |
|
|
433
|
+
| `failedMessage` | `ReactNode` | `"Couldn't send the report. Try again."` | Said when `onSubmit` throws. |
|
|
434
|
+
| `minLength`, `maxLength` | `number` | `3`, none | The reason's length limits. |
|
|
435
|
+
| `rows` | `number` | `3` | The field's height. |
|
|
436
|
+
| `className` | `string` | none | Classes for the wrapper (or the status line once sent). |
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
<ReportDisclosure
|
|
440
|
+
summary="Report this template"
|
|
441
|
+
maxLength={500}
|
|
442
|
+
onSubmit={async (reason) => {
|
|
443
|
+
const response = await fetch(`/api/templates/${id}/report`, { method: "POST", body: JSON.stringify({ reason }) });
|
|
444
|
+
return response.ok ? { ok: true } : { ok: false, message: "Reporting failed." };
|
|
445
|
+
}}
|
|
446
|
+
/>
|
|
447
|
+
```
|
|
448
|
+
|
|
366
449
|
#### `fieldClasses`
|
|
367
450
|
|
|
368
451
|
`fieldClasses(className?: string): string` returns the field look (`b6` background, `b3` border, `h1` border on focus, rose border when `aria-invalid`, and an `h1` border and ring when an invalid field has focus). Use it on a bare control that labels itself:
|
|
@@ -803,6 +886,7 @@ Since 0.4.0. `cx(...classes: ClassValue[]): string` is the class merger every co
|
|
|
803
886
|
- `FilterPanel`'s phone toggle carries `aria-expanded` and `aria-controls`. The result count is a live region. When "Clear filters" disappears after use, focus moves to the panel's heading instead of getting lost.
|
|
804
887
|
- `CopyButton` announces "Copied." (or the failure) through an `<output>`, on every press.
|
|
805
888
|
- `Pagination` moves focus to its "Page X of Y" text when the link you pressed goes away on the first or last page.
|
|
889
|
+
- `Tabs` follows the ARIA tabs pattern: one tab in the Tab order, arrows, Home and End to move, `aria-controls` to its panel. `ReportDisclosure` says the outcome in a `role="status"` line and keeps a failed reason in the field.
|
|
806
890
|
- `SiteHeader` marks the current page with `aria-current`. `PageShell` starts with a skip link to `<main>`.
|
|
807
891
|
- `DiscordIcon` and `GitHubIcon` are hidden from screen readers by default: give the link around each one an `aria-label`, as `SiteFooter` does.
|
|
808
892
|
- You supply the text, so you also supply labels: give icon-only buttons an `aria-label`, and keep `label` props meaningful.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Tabs.tsx
|
|
3
|
+
* @desc A tab list for panels on the same page (not links; LinkTabs is for those): buttons with
|
|
4
|
+
* role="tab", the chosen one aria-selected and the only one in the Tab order. Left and Right
|
|
5
|
+
* (wrapping), Home and End pick and focus a tab. The panels are the caller's: give each
|
|
6
|
+
* `id={tabPanelId(idPrefix, tab)}`, role="tabpanel" and
|
|
7
|
+
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
13
|
+
/** One tab: its id (part of the tab's and panel's ids) and its text. */
|
|
14
|
+
export type TabItem<T extends string = string> = {
|
|
15
|
+
id: T;
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
/** Every native `<div>` prop except `onChange` and children, plus the list's parts. */
|
|
19
|
+
export type TabsProps<T extends string = string> = Omit<ComponentProps<"div">, "onChange" | "children"> & {
|
|
20
|
+
/** Names the tab list. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Prefix for the tabs' and panels' ids (tabId, tabPanelId). */
|
|
23
|
+
idPrefix: string;
|
|
24
|
+
/** The tabs, left to right. */
|
|
25
|
+
tabs: readonly TabItem<T>[];
|
|
26
|
+
/** The chosen tab's id. */
|
|
27
|
+
value: T;
|
|
28
|
+
/** Called with the picked tab's id. */
|
|
29
|
+
onChange: (id: T) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* @function Tabs
|
|
33
|
+
* @param props {TabsProps<T>} the list's name, id prefix, tabs, chosen tab and handler, and
|
|
34
|
+
* native div props
|
|
35
|
+
* @returns {JSX.Element} the tablist with one pill button per tab
|
|
36
|
+
*/
|
|
37
|
+
export declare function Tabs<T extends string>({ label, idPrefix, tabs, value, onChange, className, onKeyDown, ...props }: TabsProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/Tabs.tsx
|
|
3
|
+
* @desc A tab list for panels on the same page (not links; LinkTabs is for those): buttons with
|
|
4
|
+
* role="tab", the chosen one aria-selected and the only one in the Tab order. Left and Right
|
|
5
|
+
* (wrapping), Home and End pick and focus a tab. The panels are the caller's: give each
|
|
6
|
+
* `id={tabPanelId(idPrefix, tab)}`, role="tabpanel" and
|
|
7
|
+
* `aria-labelledby={tabId(idPrefix, tab)}`. Controlled. Moved from bb.haruhime.moe.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Mon Sep 28, 2026
|
|
10
|
+
* @modified Mon Sep 28, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import { cx } from "../../utils/cx.js";
|
|
15
|
+
import { tabId, tabPanelId } from "./tabIds.js";
|
|
16
|
+
const KEYS = {
|
|
17
|
+
ArrowRight: (at, count) => (at + 1) % count,
|
|
18
|
+
ArrowLeft: (at, count) => (at - 1 + count) % count,
|
|
19
|
+
Home: () => 0,
|
|
20
|
+
End: (_at, count) => count - 1,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @function Tabs
|
|
24
|
+
* @param props {TabsProps<T>} the list's name, id prefix, tabs, chosen tab and handler, and
|
|
25
|
+
* native div props
|
|
26
|
+
* @returns {JSX.Element} the tablist with one pill button per tab
|
|
27
|
+
*/
|
|
28
|
+
export function Tabs({ label, idPrefix, tabs, value, onChange, className, onKeyDown, ...props }) {
|
|
29
|
+
// With no tab chosen, the first one is still reachable with Tab.
|
|
30
|
+
const at = Math.max(0, tabs.findIndex((item) => item.id === value));
|
|
31
|
+
const move = (event) => {
|
|
32
|
+
onKeyDown?.(event);
|
|
33
|
+
const step = KEYS[event.key];
|
|
34
|
+
if (!step || tabs.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const tab = tabs[step(at, tabs.length)];
|
|
37
|
+
if (!tab)
|
|
38
|
+
return;
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
onChange(tab.id);
|
|
41
|
+
document.getElementById(tabId(idPrefix, tab.id))?.focus();
|
|
42
|
+
};
|
|
43
|
+
return (_jsx("div", { role: "tablist", "aria-label": label, onKeyDown: move, className: cx("flex gap-1 rounded-full bg-b4 p-1", className), ...props, children: tabs.map((tab, index) => {
|
|
44
|
+
const selected = tab.id === value;
|
|
45
|
+
return (_jsx("button", { type: "button", role: "tab", id: tabId(idPrefix, tab.id), "aria-selected": selected, "aria-controls": tabPanelId(idPrefix, tab.id), tabIndex: index === at ? 0 : -1, onClick: () => onChange(tab.id), className: cx("flex-1 rounded-full px-4 py-1.5 font-bold text-sm transition-colors", selected ? "bg-h2 text-c1 forced-colors:underline" : "text-c3 hover:text-c1"), children: tab.label }, tab.id));
|
|
46
|
+
}) }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/tabIds.ts
|
|
3
|
+
* @desc The ids that tie a Tabs tab to its panel: the tab button is `<prefix>-tab-<tab>` and its
|
|
4
|
+
* panel `<prefix>-panel-<tab>`. Pure and server-safe, so a server page can give its panels
|
|
5
|
+
* the ids. Moved from bb.haruhime.moe (src/utils/tabs.ts).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function tabId
|
|
12
|
+
* @param prefix {string} the tab list's id prefix
|
|
13
|
+
* @param tab {string} the tab
|
|
14
|
+
* @returns {string} the tab button's id
|
|
15
|
+
*/
|
|
16
|
+
export declare const tabId: (prefix: string, tab: string) => string;
|
|
17
|
+
/**
|
|
18
|
+
* @function tabPanelId
|
|
19
|
+
* @param prefix {string} the tab list's id prefix
|
|
20
|
+
* @param tab {string} the tab
|
|
21
|
+
* @returns {string} its panel's id
|
|
22
|
+
*/
|
|
23
|
+
export declare const tabPanelId: (prefix: string, tab: string) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/basics/tabIds.ts
|
|
3
|
+
* @desc The ids that tie a Tabs tab to its panel: the tab button is `<prefix>-tab-<tab>` and its
|
|
4
|
+
* panel `<prefix>-panel-<tab>`. Pure and server-safe, so a server page can give its panels
|
|
5
|
+
* the ids. Moved from bb.haruhime.moe (src/utils/tabs.ts).
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function tabId
|
|
12
|
+
* @param prefix {string} the tab list's id prefix
|
|
13
|
+
* @param tab {string} the tab
|
|
14
|
+
* @returns {string} the tab button's id
|
|
15
|
+
*/
|
|
16
|
+
export const tabId = (prefix, tab) => `${prefix}-tab-${tab}`;
|
|
17
|
+
/**
|
|
18
|
+
* @function tabPanelId
|
|
19
|
+
* @param prefix {string} the tab list's id prefix
|
|
20
|
+
* @param tab {string} the tab
|
|
21
|
+
* @returns {string} its panel's id
|
|
22
|
+
*/
|
|
23
|
+
export const tabPanelId = (prefix, tab) => `${prefix}-panel-${tab}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/CharCounter.tsx
|
|
3
|
+
* @desc How much of a length limit a text uses ("1,234 / 60,000 characters"), turning rose and
|
|
4
|
+
* saying how many to cut once it's over. The caller counts (a BBCode counter, a string's
|
|
5
|
+
* length), so it fits any rule. Server-safe. Moved from bb.haruhime.moe.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Mon Sep 28, 2026
|
|
8
|
+
* @modified Mon Sep 28, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
11
|
+
/** Every native `<p>` prop except children, plus the count, the limit and the unit. */
|
|
12
|
+
export type CharCounterProps = Omit<ComponentProps<"p">, "children"> & {
|
|
13
|
+
/** How many are used. */
|
|
14
|
+
count: number;
|
|
15
|
+
/** The most allowed. */
|
|
16
|
+
limit: number;
|
|
17
|
+
/** What is counted (default "characters"). */
|
|
18
|
+
unit?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* @function CharCounter
|
|
22
|
+
* @param props {CharCounterProps} the count, the limit, the unit, and native `<p>` props
|
|
23
|
+
* @returns {JSX.Element} "count / limit unit" in c3, or in bold rose with ": N over the limit"
|
|
24
|
+
*/
|
|
25
|
+
export declare function CharCounter({ count, limit, unit, className, ...props }: CharCounterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
const format = (value) => value.toLocaleString("en-US");
|
|
4
|
+
/**
|
|
5
|
+
* @function CharCounter
|
|
6
|
+
* @param props {CharCounterProps} the count, the limit, the unit, and native `<p>` props
|
|
7
|
+
* @returns {JSX.Element} "count / limit unit" in c3, or in bold rose with ": N over the limit"
|
|
8
|
+
*/
|
|
9
|
+
export function CharCounter({ count, limit, unit = "characters", className, ...props }) {
|
|
10
|
+
const over = count - limit;
|
|
11
|
+
return (_jsxs("p", { className: cx("text-sm tabular-nums", over > 0 ? "font-bold text-rose-300" : "text-c3", className), ...props, children: [`${format(count)} / ${format(limit)} ${unit}`, over > 0 ? `: ${format(over)} over the limit` : ""] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/ReportDisclosure.tsx
|
|
3
|
+
* @desc "Report this": a Disclosure holding a reason field and a submit button. The caller sends
|
|
4
|
+
* the reason (`onSubmit`) and says how it went: done (the form goes away and the page says
|
|
5
|
+
* so, thanks or "already reported") or an error (shown on the field, for a retry). One send
|
|
6
|
+
* at a time; a throw reads as `failedMessage`. Moved from bb.haruhime.moe (ReportForm).
|
|
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
|
+
/** What `onSubmit` says: done (with what to say) or not (with the error). */
|
|
13
|
+
export type ReportResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
message?: ReactNode;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
message: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
/** ReportDisclosure's props. */
|
|
21
|
+
export type ReportDisclosureProps = {
|
|
22
|
+
/** Sends the trimmed reason. */
|
|
23
|
+
onSubmit: (reason: string) => Promise<ReportResult>;
|
|
24
|
+
/** The disclosure button's text (default "Report"). */
|
|
25
|
+
summary?: ReactNode;
|
|
26
|
+
/** The reason field's label (default "What's wrong with it?"). */
|
|
27
|
+
label?: ReactNode;
|
|
28
|
+
/** Help under the field. */
|
|
29
|
+
hint?: ReactNode;
|
|
30
|
+
/** The submit button's text (default "Send report"). */
|
|
31
|
+
submitLabel?: ReactNode;
|
|
32
|
+
/** The submit button's text while sending (default "Sending…"). */
|
|
33
|
+
pendingLabel?: ReactNode;
|
|
34
|
+
/** Said once sent, when the result has no message (default "Thanks. Your report was sent."). */
|
|
35
|
+
sentMessage?: ReactNode;
|
|
36
|
+
/** Said when `onSubmit` throws (default "Couldn't send the report. Try again."). */
|
|
37
|
+
failedMessage?: ReactNode;
|
|
38
|
+
/** The reason's shortest length (default 3). */
|
|
39
|
+
minLength?: number | undefined;
|
|
40
|
+
/** The reason's longest length. */
|
|
41
|
+
maxLength?: number | undefined;
|
|
42
|
+
/** The field's height in rows (default 3). */
|
|
43
|
+
rows?: number | undefined;
|
|
44
|
+
/** Classes for the wrapper. */
|
|
45
|
+
className?: string | undefined;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* @function ReportDisclosure
|
|
49
|
+
* @param props {ReportDisclosureProps} the send callback, the words and the reason's limits
|
|
50
|
+
* @returns {JSX.Element} the disclosure with its form, or the status once sent
|
|
51
|
+
*/
|
|
52
|
+
export declare function ReportDisclosure({ onSubmit, summary, label, hint, submitLabel, pendingLabel, sentMessage, failedMessage, minLength, maxLength, rows, className, }: ReportDisclosureProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/ReportDisclosure.tsx
|
|
3
|
+
* @desc "Report this": a Disclosure holding a reason field and a submit button. The caller sends
|
|
4
|
+
* the reason (`onSubmit`) and says how it went: done (the form goes away and the page says
|
|
5
|
+
* so, thanks or "already reported") or an error (shown on the field, for a retry). One send
|
|
6
|
+
* at a time; a throw reads as `failedMessage`. Moved from bb.haruhime.moe (ReportForm).
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useId, useRef, useState } from "react";
|
|
14
|
+
import { cx } from "../../utils/cx.js";
|
|
15
|
+
import { Button } from "../basics/Button.js";
|
|
16
|
+
import { Disclosure } from "../basics/Disclosure.js";
|
|
17
|
+
import { Textarea } from "./Textarea.js";
|
|
18
|
+
/**
|
|
19
|
+
* @function ReportDisclosure
|
|
20
|
+
* @param props {ReportDisclosureProps} the send callback, the words and the reason's limits
|
|
21
|
+
* @returns {JSX.Element} the disclosure with its form, or the status once sent
|
|
22
|
+
*/
|
|
23
|
+
export function ReportDisclosure({ onSubmit, summary = "Report", label = "What's wrong with it?", hint, submitLabel = "Send report", pendingLabel = "Sending…", sentMessage = "Thanks. Your report was sent.", failedMessage = "Couldn't send the report. Try again.", minLength = 3, maxLength, rows = 3, className, }) {
|
|
24
|
+
const id = useId();
|
|
25
|
+
const [reason, setReason] = useState("");
|
|
26
|
+
const [pending, setPending] = useState(false);
|
|
27
|
+
const [error, setError] = useState(null);
|
|
28
|
+
const [done, setDone] = useState(null);
|
|
29
|
+
const running = useRef(false);
|
|
30
|
+
if (done !== null) {
|
|
31
|
+
return (_jsx("p", { role: "status", className: cx("text-c2 text-sm", className), children: done }));
|
|
32
|
+
}
|
|
33
|
+
const send = async (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
if (running.current)
|
|
36
|
+
return;
|
|
37
|
+
running.current = true;
|
|
38
|
+
setPending(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
const result = await onSubmit(reason.trim());
|
|
42
|
+
if (result.ok)
|
|
43
|
+
setDone(result.message ?? sentMessage);
|
|
44
|
+
else
|
|
45
|
+
setError(result.message);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
setError(failedMessage);
|
|
49
|
+
}
|
|
50
|
+
running.current = false;
|
|
51
|
+
setPending(false);
|
|
52
|
+
};
|
|
53
|
+
return (_jsx(Disclosure, { summary: summary, className: className, children: _jsxs("form", { className: "flex flex-col gap-2", onSubmit: send, children: [_jsx(Textarea, { id: `${id}-reason`, label: label, hint: hint, error: error ?? undefined, value: reason, rows: rows, minLength: minLength, maxLength: maxLength, required: true, onChange: (event) => setReason(event.currentTarget.value) }), _jsx(Button, { type: "submit", variant: "secondary", disabled: pending, className: "self-start", children: pending ? pendingLabel : submitLabel })] }) }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/VisibilitySelect.tsx
|
|
3
|
+
* @desc Who can see something: private, unlisted or public, each with a line saying who that is.
|
|
4
|
+
* As radios (RadioGroup, every option's line shown) or as a native select (Select, the
|
|
5
|
+
* picked option's line as the hint). Controlled. The pools pool editor and the bb template
|
|
6
|
+
* forms, as one component; the words come from props, with generic defaults.
|
|
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
|
+
/** The three visibilities, from most to least closed. */
|
|
13
|
+
export declare const VISIBILITIES: readonly ["private", "unlisted", "public"];
|
|
14
|
+
/** One of VISIBILITIES. */
|
|
15
|
+
export type Visibility = (typeof VISIBILITIES)[number];
|
|
16
|
+
/** A visibility's name and the line that says who sees it. */
|
|
17
|
+
export type VisibilityText = {
|
|
18
|
+
label: string;
|
|
19
|
+
hint?: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
/** The default words, generic English. */
|
|
22
|
+
export declare const VISIBILITY_TEXT: Readonly<Record<Visibility, VisibilityText>>;
|
|
23
|
+
/** VisibilitySelect's props. */
|
|
24
|
+
export type VisibilitySelectProps = {
|
|
25
|
+
/** The picked visibility. */
|
|
26
|
+
value: Visibility;
|
|
27
|
+
/** Called with the new visibility. */
|
|
28
|
+
onChange: (value: Visibility) => void;
|
|
29
|
+
/** The group's legend or the select's label (default "Who can see it"). */
|
|
30
|
+
label?: ReactNode;
|
|
31
|
+
/** "radio" (default) shows every option's line; "select" is a dropdown. */
|
|
32
|
+
as?: "radio" | "select" | undefined;
|
|
33
|
+
/** Words per visibility, merged over VISIBILITY_TEXT. */
|
|
34
|
+
text?: Partial<Record<Visibility, Partial<VisibilityText>>> | undefined;
|
|
35
|
+
/** The select's id, or the radios' name (default: generated). */
|
|
36
|
+
id?: string | undefined;
|
|
37
|
+
/** Help under the radios; for a select, replaces the picked option's line. */
|
|
38
|
+
hint?: ReactNode;
|
|
39
|
+
/** Error text; marks the field invalid. */
|
|
40
|
+
error?: ReactNode;
|
|
41
|
+
/** Turns the field off. */
|
|
42
|
+
disabled?: boolean | undefined;
|
|
43
|
+
/** Classes for the fieldset, or the select's wrapper. */
|
|
44
|
+
className?: string | undefined;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* @function VisibilitySelect
|
|
48
|
+
* @param props {VisibilitySelectProps} the value, the handler, the look, the words, id, hint,
|
|
49
|
+
* error and disabled
|
|
50
|
+
* @returns {JSX.Element} a RadioGroup of the three, or a Select with the picked one's line
|
|
51
|
+
*/
|
|
52
|
+
export declare function VisibilitySelect({ value, onChange, label, as, text, id, hint, error, disabled, className, }: VisibilitySelectProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/forms/VisibilitySelect.tsx
|
|
3
|
+
* @desc Who can see something: private, unlisted or public, each with a line saying who that is.
|
|
4
|
+
* As radios (RadioGroup, every option's line shown) or as a native select (Select, the
|
|
5
|
+
* picked option's line as the hint). Controlled. The pools pool editor and the bb template
|
|
6
|
+
* forms, as one component; the words come from props, with generic defaults.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Mon Sep 28, 2026
|
|
9
|
+
* @modified Mon Sep 28, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { useId } from "react";
|
|
14
|
+
import { RadioGroup } from "./RadioGroup.js";
|
|
15
|
+
import { Select } from "./Select.js";
|
|
16
|
+
/** The three visibilities, from most to least closed. */
|
|
17
|
+
export const VISIBILITIES = ["private", "unlisted", "public"];
|
|
18
|
+
/** The default words, generic English. */
|
|
19
|
+
export const VISIBILITY_TEXT = {
|
|
20
|
+
private: { label: "Private", hint: "Only you can see it." },
|
|
21
|
+
unlisted: { label: "Unlisted", hint: "Anyone with the link can see it. It isn't listed." },
|
|
22
|
+
public: { label: "Public", hint: "Anyone can see it, and it's listed." },
|
|
23
|
+
};
|
|
24
|
+
const isVisibility = (value) => VISIBILITIES.includes(value);
|
|
25
|
+
/**
|
|
26
|
+
* @function VisibilitySelect
|
|
27
|
+
* @param props {VisibilitySelectProps} the value, the handler, the look, the words, id, hint,
|
|
28
|
+
* error and disabled
|
|
29
|
+
* @returns {JSX.Element} a RadioGroup of the three, or a Select with the picked one's line
|
|
30
|
+
*/
|
|
31
|
+
export function VisibilitySelect({ value, onChange, label = "Who can see it", as = "radio", text, id, hint, error, disabled, className, }) {
|
|
32
|
+
const generated = useId();
|
|
33
|
+
const words = (visibility) => ({
|
|
34
|
+
...VISIBILITY_TEXT[visibility],
|
|
35
|
+
...text?.[visibility],
|
|
36
|
+
});
|
|
37
|
+
const pick = (next) => {
|
|
38
|
+
if (isVisibility(next))
|
|
39
|
+
onChange(next);
|
|
40
|
+
};
|
|
41
|
+
if (as === "select") {
|
|
42
|
+
return (_jsx(Select, { id: id ?? generated, label: label, hint: hint ?? words(value).hint, error: error, value: value, disabled: disabled, wrapperClassName: className, onChange: (event) => pick(event.currentTarget.value), children: VISIBILITIES.map((visibility) => (_jsx("option", { value: visibility, children: words(visibility).label }, visibility))) }));
|
|
43
|
+
}
|
|
44
|
+
return (_jsx(RadioGroup, { label: label, name: id, options: VISIBILITIES.map((visibility) => ({ value: visibility, ...words(visibility) })), value: value, onChange: pick, hint: hint, error: error, disabled: disabled, className: className }));
|
|
45
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/components/meta/JsonLd.tsx
|
|
3
|
-
* @desc schema.org structured data as a JSON-LD script tag.
|
|
4
|
-
*
|
|
3
|
+
* @desc schema.org structured data as a JSON-LD script tag. "<", ">", "&" and the line and
|
|
4
|
+
* paragraph separators (U+2028, U+2029) are written as JSON escapes, so no string in the
|
|
5
|
+
* data (a user-supplied name, say) can close the tag, open a comment or break the script.
|
|
5
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
6
7
|
* @created Wed Sep 23, 2026
|
|
7
8
|
* @modified Mon Sep 28, 2026
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** Characters written as JSON unicode escapes inside the script tag. */
|
|
3
|
+
const UNSAFE = /[<>&\u2028\u2029]/g;
|
|
2
4
|
/**
|
|
3
5
|
* @function jsonLdString
|
|
4
6
|
* @param data {Record<string, unknown>} a schema.org object
|
|
5
|
-
* @returns {string} JSON with `@context` added and
|
|
7
|
+
* @returns {string} JSON with `@context` added and "<", ">", "&", U+2028 and U+2029 written as
|
|
8
|
+
* `\uXXXX` escapes (still the same JSON once parsed)
|
|
6
9
|
*/
|
|
7
|
-
const jsonLdString = (data) => JSON.stringify({ "@context": "https://schema.org", ...data }).replace(
|
|
10
|
+
const jsonLdString = (data) => JSON.stringify({ "@context": "https://schema.org", ...data }).replace(UNSAFE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
8
11
|
/**
|
|
9
12
|
* @function JsonLd
|
|
10
13
|
* @param props {JsonLdProps} the schema.org data, plus native script props
|
|
@@ -12,6 +15,6 @@ const jsonLdString = (data) => JSON.stringify({ "@context": "https://schema.org"
|
|
|
12
15
|
*/
|
|
13
16
|
export function JsonLd({ data, ...props }) {
|
|
14
17
|
return (_jsx("script", { ...props, type: "application/ld+json",
|
|
15
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD must be raw JSON; jsonLdString escapes
|
|
18
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD must be raw JSON; jsonLdString escapes the unsafe characters.
|
|
16
19
|
dangerouslySetInnerHTML: { __html: jsonLdString(data) } }));
|
|
17
20
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,21 +22,26 @@ export { type LinkClassOptions, linkClasses, type TextLinkVariant, } from "./com
|
|
|
22
22
|
export { Notice, type NoticeProps, type NoticeTone } from "./components/basics/Notice.js";
|
|
23
23
|
export { PageHeader, type PageHeaderProps } from "./components/basics/PageHeader.js";
|
|
24
24
|
export { Prose, type ProseProps } from "./components/basics/Prose.js";
|
|
25
|
+
export { type TabItem, Tabs, type TabsProps } from "./components/basics/Tabs.js";
|
|
25
26
|
export { TextLink, type TextLinkProps } from "./components/basics/TextLink.js";
|
|
27
|
+
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
26
28
|
export { Chip, type ChipProps } from "./components/filters/Chip.js";
|
|
27
29
|
export { ChipGroup, type ChipGroupProps, type ChipOption } from "./components/filters/ChipGroup.js";
|
|
28
30
|
export { type ChoiceChipOption, ChoiceChips, type ChoiceChipsProps, } from "./components/filters/ChoiceChips.js";
|
|
29
31
|
export { FilterPanel, type FilterPanelProps } from "./components/filters/FilterPanel.js";
|
|
30
32
|
export { FilterRow, type FilterRowProps } from "./components/filters/FilterRow.js";
|
|
31
33
|
export { RangeSlider, type RangeSliderProps, type RangeSliderValue, } from "./components/filters/RangeSlider.js";
|
|
34
|
+
export { CharCounter, type CharCounterProps } from "./components/forms/CharCounter.js";
|
|
32
35
|
export { Checkbox, type CheckboxProps } from "./components/forms/Checkbox.js";
|
|
33
36
|
export type { FieldProps } from "./components/forms/FieldFrame.js";
|
|
34
37
|
export { fieldClasses } from "./components/forms/fieldStyles.js";
|
|
35
38
|
export { RadioGroup, type RadioGroupProps, type RadioOption, } from "./components/forms/RadioGroup.js";
|
|
39
|
+
export { ReportDisclosure, type ReportDisclosureProps, type ReportResult, } from "./components/forms/ReportDisclosure.js";
|
|
36
40
|
export { Select, type SelectProps } from "./components/forms/Select.js";
|
|
37
41
|
export { Textarea, type TextareaProps } from "./components/forms/Textarea.js";
|
|
38
42
|
export { TextInput, type TextInputProps } from "./components/forms/TextInput.js";
|
|
39
43
|
export { TypeToConfirm, type TypeToConfirmProps } from "./components/forms/TypeToConfirm.js";
|
|
44
|
+
export { VISIBILITIES, VISIBILITY_TEXT, type Visibility, VisibilitySelect, type VisibilitySelectProps, type VisibilityText, } from "./components/forms/VisibilitySelect.js";
|
|
40
45
|
export { DiscordIcon, type DiscordIconProps } from "./components/icons/DiscordIcon.js";
|
|
41
46
|
export { GitHubIcon, type GitHubIconProps } from "./components/icons/GitHubIcon.js";
|
|
42
47
|
export { HaruhimeWordmark, type HaruhimeWordmarkProps, } from "./components/icons/HaruhimeWordmark.js";
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,9 @@ export { linkClasses, } from "./components/basics/linkStyles.js";
|
|
|
23
23
|
export { Notice } from "./components/basics/Notice.js";
|
|
24
24
|
export { PageHeader } from "./components/basics/PageHeader.js";
|
|
25
25
|
export { Prose } from "./components/basics/Prose.js";
|
|
26
|
+
export { Tabs } from "./components/basics/Tabs.js";
|
|
26
27
|
export { TextLink } from "./components/basics/TextLink.js";
|
|
28
|
+
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
27
29
|
// Filters
|
|
28
30
|
export { Chip } from "./components/filters/Chip.js";
|
|
29
31
|
export { ChipGroup } from "./components/filters/ChipGroup.js";
|
|
@@ -32,13 +34,16 @@ export { FilterPanel } from "./components/filters/FilterPanel.js";
|
|
|
32
34
|
export { FilterRow } from "./components/filters/FilterRow.js";
|
|
33
35
|
export { RangeSlider, } from "./components/filters/RangeSlider.js";
|
|
34
36
|
// Forms
|
|
37
|
+
export { CharCounter } from "./components/forms/CharCounter.js";
|
|
35
38
|
export { Checkbox } from "./components/forms/Checkbox.js";
|
|
36
39
|
export { fieldClasses } from "./components/forms/fieldStyles.js";
|
|
37
40
|
export { RadioGroup, } from "./components/forms/RadioGroup.js";
|
|
41
|
+
export { ReportDisclosure, } from "./components/forms/ReportDisclosure.js";
|
|
38
42
|
export { Select } from "./components/forms/Select.js";
|
|
39
43
|
export { Textarea } from "./components/forms/Textarea.js";
|
|
40
44
|
export { TextInput } from "./components/forms/TextInput.js";
|
|
41
45
|
export { TypeToConfirm } from "./components/forms/TypeToConfirm.js";
|
|
46
|
+
export { VISIBILITIES, VISIBILITY_TEXT, VisibilitySelect, } from "./components/forms/VisibilitySelect.js";
|
|
42
47
|
// Icons
|
|
43
48
|
export { DiscordIcon } from "./components/icons/DiscordIcon.js";
|
|
44
49
|
export { GitHubIcon } from "./components/icons/GitHubIcon.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "React components for the haruhime.moe osu! tools on Next.js: the osu!-web-style palette as a Tailwind theme, buttons, cards, form fields and confirms, filter controls, tables, osu! beatmap display pieces and the site header and footer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|