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
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
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
123 changes: 123 additions & 0 deletions cypress/e2e/responsive.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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");
// 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