@haruhimemoe/ui 0.11.0 → 0.11.2

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.11.2] - 2026-10-04
10
+
11
+ ### Fixed
12
+
13
+ - `CopyMarkdownButton` copies on Safari and iOS: where `ClipboardItem` exists, the clipboard write starts inside the click with the fetched Markdown as a promised Blob, instead of after an awaited fetch that loses the user activation. Other browsers keep the fetch-then-`writeText` path.
14
+
15
+ ## [0.11.1] - 2026-10-04
16
+
17
+ ### Fixed
18
+
19
+ - BrandPage asset previews use an empty alt, so axe no longer reports image-redundant-alt next to the download link.
20
+
9
21
  ## [0.11.0] - 2026-10-04
10
22
 
11
23
  ### Added
@@ -153,7 +165,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
153
165
  - `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`).
154
166
  - 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).
155
167
 
156
- [unreleased]: https://github.com/haruhimemoe/ui/compare/v0.11.0...HEAD
168
+ [unreleased]: https://github.com/haruhimemoe/ui/compare/v0.11.2...HEAD
169
+ [0.11.2]: https://github.com/haruhimemoe/ui/compare/v0.11.1...v0.11.2
170
+ [0.11.1]: https://github.com/haruhimemoe/ui/compare/v0.11.0...v0.11.1
157
171
  [0.11.0]: https://github.com/haruhimemoe/ui/compare/v0.10.0...v0.11.0
158
172
  [0.10.0]: https://github.com/haruhimemoe/ui/compare/v0.9.0...v0.10.0
159
173
  [0.9.0]: https://github.com/haruhimemoe/ui/compare/v0.8.0...v0.9.0
@@ -13,5 +13,5 @@ const IMAGE = /\.(svg|png|jpe?g|webp)$/i;
13
13
  * @returns {JSX.Element} the brand page's sections, in order
14
14
  */
15
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] }));
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: "", 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
17
  }
@@ -2,7 +2,9 @@
2
2
  * @file src/components/content/CopyMarkdownButton.tsx
3
3
  * @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
4
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
5
+ * Markdown" action. Where `ClipboardItem` exists, the clipboard write starts inside the click
6
+ * with the fetch as a promised Blob, since Safari rejects a write that starts after an
7
+ * awaited fetch; elsewhere it fetches, then calls `writeText`. Any failure (the fetch rejects, the response isn't ok, or the clipboard
6
8
  * refuses) announces the same failure message; the button never throws. Its own client file
7
9
  * with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
8
10
  * for `ContentPage`, the Server Component that renders it. Not a wrapper around
@@ -2,7 +2,9 @@
2
2
  * @file src/components/content/CopyMarkdownButton.tsx
3
3
  * @desc A button that fetches a page's raw Markdown and copies it to the clipboard, reporting the
4
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
5
+ * Markdown" action. Where `ClipboardItem` exists, the clipboard write starts inside the click
6
+ * with the fetch as a promised Blob, since Safari rejects a write that starts after an
7
+ * awaited fetch; elsewhere it fetches, then calls `writeText`. Any failure (the fetch rejects, the response isn't ok, or the clipboard
6
8
  * refuses) announces the same failure message; the button never throws. Its own client file
7
9
  * with finished class strings (no `cx`), so tailwind-merge stays out of the browser bundle
8
10
  * for `ContentPage`, the Server Component that renders it. Not a wrapper around
@@ -28,11 +30,23 @@ export function CopyMarkdownButton({ href, label = "Copy as Markdown", copiedLab
28
30
  const copy = async () => {
29
31
  const run = start();
30
32
  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);
33
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
34
+ // Started synchronously in the click so Safari keeps the user activation.
35
+ const blob = fetch(href).then(async (response) => {
36
+ if (!response.ok)
37
+ throw new Error(String(response.status));
38
+ return new Blob([await response.text()], { type: "text/plain" });
39
+ });
40
+ // If the write is refused before reading the blob, its own rejection stays handled.
41
+ blob.catch(() => undefined);
42
+ await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
43
+ }
44
+ else {
45
+ const response = await fetch(href);
46
+ if (!response.ok)
47
+ throw new Error(String(response.status));
48
+ await navigator.clipboard.writeText(await response.text());
49
+ }
36
50
  settle(run, "copied");
37
51
  }
38
52
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haruhimemoe/ui",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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 player cards, a command palette, content section tooling, brand pages and the site header and footer.",
5
5
  "keywords": [
6
6
  "osu",