diff --git a/e2e/atlas.spec.ts b/e2e/atlas.spec.ts index 8e3b57c..88af662 100644 --- a/e2e/atlas.spec.ts +++ b/e2e/atlas.spec.ts @@ -21,6 +21,8 @@ async function fixture(page: any, api: any) { } async function showList(page: any) { const button = page.getByRole("button", { name: "Repository list", exact: true }); if (await button.isVisible()) await button.click(); } +/** Inspect without entering: the explicit "Check cached facts" path. Card clicks enter directly. */ +async function inspect(page: any, key: string) { await showList(page); const input = page.locator("#atlas-repository"); await input.fill(`https://github.com/${key}`); await input.press("Enter"); } function row(page: any, key: string) { return page.locator(".atlas-list-rows").getByRole("button", { name: new RegExp(key) }); } test("SYNTHETIC Atlas two-repository cached round trips preserve identity/source/styles/view/focus without stars or POST", async ({ page, api }) => { const worlds = await fixture(page, api); @@ -28,7 +30,8 @@ test("SYNTHETIC Atlas two-repository cached round trips preserve identity/source await expect(row(page, keys[0])).toContainText("Cached status not checked"); expect(api.requests.filter((r: any) => r.path.includes("/api/world/"))).toEqual([]); for (const [i, key] of keys.slice(0, 2).entries()) { - await row(page, key).click(); + await inspect(page, key); + await expect(page).toHaveURL(/\/atlas/); const card = page.locator(".atlas-card"); await expect(card.getByText("Discovered on this device · No Exploration star awarded.", { exact: true })).toBeVisible(); await expect(card.getByRole("link", { name: "GitHub source" })).toHaveAttribute("href", worlds[i].repo.url); @@ -38,9 +41,10 @@ test("SYNTHETIC Atlas two-repository cached round trips preserve identity/source await cv.focus(); await cv.press("="); await cv.press("ArrowDown"); - const before = await page.evaluate(() => JSON.parse(sessionStorage.getItem("moonfactory.atlas.session.v1")!)); - await card.getByRole("link", { name: "Enter cached world" }).click(); + await row(page, key).click(); await expect(page).toHaveURL(new RegExp(`/world/${key}\\?`)); + // The view saved at entry (after the list click located the landmark) is what Back restores. + const before = await page.evaluate(() => JSON.parse(sessionStorage.getItem("moonfactory.atlas.session.v1")!)); await expect(page.locator(".explorer-visit-status")).toContainText("Visited on this device"); await expect(page.locator(".explorer-visit-status")).toContainText("No Exploration star"); await page.locator(".district-list").getByRole("button", { name: /SYNTHETIC 源码/ }).click(); @@ -78,13 +82,16 @@ test("SYNTHETIC actual painted map label selects the same repository as its visi await page.emulateMedia({ reducedMotion: "no-preference" }); await fixture(page, api); await page.goto(atlasPath); - await row(page, keys[0]).click(); + await inspect(page, keys[0]); await expect(page.getByRole("link", { name: "Enter cached world" })).toBeVisible(); await page.getByRole("button", { name: "Close selected landmark" }).click(); + const mapTab = page.getByRole("button", { name: "Map", exact: true }); if (await mapTab.isVisible()) await mapTab.click(); const cv = page.locator(".atlas-map > canvas"); await expect.poll(() => cv.evaluate((c: any, key: string) => c.__labels?.some((p: any) => p.text === key), keys[0])).toBe(true); const label = await cv.evaluate((c: any, key: string) => c.__labels.find((p: any) => p.text === key), keys[0]), box = (await cv.boundingBox())!; await page.mouse.click(box.x + label.x, box.y + label.y); + await expect(page).toHaveURL(new RegExp(`/world/${keys[0]}\\?`)); + await page.getByRole("link", { name: "Back to Atlas", exact: true }).click(); await expect(row(page, keys[0])).toHaveAttribute("aria-pressed", "true"); await expect(page.locator(".atlas-card")).toContainText("a".repeat(40)); await page.waitForTimeout(150); @@ -105,14 +112,13 @@ for (const width of [320, 390, 768]) await page.keyboard.press("ArrowDown"); await expect(row(page, keys[1])).toBeFocused(); await page.keyboard.press("Enter"); + await expect(page.locator(".explorer-visit-status")).toContainText("Visited on this device"); + await page.getByRole("link", { name: "Back to Atlas", exact: true }).click(); + await expect(row(page, keys[1])).toBeFocused(); await expect(page.locator(".atlas-card")).toContainText("b".repeat(40)); await expect(page.locator(".atlas-card").getByRole("link", { name: "GitHub source" })).toHaveAttribute("href", "https://github.com/" + keys[1]); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); expect(await page.locator(".atlas-card").evaluate(e => e.scrollWidth <= e.clientWidth + 1)).toBe(true); - await page.getByRole("link", { name: "Enter cached world" }).click(); - await expect(page.locator(".explorer-visit-status")).toContainText("Visited on this device"); - await page.getByRole("link", { name: "Back to Atlas", exact: true }).click(); - await expect(row(page, keys[1])).toBeFocused(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); }); test("SYNTHETIC hidden Atlas map stops drawing, resumes one state-correct frame and never polls", async ({ page, api }) => { @@ -149,7 +155,7 @@ test("SYNTHETIC real two-touch pinch and cancellation never become repository en await cv.press("0"); await showList(page); await row(page, keys[0]).click(); - await expect(page.getByRole("link", { name: "Enter cached world" })).toBeVisible(); + await expect(page).toHaveURL(new RegExp(`/world/${keys[0]}\\?`)); expect(api.requests.every((r: any) => r.method === "GET")).toBe(true); }); @@ -159,8 +165,7 @@ test("SYNTHETIC Atlas light/dark chrome computes readable contrast and mobile en await page.goto(basePath + "/"); await expect(page.getByRole("link", { name: "Explore Atlas", exact: true }).filter({ visible: true })).toBeVisible(); await page.getByRole("link", { name: "Explore Atlas", exact: true }).filter({ visible: true }).click(); - await showList(page); - await row(page, keys[0]).click(); + await inspect(page, keys[0]); await expect(page.getByRole("link", { name: "Enter cached world" })).toBeVisible(); const audit = await page.evaluate(() => { const rgb = (value: string) => { const a = value.match(/[\d.]+/g)?.map(Number) ?? []; return [a[0] ?? 0, a[1] ?? 0, a[2] ?? 0, a[3] ?? 1]; }; diff --git a/e2e/gallery-preview.spec.ts b/e2e/gallery-preview.spec.ts index 49b7058..6dc22c6 100644 --- a/e2e/gallery-preview.spec.ts +++ b/e2e/gallery-preview.spec.ts @@ -5,13 +5,19 @@ import type { StyleId } from "../src/contracts"; const previewApi = "/api/gallery-preview/synthetic-moonshot/browser-fixture"; test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 1440, height: 600 }); }); -test("SYNTHETIC unknown gallery worlds cannot navigate to an empty route", async ({ page, api }) => { +test("SYNTHETIC unknown gallery worlds hand the repository to the explicit generate form without navigating", async ({ page, api }) => { api.respond("GET", "/api/gallery", () => ({ body: { projects: syntheticGallery.map((p) => ({ ...p, ready: false, collectedAt: null })) } })); await page.goto(`${basePath}/`); const card = page.locator(".project-card").filter({ has: page.getByRole("heading", { name: "SYNTHETIC Atlas", exact: true }) }); - await expect(card).toHaveAttribute("aria-disabled", "true"); + await expect(card).toHaveAttribute("data-ready", "false"); expect(await card.getAttribute("href")).toBeNull(); - await expect(card).toContainText("Map unavailable"); + await expect(card).toContainText("Build this world"); + const before = page.url(); + await card.click(); + expect(page.url()).toBe(before); + const fullName = syntheticGallery.find((p) => p.name === "SYNTHETIC Atlas")!.fullName; + await expect(page.locator("#repository")).toHaveValue(`https://github.com/${fullName}`); + await expect(page.locator("#repository")).toBeFocused(); await page.locator(".site-footer").scrollIntoViewIfNeeded(); await expect(page.getByText("Powered by Meta Muse Spark 1.3", { exact: true })).toBeVisible(); expect(api.requests.filter((r) => r.method === "POST")).toEqual([]); diff --git a/e2e/progression.spec.ts b/e2e/progression.spec.ts index 481af21..592f16a 100644 --- a/e2e/progression.spec.ts +++ b/e2e/progression.spec.ts @@ -11,8 +11,8 @@ async function harness(page:any){const result=await Bun.build({entrypoints:["./e test("SYNTHETIC two-repo source challenge loop: wrong answer, Saved receipt, Atlas lamp/back/focus, reload and repeat retain one slot",async({page,api})=>{ await fixtures(api);await page.goto(basePath+"/atlas"); for(const key of [keyA,keyB]){ - const row=page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(key)});await row.click();await expect(page.locator(".atlas-card")).toContainText("Discovered on this device");await expect(page.locator(".atlas-lantern")).toHaveCount(key===keyA?0:0); - await page.getByRole("link",{name:"Enter cached world",exact:true}).click();await expect(page.locator(".explorer-visit-status")).toContainText("No Exploration star awarded for entry"); + const row=page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(key)});await row.click();await expect(page).toHaveURL(new RegExp(`/world/${key}\\?`)); + await expect(page.locator(".explorer-visit-status")).toContainText("No Exploration star awarded for entry"); await answer(page,true);await expect(challenge(page).getByRole("status")).toContainText("Try again");expect((await receipts(page)).filter((r:any)=>r.completedIdentity.repoKey===key)).toHaveLength(0); for (const name of ["Moon Factory","Factorio","Red Alert","Red Alert 2","Civilization"]) { await page.getByRole("group",{name:"World perspective"}).getByRole("button",{name:new RegExp(name+"$")}).click();await expect(challenge(page).getByRole("radio",{name:"This proves the service is healthy in production.",exact:true})).toBeChecked(); } await challenge(page).getByRole("radio",{name:"The map records this directory source; its role is inferred and runtime behavior is unknown.",exact:true}).check();await challenge(page).getByRole("button",{name:"Submit source answers"}).click();await expect(challenge(page).getByRole("status")).toHaveText("Exploration star earned. Saved on this device."); @@ -99,14 +99,14 @@ test("F4 SYNTHETIC confirmed clear removes corrupt v1 rows atomically but reject test("F6 SYNTHETIC unsaved receipt stays session-only after Back/read, recovery saves original once and reload drops only unsaved",async({page,api})=>{ await fixtures(api);await page.addInitScript(()=>{const original=IDBObjectStore.prototype.add;let fail=true;IDBObjectStore.prototype.add=function(...args:any[]){if(this.name==="completions"&&fail){fail=false;throw new DOMException("SYNTHETIC save failure","QuotaExceededError");}return original.apply(this,args as any);};}); - await page.goto(basePath+"/atlas");await page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(keyA)}).click();await page.getByRole("link",{name:"Enter cached world"}).click();await answer(page);await expect(challenge(page).getByRole("status")).toContainText("Session progress — not saved");expect(await receipts(page)).toHaveLength(0); + await page.goto(basePath+"/atlas");await page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(keyA)}).click();await answer(page);await expect(challenge(page).getByRole("status")).toContainText("Session progress — not saved");expect(await receipts(page)).toHaveLength(0); await page.getByRole("link",{name:"Back to Atlas",exact:true}).click();await expect(page.locator(".atlas-progress-controls")).toContainText("0 saved Exploration stars · 1 session Exploration stars — not saved");await page.getByRole("button",{name:"Reload local progress"}).click();await expect(page.locator(".atlas-progress-controls")).toContainText("0 saved Exploration stars · 1 session"); await page.getByRole("link",{name:"Enter cached world"}).click();await answer(page);await expect(challenge(page).getByRole("status")).toContainText("Saved on this device");const saved=await receipts(page);expect(saved).toHaveLength(1);await answer(page);expect(await receipts(page)).toEqual(saved); await page.getByRole("link",{name:"Back to Atlas",exact:true}).click();await expect(page.locator(".atlas-progress-controls")).toContainText("1 saved Exploration stars · 0 session");await page.reload();await expect(page.locator(".atlas-progress-controls")).toContainText("1 saved Exploration stars · 0 session"); }); test("F6 SYNTHETIC unsaved discovery/visit are labelled session-only and disappear after reload",async({page,api})=>{ - await fixtures(api);await page.addInitScript(()=>{const original=IDBObjectStore.prototype.put;IDBObjectStore.prototype.put=function(...args:any[]){if(this.name==="repoProgress")throw new DOMException("SYNTHETIC flag save failure","QuotaExceededError");return original.apply(this,args as any);};});await page.goto(basePath+"/atlas");await page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(keyA)}).click();await expect(page.locator(".atlas-observation")).toContainText("Discovered this session");await page.getByRole("link",{name:"Enter cached world"}).click();await expect(page.locator(".explorer-visit-status")).toContainText("Visited this session — not saved");await answer(page);await expect(challenge(page).getByRole("status")).toContainText("Session progress — not saved");await page.getByRole("link",{name:"Back to Atlas",exact:true}).click();await expect(page.locator(".atlas-progress-controls")).toContainText("0 saved Exploration stars · 1 session");await expect(page.locator(".atlas-observation")).toContainText("Visited this session");await page.reload();await expect(page.locator(".atlas-progress-controls")).toContainText("Saved Exploration star count unknown · 0 session");expect(await receipts(page)).toHaveLength(0); + await fixtures(api);await page.addInitScript(()=>{const original=IDBObjectStore.prototype.put;IDBObjectStore.prototype.put=function(...args:any[]){if(this.name==="repoProgress")throw new DOMException("SYNTHETIC flag save failure","QuotaExceededError");return original.apply(this,args as any);};});await page.goto(basePath+"/atlas");await page.locator(".atlas-list-rows").getByRole("button",{name:new RegExp(keyA)}).click();await expect(page.locator(".explorer-visit-status")).toContainText("Visited this session — not saved");await answer(page);await expect(challenge(page).getByRole("status")).toContainText("Session progress — not saved");await page.getByRole("link",{name:"Back to Atlas",exact:true}).click();await expect(page.locator(".atlas-progress-controls")).toContainText("0 saved Exploration stars · 1 session");await expect(page.locator(".atlas-observation")).toContainText("Visited this session");await page.reload();await expect(page.locator(".atlas-progress-controls")).toContainText("Saved Exploration star count unknown · 0 session");expect(await receipts(page)).toHaveLength(0); }); test("F5 SYNTHETIC malformed cached graph is unavailable in Atlas and Explorer without an error boundary",async({page,api})=>{ diff --git a/src/atlas/AtlasPage.tsx b/src/atlas/AtlasPage.tsx index 2506773..4646166 100644 --- a/src/atlas/AtlasPage.tsx +++ b/src/atlas/AtlasPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Link } from "@tanstack/react-router"; +import { Link, useNavigate } from "@tanstack/react-router"; import type { FeaturedProject } from "../contracts"; import { getCachedWorld, getGallery } from "../client/api"; import { Brand, External } from "../components/shared"; @@ -18,8 +18,8 @@ export function AtlasPage() { const [catalog, setCatalog] = useState(null), [catalogError, setCatalogError] = useState(null), [selected, setSelected] = useState(null), [statuses, setStatuses] = useState(new Map()), [imports, setImports] = useState([]), [query, setQuery] = useState(""), [page, setPage] = useState(0), [pane, setPane] = useState<"map" | "list">("map"), [chooser, setChooser] = useState([]), [locateRequest, setLocateRequest] = useState<{ key: string; revision: number; - } | null>(null), [positionSaved, setPositionSaved] = useState(true), [initial, setInitial] = useState(null), [progressRevision, setProgressRevision] = useState(0), [focusedKey, setFocusedKey] = useState(null), [inspectInput, setInspectInput] = useState(""), [inspectError, setInspectError] = useState(null); - const session = useRef(null), view = useRef({ centerX: 1400, centerY: 1000, zoomRatio: 1 }), selectedRef = useRef(null), origin = useRef<"list" | "canvas">("list"), importRef = useRef([]), statusRef = useRef(statuses), list = useRef(null), pendingRestore = useRef(false), mounted = useRef(true); + } | null>(null), [positionSaved, setPositionSaved] = useState(true), [initial, setInitial] = useState(null), [progressRevision, setProgressRevision] = useState(0), [focusedKey, setFocusedKey] = useState(null), [inspectInput, setInspectInput] = useState(""), [inspectError, setInspectError] = useState(null), [enterRequest, setEnterRequest] = useState(0); + const session = useRef(null), view = useRef({ centerX: 1400, centerY: 1000, zoomRatio: 1 }), selectedRef = useRef(null), origin = useRef<"list" | "canvas">("list"), importRef = useRef([]), statusRef = useRef(statuses), list = useRef(null), pendingRestore = useRef(false), mounted = useRef(true), enterIntent = useRef(null), navigate = useNavigate(); const loader = useMemo(() => selectionLoader((key: string) => { const [owner, repo] = key.split("/"); return getCachedWorld(owner, repo); }), []); const updateStatus = useCallback((key: string, value: CacheStatus) => { const next = new Map(statusRef.current); next.set(key, value); statusRef.current = next; setStatuses(next); }, []); const save = useCallback((token?: string) => { @@ -43,10 +43,13 @@ export function AtlasPage() { }, [loader, updateStatus]); const cancelRead = useCallback(() => { loader.cancel(); + enterIntent.current = null; const key = selectedRef.current, state = key ? statusRef.current.get(key) : undefined; if (key && state?.state === "loading") updateStatus(key, { state: "unchecked", lastReceived: state.lastReceived }); }, [loader, updateStatus]); - const choose = useCallback((keys: string[], via: "list" | "canvas") => { + // A click on a landmark card or map label enters its cached world directly once + // its facts verify; "Check cached facts" only inspects (enter = false). + const choose = useCallback((keys: string[], via: "list" | "canvas", enter = true) => { if (keys.length !== 1) { cancelRead(); setChooser(keys); @@ -57,6 +60,9 @@ export function AtlasPage() { if (previous && previous !== key && prior?.state === "loading") updateStatus(previous, { state: "unchecked", lastReceived: prior.lastReceived }); selectedRef.current = key; + enterIntent.current = enter ? key : null; + if (enter) + setEnterRequest(n => n + 1); origin.current = via; setSelected(key); setChooser([]); @@ -118,6 +124,14 @@ export function AtlasPage() { useEffect(() => { if (chooser.length === 0 && facts && selected === facts.identity.repoKey) noteExploration(facts.identity, "discovered"); }, [facts, selected, chooser.length]); const selectedLandmark = landmarks.find(x => x.repoKey === selected), filtered = landmarks.filter(x => (x.fullName + " " + x.title + " " + x.category).toLowerCase().includes(query.toLowerCase())), rows = filtered.slice(page * 50, page * 50 + 50), enterToken = useMemo(() => selected ? crypto.randomUUID() : null, [selected]); + useEffect(() => { + if (!facts || !selected || !enterToken || chooser.length > 0 || enterIntent.current !== selected || facts.identity.repoKey !== selected) + return; + enterIntent.current = null; + const [owner, repo] = facts.fullName.split("/"); + save(enterToken); + void navigate({ to: "/world/$owner/$repo", params: { owner, repo }, search: { mode: "cached", from: "atlas", atlasReturn: enterToken, style: selectedLandmark?.style ?? "moonshot" } }); + }, [facts, selected, enterToken, chooser.length, selectedLandmark, navigate, save, enterRequest]); const completion = selected ? explorationCompletion(selected) : null; const flags = selected ? explorationFlags(selected) : { discovered: false, visited: false }; void progressRevision; @@ -132,7 +146,7 @@ export function AtlasPage() { setInspectError("The 100-repository survey capacity is full. Existing positions are retained."); return; } - choose([key], "list"); + choose([key], "list", false); } catch { setInspectError("Enter a public HTTPS GitHub repository URL. This action only checks existing cached facts."); @@ -146,7 +160,7 @@ export function AtlasPage() { return

Repository Atlas

Mission Control
{progress.durableCountKnown ? `${counts.saved} saved Exploration stars` : "Saved Exploration star count unknown"} · {counts.session} session Exploration stars — not saved{resetConfirm &&

Clear only this browser’s exploration flags and stars? Atlas positions, public source caches and server budgets are preserved.

}{resetError &&

{resetError}

}{progress.warning &&

{progress.warning}

}
-

Choose a landmark, inspect its captured identity, then enter its world. This atlas shows its catalog and your cached survey entries.

{catalog === null ? "Loading catalog" : `${landmarks.length} listed landmarks · ${[...statuses.values()].filter(s => s.state === "cached").length} captures checked this visit`} · No full GitHub map or complete history is claimed.

+

Click a landmark to enter its cached world, or inspect a repository first. This atlas shows its catalog and your cached survey entries.

{catalog === null ? "Loading catalog" : `${landmarks.length} listed landmarks · ${[...statuses.values()].filter(s => s.state === "cached").length} captures checked this visit`} · No full GitHub map or complete history is claimed.

{catalogError &&

{catalogError}

} {!positionSaved &&

View position is temporary. Browser session storage is unavailable; a reload may lose it.

}
@@ -164,7 +178,7 @@ export function AtlasPage() { {catalog !== null && !(initial?.selectedRepoKey === selected && selected && (!current || current.state === "loading")) ? { cancelRead(); setChooser([]); selectedRef.current = null; setSelected(null); save(); }} onView={(camera: Camera, homeZoom: number, commit: boolean) => { view.current = { centerX: camera.centerX, centerY: camera.centerY, zoomRatio: camera.zoom / homeZoom }; if (commit) save(); }}/> :

Loading the bounded catalog…

}
Public cached source · Read only · Original scenery · Local exploration is separate from GitHub stars and contributions
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx index 99ff29e..9554101 100644 --- a/src/components/LandingPage.tsx +++ b/src/components/LandingPage.tsx @@ -60,6 +60,12 @@ export function LandingPage() { [gallery, setGallery] = useState(CURATED_PROJECTS), [style, setStyle] = useState("moonshot"); const [ready, setReady] = useState(false); + const build = (fullName: string) => { + setRepo(`https://github.com/${fullName}`); + const input = document.getElementById("repository") as HTMLInputElement | null; + input?.scrollIntoView({ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "center" }); + input?.focus({ preventScroll: true }); + }; const [galleryLoaded, setGalleryLoaded] = useState(false); useEffect(() => setReady(true), []); const root = useRef(null); @@ -258,7 +264,7 @@ export function LandingPage() {
{gallery.map((p, index) => ( - + ))}
diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index 96c5519..d33ae5c 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -12,7 +12,7 @@ const reduced = () => typeof window !== "undefined" && window.matchMedia("(prefe * world page. Pointer handling writes CSS custom properties once per animation frame * (no layout reads inside the frame) and shares the offset with the card canvas. */ -export function ProjectCard({ project: p, enabled, index }: { project: FeaturedProject; enabled: boolean; index: number }) { +export function ProjectCard({ project: p, enabled, index, onBuild }: { project: FeaturedProject; enabled: boolean; index: number; onBuild?: (fullName: string) => void }) { const shell = useRef(null); const tilt = useRef({ x: 0, y: 0 }); const frame = useRef(0); @@ -54,7 +54,8 @@ export function ProjectCard({ project: p, enabled, index }: { project: FeaturedP schedule(); }; const onClick = (e: React.MouseEvent) => { - if (!p.ready) { e.preventDefault(); return; } + // No cached map: hand the repository to the explicit "Generate world" form. No request is made. + if (!p.ready) { e.preventDefault(); onBuild?.(p.fullName); return; } if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || reduced() || !href) return; // Brief zoom-into-the-map transition, then navigate to the same URL. e.preventDefault(); @@ -64,7 +65,7 @@ export function ProjectCard({ project: p, enabled, index }: { project: FeaturedP return (
- + { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onBuild?.(p.fullName); } }}>
{p.category} @@ -89,7 +90,7 @@ export function ProjectCard({ project: p, enabled, index }: { project: FeaturedP )} - {p.ready ? "Explore world" : "Map unavailable"} + {p.ready ? "Explore world" : "Build this world"}
diff --git a/src/components/landing-motion.css b/src/components/landing-motion.css index 4f39252..c0faf18 100644 --- a/src/components/landing-motion.css +++ b/src/components/landing-motion.css @@ -106,7 +106,7 @@ will-change: transform; } .landing .project-card-shell.is-tilting > .project-card { transition: transform .08s linear, box-shadow .3s, border-color .3s; } -.landing .project-card-shell:hover > .project-card[aria-disabled="false"] { box-shadow: 0 24px 40px -18px #1b2a1a55; border-color: var(--geist-secondary); } +.landing .project-card-shell:hover > .project-card[data-ready="true"] { box-shadow: 0 24px 40px -18px #1b2a1a55; border-color: var(--geist-secondary); } .landing .project-card:hover { transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y)); } .project-glint { position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0; transition: opacity .3s; background: radial-gradient(circle at var(--glint-x) var(--glint-y), #ffffff40, transparent 45%); mix-blend-mode: screen; } .project-card-shell:hover .project-glint { opacity: 1; } diff --git a/src/styles.css b/src/styles.css index caaa408..b03fbaf 100644 --- a/src/styles.css +++ b/src/styles.css @@ -533,10 +533,10 @@ svg { transform: translateY(-4px); box-shadow: 0 12px 28px #26301d12; } -.project-card[aria-disabled="true"] { - cursor: default; +.project-card[data-ready="false"] { + cursor: pointer; } -.project-card[aria-disabled="true"] .project-arrow { +.project-card[data-ready="false"] .project-arrow { opacity: 1; color: var(--geist-disabled-fg); background: var(--geist-disabled-bg); diff --git a/test/catalog.test.ts b/test/catalog.test.ts index 5a647f9..ea222da 100644 --- a/test/catalog.test.ts +++ b/test/catalog.test.ts @@ -39,7 +39,7 @@ test("initial English landing disables uncached world links without fabricated s const html = renderToStaticMarkup(createElement(LandingPage)); const cards = [...html.matchAll(/
match[1]); expect(cards).toEqual([]); - expect([...html.matchAll(/aria-disabled="true"/g)]).toHaveLength(19); + expect([...html.matchAll(/data-ready="false"/g)]).toHaveLength(19); expect(html).toContain("Powered by Meta Muse Spark 1.3"); expect(html).toContain("19 CURATED POPULAR PROJECTS"); expect(html).not.toContain("World ready");