Skip to content

Commit 22bb94c

Browse files
authored
fix: restore ClawHub public UI
Restore the public header, hero, featured carousel, Trending Now, category grid, footer, and UI design-contract guardrails. Remove tweakcn/custom visual overlay settings and stale density preference plumbing, while preserving reviewed search/typeahead behavior and latest review fixes.
1 parent b3c42b6 commit 22bb94c

23 files changed

Lines changed: 2451 additions & 2106 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Hero Slot Easter Egg Regression Note
2+
3+
Date: 2026-04-29
4+
5+
The homepage hero easter egg was originally added in commit `91c7e44` by Val Alexander on 2026-04-18 as `feat: slot machine Easter egg on hero label triple-click`.
6+
7+
Follow-up behavior changes:
8+
9+
- `cb75011` by Val Alexander on 2026-04-18 tuned odds to 1/25 for any jackpot and 1/100 for the Hack jackpot.
10+
- `fec5db7` by Val Alexander on 2026-04-18 added timer and interval cleanup on unmount.
11+
12+
The easter egg was removed from `src/routes/index.tsx` in commit `6c0163f` by Patrick Erichsen on 2026-04-28 as part of `feat: add skills plugins search typeahead`.
13+
14+
Expected behavior:
15+
16+
- Triple-click `BUILT BY THE COMMUNITY.` within 800ms to trigger the slot-machine headline.
17+
- Reels stop at 1200ms, 1800ms, and 2400ms.
18+
- Non-jackpot spins reroll accidental triples so jackpot odds stay controlled.
19+
- Jackpot odds are 1/25 overall.
20+
- Hack jackpot odds are 1/100 overall.
21+
- Wins fire confetti; Hack wins use the aquatic Hack-specific effect.
22+
- Wins display for 10s and cool down for 18s; losses display for 2.4s and cool down for 3s.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# UI Design Contract Regression Guard
2+
3+
Date: 2026-04-29
4+
5+
The restored ClawHub public UI has strict regression guards in `src/__tests__/ui-design-contract.test.ts`.
6+
7+
Protected fundamentals:
8+
9+
- Two-row header: brand, full-width desktop search, rectangular theme mode control, auth action, then content nav with `Skills`, `Plugins`, `Users`, and `About`.
10+
- Compact header: menu button, inline search, GitHub action, and visible content nav row.
11+
- Home hero: `BUILT BY THE COMMUNITY.`, `Tools built by thousands, ready in one search.`, search focus border behavior, slot-machine easter egg support, featured carousel, category grid breakpoints, and `Trending Now`.
12+
- Footer: restored four public sections and mobile section toggles.
13+
- Visual settings: no tweakcn overlay, custom-theme file, relaxed/compact density controls, or other nonfunctional visual preferences.
14+
15+
Intentional changes to these fundamentals must update the design-contract test and this note in the same PR. A removal without a matching contract update should be treated as an accidental regression.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"test:e2e:local": "bash scripts/run-playwright-local.sh",
2929
"test:e2e:prod-http": "vitest run -c vitest.e2e.config.ts e2e/prod-http-smoke.e2e.test.ts",
3030
"test:pw": "playwright test",
31+
"test:ui-contract": "vitest run src/__tests__/ui-design-contract.test.ts src/__tests__/header.test.tsx src/__tests__/home-route.test.tsx src/components/Footer.test.tsx src/lib/theme.test.tsx src/routes/-settings.test.tsx",
3132
"test:watch": "vitest",
3233
"verify:convex-contract": "bun scripts/verify-convex-contract.ts"
3334
},

src/__tests__/header.test.tsx

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* @vitest-environment jsdom */
22

3+
import { readFileSync } from "node:fs";
4+
import { resolve } from "node:path";
35
import { fireEvent, render, screen, within } from "@testing-library/react";
46
import type { ReactNode } from "react";
57
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -86,19 +88,13 @@ vi.mock("../lib/useAuthStatus", () => ({
8688
useAuthStatus: () => authStatusMock(),
8789
}));
8890

89-
const setThemeMock = vi.fn();
9091
const setModeMock = vi.fn();
9192

