Skip to content
Closed
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
7 changes: 7 additions & 0 deletions src/app/routes/concepts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";

import { ConceptsPage } from "@/pages/concepts";

export const Route = createFileRoute("/concepts")({
component: ConceptsPage,
});
41 changes: 41 additions & 0 deletions src/pages/concepts/config/conceptsCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const CONCEPTS_COPY = {
CTA_LABEL: "Enter Dungeon",
HEADING: "System concepts.",
SECONDARY_LINK_LABEL: "Read the Guide",
SUBTITLE:
"Runestone maps statechart ideas to dungeon objects so software behavior can be explored spatially.",
CTA_HEADING: "Ready to inspect the dungeon?",
} as const;

export const CONCEPTS_SECTIONS = [
{
detail:
"A state describes where the system currently is. In Runestone, that state is represented by the room the player occupies.",
label: "State → Room",
},
{
detail:
"A transition describes how the system moves from one state to another. In the dungeon, transitions appear as corridors and doorways.",
label: "Transition → Corridor",
},
{
detail:
"Events are messages sent to the system. Movement, interaction, attack, run, and jump actions all dispatch events.",
label: "Event → Input or prompt",
},
{
detail:
"A guard decides whether a transition is allowed. Locked doors, keys, enemies, and room conditions make those rules visible.",
label: "Guard → Locked door",
},
{
detail:
"Context is the data carried by the run. Keys, HP, current room, and discovered rooms can change what paths are available.",
label: "Context → Inventory and HP",
},
{
detail:
"Actors isolate behavior. Camera, player, input, audio, and dungeon logic can each respond to events without becoming one tangled system.",
label: "Actor → Independent loop",
},
] as const;
1 change: 1 addition & 0 deletions src/pages/concepts/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CONCEPTS_COPY, CONCEPTS_SECTIONS } from "./conceptsCopy";
1 change: 1 addition & 0 deletions src/pages/concepts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ConceptsPage } from "./ui";
58 changes: 58 additions & 0 deletions src/pages/concepts/ui/ConceptsPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @vitest-environment happy-dom

import { cleanup, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ConceptsPage } from "./ConceptsPage";

const mockUseAuthContext = vi.hoisted(() => vi.fn());

vi.mock("@/features/auth", async () => {
const actual =
await vi.importActual<typeof import("@/features/auth")>("@/features/auth");

return {
...actual,
useAuthContext: mockUseAuthContext,
};
});

vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to, ...props }: { children: ReactNode; to: string }) => (
<a href={to} {...props}>
{children}
</a>
),
}));

afterEach(cleanup);

beforeEach(() => {
mockUseAuthContext.mockReturnValue({
isAuthenticated: true,
});
});

describe("ConceptsPage", () => {
it("renders the standalone concepts page without tabs", () => {
render(<ConceptsPage />);

expect(screen.getByText("System concepts.")).not.toBeNull();
expect(screen.getByText("State → Room")).not.toBeNull();
expect(screen.getByText("Actor → Independent loop")).not.toBeNull();
expect(screen.queryByRole("tab", { name: "Controls" })).toBeNull();
expect(screen.queryByRole("tab", { name: "First Run" })).toBeNull();
expect(screen.queryByRole("tab", { name: "Concepts" })).toBeNull();
});

it("includes the GitHub footer link from the shared marketing shell", () => {
render(<ConceptsPage />);

expect(
screen.getByRole("link", {
name: "GitHub",
}),
).not.toBeNull();
});
});
86 changes: 86 additions & 0 deletions src/pages/concepts/ui/ConceptsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Link } from "@tanstack/react-router";
import { ArrowRight, DoorOpen } from "lucide-react";

import { useAuthContext } from "@/features/auth";
import { Button, Card, CardContent } from "@/shared/ui";
import {
MARKETING_NAVIGATION_ITEM_IDS,
MARKETING_ROUTES,
MarketingShell,
} from "@/widgets/marketing-shell";

import { CONCEPTS_COPY, CONCEPTS_SECTIONS } from "../config";

