@haruhimemoe/ui 0.5.0 → 0.6.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 CHANGED
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-09-28
10
+
11
+ ### Added
12
+
13
+ - `SiteFooter`'s `tools` prop: a "haruhime tools" column linking the other live haruhime.moe tools (packs, pools, bb) and "All tools" on www, with the current tool left out. `HARUHIME_TOOLS` and `haruhimeToolsColumn` export the same data for a custom footer.
14
+
15
+ ## [0.5.1] - 2026-09-28
16
+
17
+ ### Security
18
+
19
+ - `JsonLd` also escapes `>`, `&`, U+2028 and U+2029 as JSON unicode escapes, not only `<`. The data parses back unchanged.
20
+
9
21
  ## [0.5.0] - 2026-09-28
10
22
 
11
23
  ### Added
@@ -86,7 +98,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
86
98
  - `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`).
87
99
  - 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).
88
100
 
89
- [unreleased]: https://github.com/haruhimemoe/ui/compare/v0.5.0...HEAD
101
+ [unreleased]: https://github.com/haruhimemoe/ui/compare/v0.6.0...HEAD
102
+ [0.6.0]: https://github.com/haruhimemoe/ui/compare/v0.5.1...v0.6.0
103
+ [0.5.1]: https://github.com/haruhimemoe/ui/compare/v0.5.0...v0.5.1
90
104
  [0.5.0]: https://github.com/haruhimemoe/ui/compare/v0.4.0...v0.5.0
91
105
  [0.4.0]: https://github.com/haruhimemoe/ui/compare/v0.3.0...v0.4.0
92
106
  [0.3.0]: https://github.com/haruhimemoe/ui/compare/v0.2.0...v0.3.0
package/README.md CHANGED
@@ -760,6 +760,7 @@ Link columns, an extra slot, fine print, the haruhime.moe wordmark, a GitHub ico
760
760
  | Prop | Type | Default | What it does |
761
761
  | --- | --- | --- | --- |
762
762
  | `columns` | `readonly SiteFooterColumn[]` | `[]` | Each column is a `<nav>` named by its title, which shows above the list. Up to four columns side by side from `sm` up. |
763
+ | `tools` | `HaruhimeToolsOptions` | none | Adds a "haruhime tools" column: the other live haruhime.moe tools as "name: blurb" links, then "All tools" on www. Options: `current` (the tool this footer is on, left out), `title` (`"haruhime tools"`), `allLabel` (`"All tools"`, `false` drops it), `allHref` (`"https://www.haruhime.moe"`), `position` (where among `columns`, default `1`, clamped). Since 0.6.0. |
763
764
  | `extra` | `ReactNode` | none | Shown above the fine print, e.g. a "clear local data" button. |
764
765
  | `finePrint` | `ReactNode` | none | One line of small print, in a `<p>`. |
765
766
  | `parentLink` | `boolean` | `true` | Show the haruhime.moe wordmark linking the parent site. With it, the last row holds the wordmark and the icons, and the fine print sits above. Without it, the fine print shares the row with the icons. |
@@ -769,6 +770,8 @@ Link columns, an extra slot, fine print, the haruhime.moe wordmark, a GitHub ico
769
770
  | `discordHref` | `string` | none | Where the Discord icon links, such as your server's invite (`https://discord.gg/...`). Without it there is no Discord icon. The icon sits before the GitHub icon at the same size. It stays white (`text-c1`) and dims on hover instead of changing color, since Discord's brand guidelines ask that the logo not be recolored. Since 0.3.0. |
770
771
  | `discordLabel` | `string` | `"Discord"` | The Discord link's accessible name. Since 0.4.0. |
771
772
 
773
+ `HARUHIME_TOOLS` (each `{ id, name, href, blurb }`, type `HaruhimeTool`, ids `HaruhimeToolId`: `"packs" | "pools" | "bb"`) and `haruhimeToolsColumn(options)` (the same column as plain data, for a footer you lay out yourself) are exported too. Since 0.6.0. They are the one place outside the wordmark that names the haruhime.moe tools: a tool joins the list when it goes live.
774
+
772
775
  #### `LinkTabs`