9293
vi.mock("../lib/theme", () => ({
9394
applyTheme: vi.fn(),
94-
THEME_OPTIONS: [
95-
{ value: "claw", label: "Claw", description: "" },
96-
{ value: "hub", label: "Hub", description: "" },
97-
],
9895
useThemeMode: () => ({
99-
theme: "hub",
96+
theme: "claw",
10097
mode: "system",
101-
setTheme: setThemeMock,
10298
setMode: setModeMock,
10399
}),
104100
}));
@@ -148,14 +144,47 @@ vi.mock("../components/ui/dropdown-menu", () => ({
148144
}));
149145

150146
vi.mock("../components/ui/toggle-group", () => ({
151-
ToggleGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
152-
ToggleGroupItem: ({ children }: { children: ReactNode }) => (
153-
<button type="button">{children}</button>
147+
ToggleGroup: ({
148+
children,
149+
className,
150+
...props
151+
}: {
152+
children: ReactNode;
153+
className?: string;
154+
"aria-label"?: string;
155+
}) => (
156+
<div className={className} aria-label={props["aria-label"]}>
157+
{children}
158+
</div>
159+
),
160+
ToggleGroupItem: ({
161+
children,
162+
value,
163+
...props
164+
}: {
165+
children: ReactNode;
166+
value?: string;
167+
"aria-label"?: string;
168+
}) => (
169+
<button type="button" aria-label={props["aria-label"]} data-value={value}>
170+
{children}
171+
</button>
154172
),
155173
}));
156174

157175
import Header from "../components/Header";
158176

177+
function stylesCss() {
178+
return readFileSync(resolve(process.cwd(), "src/styles.css"), "utf8");
179+
}
180+
181+
function compactHeaderCss() {
182+
const css = stylesCss();
183+
const start = css.indexOf("@media (max-width: 760px)");
184+
const end = css.indexOf("@media (max-width: 520px)", start);
185+
return css.slice(start, end);
186+
}
187+
159188
describe("Header", () => {
160189
beforeEach(() => {
161190
authStatusMock.mockReturnValue({
@@ -175,29 +204,80 @@ describe("Header", () => {
175204
expect(screen.queryByText("Packages")).toBeNull();
176205
});
177206

178-
it("renders simplified desktop nav and theme toggle", () => {
207+
it("renders restored desktop nav rows and segmented theme controls", () => {
179208
siteModeMock.mockReturnValue("skills");
180-
setThemeMock.mockClear();
181209
setModeMock.mockClear();
182210

183211
render(<Header />);
184212

185-
expect(screen.getByRole("button", { name: /Toggle theme\. Current: system/i })).toBeTruthy();
213+
expect(document.querySelector(".navbar-tabs")).toBeTruthy();
214+
expect(document.querySelector(".navbar-tabs-secondary")).toBeTruthy();
215+
expect(document.querySelector(".theme-mode-toggle")).toBeTruthy();
216+
expect(screen.getByLabelText("Theme mode").className).toContain("theme-mode-toggle");
217+
expect(screen.getByRole("button", { name: /Cycle theme mode/i })).toBeTruthy();
218+
expect(screen.getByRole("button", { name: "System theme" })).toBeTruthy();
219+
expect(screen.getByRole("button", { name: "Light theme" })).toBeTruthy();
220+
expect(screen.getByRole("button", { name: "Dark theme" })).toBeTruthy();
186221
expect(screen.getAllByText("Skills")).toHaveLength(1);
187222
expect(screen.getAllByText("Plugins")).toHaveLength(1);
188-
expect(screen.queryByText("Users")).toBeNull();
223+
expect(screen.getAllByText("Users")).toHaveLength(1);
224+
expect(screen.getAllByText("About")).toHaveLength(1);
189225
expect(screen.queryByText("Dashboard")).toBeNull();
190226
expect(screen.queryByText("Manage")).toBeNull();
191-
expect(screen.getByPlaceholderText("Search skills and plugins")).toBeTruthy();
227+
expect(screen.getByPlaceholderText("Search skills, plugins, users")).toBeTruthy();
192228

193-
fireEvent.click(screen.getByRole("button", { name: /Toggle theme\. Current: system/i }));
194-
expect(setModeMock).toHaveBeenCalledWith("dark");
229+
fireEvent.click(screen.getByRole("button", { name: /Cycle theme mode/i }));
230+
expect(setModeMock).toHaveBeenCalledWith("light");
195231

196232
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
197233

198234
expect(screen.getAllByText("Home")).toHaveLength(1);
199235
expect(screen.getAllByText("Skills")).toHaveLength(2);
200236
expect(screen.getAllByText("Plugins")).toHaveLength(2);
237+
expect(screen.getAllByText("Users")).toHaveLength(2);
238+
expect(screen.getAllByText("About")).toHaveLength(2);
239+
});
240+
241+
it("renders the GitHub sign-in button with desktop and compact labels", () => {
242+
siteModeMock.mockReturnValue("skills");
243+
244+
render(<Header />);
245+
246+
const signInButton = screen.getByRole("button", { name: "Sign in with GitHub" });
247+
expect(signInButton.className).toContain("github-sign-in-button");
248+
const fullCopy = signInButton.querySelector(".sign-in-full-copy");
249+
expect(fullCopy?.textContent).toBe("Sign in with GitHub");
250+
expect(fullCopy?.childNodes).toHaveLength(1);
251+
expect(signInButton.querySelector(".sign-in-with")).toBeNull();
252+
expect(signInButton.querySelector(".sign-in-compact-copy")?.textContent).toBe("GitHub");
253+
});
254+
255+
it("keeps inline search and content nav visible in the compact header", () => {
256+
const css = compactHeaderCss();
257+
258+
expect(css).toContain(".navbar-search-wrap");
259+
expect(css).toContain("grid-template-columns");
260+
expect(css).toContain(".navbar-search {");
261+
expect(css).toContain("display: flex;");
262+
expect(css).toContain(".navbar-search-mobile-trigger");
263+
expect(css).toContain("display: none;");
264+
expect(css).toContain(".navbar-tabs {");
265+
expect(css).toContain("display: flex;");
266+
expect(css).toContain(".navbar-tabs-secondary");
267+
expect(css).toContain("display: inline-flex;");
268+
expect(css).not.toContain(".navbar-search {\n display: none;");
269+
expect(css).not.toContain(".navbar-tabs {\n display: none;");
270+
});
271+
272+
it("aligns the restored header shell to the browse page width", () => {
273+
const css = stylesCss();
274+
const compactCss = compactHeaderCss();
275+
276+
expect(css).toContain(".navbar-inner {\n width: 100%;\n max-width: var(--page-max);");
277+
expect(css).toContain("margin: 0 auto;\n padding: 0 var(--space-5);");
278+
expect(compactCss).toContain("padding: 10px 16px;");
279+
expect(compactCss).toContain("scroll-padding-inline: 16px;");
280+
expect(css).not.toContain(".navbar-inner,\n .section.detail-page-section");
201281
});
202282

203283
it("shows grouped skills and plugins typeahead without users", () => {
@@ -206,7 +286,7 @@ describe("Header", () => {
206286

207287
render(<Header />);
208288

209-
const input = screen.getByPlaceholderText("Search skills and plugins");
289+
const input = screen.getByPlaceholderText("Search skills, plugins, users");
210290
fireEvent.focus(input);
211291
fireEvent.change(input, { target: { value: "weather" } });
212292

@@ -249,7 +329,7 @@ describe("Header", () => {
249329

250330
render(<Header />);
251331

252-
const input = screen.getByPlaceholderText("Search skills and plugins");
332+
const input = screen.getByPlaceholderText("Search skills, plugins, users");
253333
fireEvent.focus(input);
254334
fireEvent.change(input, { target: { value: "weather" } });
255335
fireEvent.click(screen.getByRole("option", { name: /Weather Skill/i }));
@@ -278,7 +358,7 @@ describe("Header", () => {
278358

279359
render(<Header />);
280360

281-
const input = screen.getByPlaceholderText("Search skills and plugins");
361+
const input = screen.getByPlaceholderText("Search skills, plugins, users");
282362
fireEvent.focus(input);
283363
fireEvent.change(input, { target: { value: "zzzz" } });
284364

0 commit comments

Comments
 (0)