Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions web/src/components/guide/Guide.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1888,6 +1893,7 @@
.report:hover span,
.replay:hover span,
.nextLink:hover span,
.nextLink[data-in]:hover span,
.openingBack a:hover span {
transform: none;
}
Expand Down
31 changes: 24 additions & 7 deletions web/src/components/guide/GuideExperience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -585,12 +585,29 @@ export default function GuideExperience({ lang, page = "play" }: { lang: GuideLa
{next.until}
</p>
<div className={styles.nextLinks} data-line="" style={{ "--n": next.groups.length + 2 } as CSSProperties}>
{next.links.map((l) => (
<a key={l.href} href={l.href} className={styles.nextLink}>
{l.label}
<span aria-hidden="true">↗</span>
</a>
))}
{next.links.map((l) => {
const to = linkWay(l.href);
if (to.way === "guide")
return (
<GuideLink key={l.href} to={to.page} lang={to.lang} className={styles.nextLink} data-in="">
{l.label}
<span aria-hidden="true">→</span>
</GuideLink>
);
if (to.way === "site")
return (
<Link key={l.href} href={l.href} className={styles.nextLink} data-in="">
{l.label}
<span aria-hidden="true">→</span>
</Link>
);
return (
<a key={l.href} href={l.href} className={styles.nextLink} target="_blank" rel="noopener noreferrer">
{l.label}
<span aria-hidden="true">↗</span>
</a>
);
})}
</div>
<div data-line="" style={{ "--n": next.groups.length + 3 } as CSSProperties}>
<Report
Expand Down
23 changes: 22 additions & 1 deletion web/src/components/guide/pages.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { GUIDE_ORDER, HUB_PATH, PAGES, pagePath } from "./pages";
import { GUIDE_ORDER, HUB_PATH, PAGES, linkWay, pagePath } from "./pages";
import { scriptMismatches } from "./checks";
import { scenesOf, type Step } from "./scenes";
import { HUB_COPY } from "./copy/hub";
Expand Down Expand Up @@ -134,6 +134,27 @@ describe("guide pages", () => {
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);
Expand Down
15 changes: 15 additions & 0 deletions web/src/components/guide/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading