@haruhimemoe/ui 0.10.0 → 0.11.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 +11 -1
- package/README.md +126 -2
- package/dist/components/brand/BrandPage.d.ts +58 -0
- package/dist/components/brand/BrandPage.js +17 -0
- package/dist/components/brand/BrandSwatch.d.ts +29 -0
- package/dist/components/brand/BrandSwatch.js +35 -0
- package/dist/components/content/ContentIndex.d.ts +22 -0
- package/dist/components/content/ContentIndex.js +21 -0
- package/dist/components/content/ContentLayout.d.ts +23 -0
- package/dist/components/content/ContentLayout.js +11 -0
- package/dist/components/content/ContentNav.d.ts +29 -0
- package/dist/components/content/ContentNav.js +32 -0
- package/dist/components/content/ContentPage.d.ts +39 -0
- package/dist/components/content/ContentPage.js +16 -0
- package/dist/components/content/ContentSearch.d.ts +27 -0
- package/dist/components/content/ContentSearch.js +32 -0
- package/dist/components/content/CopyMarkdownButton.d.ts +32 -0
- package/dist/components/content/CopyMarkdownButton.js +43 -0
- package/dist/components/content/searchContent.d.ts +22 -0
- package/dist/components/content/searchContent.js +37 -0
- package/dist/components/content/types.d.ts +30 -0
- package/dist/components/content/types.js +12 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +12 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-10-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `ContentNav` and `ContentLayout`: a content section's side navigation and page grid, a port of bb's docs sidebar and layout. `ContentNav` (client) takes `groups` (`ContentNavGroup[]`, each an optional `heading` and `ContentNavItem[]`) and an index link, marks the current page `aria-current="page"`, shows `navTitle ?? title` (clamped to two lines, the full title on the link's `title` attribute) and an optional `badge`. `ContentLayout` (server) lays the nav in a 14rem column beside the page from `lg` up, taking `nav` as an already-rendered slot so it stays section-agnostic (docs today, a blog section later). New types: `ContentNavItem`, `ContentNavGroup`, `ContentSearchItem`.
|
|
14
|
+
- `searchContent`, `ContentSearch` and `ContentIndex`: a content section's search, a port of bb's docs search. `searchContent(items, query)` is pure: a blank query returns everything, otherwise every typed word must appear in an item's `title`, `navTitle`, `description`, `badge` or `keywords`, title-prefix matches first. `ContentSearch` (client) is a `TextInput` plus a polite live-region count ("12 pages.", "1 match.", "3 matches.", the unfiltered noun set by `countNoun`, default `["page", "pages"]`) over `ContentIndex`. `ContentIndex` (server) is the bare card grid, for a section with too few entries to search (`/legal`, a handful of guides).
|
|
15
|
+
- `ContentPage` and `CopyMarkdownButton`: a content section's page. `ContentPage` (server) is `PageHeader` (`title`, `description` as its `lead`), a meta row with `lastUpdated` (a `<time dateTime>`) and, when `markdownHref` is set, `CopyMarkdownButton`, plus `actions`; optional `jsonLd` (one `Record<string, unknown>`, passed to `JsonLd`); then the body in `Prose`. `CopyMarkdownButton` (client) fetches `href` and copies the response body to the clipboard, reporting the result like `CopyButton`; any failure (the fetch, the response, or the clipboard) shows the same message and it never throws. Finished class strings (no `cx`), so `ContentPage` ships no tailwind-merge for it.
|
|
16
|
+
- `BrandPage` and `BrandSwatch`: a product's `/brand` page, `<BrandPage {...brandPageData("pools")} />` with `@haruhimemoe/brand`'s data (typed structurally; ui doesn't depend on brand). Sections: Name, Logo (each file previewed on a dark or light tile, with a `download` link), Colors (`BrandSwatch`, click to copy the hex), Type (`fonts`, default Nunito), Do's and don'ts, osu!, Family (only with `familyHref`), Contact (`mailto:`), plus `slots.afterLogo`, `slots.afterColors` and `slots.end`. `BrandSwatch` (client) has finished class strings, so `BrandPage` ships no tailwind-merge. New types: `BrandPageProps`, `BrandPageAsset`, `BrandPageFont`, `BrandSwatchProps`.
|
|
17
|
+
|
|
9
18
|
## [0.10.0] - 2026-10-04
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -144,7 +153,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
144
153
|
- `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`).
|
|
145
154
|
- 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).
|
|
146
155
|
|
|
147
|
-
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.
|
|
156
|
+
[unreleased]: https://github.com/haruhimemoe/ui/compare/v0.11.0...HEAD
|
|
157
|
+
[0.11.0]: https://github.com/haruhimemoe/ui/compare/v0.10.0...v0.11.0
|
|
148
158
|
[0.10.0]: https://github.com/haruhimemoe/ui/compare/v0.9.0...v0.10.0
|
|
149
159
|
[0.9.0]: https://github.com/haruhimemoe/ui/compare/v0.8.0...v0.9.0
|
|
150
160
|
[0.8.0]: https://github.com/haruhimemoe/ui/compare/v0.7.0...v0.8.0
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# @haruhimemoe/ui
|
|
4
4
|
|
|
5
|
-
React components for the haruhime.moe osu! tools on Next.js. It ships the osu!-web-style palette as a Tailwind 4 theme, plus buttons, links, badges, form fields and confirms, filter controls (toggle and choice chips, a two-thumb range slider, a filter panel), tables, osu! beatmap display pieces and player cards, a command palette (mod+k, with the defaults every tool shares) and the site header, footer, tabs, account menu and page frame. Most components are Server Components. The few that need the browser carry `"use client"` in their own files, so you import everything from one place.
|
|
5
|
+
React components for the haruhime.moe osu! tools on Next.js. It ships the osu!-web-style palette as a Tailwind 4 theme, plus buttons, links, badges, form fields and confirms, filter controls (toggle and choice chips, a two-thumb range slider, a filter panel), tables, osu! beatmap display pieces and player cards, a command palette (mod+k, with the defaults every tool shares), content section tooling (nav, search, index and page), brand pages and the site header, footer, tabs, account menu and page frame. Most components are Server Components. The few that need the browser carry `"use client"` in their own files, so you import everything from one place.
|
|
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.11.0. 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
|
|
|
@@ -155,6 +155,7 @@ Import every component from `@haruhimemoe/ui`, in Server and Client Components a
|
|
|
155
155
|
|
|
156
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`, and since 0.8.0 `CommandPalette` and `CommandPaletteButton`. 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
|
+
- **`ContentNav`** (since 0.11.0) reads the path to mark the current page, like `NavLinks`, but it always hydrates: its class strings are finished (no `cx`), so it ships no tailwind-merge to the browser. `CopyMarkdownButton` (since 0.11.0) is the same: finished classes, no tailwind-merge, even though `ContentPage` (a Server Component) renders it. `ContentLayout` and `ContentPage` are Server Components. `BrandSwatch` (since 0.11.0) is the same again: `BrandPage` is a Server Component and only its swatches hydrate, with no tailwind-merge.
|
|
158
159
|
- **Everything else is server-safe:** no state, no effects, no browser APIs.
|
|
159
160
|
|
|
160
161
|
A Server Component can't pass a function to a Client Component. So callback props (`onChange`, `onPressedChange`, `onClear`) have to come from your own `"use client"` file, like the filters example below. Props that are plain data (`CopyButton`'s `text`, `Chip`'s `pressed`) work from a Server Component. `Pagination` takes a function (`hrefFor`), but it is a Server Component itself, so that is fine anywhere. Its button mode (`onPageChange`) is a callback, so render that from a `"use client"` file.
|
|
@@ -717,6 +718,129 @@ schema.org structured data in a `<script type="application/ld+json">`. Every `<`
|
|
|
717
718
|
| --- | --- | --- | --- |
|
|
718
719
|
| `data` | `Record<string, unknown>` | required | The schema.org object. `@context` defaults to `https://schema.org`; set it in `data` to change it. |
|
|
719
720
|
|
|
721
|
+
### Content
|
|
722
|
+
|
|
723
|
+
Since 0.11.0. A content section's side navigation, page grid and search, ports of bb's docs sidebar, layout grid and search. `ui` has no next-kit or brand dependency: resolve your section's entries (a docs registry, a future blog) into these plain, structural types yourself.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
type ContentNavItem = { href: string; title: string; navTitle?: string; badge?: string };
|
|
727
|
+
type ContentNavGroup = { heading?: string; items: readonly ContentNavItem[] };
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
`ContentSearchItem` (`ContentNavItem & { description: string; keywords?: readonly string[] }`) is exported too, for `searchContent`, `ContentSearch` and `ContentIndex` below.
|
|
731
|
+
|
|
732
|
+
#### `ContentNav` (client)
|
|
733
|
+
|
|
734
|
+
The index link, then each group of links, the current page marked. A column from `lg` up; a "Contents" disclosure above it on phones. Finished class strings (no `cx`).
|
|
735
|
+
|
|
736
|
+
| Prop | Type | Default | What it does |
|
|
737
|
+
| --- | --- | --- | --- |
|
|
738
|
+
| `label` | `string` | required | The nav landmark's accessible name, on both the column and the phone disclosure. |
|
|
739
|
+
| `indexHref` | `string` | required | Where the index link (the section's overview page) points. |
|
|
740
|
+
| `indexLabel` | `string` | `"Overview"` | The index link's label. |
|
|
741
|
+
| `groups` | `readonly ContentNavGroup[]` | required | Each group's heading (left out when `heading` is unset) and links. |
|
|
742
|
+
|
|
743
|
+
A link's label is `navTitle ?? title`, in a `line-clamp-2` span so a long title wraps to two lines instead of overflowing the 14rem column; the link's `title` attribute always holds the full `title`. The current page's link gets `aria-current="page"`. A `badge` (e.g. a count) shows beside the label, in `font-mono`.
|
|
744
|
+
|
|
745
|
+
#### `ContentLayout`
|
|
746
|
+
|
|
747
|
+
The grid: `nav` in a 14rem column from `lg` up, the page beside it; one column on phones. A Server Component. `nav` takes an already-rendered node (usually a `<ContentNav>`), so this stays section-agnostic. Every native `<div>` prop; `className` merges last.
|
|
748
|
+
|
|
749
|
+
| Prop | Type | Default | What it does |
|
|
750
|
+
| --- | --- | --- | --- |
|
|
751
|
+
| `nav` | `ReactNode` | required | The section's navigation, already rendered. |
|
|
752
|
+
| `children` | `ReactNode` | required | The page, inside a `min-w-0` wrapper (so a long line inside it can still shrink and wrap). |
|
|
753
|
+
|
|
754
|
+
#### `searchContent`
|
|
755
|
+
|
|
756
|
+
`searchContent(items: readonly ContentSearchItem[], query: string): ContentSearchItem[]`. Pure and server-safe. A blank (or whitespace-only) query returns every item, in input order. Otherwise `query` is lowercased and split on spaces; an item matches when every word appears somewhere in its `title`, `navTitle`, `description`, `badge` or any `keywords`. Matching items whose `title` starts with the trimmed query come first; the rest keep their input order.
|
|
757
|
+
|
|
758
|
+
#### `ContentSearch` (client)
|
|
759
|
+
|
|
760
|
+
Port of bb's docs search. A `TextInput` (id `content-search`, `type="search"`), a polite live region with the result count, then `ContentIndex` over what `searchContent` found.
|
|
761
|
+
|
|
762
|
+
| Prop | Type | Default | What it does |
|
|
763
|
+
| --- | --- | --- | --- |
|
|
764
|
+
| `items` | `readonly ContentSearchItem[]` | required | The section's entries. |
|
|
765
|
+
| `label` | `string` | required | The field's label. |
|
|
766
|
+
| `placeholder` | `string` | none | The field's placeholder. |
|
|
767
|
+
| `countNoun` | `readonly [string, string]` | `["page", "pages"]` | Singular and plural noun for the unfiltered count ("12 pages."). A filtered count always reads "1 match." or "N matches." |
|
|
768
|
+
|
|
769
|
+
#### `ContentIndex`
|
|
770
|
+
|
|
771
|
+
The same card grid as `ContentSearch` (one link per item: title, badge, description), with no search field, for a section with too few entries to bother searching (`/legal`, a handful of guides). A Server Component.
|
|
772
|
+
|
|
773
|
+
| Prop | Type | Default | What it does |
|
|
774
|
+
| --- | --- | --- | --- |
|
|
775
|
+
| `items` | `readonly ContentSearchItem[]` | required | The entries to list. |
|
|
776
|
+
|
|
777
|
+
#### `ContentPage`
|
|
778
|
+
|
|
779
|
+
Since 0.11.0. A content section's page: `PageHeader` (the title and the description as its lead), a meta row with when the page was last updated and a "Copy as Markdown" button, then the body in `Prose`. Optional JSON-LD. A Server Component.
|
|
780
|
+
|
|
781
|
+
| Prop | Type | Default | What it does |
|
|
782
|
+
| --- | --- | --- | --- |
|
|
783
|
+
| `title` | `ReactNode` | required | The page's `<h1>`, passed to `PageHeader`. |
|
|
784
|
+
| `description` | `ReactNode` | none | Passed to `PageHeader` as `lead`. |
|
|
785
|
+
| `lastUpdated` | `string` | none | A `<time dateTime>`-ready string (an ISO date, say), shown next to `lastUpdatedLabel`. |
|
|
786
|
+
| `lastUpdatedLabel` | `ReactNode` | `"Last updated"` | The label before the time. |
|
|
787
|
+
| `markdownHref` | `string` | none | The page's raw Markdown source. When set, shows a `CopyMarkdownButton` for it. |
|
|
788
|
+
| `jsonLd` | `Record<string, unknown>` | none | One schema.org object, passed to `JsonLd`. Use `ld.graph(...)` yourself for several nodes. |
|
|
789
|
+
| `actions` | `ReactNode` | none | Extra buttons or links, shown alongside the time and the copy button. |
|
|
790
|
+
| `children` | `ReactNode` | required | The page body, rendered inside `Prose`. |
|
|
791
|
+
|
|
792
|
+
#### `CopyMarkdownButton` (client)
|
|
793
|
+
|
|
794
|
+
Since 0.11.0. A button that fetches `href` and copies the response body to the clipboard, reporting the result in an `<output>` beside it, like `CopyButton`. Any failure (the fetch rejects, the response isn't ok, or the clipboard refuses) shows the same failure message; it never throws. Its own client file with finished class strings (no `cx`): `ContentPage` is a Server Component, and this keeps tailwind-merge out of the browser bundle it ships.
|
|
795
|
+
|
|
796
|
+
| Prop | Type | Default | What it does |
|
|
797
|
+
| --- | --- | --- | --- |
|
|
798
|
+
| `href` | `string` | required | Fetched on click; the response's text is copied. |
|
|
799
|
+
| `label` | `ReactNode` | `"Copy as Markdown"` | The button's text. |
|
|
800
|
+
| `copiedLabel` | `ReactNode` | `"Copied"` | Shown after a copy works. |
|
|
801
|
+
| `failedLabel` | `ReactNode` | `"Couldn't copy"` | Shown when the fetch, the response, or the clipboard fails. |
|
|
802
|
+
|
|
803
|
+
### Brand
|
|
804
|
+
|
|
805
|
+
#### `BrandPage`
|
|
806
|
+
|
|
807
|
+
Since 0.11.0. A product's `/brand` page body. Pass it `@haruhimemoe/brand`'s data as is:
|
|
808
|
+
|
|
809
|
+
```tsx
|
|
810
|
+
import { brandPageData } from "@haruhimemoe/brand/products";
|
|
811
|
+
import { BrandPage } from "@haruhimemoe/ui";
|
|
812
|
+
|
|
813
|
+
<BrandPage {...brandPageData("pools")} />;
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
The props are typed structurally, so ui doesn't depend on brand. Sections, in order, each a `Card` with an `<h2>`: Name (name, tagline, how to write it, the site), Logo (each file previewed on a dark `b6` or light tile by `dark`, with a `download` link; non-image files get the link only), `slots.afterLogo`, Colors (a `BrandSwatch` per palette token), `slots.afterColors`, Type, Do's and don'ts, osu! ("Not affiliated with osu! or ppy. osu! is a trademark of ppy Pty Ltd."), Family ("Part of the haruhime.moe family.", linking `familyHref`; hidden when it's null, as on haruhime.moe itself), Contact (a `mailto:` link), `slots.end`. The headings and the osu! and family lines are the same on every haruhime brand page. A Server Component.
|
|
817
|
+
|
|
818
|
+
| Prop | Type | Default | What it does |
|
|
819
|
+
| --- | --- | --- | --- |
|
|
820
|
+
| `name` | `string` | required | The name as the wordmark reads it. |
|
|
821
|
+
| `mark` | `string` | required | The icon's letters (part of the data shape; not shown). |
|
|
822
|
+
| `tagline` | `string` | required | The product's one-line description. |
|
|
823
|
+
| `url` | `string` | required | The product's site, linked under Name. |
|
|
824
|
+
| `writing` | `string` | required | How to write the name in running text. |
|
|
825
|
+
| `dos`, `donts` | `readonly string[]` | required | The two lists. |
|
|
826
|
+
| `palette` | `Record<string, string>` | required | Token to `"#rrggbb"`, one swatch each. |
|
|
827
|
+
| `assets` | `readonly BrandPageAsset[]` | required | `{ label, href, dark }`: `dark: false` is an on-light variant, shown on a light tile. |
|
|
828
|
+
| `contact` | `string` | required | The contact address. |
|
|
829
|
+
| `familyHref` | `string \| null` | required | The family brand page, or null to hide the Family section. |
|
|
830
|
+
| `fonts` | `readonly BrandPageFont[]` | Nunito, "Wordmarks and headings" | `{ name, usage }` under Type. |
|
|
831
|
+
| `slots` | `{ afterLogo?, afterColors?, end?: ReactNode }` | none | Extra sections, for haruhime.moe's own page. |
|
|
832
|
+
|
|
833
|
+
#### `BrandSwatch` (client)
|
|
834
|
+
|
|
835
|
+
Since 0.11.0. One palette token: a button showing the color, the token and its hex. Clicking copies the hex and reports the result in an `<output>` beside it, like `CopyButton`. The color is set inline from the data (`style={{ backgroundColor }}`), so it doesn't follow `--hue`. Finished class strings, no tailwind-merge.
|
|
836
|
+
|
|
837
|
+
| Prop | Type | Default | What it does |
|
|
838
|
+
| --- | --- | --- | --- |
|
|
839
|
+
| `token` | `string` | required | The token's name. |
|
|
840
|
+
| `hex` | `string` | required | The color, shown and copied. |
|
|
841
|
+
| `copiedLabel` | `ReactNode` | `"Copied"` | Shown after a copy works. |
|
|
842
|
+
| `failedLabel` | `ReactNode` | `"Couldn't copy"` | Shown when the clipboard refuses. |
|
|
843
|
+
|
|
720
844
|
### Shell
|
|
721
845
|
|
|
722
846
|
Links in the header and footer are data (type `SiteLinkItem`):
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/brand/BrandPage.tsx
|
|
3
|
+
* @desc A product's `/brand` page body: name, logo files, colors, type, do's and don'ts, the
|
|
4
|
+
* osu! line, the family link and the contact, each a `Card`. Takes plain data shaped like
|
|
5
|
+
* `@haruhimemoe/brand`'s `brandPageData(...)` output (typed structurally here; ui doesn't
|
|
6
|
+
* depend on brand), so an app writes `<BrandPage {...brandPageData("pools")} />`. A Server
|
|
7
|
+
* Component; only `BrandSwatch` hydrates.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
/** One downloadable brand file. */
|
|
14
|
+
export type BrandPageAsset = {
|
|
15
|
+
/** The link's text. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** The file's URL. */
|
|
18
|
+
href: string;
|
|
19
|
+
/** True for a file drawn for a dark background, false for its on-light variant. */
|
|
20
|
+
dark: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** One typeface and what it's used for. */
|
|
23
|
+
export type BrandPageFont = {
|
|
24
|
+
name: string;
|
|
25
|
+
usage: string;
|
|
26
|
+
};
|
|
27
|
+
/** `BrandPage`'s props: `brandPageData(...)`'s shape, plus optional fonts and slots. */
|
|
28
|
+
export type BrandPageProps = {
|
|
29
|
+
name: string;
|
|
30
|
+
mark: string;
|
|
31
|
+
tagline: string;
|
|
32
|
+
url: string;
|
|
33
|
+
/** How to write the name in running text. */
|
|
34
|
+
writing: string;
|
|
35
|
+
dos: readonly string[];
|
|
36
|
+
donts: readonly string[];
|
|
37
|
+
/** Token to `"#rrggbb"`. */
|
|
38
|
+
palette: Record<string, string>;
|
|
39
|
+
assets: readonly BrandPageAsset[];
|
|
40
|
+
/** The contact address, linked with `mailto:`. */
|
|
41
|
+
contact: string;
|
|
42
|
+
/** The family brand page; null hides the Family section. */
|
|
43
|
+
familyHref: string | null;
|
|
44
|
+
/** Defaults to Nunito for wordmarks and headings. */
|
|
45
|
+
fonts?: readonly BrandPageFont[] | undefined;
|
|
46
|
+
/** Extra sections after Logo, after Colors, and at the end. */
|
|
47
|
+
slots?: {
|
|
48
|
+
afterLogo?: ReactNode;
|
|
49
|
+
afterColors?: ReactNode;
|
|
50
|
+
end?: ReactNode;
|
|
51
|
+
} | undefined;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* @function BrandPage
|
|
55
|
+
* @param props {BrandPageProps} the product's brand data, optional fonts and slots
|
|
56
|
+
* @returns {JSX.Element} the brand page's sections, in order
|
|
57
|
+
*/
|
|
58
|
+
export declare function BrandPage({ name, tagline, url, writing, dos, donts, palette, assets, contact, familyHref, fonts, slots, }: BrandPageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Card } from "../basics/Card.js";
|
|
3
|
+
import { linkClasses } from "../basics/linkStyles.js";
|
|
4
|
+
import { BrandSwatch } from "./BrandSwatch.js";
|
|
5
|
+
const DEFAULT_FONTS = [
|
|
6
|
+
{ name: "Nunito", usage: "Wordmarks and headings" },
|
|
7
|
+
];
|
|
8
|
+
const LINK = linkClasses();
|
|
9
|
+
const IMAGE = /\.(svg|png|jpe?g|webp)$/i;
|
|
10
|
+
/**
|
|
11
|
+
* @function BrandPage
|
|
12
|
+
* @param props {BrandPageProps} the product's brand data, optional fonts and slots
|
|
13
|
+
* @returns {JSX.Element} the brand page's sections, in order
|
|
14
|
+
*/
|
|
15
|
+
export function BrandPage({ name, tagline, url, writing, dos, donts, palette, assets, contact, familyHref, fonts = DEFAULT_FONTS, slots, }) {
|
|
16
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { title: "Name", children: [_jsx("p", { className: "font-bold text-c1 text-xl", children: name }), _jsx("p", { children: tagline }), _jsx("p", { children: writing }), _jsx("p", { children: _jsx("a", { className: LINK, href: url, children: url }) })] }), _jsx(Card, { title: "Logo", children: _jsx("ul", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: assets.map((asset) => (_jsxs("li", { className: "flex flex-col gap-2", children: [IMAGE.test(asset.href) ? (_jsx("div", { className: `flex h-32 items-center justify-center rounded-md p-4 ${asset.dark ? "bg-b6" : "bg-c1"}`, children: _jsx("img", { src: asset.href, alt: asset.label, className: "max-h-full max-w-full" }) })) : null, _jsx("a", { className: LINK, href: asset.href, download: true, children: asset.label })] }, asset.href))) }) }), slots?.afterLogo, _jsx(Card, { title: "Colors", children: _jsx("ul", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3", children: Object.entries(palette).map(([token, hex]) => (_jsx("li", { children: _jsx(BrandSwatch, { token: token, hex: hex }) }, token))) }) }), slots?.afterColors, _jsx(Card, { title: "Type", children: _jsx("ul", { className: "flex flex-col gap-1", children: fonts.map((font) => (_jsxs("li", { children: [_jsx("span", { className: "font-bold text-c1", children: font.name }), ": ", font.usage] }, font.name))) }) }), _jsx(Card, { title: "Do's and don'ts", children: _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { children: [_jsx("h3", { className: "mb-1 font-bold text-c1", children: "Do" }), _jsx("ul", { className: "list-disc pl-5", children: dos.map((item) => (_jsx("li", { children: item }, item))) })] }), _jsxs("div", { children: [_jsx("h3", { className: "mb-1 font-bold text-c1", children: "Don't" }), _jsx("ul", { className: "list-disc pl-5", children: donts.map((item) => (_jsx("li", { children: item }, item))) })] })] }) }), _jsx(Card, { title: "osu!", children: _jsx("p", { children: "Not affiliated with osu! or ppy. osu! is a trademark of ppy Pty Ltd." }) }), familyHref ? (_jsx(Card, { title: "Family", children: _jsx("a", { className: LINK, href: familyHref, children: "Part of the haruhime.moe family." }) })) : null, _jsx(Card, { title: "Contact", children: _jsx("a", { className: LINK, href: `mailto:${contact}`, children: contact }) }), slots?.end] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/brand/BrandSwatch.tsx
|
|
3
|
+
* @desc One palette token on a brand page: a button showing the color, its token and its hex,
|
|
4
|
+
* that copies the hex to the clipboard and reports the result in an `<output>` beside it.
|
|
5
|
+
* The color comes from the brand data through an inline `backgroundColor` (hue-independent,
|
|
6
|
+
* like `StarRating`). Its own client file with finished class strings (no `cx`), so
|
|
7
|
+
* `BrandPage`, the Server Component that renders it, ships no tailwind-merge.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
/** `BrandSwatch`'s props. */
|
|
14
|
+
export type BrandSwatchProps = {
|
|
15
|
+
/** The palette token's name ("h1", "b6"). */
|
|
16
|
+
token: string;
|
|
17
|
+
/** The token's color as `"#rrggbb"`; shown, painted and copied. */
|
|
18
|
+
hex: string;
|
|
19
|
+
/** Status after a successful copy. Defaults to "Copied". */
|
|
20
|
+
copiedLabel?: ReactNode | undefined;
|
|
21
|
+
/** Status when the clipboard refuses. Defaults to "Couldn't copy". */
|
|
22
|
+
failedLabel?: ReactNode | undefined;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @function BrandSwatch
|
|
26
|
+
* @param props {BrandSwatchProps} the token, its hex, and optional status text
|
|
27
|
+
* @returns {JSX.Element} the click-to-copy swatch button and its status `<output>`
|
|
28
|
+
*/
|
|
29
|
+
export declare function BrandSwatch({ token, hex, copiedLabel, failedLabel, }: BrandSwatchProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/brand/BrandSwatch.tsx
|
|
3
|
+
* @desc One palette token on a brand page: a button showing the color, its token and its hex,
|
|
4
|
+
* that copies the hex to the clipboard and reports the result in an `<output>` beside it.
|
|
5
|
+
* The color comes from the brand data through an inline `backgroundColor` (hue-independent,
|
|
6
|
+
* like `StarRating`). Its own client file with finished class strings (no `cx`), so
|
|
7
|
+
* `BrandPage`, the Server Component that renders it, ships no tailwind-merge.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { StatusOutput } from "../actions/StatusOutput.js";
|
|
15
|
+
import { useLatestStatus } from "../actions/useLatestStatus.js";
|
|
16
|
+
const BUTTON_CLASS_NAME = "flex w-full items-center gap-3 rounded-md bg-b5 p-2 text-left transition-colors hover:bg-b3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-h1";
|
|
17
|
+
/**
|
|
18
|
+
* @function BrandSwatch
|
|
19
|
+
* @param props {BrandSwatchProps} the token, its hex, and optional status text
|
|
20
|
+
* @returns {JSX.Element} the click-to-copy swatch button and its status `<output>`
|
|
21
|
+
*/
|
|
22
|
+
export function BrandSwatch({ token, hex, copiedLabel = "Copied", failedLabel = "Couldn't copy", }) {
|
|
23
|
+
const { status, start, settle } = useLatestStatus();
|
|
24
|
+
const copy = async () => {
|
|
25
|
+
const run = start();
|
|
26
|
+
try {
|
|
27
|
+
await navigator.clipboard.writeText(hex);
|
|
28
|
+
settle(run, "copied");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
settle(run, "failed");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("button", { type: "button", className: BUTTON_CLASS_NAME, onClick: copy, children: [_jsx("span", { "aria-hidden": "true", className: "size-10 shrink-0 rounded-md border border-b2", style: { backgroundColor: hex } }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "font-bold text-c1", children: token }), _jsx("span", { className: "font-mono text-c3 text-sm", children: hex })] })] }), _jsx(StatusOutput, { run: status?.run ?? null, children: status?.result === "copied" ? copiedLabel : failedLabel })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentIndex.tsx
|
|
3
|
+
* @desc A content section's card grid with no search: one link per entry, its title, badge and
|
|
4
|
+
* description. For short sections (`/legal`, a handful of guides) where `ContentSearch`'s
|
|
5
|
+
* field would be overkill. Server-safe: no directive, no state; `ContentSearch` renders
|
|
6
|
+
* this for its own filtered list, so the card look lives in one place.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sun Oct 4, 2026
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
|
+
*/
|
|
11
|
+
import type { ContentSearchItem } from "./types.js";
|
|
12
|
+
/** `ContentIndex`'s props. */
|
|
13
|
+
export type ContentIndexProps = {
|
|
14
|
+
items: readonly ContentSearchItem[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @function ContentIndex
|
|
18
|
+
* @param props {ContentIndexProps} the section's entries
|
|
19
|
+
* @returns {JSX.Element} a two-column card grid (one column on phones), each card the entry's
|
|
20
|
+
* title, badge and description
|
|
21
|
+
*/
|
|
22
|
+
export declare function ContentIndex({ items }: ContentIndexProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/content/ContentIndex.tsx
|
|
4
|
+
* @desc A content section's card grid with no search: one link per entry, its title, badge and
|
|
5
|
+
* description. For short sections (`/legal`, a handful of guides) where `ContentSearch`'s
|
|
6
|
+
* field would be overkill. Server-safe: no directive, no state; `ContentSearch` renders
|
|
7
|
+
* this for its own filtered list, so the card look lives in one place.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import Link from "next/link.js";
|
|
13
|
+
/**
|
|
14
|
+
* @function ContentIndex
|
|
15
|
+
* @param props {ContentIndexProps} the section's entries
|
|
16
|
+
* @returns {JSX.Element} a two-column card grid (one column on phones), each card the entry's
|
|
17
|
+
* title, badge and description
|
|
18
|
+
*/
|
|
19
|
+
export function ContentIndex({ items }) {
|
|
20
|
+
return (_jsx("ul", { className: "grid grid-cols-1 gap-2 sm:grid-cols-2", children: items.map((item) => (_jsx("li", { children: _jsxs(Link, { href: item.href, className: "flex h-full flex-col gap-1 rounded-md bg-b4 p-3 hover:bg-b3", children: [_jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [_jsx("span", { className: "font-bold text-c1", children: item.title }), item.badge ? _jsx("span", { className: "font-mono text-c4 text-xs", children: item.badge }) : null] }), _jsx("span", { className: "text-c3 text-sm", children: item.description })] }) }, item.href))) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentLayout.tsx
|
|
3
|
+
* @desc A content section's page grid: the nav in a 14rem column from `lg` up, the page beside
|
|
4
|
+
* it. Port of bb's docs layout grid (`bb.haruhime.moe/src/app/docs/layout.tsx`), generic
|
|
5
|
+
* over what `nav` is so a docs section and a later blog section share it.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sun Oct 4, 2026
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
11
|
+
/** Every native `<div>` prop (including `ref`), plus the nav slot. */
|
|
12
|
+
export type ContentLayoutProps = Omit<ComponentProps<"div">, "children"> & {
|
|
13
|
+
/** Usually a `<ContentNav>`, already rendered by the caller. */
|
|
14
|
+
nav: ReactNode;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @function ContentLayout
|
|
19
|
+
* @param props {ContentLayoutProps} the nav slot, the page as children, and native div props
|
|
20
|
+
* @returns {JSX.Element} a one-column grid on phones, a 14rem nav column beside the page from
|
|
21
|
+
* `lg` up
|
|
22
|
+
*/
|
|
23
|
+
export declare function ContentLayout({ nav, children, className, ...props }: ContentLayoutProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* @function ContentLayout
|
|
5
|
+
* @param props {ContentLayoutProps} the nav slot, the page as children, and native div props
|
|
6
|
+
* @returns {JSX.Element} a one-column grid on phones, a 14rem nav column beside the page from
|
|
7
|
+
* `lg` up
|
|
8
|
+
*/
|
|
9
|
+
export function ContentLayout({ nav, children, className, ...props }) {
|
|
10
|
+
return (_jsxs("div", { className: cx("grid grid-cols-1 gap-6 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-10", className), ...props, children: [nav, _jsx("div", { className: "min-w-0", children: children })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentNav.tsx
|
|
3
|
+
* @desc A content section's side navigation: an index link, then each group of links, the
|
|
4
|
+
* current page marked. A column beside the page on wide screens; a phone disclosure above
|
|
5
|
+
* it. Port of bb's docs sidebar (`bb.haruhime.moe/src/components/docs/DocsNav.tsx`), with
|
|
6
|
+
* the index link and groups taken from props instead of a hardcoded "docs" shape, so a
|
|
7
|
+
* docs section, and later a blog section, can both use it.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ContentNavGroup } from "./types.js";
|
|
13
|
+
/** `ContentNav`'s props. */
|
|
14
|
+
export type ContentNavProps = {
|
|
15
|
+
/** The nav landmark's accessible name, for both the column and the phone disclosure. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Where the index link (the section's overview page) points. */
|
|
18
|
+
indexHref: string;
|
|
19
|
+
/** The index link's label. Default `"Overview"`. */
|
|
20
|
+
indexLabel?: string;
|
|
21
|
+
groups: readonly ContentNavGroup[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* @function ContentNav
|
|
25
|
+
* @param props {ContentNavProps} the nav's accessible name, the index link, and the groups
|
|
26
|
+
* beneath it
|
|
27
|
+
* @returns {JSX.Element} the navigation, as a column and as a phone disclosure
|
|
28
|
+
*/
|
|
29
|
+
export declare function ContentNav({ label, indexHref, indexLabel, groups }: ContentNavProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentNav.tsx
|
|
3
|
+
* @desc A content section's side navigation: an index link, then each group of links, the
|
|
4
|
+
* current page marked. A column beside the page on wide screens; a phone disclosure above
|
|
5
|
+
* it. Port of bb's docs sidebar (`bb.haruhime.moe/src/components/docs/DocsNav.tsx`), with
|
|
6
|
+
* the index link and groups taken from props instead of a hardcoded "docs" shape, so a
|
|
7
|
+
* docs section, and later a blog section, can both use it.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
"use client";
|
|
13
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import Link from "next/link.js";
|
|
15
|
+
import { usePathname } from "next/navigation.js";
|
|
16
|
+
const LINK_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 text-c3";
|
|
17
|
+
const CURRENT_CLASS_NAME = "flex items-baseline justify-between gap-2 rounded px-2 py-1 text-sm hover:bg-b4 hover:text-c1 bg-b4 font-bold text-c1";
|
|
18
|
+
/**
|
|
19
|
+
* @function ContentNav
|
|
20
|
+
* @param props {ContentNavProps} the nav's accessible name, the index link, and the groups
|
|
21
|
+
* beneath it
|
|
22
|
+
* @returns {JSX.Element} the navigation, as a column and as a phone disclosure
|
|
23
|
+
*/
|
|
24
|
+
export function ContentNav({ label, indexHref, indexLabel = "Overview", groups }) {
|
|
25
|
+
const path = usePathname();
|
|
26
|
+
const link = (item) => {
|
|
27
|
+
const current = path === item.href;
|
|
28
|
+
return (_jsx("li", { children: _jsxs(Link, { href: item.href, "aria-current": current ? "page" : undefined, title: item.title, className: current ? CURRENT_CLASS_NAME : LINK_CLASS_NAME, children: [_jsx("span", { className: "line-clamp-2 min-w-0 break-words", children: item.navTitle ?? item.title }), item.badge ? _jsx("span", { className: "font-mono text-c4 text-xs", children: item.badge }) : null] }) }, item.href));
|
|
29
|
+
};
|
|
30
|
+
const list = (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("ul", { children: link({ href: indexHref, title: indexLabel }) }), groups.map((group, index) => (_jsxs("div", { children: [group.heading ? (_jsx("p", { className: "mb-1 px-2 font-bold text-c4 text-xs uppercase tracking-wide", children: group.heading })) : null, _jsx("ul", { children: group.items.map((item) => link(item)) })] }, group.heading ?? index)))] }));
|
|
31
|
+
return (_jsxs(_Fragment, { children: [_jsx("nav", { "aria-label": label, className: "hidden lg:block", children: _jsx("div", { className: "sticky top-4 max-h-[calc(100vh-2rem)] overflow-y-auto pr-1", children: list }) }), _jsxs("details", { className: "rounded-md bg-b4 p-2 lg:hidden", children: [_jsx("summary", { className: "cursor-pointer px-2 py-1 font-bold text-c2 text-sm", children: "Contents" }), _jsx("nav", { "aria-label": label, className: "mt-2", children: list })] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentPage.tsx
|
|
3
|
+
* @desc A content section's page: the `PageHeader` (title, description as its lead), a meta row
|
|
4
|
+
* with when the page was last updated and a "Copy as Markdown" button, and the body in
|
|
5
|
+
* `Prose`. Optional JSON-LD structured data. Server-safe: no state, no browser APIs. A
|
|
6
|
+
* caller resolves its own entries into these props (a docs page today, a blog post later),
|
|
7
|
+
* the way `ContentNav` and `ContentLayout` do.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
/** `ContentPage`'s props. */
|
|
14
|
+
export type ContentPageProps = {
|
|
15
|
+
/** The page's h1. */
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
/** A sentence under the title, passed to `PageHeader` as its `lead`. */
|
|
18
|
+
description?: ReactNode | undefined;
|
|
19
|
+
/** When the page was last updated, as a `<time dateTime>`-ready string (e.g. an ISO date). */
|
|
20
|
+
lastUpdated?: string | undefined;
|
|
21
|
+
/** The label before the last-updated time. Defaults to "Last updated". */
|
|
22
|
+
lastUpdatedLabel?: ReactNode | undefined;
|
|
23
|
+
/** The page's raw Markdown source. When set, shows a "Copy as Markdown" button that fetches it. */
|
|
24
|
+
markdownHref?: string | undefined;
|
|
25
|
+
/** A schema.org object for `JsonLd`. Pass `ld.graph(...)` yourself for several nodes. */
|
|
26
|
+
jsonLd?: Record<string, unknown> | undefined;
|
|
27
|
+
/** Extra buttons or links, shown alongside the last-updated time and the copy button. */
|
|
28
|
+
actions?: ReactNode | undefined;
|
|
29
|
+
/** The page body, rendered inside `Prose`. */
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* @function ContentPage
|
|
34
|
+
* @param props {ContentPageProps} the title and optional description, last-updated time, Markdown
|
|
35
|
+
* source, JSON-LD data and actions, plus the page body as children
|
|
36
|
+
* @returns {JSX.Element} optional `JsonLd`, a `PageHeader` whose actions hold the last-updated
|
|
37
|
+
* time, the copy button and the caller's actions, then the body in `Prose`
|
|
38
|
+
*/
|
|
39
|
+
export declare function ContentPage({ title, description, lastUpdated, lastUpdatedLabel, markdownHref, jsonLd, actions, children, }: ContentPageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PageHeader } from "../basics/PageHeader.js";
|
|
3
|
+
import { Prose } from "../basics/Prose.js";
|
|
4
|
+
import { JsonLd } from "../meta/JsonLd.js";
|
|
5
|
+
import { CopyMarkdownButton } from "./CopyMarkdownButton.js";
|
|
6
|
+
/**
|
|
7
|
+
* @function ContentPage
|
|
8
|
+
* @param props {ContentPageProps} the title and optional description, last-updated time, Markdown
|
|
9
|
+
* source, JSON-LD data and actions, plus the page body as children
|
|
10
|
+
* @returns {JSX.Element} optional `JsonLd`, a `PageHeader` whose actions hold the last-updated
|
|
11
|
+
* time, the copy button and the caller's actions, then the body in `Prose`
|
|
12
|
+
*/
|
|
13
|
+
export function ContentPage({ title, description, lastUpdated, lastUpdatedLabel = "Last updated", markdownHref, jsonLd, actions, children, }) {
|
|
14
|
+
const meta = lastUpdated || markdownHref || actions;
|
|
15
|
+
return (_jsxs(_Fragment, { children: [jsonLd ? _jsx(JsonLd, { data: jsonLd }) : null, _jsx(PageHeader, { title: title, lead: description, actions: meta ? (_jsxs(_Fragment, { children: [lastUpdated ? (_jsxs("div", { className: "text-c4 text-sm", children: [lastUpdatedLabel, " ", _jsx("time", { dateTime: lastUpdated, children: lastUpdated })] })) : null, markdownHref ? _jsx(CopyMarkdownButton, { href: markdownHref }) : null, actions] })) : undefined }), _jsx(Prose, { children: children })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentSearch.tsx
|
|
3
|
+
* @desc A content section's search: one field that narrows its entries as it's typed, with the
|
|
4
|
+
* result count said to screen readers, then `ContentIndex`'s card grid over what matched.
|
|
5
|
+
* Port of bb's docs search (`bb.haruhime.moe/src/components/docs/DocsSearch.tsx`), with the
|
|
6
|
+
* entries, label and count noun taken from props instead of a hardcoded docs shape.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sun Oct 4, 2026
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
|
+
*/
|
|
11
|
+
import type { ContentSearchItem } from "./types.js";
|
|
12
|
+
/** `ContentSearch`'s props. */
|
|
13
|
+
export type ContentSearchProps = {
|
|
14
|
+
items: readonly ContentSearchItem[];
|
|
15
|
+
/** The field's label, and the search landmark's name read by nothing else here. */
|
|
16
|
+
label: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Singular and plural noun for the unfiltered count ("12 pages."). Default `["page", "pages"]`. */
|
|
19
|
+
countNoun?: readonly [string, string];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* @function ContentSearch
|
|
23
|
+
* @param props {ContentSearchProps} the section's entries, the field's label, an optional
|
|
24
|
+
* placeholder, and an optional singular/plural noun for the unfiltered count
|
|
25
|
+
* @returns {JSX.Element} the field, the result count (a live region) and the matching entries
|
|
26
|
+
*/
|
|
27
|
+
export declare function ContentSearch({ items, label, placeholder, countNoun, }: ContentSearchProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/ContentSearch.tsx
|
|
3
|
+
* @desc A content section's search: one field that narrows its entries as it's typed, with the
|
|
4
|
+
* result count said to screen readers, then `ContentIndex`'s card grid over what matched.
|
|
5
|
+
* Port of bb's docs search (`bb.haruhime.moe/src/components/docs/DocsSearch.tsx`), with the
|
|
6
|
+
* entries, label and count noun taken from props instead of a hardcoded docs shape.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sun Oct 4, 2026
|
|
9
|
+
* @modified Sun Oct 4, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { TextInput } from "../forms/TextInput.js";
|
|
15
|
+
import { ContentIndex } from "./ContentIndex.js";
|
|
16
|
+
import { searchContent } from "./searchContent.js";
|
|
17
|
+
/**
|
|
18
|
+
* @function ContentSearch
|
|
19
|
+
* @param props {ContentSearchProps} the section's entries, the field's label, an optional
|
|
20
|
+
* placeholder, and an optional singular/plural noun for the unfiltered count
|
|
21
|
+
* @returns {JSX.Element} the field, the result count (a live region) and the matching entries
|
|
22
|
+
*/
|
|
23
|
+
export function ContentSearch({ items, label, placeholder, countNoun = ["page", "pages"], }) {
|
|
24
|
+
const [query, setQuery] = useState("");
|
|
25
|
+
const found = searchContent(items, query);
|
|
26
|
+
const trimmed = query.trim();
|
|
27
|
+
const [singular, plural] = countNoun;
|
|
28
|
+
const countText = trimmed === ""
|
|
29
|
+
? `${items.length} ${items.length === 1 ? singular : plural}.`
|
|
30
|
+
: `${found.length} ${found.length === 1 ? "match" : "matches"}.`;
|
|
31
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(TextInput, { id: "content-search", type: "search", label: label, placeholder: placeholder, value: query, onChange: (event) => setQuery(event.target.value), autoComplete: "off" }), _jsx("p", { className: "text-c3 text-sm", "aria-live": "polite", children: countText }), _jsx(ContentIndex, { items: found })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/CopyMarkdownButton.tsx
|
|
3
|
+
* @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
|
|
4
|
+
* result in an `<output>` beside it (a polite live region), for a doc page's "Copy as
|
|
5
|
+
* Markdown" action. Any failure (the fetch rejects, the response isn't ok, or the clipboard
|
|
6
|
+
* refuses) announces the same failure message; the button never throws. Its own client file
|
|
7
|
+
* with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
|
|
8
|
+
* for `ContentPage`, the Server Component that renders it. Not a wrapper around
|
|
9
|
+
* `CopyButton`, which imports `cx`.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Sun Oct 4, 2026
|
|
12
|
+
* @modified Sun Oct 4, 2026
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
/** `CopyMarkdownButton`'s props. */
|
|
16
|
+
export type CopyMarkdownButtonProps = {
|
|
17
|
+
/** Fetched on click; the response body is copied to the clipboard. */
|
|
18
|
+
href: string;
|
|
19
|
+
/** The button's text. Defaults to "Copy as Markdown". */
|
|
20
|
+
label?: ReactNode | undefined;
|
|
21
|
+
/** Status after a successful copy. Defaults to "Copied". */
|
|
22
|
+
copiedLabel?: ReactNode | undefined;
|
|
23
|
+
/** Status when the fetch, the response, or the clipboard fails. Defaults to "Couldn't copy". */
|
|
24
|
+
failedLabel?: ReactNode | undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* @function CopyMarkdownButton
|
|
28
|
+
* @param props {CopyMarkdownButtonProps} the Markdown source's URL, plus optional label and
|
|
29
|
+
* status text
|
|
30
|
+
* @returns {JSX.Element} the button and an `<output>` that announces the copy's result
|
|
31
|
+
*/
|
|
32
|
+
export declare function CopyMarkdownButton({ href, label, copiedLabel, failedLabel, }: CopyMarkdownButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/CopyMarkdownButton.tsx
|
|
3
|
+
* @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
|
|
4
|
+
* result in an `<output>` beside it (a polite live region), for a doc page's "Copy as
|
|
5
|
+
* Markdown" action. Any failure (the fetch rejects, the response isn't ok, or the clipboard
|
|
6
|
+
* refuses) announces the same failure message; the button never throws. Its own client file
|
|
7
|
+
* with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
|
|
8
|
+
* for `ContentPage`, the Server Component that renders it. Not a wrapper around
|
|
9
|
+
* `CopyButton`, which imports `cx`.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Sun Oct 4, 2026
|
|
12
|
+
* @modified Sun Oct 4, 2026
|
|
13
|
+
*/
|
|
14
|
+
"use client";
|
|
15
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { StatusOutput } from "../actions/StatusOutput.js";
|
|
17
|
+
import { useLatestStatus } from "../actions/useLatestStatus.js";
|
|
18
|
+
// buttonClasses({ variant: "secondary" }) copied as a literal, so this file imports no cx.
|
|
19
|
+
const BUTTON_CLASS_NAME = "inline-flex items-center justify-center gap-2 rounded-full font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-h1 disabled:cursor-not-allowed disabled:opacity-50 bg-b3 text-c1 not-disabled:hover:bg-b2 h-9 px-4 text-sm";
|
|
20
|
+
/**
|
|
21
|
+
* @function CopyMarkdownButton
|
|
22
|
+
* @param props {CopyMarkdownButtonProps} the Markdown source's URL, plus optional label and
|
|
23
|
+
* status text
|
|
24
|
+
* @returns {JSX.Element} the button and an `<output>` that announces the copy's result
|
|
25
|
+
*/
|
|
26
|
+
export function CopyMarkdownButton({ href, label = "Copy as Markdown", copiedLabel = "Copied", failedLabel = "Couldn't copy", }) {
|
|
27
|
+
const { status, start, settle } = useLatestStatus();
|
|
28
|
+
const copy = async () => {
|
|
29
|
+
const run = start();
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch(href);
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
throw new Error(String(response.status));
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
await navigator.clipboard.writeText(text);
|
|
36
|
+
settle(run, "copied");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
settle(run, "failed");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("button", { type: "button", className: BUTTON_CLASS_NAME, onClick: copy, children: label }), _jsx(StatusOutput, { run: status?.run ?? null, children: status?.result === "copied" ? copiedLabel : failedLabel })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/searchContent.ts
|
|
3
|
+
* @desc The search over a content section's entries: a blank query returns everything, otherwise
|
|
4
|
+
* every typed word must appear somewhere in the entry (title, navTitle, description, badge
|
|
5
|
+
* or any keyword). Pure and server-safe, so `ContentSearch` and a future server-rendered
|
|
6
|
+
* index can both call it. Port of bb's `searchDocs` (`bb.haruhime.moe/src/utils/docs.ts`),
|
|
7
|
+
* simplified to a plain AND match over one haystack per entry instead of per-word scoring.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ContentSearchItem } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* @function searchContent
|
|
15
|
+
* @param items {readonly ContentSearchItem[]} the section's entries
|
|
16
|
+
* @param query {string} what was typed
|
|
17
|
+
* @returns {ContentSearchItem[]} every entry (in input order) when `query` is blank, otherwise
|
|
18
|
+
* the entries where every lowercased, space-split word of `query` appears in the
|
|
19
|
+
* entry's title, navTitle, description, badge or keywords; entries whose title starts
|
|
20
|
+
* with the trimmed query come first, the rest keep their input order
|
|
21
|
+
*/
|
|
22
|
+
export declare function searchContent(items: readonly ContentSearchItem[], query: string): ContentSearchItem[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/searchContent.ts
|
|
3
|
+
* @desc The search over a content section's entries: a blank query returns everything, otherwise
|
|
4
|
+
* every typed word must appear somewhere in the entry (title, navTitle, description, badge
|
|
5
|
+
* or any keyword). Pure and server-safe, so `ContentSearch` and a future server-rendered
|
|
6
|
+
* index can both call it. Port of bb's `searchDocs` (`bb.haruhime.moe/src/utils/docs.ts`),
|
|
7
|
+
* simplified to a plain AND match over one haystack per entry instead of per-word scoring.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
/** The text of an entry a query's words are matched against, lowercased. */
|
|
13
|
+
const haystackOf = (item) => [item.title, item.navTitle, item.description, item.badge, ...(item.keywords ?? [])]
|
|
14
|
+
.filter((part) => Boolean(part))
|
|
15
|
+
.join(" ")
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
/**
|
|
18
|
+
* @function searchContent
|
|
19
|
+
* @param items {readonly ContentSearchItem[]} the section's entries
|
|
20
|
+
* @param query {string} what was typed
|
|
21
|
+
* @returns {ContentSearchItem[]} every entry (in input order) when `query` is blank, otherwise
|
|
22
|
+
* the entries where every lowercased, space-split word of `query` appears in the
|
|
23
|
+
* entry's title, navTitle, description, badge or keywords; entries whose title starts
|
|
24
|
+
* with the trimmed query come first, the rest keep their input order
|
|
25
|
+
*/
|
|
26
|
+
export function searchContent(items, query) {
|
|
27
|
+
const trimmed = query.trim().toLowerCase();
|
|
28
|
+
if (trimmed === "")
|
|
29
|
+
return [...items];
|
|
30
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
31
|
+
const matches = items.filter((item) => {
|
|
32
|
+
const haystack = haystackOf(item);
|
|
33
|
+
return words.every((word) => haystack.includes(word));
|
|
34
|
+
});
|
|
35
|
+
const isTitlePrefix = (item) => item.title.toLowerCase().startsWith(trimmed);
|
|
36
|
+
return [...matches.filter(isTitlePrefix), ...matches.filter((item) => !isTitlePrefix(item))];
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/types.ts
|
|
3
|
+
* @desc Content navigation's data: a link (`ContentNavItem`), a run of links under an optional
|
|
4
|
+
* heading (`ContentNavGroup`), and a searchable item (`ContentSearchItem`, for task 7's
|
|
5
|
+
* `ContentSearch`). Plain, server-safe types with no next-kit or brand import: a caller
|
|
6
|
+
* resolves its own entries (a docs registry today, a blog section later) into these
|
|
7
|
+
* shapes, so `ui` stays section-agnostic.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
/** One sidebar link: a resolved href, its full title, and an optional nav-length title. */
|
|
13
|
+
export type ContentNavItem = {
|
|
14
|
+
href: string;
|
|
15
|
+
title: string;
|
|
16
|
+
/** Shown in the nav instead of `title`, for a long title. */
|
|
17
|
+
navTitle?: string;
|
|
18
|
+
/** A short tag beside the label, e.g. a count or "new". */
|
|
19
|
+
badge?: string;
|
|
20
|
+
};
|
|
21
|
+
/** A run of links under an optional heading. No `heading` renders no label above them. */
|
|
22
|
+
export type ContentNavGroup = {
|
|
23
|
+
heading?: string;
|
|
24
|
+
items: readonly ContentNavItem[];
|
|
25
|
+
};
|
|
26
|
+
/** A `ContentNavItem` plus the text a fuzzy search matches against. */
|
|
27
|
+
export type ContentSearchItem = ContentNavItem & {
|
|
28
|
+
description: string;
|
|
29
|
+
keywords?: readonly string[];
|
|
30
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/content/types.ts
|
|
3
|
+
* @desc Content navigation's data: a link (`ContentNavItem`), a run of links under an optional
|
|
4
|
+
* heading (`ContentNavGroup`), and a searchable item (`ContentSearchItem`, for task 7's
|
|
5
|
+
* `ContentSearch`). Plain, server-safe types with no next-kit or brand import: a caller
|
|
6
|
+
* resolves its own entries (a docs registry today, a blog section later) into these
|
|
7
|
+
* shapes, so `ui` stays section-agnostic.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sun Oct 4, 2026
|
|
10
|
+
* @modified Sun Oct 4, 2026
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* their own "use client" directive, so this barrel is safe to import from Server Components.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
export { AsyncButton, type AsyncButtonProps } from "./components/actions/AsyncButton.js";
|
|
11
11
|
export { CopyButton, type CopyButtonProps } from "./components/actions/CopyButton.js";
|
|
@@ -25,6 +25,16 @@ export { Prose, type ProseProps } from "./components/basics/Prose.js";
|
|
|
25
25
|
export { type TabItem, Tabs, type TabsProps } from "./components/basics/Tabs.js";
|
|
26
26
|
export { TextLink, type TextLinkProps } from "./components/basics/TextLink.js";
|
|
27
27
|
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
28
|
+
export { BrandPage, type BrandPageAsset, type BrandPageFont, type BrandPageProps, } from "./components/brand/BrandPage.js";
|
|
29
|
+
export { BrandSwatch, type BrandSwatchProps } from "./components/brand/BrandSwatch.js";
|
|
30
|
+
export { ContentIndex, type ContentIndexProps } from "./components/content/ContentIndex.js";
|
|
31
|
+
export { ContentLayout, type ContentLayoutProps } from "./components/content/ContentLayout.js";
|
|
32
|
+
export { ContentNav, type ContentNavProps } from "./components/content/ContentNav.js";
|
|
33
|
+
export { ContentPage, type ContentPageProps } from "./components/content/ContentPage.js";
|
|
34
|
+
export { ContentSearch, type ContentSearchProps } from "./components/content/ContentSearch.js";
|
|
35
|
+
export { CopyMarkdownButton, type CopyMarkdownButtonProps, } from "./components/content/CopyMarkdownButton.js";
|
|
36
|
+
export { searchContent } from "./components/content/searchContent.js";
|
|
37
|
+
export type { ContentNavGroup, ContentNavItem, ContentSearchItem, } from "./components/content/types.js";
|
|
28
38
|
export { Chip, type ChipProps } from "./components/filters/Chip.js";
|
|
29
39
|
export { ChipGroup, type ChipGroupProps, type ChipOption } from "./components/filters/ChipGroup.js";
|
|
30
40
|
export { type ChoiceChipOption, ChoiceChips, type ChoiceChipsProps, } from "./components/filters/ChoiceChips.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* their own "use client" directive, so this barrel is safe to import from Server Components.
|
|
6
6
|
* @author David @dvhsh (https://dvh.sh)
|
|
7
7
|
* @created Wed Sep 23, 2026
|
|
8
|
-
* @modified
|
|
8
|
+
* @modified Sun Oct 4, 2026
|
|
9
9
|
*/
|
|
10
10
|
// Actions
|
|
11
11
|
export { AsyncButton } from "./components/actions/AsyncButton.js";
|
|
@@ -26,6 +26,17 @@ export { Prose } from "./components/basics/Prose.js";
|
|
|
26
26
|
export { Tabs } from "./components/basics/Tabs.js";
|
|
27
27
|
export { TextLink } from "./components/basics/TextLink.js";
|
|
28
28
|
export { tabId, tabPanelId } from "./components/basics/tabIds.js";
|
|
29
|
+
// Brand
|
|
30
|
+
export { BrandPage, } from "./components/brand/BrandPage.js";
|
|
31
|
+
export { BrandSwatch } from "./components/brand/BrandSwatch.js";
|
|
32
|
+
// Content
|
|
33
|
+
export { ContentIndex } from "./components/content/ContentIndex.js";
|
|
34
|
+
export { ContentLayout } from "./components/content/ContentLayout.js";
|
|
35
|
+
export { ContentNav } from "./components/content/ContentNav.js";
|
|
36
|
+
export { ContentPage } from "./components/content/ContentPage.js";
|
|
37
|
+
export { ContentSearch } from "./components/content/ContentSearch.js";
|
|
38
|
+
export { CopyMarkdownButton, } from "./components/content/CopyMarkdownButton.js";
|
|
39
|
+
export { searchContent } from "./components/content/searchContent.js";
|
|
29
40
|
// Filters
|
|
30
41
|
export { Chip } from "./components/filters/Chip.js";
|
|
31
42
|
export { ChipGroup } from "./components/filters/ChipGroup.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/ui",
|
|
3
|
-
"version": "0.
|
|
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 player cards, a command palette and the site header and footer.",
|
|
3
|
+
"version": "0.11.0",
|
|
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 player cards, a command palette, content section tooling, brand pages and the site header and footer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
7
7
|
"osu!",
|