@haruhimemoe/next-kit 0.7.0 → 0.8.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,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] - 2026-10-05
10
+
11
+ ### Changed
12
+ - **Breaking.** The `@haruhimemoe/ui` peer range is now `^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0` (0.15 to 0.17 not yet published): `DeleteAccountForm` needs ui's `ConfirmDialog`.
13
+ - `DeleteAccountForm` is a "Delete my account" button that opens a dialog: what goes is its description, the username is typed there, and a refusal or no answer is said in the dialog, which stays open. Once the account is deleted, focus moves to the "Your account is deleted." line.
14
+ - The account components use ui's text tones (`Text`, `textClasses`) and Button's own width. No change in how they look.
15
+
9
16
  ## [0.7.0] - 2026-10-05
10
17
 
11
18
  ### Added
@@ -89,7 +96,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
89
96
  - `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
90
97
  - `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
91
98
 
92
- [unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.6.2...HEAD
99
+ [unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.8.0...HEAD
100
+ [0.8.0]: https://github.com/haruhimemoe/next-kit/compare/v0.7.0...v0.8.0
93
101
  [0.6.2]: https://github.com/haruhimemoe/next-kit/compare/v0.6.1...v0.6.2
94
102
  [0.6.1]: https://github.com/haruhimemoe/next-kit/compare/v0.6.0...v0.6.1
95
103
  [0.6.0]: https://github.com/haruhimemoe/next-kit/compare/v0.5.0...v0.6.0
package/README.md CHANGED
@@ -32,7 +32,7 @@ bun add @haruhimemoe/next-kit zod
32
32
  | `env` | nothing else |
33
33
  | `mongo` | `mongodb` ^7.6.0, `mongoose` ^9.10.2 |
34
34
  | `auth` | `better-auth` ^1.7.5, `mongodb`, `@haruhimemoe/osu` 0.2 or 0.3 |
35
- | `auth-react` | `react` ^19.3.0, `next` ^16.3.6, `@haruhimemoe/ui` ^0.5.0 \|\| ^0.6.0 \|\| ^0.7.0 \|\| ^0.8.0 \|\| ^0.9.0 \|\| ^0.10.0 \|\| ^0.11.0 \|\| ^0.12.0 \|\| ^0.13.0 (with its theme set up) |
35
+ | `auth-react` | `react` ^19.3.0, `next` ^16.3.6, `@haruhimemoe/ui` ^0.14.0 \|\| ^0.15.0 \|\| ^0.16.0 \|\| ^0.17.0 (with its theme set up) |
36
36
  | `seo` | `next` ^16.3.6 types only (nothing loads at runtime) |
37
37
  | `testing` | `vitest` ^5.0.1, `msw` ^2.15.0, `mongodb-memory-server` ^11.3.0 |
38
38
  | `api-keys` | `mongodb` ^7.6.0 |
@@ -304,7 +304,7 @@ The command prints one `pass` or `FAIL` line per standard, names each missing fi
304
304
  | `SignInWithOsu({ next, signIn, signInPath?, label?, pendingLabel?, failedMessage? })` | Since 0.2.0. The large "Sign in with osu!" button. Calls `signIn(osuSignIn(next))`; an error (better-auth's `{ message }` or `{ error: { message } }`, or a throw) shows in a `role="alert"` line and the button comes back. `signInErrorMessage(error, fallback?)` reads it. |
305
305
  | `SignOutButton({ signOut, onSignedOut, redirectTo?, variant?, className?, label?, pendingLabel? })` | Since 0.2.0. Signs out, runs `onSignedOut` only once the session is gone, then `router.replace(redirectTo ?? "/")` and `router.refresh()`. |
306
306
  | `AccountMenu({ account, items, signOut, onSignedOut, avatarSrc?, signInPath?, signInLabel?, signOutLabel?, signOutRedirect? })` | Since 0.2.0. A sized blank while loading, a "Sign in" link back to this page when signed out, else ui's `HeaderMenu` with the avatar and name, `items` and Sign out. The avatar is a plain `<img>` (your CSP's img-src must allow a.ppy.sh and osu.ppy.sh); `avatarSrc` defaults to `osuAvatarSrc`. |
307
- | `DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint?, homeHref?, homeLabel?, fetcher? })` | Since 0.2.0. ui's `TypeToConfirm` on the username, then `DELETE endpoint` (default `/api/account`) with `{ username }`. 204: `onDeleted`, "Your account is deleted." and home. Another 2xx: `onDeleted` and the answer's `notice` after that line, staying. A refusal shows `error.message` (else "Deleting failed (status)."); no answer, "Couldn't reach <appName>. Your account is still there." `deletes` is inline content in a `<p>`. |
307
+ | `DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint?, homeHref?, homeLabel?, fetcher? })` | Since 0.2.0. A "Delete my account" button (0.8.0; an in-page `TypeToConfirm` before) that opens ui's `ConfirmDialog`: `deletes` is its description, the username must be typed, then "Delete for good" sends `DELETE endpoint` (default `/api/account`) with `{ username }`. 204: `onDeleted`, "Your account is deleted." (focused) and home. Another 2xx: `onDeleted` and the answer's `notice` after that line, staying. A refusal shows `error.message` (else "Deleting failed (status).") in the dialog; no answer, "Couldn't reach <appName>. Your account is still there." The dialog stays open on both. Needs ui 0.14.0. |
308
308
  | `osuAvatarSrc(url)`, `OSU_AVATAR_HOSTS` | Since 0.2.0. An osu! avatar URL on https when it's on a.ppy.sh or osu.ppy.sh (a bare path is osu.ppy.sh's), else null. |
309
309
 
310
310
  ### seo
@@ -9,7 +9,7 @@
9
9
  * (src/components/layout/AccountMenu.tsx), which differed only in the links.
10
10
  * @author David @dvhsh (https://dvh.sh)
11
11
  * @created Mon Sep 28, 2026
12
- * @modified Mon Sep 28, 2026
12
+ * @modified Mon Oct 5, 2026
13
13
  */
14
14
  import { type HeaderMenuItem } from "@haruhimemoe/ui";
15
15
  import type { ReactNode } from "react";
@@ -9,11 +9,11 @@
9
9
  * (src/components/layout/AccountMenu.tsx), which differed only in the links.
10
10
  * @author David @dvhsh (https://dvh.sh)
11
11
  * @created Mon Sep 28, 2026
12
- * @modified Mon Sep 28, 2026
12
+ * @modified Mon Oct 5, 2026
13
13
  */
14
14
  "use client";
15
15
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
- import { HeaderMenu } from "@haruhimemoe/ui";
16
+ import { HeaderMenu, textClasses } from "@haruhimemoe/ui";
17
17
  import { usePathname } from "next/navigation.js";
18
18
  import { DEFAULT_SIGN_IN_PATH, signInHref } from "../server/safe-next.js";
19
19
  import { osuAvatarSrc } from "./avatar.js";
@@ -29,7 +29,11 @@ export function AccountMenu({ account, items, signOut, onSignedOut, avatarSrc =
29
29
  if (account.status === "loading")
30
30
  return _jsx("span", { "aria-hidden": "true", className: "block h-7 w-16" });
31
31
  if (account.status === "signed-out") {
32
- return (_jsx("a", { href: signInHref(pathname || "/", signInPath), className: "font-bold text-c3 text-sm transition-colors hover:text-c1", children: signInLabel }));
32
+ return (_jsx("a", { href: signInHref(pathname || "/", signInPath), className: textClasses({
33
+ tone: "muted",
34
+ bold: true,
35
+ className: "transition-colors hover:text-c1",
36
+ }), children: signInLabel }));
33
37
  }
34
38
  const avatar = avatarSrc(account.user.avatarUrl);
35
39
  return (_jsx(HeaderMenu, { label: _jsxs(_Fragment, { children: [avatar ? (_jsx("img", { src: avatar, alt: "", width: 28, height: 28, className: "rounded-full" })) : null, _jsx("span", { children: account.user.username })] }), items: items, children: _jsx(SignOutButton, { signOut: signOut, onSignedOut: onSignedOut, redirectTo: signOutRedirect, label: signOutLabel, variant: "ghost", className: "h-auto justify-start rounded px-3 py-2" }) }));
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * @file src/auth-react/DeleteAccountForm.tsx
3
- * @desc "Delete my account". The confirmation is built into the page (ui's TypeToConfirm, no
4
- * confirm() dialog): the button stays off until the osu! username is typed exactly, then
5
- * one DELETE to the app's endpoint carries it. 204 means gone: signed out, the page says
6
- * so and goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work
7
- * still waiting elsewhere). A refusal says the server's message, and no answer says the app
8
- * couldn't be reached; nothing was deleted. The form never comes back once it's gone.
3
+ * @desc "Delete my account": a button that opens ui's ConfirmDialog (no confirm()), where what
4
+ * goes is the description and the osu! username is typed before "Delete for good" sends one
5
+ * DELETE with it to the app's endpoint. 204 means gone: signed out, the page says so and
6
+ * goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work still
7
+ * waiting elsewhere). A refusal says the server's message in the dialog, no answer says the
8
+ * app couldn't be reached, nothing was deleted and the dialog stays open. Once it's gone
9
+ * the button never comes back and focus lands on the line that says so.
9
10
  * Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
10
11
  * @author David @dvhsh (https://dvh.sh)
11
12
  * @created Mon Sep 28, 2026
12
- * @modified Mon Sep 28, 2026
13
+ * @modified Mon Oct 5, 2026
13
14
  */
14
15
  import { type ReactNode } from "react";
15
16
  /** DeleteAccountForm's props. */
@@ -35,6 +36,6 @@ export type DeleteAccountFormProps = {
35
36
  * @function DeleteAccountForm
36
37
  * @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
37
38
  * and what to do after
38
- * @returns {ReactNode} the typed-name confirmation, or what happened
39
+ * @returns {ReactNode} the "Delete my account" trigger and its confirm dialog, or what happened
39
40
  */
40
41
  export declare function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint, homeHref, homeLabel, fetcher, }: DeleteAccountFormProps): ReactNode;
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * @file src/auth-react/DeleteAccountForm.tsx
3
- * @desc "Delete my account". The confirmation is built into the page (ui's TypeToConfirm, no
4
- * confirm() dialog): the button stays off until the osu! username is typed exactly, then
5
- * one DELETE to the app's endpoint carries it. 204 means gone: signed out, the page says
6
- * so and goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work
7
- * still waiting elsewhere). A refusal says the server's message, and no answer says the app
8
- * couldn't be reached; nothing was deleted. The form never comes back once it's gone.
3
+ * @desc "Delete my account": a button that opens ui's ConfirmDialog (no confirm()), where what
4
+ * goes is the description and the osu! username is typed before "Delete for good" sends one
5
+ * DELETE with it to the app's endpoint. 204 means gone: signed out, the page says so and
6
+ * goes home. Another 2xx is gone too but stays, to show the answer's `notice` (work still
7
+ * waiting elsewhere). A refusal says the server's message in the dialog, no answer says the
8
+ * app couldn't be reached, nothing was deleted and the dialog stays open. Once it's gone
9
+ * the button never comes back and focus lands on the line that says so.
9
10
  * Moved from pools and bb (src/components/account/DeleteAccountForm.tsx).
10
11
  * @author David @dvhsh (https://dvh.sh)
11
12
  * @created Mon Sep 28, 2026
12
- * @modified Mon Sep 28, 2026
13
+ * @modified Mon Oct 5, 2026
13
14
  */
14
15
  "use client";
15
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
- import { buttonClasses, TypeToConfirm } from "@haruhimemoe/ui";
17
+ import { buttonClasses, ConfirmDialog } from "@haruhimemoe/ui";
17
18
  import { useRouter } from "next/navigation.js";
18
19
  import { useId, useState } from "react";
19
20
  const messageOf = async (response) => {
@@ -30,41 +31,40 @@ const messageOf = async (response) => {
30
31
  * @function DeleteAccountForm
31
32
  * @param props {DeleteAccountFormProps} the username, the app's name, what goes, the endpoint
32
33
  * and what to do after
33
- * @returns {ReactNode} the typed-name confirmation, or what happened
34
+ * @returns {ReactNode} the "Delete my account" trigger and its confirm dialog, or what happened
34
35
  */
35
36
  export function DeleteAccountForm({ username, appName, deletes, onDeleted, endpoint = "/api/account", homeHref = "/", homeLabel = "Go to the home page", fetcher = fetch, }) {
36
37
  const router = useRouter();
37
- const id = useId();
38
- const [error, setError] = useState(null);
39
- // Once it's gone: what more to say. The form doesn't come back, so no second delete.
38
+ const doneId = `${useId()}-done`;
39
+ // Once it's gone: what more to say. The button doesn't come back, so no second delete.
40
40
  const [done, setDone] = useState(null);
41
+ const unreachable = `Couldn't reach ${appName}. Your account is still there.`;
42
+ // Throws to keep the dialog open with what went wrong.
41
43
  const remove = async () => {
42
- setError(null);
44
+ let response;
43
45
  try {
44
- const response = await fetcher(endpoint, {
46
+ response = await fetcher(endpoint, {
45
47
  method: "DELETE",
46
48
  headers: { "Content-Type": "application/json" },
47
49
  body: JSON.stringify({ username }),
48
50
  });
49
- if (!response.ok) {
50
- setError(await messageOf(response));
51
- return;
52
- }
53
- onDeleted();
54
- if (response.status === 204) {
55
- setDone("");
56
- router.push(homeHref);
57
- return;
58
- }
59
- const body = (await response.json().catch(() => ({})));
60
- setDone(typeof body.notice === "string" ? body.notice : "");
61
51
  }
62
52
  catch {
63
- setError(`Couldn't reach ${appName}. Your account is still there.`);
53
+ throw new Error(unreachable);
64
54
  }
55
+ if (!response.ok)
56
+ throw new Error(await messageOf(response));
57
+ onDeleted();
58
+ if (response.status === 204) {
59
+ setDone("");
60
+ router.push(homeHref);
61
+ return;
62
+ }
63
+ const body = (await response.json().catch(() => ({})));
64
+ setDone(typeof body.notice === "string" ? body.notice : "");
65
65
  };
66
66
  if (done !== null) {
67
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { role: "status", className: "text-c2 text-sm", children: `Your account is deleted. ${done}`.trim() }), _jsx("a", { href: homeHref, className: buttonClasses({ variant: "secondary", className: "self-start" }), children: homeLabel })] }));
67
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { id: doneId, role: "status", tabIndex: -1, className: "text-c2 text-sm outline-none", children: `Your account is deleted. ${done}`.trim() }), _jsx("a", { href: homeHref, className: buttonClasses({ variant: "secondary" }), children: homeLabel })] }));
68
68
  }