export function ConceptsPage() {
const { isAuthenticated } = useAuthContext();

return (
<MarketingShell
activeNavigationItemId={MARKETING_NAVIGATION_ITEM_IDS.CONCEPTS}
isAuthenticated={isAuthenticated}
>
<div className="mx-auto flex w-full max-w-7xl flex-col gap-10 px-5 py-8 sm:px-8 sm:py-10 lg:px-12 lg:py-14">
<header className="max-w-3xl space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-panel-title sm:text-5xl">
{CONCEPTS_COPY.HEADING}
</h1>
<p className="text-base leading-7 text-panel-body sm:text-lg">
{CONCEPTS_COPY.SUBTITLE}
</p>
</header>

<section
aria-label={CONCEPTS_COPY.HEADING}
className="grid gap-3 md:grid-cols-2 lg:grid-cols-3"
>
{CONCEPTS_SECTIONS.map((section) => (
<Card
key={section.label}
className="border border-panel-border bg-panel p-0 shadow-none ring-0"
>
<CardContent className="space-y-3 p-5">
<div className="flex items-center justify-between gap-4">
<h2 className="text-sm font-semibold text-panel-title">
{section.label}
</h2>
<ArrowRight className="size-4 text-dungeon-gold" />
</div>
<p className="text-sm leading-6 text-panel-body">
{section.detail}
</p>
</CardContent>
</Card>
))}
</section>

<Card className="border border-dungeon-gold/40 bg-panel p-0 shadow-none ring-0">
<CardContent className="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-6">
<h2 className="text-2xl font-semibold text-panel-title">
{CONCEPTS_COPY.CTA_HEADING}
</h2>
<div className="flex flex-col gap-3 sm:flex-row">
{isAuthenticated ? (
<Button asChild size="lg">
<Link to={MARKETING_ROUTES.GAME}>
<DoorOpen className="size-4" />
{CONCEPTS_COPY.CTA_LABEL}
</Link>
</Button>
) : (
<Button disabled size="lg">
<DoorOpen className="size-4" />
{CONCEPTS_COPY.CTA_LABEL}
</Button>
)}
<Button asChild size="lg" variant="dungeon-gold">
<Link to={MARKETING_ROUTES.GUIDE}>
{CONCEPTS_COPY.SECONDARY_LINK_LABEL}
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
</MarketingShell>
);
}
1 change: 1 addition & 0 deletions src/pages/concepts/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ConceptsPage } from "./ConceptsPage";
99 changes: 85 additions & 14 deletions src/pages/home/config/homeCopy.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,99 @@
export const HOME_COPY = {
BADGE: "Dungeon briefing",
BADGE: "Playable architecture",
CTA_LABEL: "Enter Dungeon",
FEATURES_HEADING: "What you’ll learn",
HEADING: "Runestone",
SUBTITLE: "A living dungeon where rooms change as you move through them.",
TUTORIAL_LABEL: "How to Play",
SESSION_NOTE:
"We’re getting your game ready. You can keep reading the guide while we sign you in.",
FEATURES_HEADING: "What the dungeon teaches",
HEADING: "Walk through executable logic.",
MANIFEST_PATH_HEADING: "Manifest Path",
MANIFEST_PATH_SUBTITLE: "Follow the sequence. Decode the statechart.",
MOBILE_ORIENTATION_NOTICE:
"Landscape mode is recommended for gameplay and full logic visualization.",
RUNTIME_HEADING: "Read the system while you play.",
RUNTIME_SUBTITLE:
"Gameplay, state, and context are shown together so the dungeon can be read as a running statechart.",
SUBTITLE:
"Runestone turns statecharts into a 3D dungeon: rooms are states, corridors are transitions, and every action is driven by explicit events.",
TUTORIAL_LABEL: "Read the Guide",
} as const;

export const HOME_MANIFEST_TONE_CLASS_NAMES = {
active: "border-primary/50 bg-primary/10 text-primary",
available: "border-accent/50 bg-accent/10 text-accent",
sealed:
"border-dungeon-rune-sealed/50 bg-dungeon-rune-sealed/10 text-dungeon-rune-sealed",
} as const;

export const HOME_MANIFEST_PATH = [
{
detail: "Initialize run. Establish current room.",
label: "Entrance",
tone: "active",
},
{
detail: "Move through a valid transition.",
label: "Corridor",
tone: "available",
},
{
detail: "Guard condition blocks progress.",
label: "Locked Door",
tone: "sealed",
},
{
detail: "Use context to unlock the path.",
label: "Inventory Check",
tone: "available",
},
{
detail: "Camera, input, and dungeon logic process events independently.",
label: "Actor Loops",
tone: "active",
},
] as const;

export const HOME_TRANSLATION_RAIL = [
"State → Room",
"Transition → Corridor",
"Guard → Locked Door",
"Context → Inventory",
"Actor → Loop",
] as const;

export const HOME_RUNTIME_PANELS = [
{
detail: "The playable dungeon frame anchors the learning experience.",
label: "Viewport",
},
{
detail: "The active room maps to the current state.",
label: "Statechart",
},
{
detail: "Context explains why paths open or remain locked.",
label: "Inspector",
},
] as const;

export const HOME_FEATURES = [
{
detail: "Rooms and doors react as you move forward.",
title: "The dungeon shifts as you progress.",
detail: "Each chamber shows where the system currently is.",
title: "States become rooms",
},
{
detail:
"Switch between third-person, top-down, first-person, and free-orbital views.",
title: "Pick the view that feels easiest to play.",
detail: "Input, prompts, and combat dispatch explicit events.",
title: "Events move the system",
},
{
detail: "Doors open only when context satisfies the transition rule.",
title: "Guards control progression",
},
{
detail:
"The guide covers movement, interaction, attack, and camera controls.",
title: "Learn the basics before you descend.",
"Camera, player, input, audio, and dungeon logic run through focused loops.",
title: "Actors stay isolated",
},
{
detail: "Keys, HP, and current room data shape what can happen next.",
title: "Context changes valid paths",
},
] as const;

Expand Down
10 changes: 9 additions & 1 deletion src/pages/home/config/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export { HOME_COPY, HOME_FEATURES, HOME_STATUS_COPY } from "./homeCopy";
export {
HOME_COPY,
HOME_FEATURES,
HOME_MANIFEST_PATH,
HOME_MANIFEST_TONE_CLASS_NAMES,
HOME_RUNTIME_PANELS,
HOME_STATUS_COPY,
HOME_TRANSLATION_RAIL,
} from "./homeCopy";
Loading
Loading