Skip to content

Commit 2e584a3

Browse files
committed
fix(ai): make the lock reason readable on click, not just hover (#842)
The first pass moved the countdown out of the collapse toggle, which stopped clicks from folding the pane — but the reason still could not be read by clicking, because it was a native `title` attribute. Browsers DISMISS native tooltips on pointer-down, so "click to read it" was never going to work with that primitive, and `title` never appears on touch at all. The countdown is now a real button that discloses the reason: - hover / focus reveals it transiently - click PINS it, so it survives the pointer leaving (Escape or a second click unpins) - the text renders INLINE under the header, not as a floating popover: the pane root is overflow-hidden and would clip an absolutely-positioned panel, most visibly when the pane is collapsed to just this header Radix Tooltip is a dependency but is deliberately not used here — it is hover-only by design and also closes on click, so it would reproduce the same complaint. The ticking value keeps its own role="status" live region inside the button, so the countdown is still announced while the button carries the control semantics. 7 new tests: hidden by default, hover reveal/hide, pinned-through-mouseleave (the actual bug), unpin, Escape, focus reveal, and a regression guard that the badge is not nested inside the collapse toggle. The existing pane test asserted `title` on the chip — the mechanism being removed — so it now asserts the disclosure instead.
1 parent b63c811 commit 2e584a3

16 files changed

Lines changed: 219 additions & 78 deletions

File tree

apps/web/src/app/[locale]/(marketing)/start/page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,12 @@ export const metadata: Metadata = {
1414
// the resolved routes fresh if the bundle changes without a per-request cost.
1515
export const revalidate = 300;
1616

17-
export default async function StartPage(
18-
props: {
19-
params: Promise<{ locale: string }>;
20-
}
21-
) {
17+
export default async function StartPage(props: {
18+
params: Promise<{ locale: string }>;
19+
}) {
2220
const params = await props.params;
2321

24-
const {
25-
locale
26-
} = params;
22+
const { locale } = params;
2723

2824
const [routes, questData] = await Promise.all([
2925
resolveSegmentRoutes(locale),

apps/web/src/app/[locale]/(platform)/courses/[slug]/layout.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import type { Metadata } from "next";
22
import { getCourseBySlug } from "@/lib/content/queries";
33

4-
export async function generateMetadata(
5-
props: {
6-
params: Promise<{ slug: string }>;
7-
}
8-
): Promise<Metadata> {
4+
export async function generateMetadata(props: {
5+
params: Promise<{ slug: string }>;
6+
}): Promise<Metadata> {
97
const params = await props.params;
108
const course = await getCourseBySlug(params.slug);
119

apps/web/src/app/[locale]/(platform)/profile/[username]/page.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import { fetchPublicProfile } from "@/lib/profile/profile-data";
44
import { ProfileBody } from "@/components/gamification/profile-body";
55
import { ProfileBackButton } from "./profile-back-button";
66

7-
export default async function PublicProfilePage(
8-
props: {
9-
params: Promise<{ username: string }>;
10-
}
11-
) {
7+
export default async function PublicProfilePage(props: {
8+
params: Promise<{ username: string }>;
9+
}) {
1210
const params = await props.params;
1311
const username = decodeURIComponent(params.username);
1412
const supabase = await createClient();

apps/web/src/app/[locale]/(platform)/settings/_components/account-tab.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,10 @@ export function AccountTab({
8484
| undefined;
8585

8686
if (googleId) {
87-
const updatePayload: Database["public"]["Tables"]["profiles"]["Update"] = {
88-
google_id: googleId,
89-
};
87+
const updatePayload: Database["public"]["Tables"]["profiles"]["Update"] =
88+
{
89+
google_id: googleId,
90+
};
9091
if (!avatarUrl && googleAvatar) {
9192
updatePayload.avatar_url = googleAvatar;
9293
}
@@ -147,9 +148,10 @@ export function AccountTab({
147148
| undefined;
148149

149150
if (githubId) {
150-
const updatePayload: Database["public"]["Tables"]["profiles"]["Update"] = {
151-
github_id: githubId,
152-
};
151+
const updatePayload: Database["public"]["Tables"]["profiles"]["Update"] =
152+
{
153+
github_id: githubId,
154+
};
153155
if (!avatarUrl && ghAvatar) {
154156
updatePayload.avatar_url = ghAvatar;
155157
}

apps/web/src/app/[locale]/admin/__tests__/admin-routing.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ describe("/admin default landing", () => {
5454
cookieGetMock.mockReturnValue(undefined);
5555

5656
render(
57-
(await AdminPage({ params: Promise.resolve({ locale: "en" }) })) as React.ReactElement
57+
(await AdminPage({
58+
params: Promise.resolve({ locale: "en" }),
59+
})) as React.ReactElement
5860
);
5961

6062
expect(screen.getByTestId("admin-login-form")).toBeInTheDocument();

apps/web/src/app/[locale]/admin/content/__tests__/paths-table.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
import { describe, it, expect, vi } from "vitest";
33
import { render, screen } from "@testing-library/react";
44
import { createTranslator } from "next-intl";
5-
import { PathsTable } from "../paths-table";
65
import type { AdminLearningPathWithRefs } from "@/lib/content/queries";
76
import messages from "@/messages/en.json";
7+
import { PathsTable } from "../paths-table";
88

99
// Real ICU-aware translator (supports the `danglingCount` plural rule),
1010
// rather than a raw key lookup — `next-intl/server`'s `getTranslations` needs
1111
// a request context this test environment doesn't have.
1212
vi.mock("next-intl/server", () => ({
1313
getTranslations: async (
14-
namespace: Parameters<typeof createTranslator<typeof messages>>[0]["namespace"]
14+
namespace: Parameters<
15+
typeof createTranslator<typeof messages>
16+
>[0]["namespace"]
1517
) => createTranslator({ locale: "en", messages, namespace }),
1618
}));
1719

apps/web/src/app/[locale]/admin/content/__tests__/quests-table.test.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import { describe, it, expect, vi } from "vitest";
33
import { render, screen } from "@testing-library/react";
44
import { createTranslator } from "next-intl";
5-
import { QuestsTable } from "../quests-table";
65
import type { ContentQuest } from "@/lib/content/queries";
76
import messages from "@/messages/en.json";
7+
import { QuestsTable } from "../quests-table";
88

99
vi.mock("next-intl/server", () => ({
1010
getTranslations: async (
11-
namespace: Parameters<typeof createTranslator<typeof messages>>[0]["namespace"]
11+
namespace: Parameters<
12+
typeof createTranslator<typeof messages>
13+
>[0]["namespace"]
1214
) => createTranslator({ locale: "en", messages, namespace }),
1315
}));
1416

apps/web/src/app/[locale]/admin/deploy/page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ import { redirect } from "next/navigation";
55
* screen, not a screen of its own). Kept as a redirect so bookmarks, docs and
66
* muscle memory land on the merged screen instead of a 404.
77
*/
8-
export default async function AdminDeployRedirect(
9-
props: {
10-
params: Promise<{ locale: string }>;
11-
}
12-
) {
8+
export default async function AdminDeployRedirect(props: {
9+
params: Promise<{ locale: string }>;
10+
}) {
1311
const params = await props.params;
1412

15-
const {
16-
locale
17-
} = params;
13+
const { locale } = params;
1814

1915
redirect(`/${locale}/admin/courses`);
2016
}

apps/web/src/app/[locale]/admin/page.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { cookies } from "next/headers";
22
import { redirect } from "next/navigation";
3-
import { AdminLoginForm } from "./admin-login-form";
43
import { isValidAdminSession } from "@/lib/admin/auth";
4+
import { AdminLoginForm } from "./admin-login-form";
55

66
/**
77
* `/admin` root. Unauthenticated → render `<AdminLoginForm/>` (the layout
@@ -12,16 +12,12 @@ import { isValidAdminSession } from "@/lib/admin/auth";
1212
* `/admin/*` sub-routes back to `/admin`, so an unconditional redirect would
1313
* loop (`/admin` → `/admin/courses` → middleware → `/admin` → …).
1414
*/
15-
export default async function AdminPage(
16-
props: {
17-
params: Promise<{ locale: string }>;
18-
}
19-
) {
15+
export default async function AdminPage(props: {
16+
params: Promise<{ locale: string }>;
17+
}) {
2018
const params = await props.params;
2119

22-
const {
23-
locale
24-
} = params;
20+
const { locale } = params;
2521

2622
const cookieStore = await cookies();
2723
const session = cookieStore.get("admin_session");

apps/web/src/app/[locale]/admin/publish/page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@ import { redirect } from "next/navigation";
55
* screen, not a screen of its own). Kept as a redirect so bookmarks, docs and
66
* muscle memory land on the merged screen instead of a 404.
77
*/
8-
export default async function AdminPublishRedirect(
9-
props: {
10-
params: Promise<{ locale: string }>;
11-
}
12-
) {
8+
export default async function AdminPublishRedirect(props: {
9+
params: Promise<{ locale: string }>;
10+
}) {
1311
const params = await props.params;
1412

15-
const {
16-
locale
17-
} = params;
13+
const { locale } = params;
1814

1915
redirect(`/${locale}/admin/courses`);
2016
}

0 commit comments

Comments
 (0)