69
- return (_jsx(TypeToConfirm, { id: id, expected: username, submitLabel: "Delete my account", pendingLabel: "Deleting\u2026", error: error, onConfirm: remove, children: _jsx("p", { className: "text-c2 text-sm", children: deletes }) }));
69
+ return (_jsx(ConfirmDialog, { trigger: "Delete my account", triggerProps: { variant: "danger" }, title: "Delete your account?", description: deletes, tone: "destructive", typeToConfirm: username, confirmLabel: "Delete for good", pendingLabel: "Deleting\u2026", failedMessage: (error) => (error instanceof Error ? error.message : unreachable), returnFocus: () => document.getElementById(doneId), onConfirm: remove }));
70
70
  }
@@ -7,7 +7,7 @@
7
7
  * and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
8
8
  * @author David @dvhsh (https://dvh.sh)
9
9
  * @created Mon Sep 28, 2026
10
- * @modified Mon Sep 28, 2026
10
+ * @modified Mon Oct 5, 2026
11
11
  */
12
12
  import { type ReactNode } from "react";
13
13
  /** RestoreSignedIn's props. */
@@ -7,14 +7,15 @@
7
7
  * and pools (src/components/auth/RestoreSignedIn.tsx, body-identical).
8
8
  * @author David @dvhsh (https://dvh.sh)
9
9
  * @created Mon Sep 28, 2026
10
- * @modified Mon Sep 28, 2026
10
+ * @modified Mon Oct 5, 2026
11
11
  */
12
12
  "use client";
13
13
  import { jsx as _jsx } from "react/jsx-runtime";
14
+ import { Text } from "@haruhimemoe/ui";
14
15
  import { useRouter } from "next/navigation.js";
15
16
  import { useEffect } from "react";
16
17
  const readDocumentCookie = () => document.cookie;
17
- const SIGNING_IN = _jsx("p", { className: "text-c3 text-sm", children: "Signing you in\u2026" });
18
+ const SIGNING_IN = _jsx(Text, { tone: "muted", children: "Signing you in\u2026" });
18
19
  /**
19
20
  * @function RestoreSignedIn
20
21
  * @param props {RestoreSignedInProps} the store, the marker check, `next` and what to show
@@ -7,7 +7,7 @@
7
7
  * (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
8
8
  * @author David @dvhsh (https://dvh.sh)
9
9
  * @created Mon Sep 28, 2026
10
- * @modified Mon Sep 28, 2026
10
+ * @modified Mon Oct 5, 2026
11
11
  */
12
12
  import { type ReactNode } from "react";
13
13
  import { type OsuSignIn } from "./sign-in.js";
@@ -7,11 +7,11 @@
7
7
  * (src/components/auth/SignInWithOsu.tsx), where only the error reading differed.
8
8
  * @author David @dvhsh (https://dvh.sh)
9
9
  * @created Mon Sep 28, 2026
10
- * @modified Mon Sep 28, 2026
10
+ * @modified Mon Oct 5, 2026
11
11
  */
12
12
  "use client";
13
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
- import { Button } from "@haruhimemoe/ui";
14
+ import { Button, Text } from "@haruhimemoe/ui";
15
15
  import { useState } from "react";
16
16
  import { osuSignIn } from "./sign-in.js";
17
17
  const FAILED = "Couldn't start osu! sign-in. Try again.";
@@ -54,5 +54,5 @@ export function SignInWithOsu({ next, signIn, signInPath, label = "Sign in with
54
54
  }
55
55
  setPending(false);
56
56
  };
57
- return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Button, { size: "lg", onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }), error ? (_jsx("p", { role: "alert", className: "font-bold text-rose-300 text-sm", children: error })) : null] }));
57
+ return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(Button, { size: "lg", onClick: onClick, disabled: pending, children: pending ? pendingLabel : label }), error ? (_jsx(Text, { role: "alert", tone: "error", bold: true, children: error })) : null] }));
58
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haruhimemoe/next-kit",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The Next.js plumbing the haruhime.moe tools share: JSON route helpers, rate limits and budgets in MongoDB, bearer machine auth, zod env parsing, a connect-once MongoDB client with safe index builds, better-auth with osu! sign-in, the signed-in marker and account store for the browser, SEO: metadata, robots.txt, sitemaps, JSON-LD and llms.txt, and per-app API keys and the /api/v1 guard.",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -104,7 +104,7 @@
104
104
  },
105
105
  "peerDependencies": {
106
106
  "@haruhimemoe/osu": "^0.2.0 || ^0.3.0 || ^0.4.0",
107
- "@haruhimemoe/ui": "^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0",
107
+ "@haruhimemoe/ui": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0",
108
108
  "@haruhimemoe/vcs": "^0.1.0",
109
109
  "better-auth": "^1.7.5",
110
110
  "mongodb": "^7.6.0",
@@ -154,7 +154,7 @@
154
154
  "devDependencies": {
155
155
  "@biomejs/biome": "2.5.14",
156
156
  "@haruhimemoe/osu": "0.2.0",
157
- "@haruhimemoe/ui": "0.5.0",
157
+ "@haruhimemoe/ui": "0.14.0",
158
158
  "@haruhimemoe/vcs": "0.1.0",
159
159
  "@testing-library/dom": "10.4.2",
160
160
  "@testing-library/react": "16.3.3",