773
776
 
774
777
  Since 0.4.0. A row of link tabs (Pools / Maps, All / Hidden): a named `<nav>` with a list of pill links. The current one gets `aria-current="page"` and a `b3` pill (underlined in forced colors mode). These are links, not ARIA tabs, since each loads its own URL. A Server Component: you say which is current. Every native `<nav>` prop except `children`. Type: `LinkTabItem`.
@@ -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. Every "<" is escaped, so no string
4
- * in the data (a user-supplied name, say) can close the tag or open a comment.
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 every "<" written as the escape `\u003c`
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(/</g, "\\u003c");
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
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @file src/components/shell/SiteFooter.tsx
3
3
  * @desc Site footer: labelled link columns from data (entries without an href show as text with
4
- * a small note, like "soon"), an optional extra slot, one line of fine print, the
4
+ * a small note, like "soon"), an optional "haruhime tools" column linking the other tools,
5
+ * an optional extra slot, one line of fine print, the
5
6
  * haruhime.moe wordmark linking the parent site, a GitHub icon link, and an optional
6
7
  * Discord icon link beside it (white, as Discord's brand guidelines ask).
7
8
  * @author David @dvhsh (https://dvh.sh)
@@ -9,6 +10,7 @@
9
10
  * @modified Mon Sep 28, 2026
10
11
  */
11
12
  import type { ComponentProps, ReactNode } from "react";
13
+ import { type HaruhimeToolsOptions } from "./haruhimeTools.js";
12
14
  import { type SiteLinkItem } from "./links.js";
13
15
  /** One footer column: a title (also the nav landmark's name) and its entries. */
14
16
  export type SiteFooterColumn = {
@@ -19,6 +21,11 @@ export type SiteFooterColumn = {
19
21
  export type SiteFooterProps = Omit<ComponentProps<"footer">, "children"> & {
20
22
  /** Link columns, left to right. Internal hrefs use `next/link`, external ones a plain `<a>`. */
21
23
  columns?: readonly SiteFooterColumn[] | undefined;
24
+ /**
25
+ * Adds the "haruhime tools" column (haruhimeToolsColumn): the other haruhime.moe tools and
26
+ * "All tools". `current` leaves this tool out; `position` places it (default 1).
27
+ */
28
+ tools?: HaruhimeToolsOptions | undefined;
22
29
  /** Rendered above the fine print, e.g. a "clear local data" control. */
23
30
  extra?: ReactNode;
24
31
  /** One line of small print, e.g. a trademark notice. */
@@ -38,8 +45,8 @@ export type SiteFooterProps = Omit<ComponentProps<"footer">, "children"> & {
38
45
  };
39
46
  /**
40
47
  * @function SiteFooter
41
- * @param props {SiteFooterProps} columns, extra slot, fine print, parent, GitHub and Discord
48
+ * @param props {SiteFooterProps} columns, the tools column, extra slot, fine print, parent, GitHub and Discord
42
49
  * links and their names, and native footer props
43
50
  * @returns {JSX.Element} the footer: columns on top, then extra, fine print and the brand row
44
51
  */
45
- export declare function SiteFooter({ columns, extra, finePrint, parentLink, parentHref, githubHref, githubLabel, discordHref, discordLabel, className, ...props }: SiteFooterProps): import("react").JSX.Element;
52
+ export declare function SiteFooter({ columns: ownColumns, tools, extra, finePrint, parentLink, parentHref, githubHref, githubLabel, discordHref, discordLabel, className, ...props }: SiteFooterProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import { AutoLink } from "../basics/AutoLink.js";
4
4
  import { DiscordIcon } from "../icons/DiscordIcon.js";
5
5
  import { GitHubIcon } from "../icons/GitHubIcon.js";
6
6
  import { HaruhimeWordmarkLink } from "../icons/HaruhimeWordmarkLink.js";
7
+ import { haruhimeToolsColumn } from "./haruhimeTools.js";
7
8
  import { LinkNote } from "./LinkNote.js";
8
9
  import { linkItemKey } from "./links.js";
9
10
  // The GitHub icon link in the bottom row.
@@ -15,11 +16,16 @@ const DISCORD_LINK = "shrink-0 text-c1 transition-opacity hover:opacity-80";
15
16
  const GRID_COLUMNS = ["", "", "sm:grid-cols-2", "sm:grid-cols-3", "sm:grid-cols-4"];
16
17
  /**
17
18
  * @function SiteFooter
18
- * @param props {SiteFooterProps} columns, extra slot, fine print, parent, GitHub and Discord
19
+ * @param props {SiteFooterProps} columns, the tools column, extra slot, fine print, parent, GitHub and Discord
19
20
  * links and their names, and native footer props
20
21
  * @returns {JSX.Element} the footer: columns on top, then extra, fine print and the brand row
21
22
  */
22
- export function SiteFooter({ columns = [], extra, finePrint, parentLink = true, parentHref = "https://www.haruhime.moe", githubHref = "https://github.com/haruhimemoe", githubLabel = "haruhimemoe on GitHub", discordHref, discordLabel = "Discord", className, ...props }) {
23
+ export function SiteFooter({ columns: ownColumns = [], tools, extra, finePrint, parentLink = true, parentHref = "https://www.haruhime.moe", githubHref = "https://github.com/haruhimemoe", githubLabel = "haruhimemoe on GitHub", discordHref, discordLabel = "Discord", className, ...props }) {
24
+ const columns = [...ownColumns];
25
+ if (tools) {
26
+ const at = Math.max(0, Math.min(tools.position ?? 1, columns.length));
27
+ columns.splice(at, 0, haruhimeToolsColumn(tools));
28
+ }
23
29
  const fine = finePrint ? _jsx("p", { className: "text-c4 text-xs", children: finePrint }) : null;
24
30
  const github = githubHref ? (_jsx("a", { href: githubHref, "aria-label": githubLabel, className: ICON_LINK, children: _jsx(GitHubIcon, {}) })) : null;
25
31
  const discord = discordHref ? (_jsx("a", { href: discordHref, "aria-label": discordLabel, className: DISCORD_LINK, children: _jsx(DiscordIcon, {}) })) : null;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @file src/components/shell/haruhimeTools.ts
3
+ * @desc The haruhime.moe tools as data (HARUHIME_TOOLS) and the footer column that links them
4
+ * from each tool (haruhimeToolsColumn): every other tool plus "All tools" on the parent
5
+ * site. Sibling subdomains are separate sites to search engines, so each tool links the
6
+ * others. Server-safe: no directive, no hooks.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ import type { SiteLinkItem } from "./links.js";
12
+ /** A live haruhime.moe tool's id: its subdomain. */
13
+ export type HaruhimeToolId = "packs" | "pools" | "bb";
14
+ /** One live haruhime.moe tool: its name, home page and what it is in a few words. */
15
+ export type HaruhimeTool = {
16
+ id: HaruhimeToolId;
17
+ name: string;
18
+ href: string;
19
+ /** A few words after the name in the footer, like "mappool downloads". */
20
+ blurb: string;
21
+ };
22
+ /** The live haruhime.moe tools in launch order. A tool joins this list once it's live. */
23
+ export declare const HARUHIME_TOOLS: readonly HaruhimeTool[];
24
+ /** haruhimeToolsColumn's (and SiteFooter's `tools` prop's) options. */
25
+ export type HaruhimeToolsOptions = {
26
+ /** The tool the footer sits on. It's left out of the list (its own column links it). */
27
+ current?: HaruhimeToolId | undefined;
28
+ /** The column's title (and nav landmark name). Default "haruhime tools". */
29
+ title?: string | undefined;
30
+ /** The last entry, linking the parent site. Default "All tools"; false leaves it out. */
31
+ allLabel?: string | false | undefined;
32
+ /** Where "All tools" links. Default "https://www.haruhime.moe". */
33
+ allHref?: string | undefined;
34
+ /** Where SiteFooter puts the column among `columns` (0 is first). Default 1, after the first. */
35
+ position?: number | undefined;
36
+ };
37
+ /**
38
+ * @function haruhimeToolsColumn
39
+ * @param options {HaruhimeToolsOptions} the current tool, title and "All tools" link
40
+ * @returns {{ title: string; items: SiteLinkItem[] }} a footer column: each other tool as
41
+ * "name: blurb", then "All tools" on the parent site
42
+ */
43
+ export declare function haruhimeToolsColumn(options?: HaruhimeToolsOptions): {
44
+ title: string;
45
+ items: SiteLinkItem[];
46
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file src/components/shell/haruhimeTools.ts
3
+ * @desc The haruhime.moe tools as data (HARUHIME_TOOLS) and the footer column that links them
4
+ * from each tool (haruhimeToolsColumn): every other tool plus "All tools" on the parent
5
+ * site. Sibling subdomains are separate sites to search engines, so each tool links the
6
+ * others. Server-safe: no directive, no hooks.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Mon Sep 28, 2026
9
+ * @modified Mon Sep 28, 2026
10
+ */
11
+ /** The live haruhime.moe tools in launch order. A tool joins this list once it's live. */
12
+ export const HARUHIME_TOOLS = [
13
+ { id: "packs", name: "packs", href: "https://packs.haruhime.moe", blurb: "mappool downloads" },
14
+ { id: "pools", name: "pools", href: "https://pools.haruhime.moe", blurb: "mappool builder" },
15
+ { id: "bb", name: "bb", href: "https://bb.haruhime.moe", blurb: "osu! BBCode editor" },
16
+ ];
17
+ /**
18
+ * @function haruhimeToolsColumn
19
+ * @param options {HaruhimeToolsOptions} the current tool, title and "All tools" link
20
+ * @returns {{ title: string; items: SiteLinkItem[] }} a footer column: each other tool as
21
+ * "name: blurb", then "All tools" on the parent site
22
+ */
23
+ export function haruhimeToolsColumn(options = {}) {
24
+ const { current, title = "haruhime tools", allLabel = "All tools" } = options;
25
+ const items = HARUHIME_TOOLS.filter((tool) => tool.id !== current).map((tool) => ({ href: tool.href, label: `${tool.name}: ${tool.blurb}` }));
26
+ if (allLabel !== false) {
27
+ items.push({ href: options.allHref ?? "https://www.haruhime.moe", label: allLabel });
28
+ }
29
+ return { title, items };
30
+ }
package/dist/index.d.ts CHANGED
@@ -51,6 +51,7 @@ export { type BeatmapStatKey, BeatmapStats, type BeatmapStatsProps, } from "./co
51
51
  export { ModBadge, type ModBadgeProps } from "./components/osu/ModBadge.js";
52
52
  export { StarRating, type StarRatingProps } from "./components/osu/StarRating.js";
53
53
  export { HeaderMenu, type HeaderMenuItem, type HeaderMenuProps, } from "./components/shell/HeaderMenu.js";
54
+ export { HARUHIME_TOOLS, type HaruhimeTool, type HaruhimeToolId, type HaruhimeToolsOptions, haruhimeToolsColumn, } from "./components/shell/haruhimeTools.js";
54
55
  export { type LinkTabItem, LinkTabs, type LinkTabsProps } from "./components/shell/LinkTabs.js";
55
56
  export type { SiteLinkItem } from "./components/shell/links.js";
56
57
  export { NavLinks, type NavLinksProps, type SiteNavAlign } from "./components/shell/NavLinks.js";
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ export { ModBadge } from "./components/osu/ModBadge.js";
57
57
  export { StarRating } from "./components/osu/StarRating.js";
58
58
  // Shell
59
59
  export { HeaderMenu, } from "./components/shell/HeaderMenu.js";
60
+ export { HARUHIME_TOOLS, haruhimeToolsColumn, } from "./components/shell/haruhimeTools.js";
60
61
  export { LinkTabs } from "./components/shell/LinkTabs.js";
61
62
  export { NavLinks } from "./components/shell/NavLinks.js";
62
63
  export { PageShell } from "./components/shell/PageShell.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haruhimemoe/ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",