diff --git a/web/src/components/guide/Guide.module.css b/web/src/components/guide/Guide.module.css index c9601a5e..1a731f71 100644 --- a/web/src/components/guide/Guide.module.css +++ b/web/src/components/guide/Guide.module.css @@ -1730,6 +1730,11 @@ color: var(--g-led); transform: translate3d(2px, -2px, 0); } +/* A way on inside the site: its → goes on, not up and out like ↗. */ +.nextLink[data-in]:hover span, +.nextLink[data-in]:focus-visible span { + transform: translate3d(2px, 0, 0); +} .nextLink:active { transform: scale(0.97); transition-duration: 0.08s; @@ -1888,6 +1893,7 @@ .report:hover span, .replay:hover span, .nextLink:hover span, + .nextLink[data-in]:hover span, .openingBack a:hover span { transform: none; } diff --git a/web/src/components/guide/GuideExperience.tsx b/web/src/components/guide/GuideExperience.tsx index 6e1f3b90..21401779 100644 --- a/web/src/components/guide/GuideExperience.tsx +++ b/web/src/components/guide/GuideExperience.tsx @@ -9,7 +9,7 @@ import { sceneById } from "./scenes"; import { useGuideStore, type GuideLang, type GuidePageId } from "./store"; import Extras, { LiveThumbs } from "./Extras"; import { hereFor, reportUrl } from "./report"; -import { PAGES, type PageChapter } from "./pages"; +import { PAGES, linkWay, type PageChapter } from "./pages"; import { scriptMismatches } from "./checks"; import { useDeskFits } from "./desk/query"; import DeskCue from "./desk/DeskCue"; @@ -585,12 +585,29 @@ export default function GuideExperience({ lang, page = "play" }: { lang: GuideLa {next.until}

- {next.links.map((l) => ( - - {l.label} - - - ))} + {next.links.map((l) => { + const to = linkWay(l.href); + if (to.way === "guide") + return ( + + {l.label} + + + ); + if (to.way === "site") + return ( + + {l.label} + + + ); + return ( + + {l.label} + + + ); + })}
{ for (const page of pages) for (const lang of ["en", "ko"] as const) expect(PAGES[page].text(lang).opening.back?.to).toBe("hub"); }); + it("sorts each guide's end links into the guide, the site and away", () => { + expect(linkWay("/guide")).toEqual({ way: "guide", page: "hub", lang: "en" }); + expect(linkWay("/guide/ko")).toEqual({ way: "guide", page: "hub", lang: "ko" }); + expect(linkWay("/guide/play/ko")).toEqual({ way: "guide", page: "play", lang: "ko" }); + expect(linkWay("/guide/play#flash")).toEqual({ way: "site" }); + expect(linkWay("/pattern-lab")).toEqual({ way: "site" }); + expect(linkWay("https://community.patternflow.work/community")).toEqual({ way: "away" }); + expect(linkWay("//example.com/")).toEqual({ way: "away" }); + for (const page of pages) { + for (const lang of ["en", "ko"] as const) { + for (const { href } of PAGES[page].text(lang).next.links) { + const to = linkWay(href); + if (to.way === "away") expect(href).toMatch(/^https:\/\//); + // A link back into the guide stays in the reader's language. + if (to.way === "guide") expect(to.lang).toBe(lang); + } + } + } + expect(PAGES.audio.text("en").next.links.map((l) => linkWay(l.href).way)).toEqual(["away", "away", "away", "away", "away", "site", "guide"]); + }); + it("never offers a kit or an assembled board", () => { const words = JSON.stringify([HUB_COPY, ...pages.flatMap((p) => [PAGES[p].text("en"), PAGES[p].text("ko")])]); expect(words).not.toMatch(/\bkits?\b|pre-?assembled|assembled board|키트|완제품/i); diff --git a/web/src/components/guide/pages.ts b/web/src/components/guide/pages.ts index 6a1f3297..b1c78246 100644 --- a/web/src/components/guide/pages.ts +++ b/web/src/components/guide/pages.ts @@ -161,6 +161,21 @@ export function screenAt(pathname: string): { page: GuideScreen; lang: GuideLang return null; } +/** + * Where a link at a guide's end goes. Another page of the guide is followed + * in place (world/GuideLink): the words leave and the stage stays. Another + * page of the site is an ordinary link, →, in the same tab. Anything else is + * away from the site, ↗, in a new tab. The community is on its own host, so + * it is away. + */ +export type LinkWay = { way: "guide"; page: GuideScreen; lang: GuideLang } | { way: "site" } | { way: "away" }; + +export function linkWay(href: string): LinkWay { + if (!href.startsWith("/") || href.startsWith("//")) return { way: "away" }; + const screen = screenAt(href); + return screen ? { way: "guide", ...screen } : { way: "site" }; +} + /** * Where the reader is on a guide, as an anchor: "#flash-3" for a step whose * card is on screen, "#flash" for a chapter's title, "#next" for the end, and