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
8 changes: 6 additions & 2 deletions components/download/DownloadExperience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function OsTabs({
aria-selected={active}
onClick={() => onSelect(os)}
className={classNames(
"flex flex-1 items-center justify-center gap-2 px-4 py-3 font-display text-sm font-semibold uppercase tracking-label transition-colors",
"flex flex-1 items-center justify-center gap-1.5 px-2 py-3 font-display text-xs font-semibold uppercase tracking-label transition-colors sm:gap-2 sm:px-4 sm:text-sm",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset",
active
? "border-b-2 border-accent bg-primary/15 text-accent"
Expand All @@ -54,7 +54,11 @@ function OsTabs({
>
<Icon className="h-4 w-4" aria-hidden />
<span>{label}</span>
{os === detected && <Badge tone="info">Detected</Badge>}
{os === detected && (
<Badge tone="info" className="hidden sm:inline-flex">
Detected
</Badge>
)}
</button>
);
})}
Expand Down
15 changes: 11 additions & 4 deletions components/home/AboutSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import SectionIntro from "../../components/ui/SectionIntro";

export default function AboutSection() {
return (
<Container width="wide" className="py-20 sm:py-28">
<Container width="wide" className="flex flex-col gap-20 py-20 sm:py-28">
<SectionIntro
kicker="What is Unitystation?"
kicker="What is Space Station 13?"
title="Pick a job and keep the station running."
lead="Every round drops a crew of real players onto a fully simulated space
station: power, atmospherics, chemistry and medicine all work (and break).
Whether the shift ends in a calm crew transfer or a reactor fire depends
entirely on the people aboard."
The crew is tasked to keep the station afloat, whilst among their midst are antagonists with nefarious objectives to sabotage the station.
Every shift provides a unique experience, with emergent gameplay and player-driven stories."
/>
<SectionIntro
kicker="Our Mission"
title="Recreating the 2017 /TG/ experience."
lead="We use 2017 /TG/ as our blueprint for this remake.
We aim to preserve the original experience, while improving the game with expanded content, modern features, and quality of life improvements.
Our goal is to make a faithful recreation of the classic Space Station 13 experience, while also making it more accessible and enjoyable for both veteran and new players."
/>
</Container>
);
Expand Down
5 changes: 2 additions & 3 deletions components/home/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ScreenshotFeed from "./ScreenshotFeed";
export default function Hero() {
return (
<section className="mx-auto flex min-h-[calc(100svh-3.5rem)] w-full max-w-[88rem] flex-col justify-center px-4 pb-14 pt-20 sm:px-6 lg:py-16">
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-10 xl:grid-cols-[minmax(0,10fr)_minmax(0,11fr)] xl:gap-14">
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-10 xl:grid-cols-[minmax(0,10fr)_minmax(0,11fr)] xl:gap-14">
{/* Title column */}
<div className="flex flex-col items-center text-center lg:items-start lg:text-left">
<div className="relative animate-fade-up">
Expand Down Expand Up @@ -40,8 +40,7 @@ export default function Hero() {
</div>
</div>

{/* screenshot feed */}
<div className="relative w-full animate-fade-up [animation-delay:360ms] lg:-mr-6 lg:w-auto">
<div className="relative w-full animate-fade-up [animation-delay:360ms] lg:w-auto">
<ScreenshotFeed />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/home/RotatingTagline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function RotatingTagline() {
}, []);

return (
<p className="splash-text pointer-events-none absolute -top-9 left-0 z-10 max-w-64 text-center font-display text-sm font-semibold leading-tight text-warning [text-shadow:2px_2px_0_rgb(var(--c-void)/0.95)] sm:-left-4 sm:-top-8">
<p className="splash-text pointer-events-none absolute -top-9 left-0 z-10 max-w-64 text-center font-display text-sm font-semibold leading-tight text-warning [text-shadow:2px_2px_0_rgb(var(--c-void)/0.95)] sm:-left-4 sm:-top-8 lg:left-0">
{tagline}
</p>
);
Expand Down
9 changes: 2 additions & 7 deletions components/home/ScreenshotFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import classNames from "classnames";
import { useEffect, useState } from "react";
import PanelBar from "../ui/PanelBar";
import StatusLight from "../ui/StatusLight";

const HERO_IMAGES: string[] = [
"https://cdn.unitystation.org/Website-Statics/heroImages/bar-engine.png",
Expand Down Expand Up @@ -57,11 +56,6 @@ export default function ScreenshotFeed() {
className="absolute -right-4 -top-4 hidden h-full w-full rounded-lg border border-seam bg-raised sm:block"
/>
<div className="relative overflow-hidden rounded-lg border border-seam bg-panel shadow-window">
<PanelBar>
<StatusLight tone="success" blink />
<p className="type-label text-dim">In-game screenshots</p>
</PanelBar>

<div className="relative aspect-video bg-void xl:aspect-[16/10]">
{HERO_IMAGES.map((src, i) =>
loaded.has(i) ? (
Expand All @@ -77,7 +71,7 @@ export default function ScreenshotFeed() {
/>
) : null,
)}
<div className="scanlines absolute inset-0" aria-hidden />
<div className="absolute inset-0" aria-hidden />
</div>

{/* Status bar: frame picker + counter */}
Expand All @@ -104,6 +98,7 @@ export default function ScreenshotFeed() {
/>
))}
</div>
<p className="type-label text-dim">In-game screenshots</p>
<p className="type-label text-faint">
{String(frame + 1).padStart(2, "0")} /{" "}
{String(HERO_IMAGES.length).padStart(2, "0")}
Expand Down
3 changes: 2 additions & 1 deletion components/ui/ButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default function ButtonGroup({ className, children, ...rest }: Props) {
<div
role="group"
className={classNames(
"flex divide-x divide-seam overflow-hidden rounded-md",
"flex flex-col divide-y divide-seam overflow-hidden rounded-md",
"min-[480px]:flex-row min-[480px]:divide-x min-[480px]:divide-y-0",
"border border-seam bg-steel shadow-bevel",
"[&>*]:flex-1 [&>*]:rounded-none",
// keep focus rings visible inside the clipped container
Expand Down
2 changes: 1 addition & 1 deletion components/ui/SectionIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function SectionIntro({
<div
className={classNames(centered && "flex flex-col items-center text-center", className)}
>
<p className="type-label text-halo-sm text-faint">{kicker}</p>
<p className="type-label text-halo-sm text-faint underline">{kicker}</p>
<h2
className={classNames(
"text-halo-lg mt-4 font-display font-bold leading-tight text-crew",
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/home.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("Home page", () => {
});

it("renders the about and community sections", () => {
cy.contains("What is Unitystation?").should("exist");
cy.contains("What is Space Station 13?").should("exist");
cy.contains("Pick a job and keep the station running.").should("exist");
cy.contains("h2", "Join the community playtest!").scrollIntoView().should("be.visible");
cy.contains("a", "Join the Discord").should(
Expand Down
124 changes: 124 additions & 0 deletions cypress/e2e/responsive.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Test if any route has unintended horizontal overflow at most common screens
*/
const WIDTHS = [320, 393, 640, 768, 1024];

type Offender = { tag: string; cls: string; left: number; right: number };

/** Elements sticking out horizontally at the current viewport width. */
function findOverflowingElements(win: Window): Offender[] {
const doc = win.document;
const vw = doc.documentElement.clientWidth;
const offenders: Offender[] = [];

for (const el of Array.from(doc.querySelectorAll<HTMLElement>("body *"))) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) continue;
if (rect.right <= vw + 1 && rect.left >= -1) continue;

// Fixed overlays (parallax space, the clown's cage) clip themselves.
if (win.getComputedStyle(el).position === "fixed") continue;

// Skip elements an ancestor already clips or scrolls horizontally
let ancestor = el.parentElement;
let clipped = false;
while (ancestor && ancestor !== doc.body) {
if (/(hidden|clip|auto|scroll)/.test(win.getComputedStyle(ancestor).overflowX)) {
clipped = true;
break;
}
ancestor = ancestor.parentElement;
}
if (clipped) continue;

const cls = typeof el.className === "string" ? el.className : "";
offenders.push({
tag: el.tagName.toLowerCase(),
cls: cls.slice(0, 100),
left: Math.round(rect.left),
right: Math.round(rect.right),
});
}
return offenders;
}

function assertNoHorizontalOverflow(route: string) {
for (const width of WIDTHS) {
cy.viewport(width, 850);
// let the resize propagate and media queries re-evaluate
cy.wait(100);
cy.window().then((win) => {
const offenders = findOverflowingElements(win);
expect(
offenders,
`${route} @ ${width}px elements past the viewport edge:\n` +
JSON.stringify(offenders, null, 2),
).to.have.length(0);
expect(
win.document.documentElement.scrollWidth,
`${route} @ ${width}px page scroll width`,
).to.be.at.most(width + 1);
});
}
}

describe("Responsive layout", () => {
it("home has no horizontal overflow at any width", () => {
cy.visit("/");
cy.contains("h1", "Unitystation!").should("be.visible");
assertNoHorizontalOverflow("/");
});

it("download has no horizontal overflow at any width", () => {
cy.visit("/download");
cy.contains("h1", "Download Pudu Launcher").should("be.visible");
assertNoHorizontalOverflow("/download");
});

it("blog has no horizontal overflow at any width", () => {
cy.intercept("GET", /changelog\.unitystation\.org\/posts\/\?page=1$/, {
fixture: "blogPosts-page1.json",
}).as("postsPage1");
cy.intercept("GET", /changelog\.unitystation\.org\/posts\/\?page=2$/, {
fixture: "blogPosts-page2.json",
}).as("postsPage2");

cy.visit("/blog");
cy.wait("@postsPage1");
cy.contains("h2", "Station reactor now explodes properly").should("be.visible");
// materialise the infinite-scroll page before scanning
cy.scrollTo("bottom");
cy.wait("@postsPage2");
assertNoHorizontalOverflow("/blog");
});

it("changelog has no horizontal overflow at any width", () => {
cy.intercept("GET", /changelog\.unitystation\.org\/all-changes/, {
fixture: "changelog-page1.json",
}).as("changelog");

cy.visit("/changelog");
cy.wait("@changelog");
assertNoHorizontalOverflow("/changelog");
});

it("ledger has no horizontal overflow at any width", () => {
cy.intercept("GET", /ledger\.unitystation\.org\/movements\/$/, {
fixture: "ledger-page1.json",
}).as("ledgerPage1");

cy.visit("/ledger");
cy.wait("@ledgerPage1");
assertNoHorizontalOverflow("/ledger");
});

for (const route of ["/login", "/register", "/reset-password"]) {
it(`${route} has no horizontal overflow at any width`, () => {
cy.visit(route);
cy.get("h1").should("be.visible");
assertNoHorizontalOverflow(route);
});
}
});

export {};
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

module.exports = {
reactCompiler: true,
// Dev only: lets phones on the home network open the dev server via the
// machine's LAN IP. Without this Next blocks its /_next dev resources for
// non-localhost origins and pages render but never hydrate (dead buttons).
allowedDevOrigins: ["192.168.1.*"],
images: {
remotePatterns: [
{
Expand Down
Loading