From 35a36e668de6695b1b36e0664b54cc36778a7a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 27 Jul 2026 09:49:46 +0200 Subject: [PATCH] Add /testdesign/h2 --- .../testdesign/h2/HaloPauseMenu.tsx | 93 +++++ .../testdesign/h2/halo-pause-menu.css | 350 ++++++++++++++++++ packages/webapp/src/routeTree.gen.ts | 52 ++- packages/webapp/src/routes/__root.tsx | 2 +- packages/webapp/src/routes/testdesign.h2.tsx | 7 + packages/webapp/src/routes/testdesign.tsx | 9 + 6 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 packages/webapp/src/components/testdesign/h2/HaloPauseMenu.tsx create mode 100644 packages/webapp/src/components/testdesign/h2/halo-pause-menu.css create mode 100644 packages/webapp/src/routes/testdesign.h2.tsx create mode 100644 packages/webapp/src/routes/testdesign.tsx diff --git a/packages/webapp/src/components/testdesign/h2/HaloPauseMenu.tsx b/packages/webapp/src/components/testdesign/h2/HaloPauseMenu.tsx new file mode 100644 index 0000000..9be0b39 --- /dev/null +++ b/packages/webapp/src/components/testdesign/h2/HaloPauseMenu.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "react" + +const menuItems = [ + "Continue", + "Revert to Last Checkpoint", + "Restart Level", + "Controller Settings", + "Save and Quit", +] as const + +const objectives = [ + { label: "Defend the Marines until help arrives.", complete: true }, + { label: "Rendezvous with the Pelican.", complete: true }, + { label: "Find the Marines from second downed Pelican.", complete: true }, + { label: "Destroy the Covenant on the shoreline.", complete: false }, +] as const + +export function HaloPauseMenu() { + const [selected, setSelected] = useState(0) + + useEffect(() => { + const selectMenuItem = (event: KeyboardEvent) => { + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault() + setSelected(current => { + if (event.key === "ArrowUp") { + return (current - 1 + menuItems.length) % menuItems.length + } + + return (current + 1) % menuItems.length + }) + } + } + + window.addEventListener("keydown", selectMenuItem) + return () => window.removeEventListener("keydown", selectMenuItem) + }, []) + + return ( +
+
+ ) +} diff --git a/packages/webapp/src/components/testdesign/h2/halo-pause-menu.css b/packages/webapp/src/components/testdesign/h2/halo-pause-menu.css new file mode 100644 index 0000000..d31a345 --- /dev/null +++ b/packages/webapp/src/components/testdesign/h2/halo-pause-menu.css @@ -0,0 +1,350 @@ +.h2-screen { + --h2-ice: #afcbea; + --h2-blue: #4b82c1; + --h2-navy: #00132f; + position: relative; + display: grid; + min-width: 320px; + min-height: max(100vh, 100svh); + overflow: hidden; + place-items: center; + isolation: isolate; + color: var(--h2-ice); + background: + radial-gradient(ellipse at 51% 46%, rgb(11 42 80 / 64%) 0, rgb(2 24 55 / 44%) 27%, transparent 58%), + radial-gradient(ellipse at 16% 76%, rgb(10 51 91 / 44%), transparent 35%), + linear-gradient(112deg, #001630 0%, #001026 43%, #000a1d 100%); + font-family: "Arial Narrow", "Roboto Condensed", "Trebuchet MS", sans-serif; + font-synthesis: none; +} + +.h2-screen::before { + position: absolute; + z-index: 5; + border: 1px solid rgb(123 177 222 / 9%); + content: ""; + inset: 0; + pointer-events: none; +} + +.h2-screen::after { + position: absolute; + z-index: 4; + background: radial-gradient(ellipse at center, transparent 35%, rgb(0 4 16 / 31%) 100%); + content: ""; + inset: 0; + pointer-events: none; +} + +.h2-screen__nebula { + position: absolute; + width: min(1100px, 130vw); + height: min(680px, 90vw); + border-radius: 50%; + background: + radial-gradient(ellipse at 18% 52%, rgb(22 68 113 / 12%), transparent 24%), + radial-gradient(ellipse at 71% 38%, rgb(10 52 102 / 15%), transparent 27%); + filter: blur(18px); + transform: rotate(-7deg); +} + +.h2-screen__scanlines { + position: absolute; + z-index: 6; + background: repeating-linear-gradient( + to bottom, + transparent 0, + transparent 2px, + rgb(0 0 0 / 10%) 3px + ); + inset: 0; + mix-blend-mode: multiply; + opacity: 0.42; + pointer-events: none; +} + +.h2-pause { + position: relative; + display: grid; + width: min(84vw, 930px); + min-height: clamp(218px, 31.5vw, 348px); + grid-template-columns: 48% 52%; + grid-template-rows: 1fr auto; + padding: + clamp(18px, 3vw, 38px) + clamp(16px, 2.5vw, 32px) + clamp(14px, 2vw, 25px) + clamp(16px, 2.45vw, 31px); + filter: drop-shadow(0 0 9px rgb(38 102 171 / 32%)); + letter-spacing: 0.012em; + text-shadow: + 0 0 2px #8fb9e1, + 0 0 7px rgb(112 165 218 / 35%); + transform: perspective(1100px) rotateX(0.7deg); +} + +.h2-pause::before { + position: absolute; + z-index: -2; + background: + linear-gradient(90deg, rgb(7 35 70 / 75%), rgb(3 28 61 / 89%) 45%, rgb(3 26 57 / 70%)), + linear-gradient(180deg, rgb(27 75 123 / 28%), transparent); + clip-path: polygon(2.5% 0, 100% 0, 100% 91%, 97% 100%, 0 100%, 0 8%); + content: ""; + inset: 0; +} + +.h2-pause::after { + position: absolute; + z-index: -1; + background: + linear-gradient(90deg, rgb(91 152 215 / 34%) 0 2px, transparent 2px) left 25px / 2px calc(100% - 50px) no-repeat, + linear-gradient(90deg, transparent, rgb(74 132 194 / 15%), transparent); + clip-path: polygon(2.5% 0, 100% 0, 100% 91%, 97% 100%, 0 100%, 0 8%); + content: ""; + inset: 1px; +} + +.h2-pause__frame { + position: absolute; + z-index: 2; + inset: 0; + pointer-events: none; +} + +.h2-pause__frame::before, +.h2-pause__frame::after { + position: absolute; + width: 31px; + height: 31px; + content: ""; +} + +.h2-pause__frame::before { + top: -1px; + left: -1px; + border-top: 3px solid rgb(116 172 225 / 76%); + border-left: 3px solid rgb(116 172 225 / 76%); + clip-path: polygon(0 0, 100% 0, 100% 10%, 11% 10%, 11% 100%, 0 100%); + filter: drop-shadow(0 0 4px #4f99df); +} + +.h2-pause__frame::after { + right: -1px; + bottom: -1px; + border-right: 3px solid rgb(116 172 225 / 76%); + border-bottom: 3px solid rgb(116 172 225 / 76%); + clip-path: polygon(89% 0, 100% 0, 100% 100%, 0 100%, 0 90%, 89% 90%); + filter: drop-shadow(0 0 4px #4f99df); +} + +.h2-pause h1, +.h2-pause h2 { + margin: 0; + color: #c4d5e9; + font-family: inherit; + font-size: clamp(16px, 1.65vw, 24px); + font-weight: 700; + letter-spacing: 0.025em; + line-height: 1; + text-transform: uppercase; +} + +.h2-pause__menu { + min-width: 0; + padding-right: 21px; +} + +.h2-pause__menu nav { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: clamp(12px, 2vw, 26px); +} + +.h2-pause__menu button { + position: relative; + width: 100%; + min-height: clamp(23px, 2.75vw, 35px); + padding: 3px 13px 3px 35px; + overflow: hidden; + border: 0; + color: rgb(128 157 188 / 41%); + background: linear-gradient(90deg, rgb(19 55 93 / 66%), rgb(18 54 91 / 14%)); + font: inherit; + font-size: clamp(14px, 1.65vw, 23px); + font-stretch: condensed; + letter-spacing: 0.025em; + line-height: 1; + text-align: left; + text-shadow: 0 0 5px rgb(110 160 211 / 26%); + transition: color 100ms ease, background 100ms ease; +} + +.h2-pause__menu button > span { + position: absolute; + top: 3px; + bottom: 3px; + left: 4px; + width: 17px; + border-left: 4px solid rgb(119 166 210 / 22%); + border-radius: 12px 0 0 12px; +} + +.h2-pause__menu button > span::before, +.h2-pause__menu button > span::after { + position: absolute; + left: 0; + width: 11px; + height: 4px; + background: rgb(119 166 210 / 22%); + content: ""; +} + +.h2-pause__menu button > span::before { + top: 0; +} + +.h2-pause__menu button > span::after { + bottom: 0; +} + +.h2-pause__menu button.is-selected { + color: #b7cde4; + background: + linear-gradient(90deg, rgb(104 147 190 / 61%), rgb(44 88 133 / 50%) 76%, transparent); + text-shadow: 0 0 4px #7eadd9; +} + +.h2-pause__menu button.is-selected > span { + border-color: #c0d6ea; + filter: drop-shadow(0 0 3px #8dc5f2); +} + +.h2-pause__menu button.is-selected > span::before, +.h2-pause__menu button.is-selected > span::after { + background: #c0d6ea; +} + +.h2-pause__objectives { + min-width: 0; + padding-left: 4px; +} + +.h2-pause__objectives ul { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0; + margin: 18px 0 0; + list-style: none; +} + +.h2-pause__objectives li { + display: grid; + grid-template-columns: 23px 1fr; + gap: 12px; + align-items: start; + color: #b6cae1; + font-size: clamp(14px, 1.55vw, 22px); + font-weight: 600; + line-height: 1.18; +} + +.h2-pause__objectives li > span { + display: grid; + width: 19px; + height: 19px; + border: 3px solid #4275ae; + margin-top: 1px; + color: #c9a900; + font-size: 21px; + font-weight: 900; + line-height: 12px; + place-items: center; + text-shadow: 0 0 4px #e2bd18; + transform: skew(-5deg); +} + +.h2-pause__objectives li > span.is-complete { + border-color: #806d19; +} + +.h2-pause__objectives p { + max-width: 390px; + margin: 0; +} + +.h2-pause__controls { + display: flex; + grid-column: 2; + align-items: center; + justify-content: flex-end; + padding-top: 15px; + color: #b7cade; + font-size: clamp(12px, 1.35vw, 19px); + font-weight: 700; + text-transform: uppercase; +} + +.h2-control { + display: inline-grid; + width: 23px; + height: 23px; + margin-right: 5px; + border-radius: 50%; + color: #e9f1ef; + font-size: 14px; + line-height: 1; + place-items: center; + text-shadow: 0 1px 1px rgb(0 0 0 / 70%); + box-shadow: + inset 1px 1px 2px rgb(255 255 255 / 55%), + 0 0 5px currentColor; +} + +.h2-control--a { + background: #54a81e; + color: #d9f2c4; +} + +.h2-control--b { + margin-left: 14px; + background: #a83045; + color: #f4c3ca; +} + +@media (max-width: 500px) { + .h2-pause { + width: min(92vw, 500px); + min-height: 0; + grid-template-columns: 1fr; + padding: 30px 25px 22px; + } + + .h2-pause__menu { + padding-right: 0; + } + + .h2-pause__menu nav { + margin-top: 17px; + } + + .h2-pause__objectives { + padding: 27px 0 0; + } + + .h2-pause__objectives ul { + margin-top: 14px; + } + + .h2-pause__controls { + grid-column: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .h2-pause__menu button { + transition: none; + } +} diff --git a/packages/webapp/src/routeTree.gen.ts b/packages/webapp/src/routeTree.gen.ts index ebe8d06..aa07e66 100644 --- a/packages/webapp/src/routeTree.gen.ts +++ b/packages/webapp/src/routeTree.gen.ts @@ -9,9 +9,16 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as TestdesignRouteImport } from './routes/testdesign' import { Route as MainRouteImport } from './routes/_main' import { Route as MainIndexRouteImport } from './routes/_main.index' +import { Route as TestdesignH2RouteImport } from './routes/testdesign.h2' +const TestdesignRoute = TestdesignRouteImport.update({ + id: '/testdesign', + path: '/testdesign', + getParentRoute: () => rootRouteImport, +} as any) const MainRoute = MainRouteImport.update({ id: '/_main', getParentRoute: () => rootRouteImport, @@ -21,32 +28,51 @@ const MainIndexRoute = MainIndexRouteImport.update({ path: '/', getParentRoute: () => MainRoute, } as any) +const TestdesignH2Route = TestdesignH2RouteImport.update({ + id: '/h2', + path: '/h2', + getParentRoute: () => TestdesignRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof MainIndexRoute + '/testdesign': typeof TestdesignRouteWithChildren + '/testdesign/h2': typeof TestdesignH2Route } export interface FileRoutesByTo { + '/testdesign': typeof TestdesignRouteWithChildren + '/testdesign/h2': typeof TestdesignH2Route '/': typeof MainIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/_main': typeof MainRouteWithChildren + '/testdesign': typeof TestdesignRouteWithChildren + '/testdesign/h2': typeof TestdesignH2Route '/_main/': typeof MainIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: '/' | '/testdesign' | '/testdesign/h2' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/_main' | '/_main/' + to: '/testdesign' | '/testdesign/h2' | '/' + id: '__root__' | '/_main' | '/testdesign' | '/testdesign/h2' | '/_main/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { MainRoute: typeof MainRouteWithChildren + TestdesignRoute: typeof TestdesignRouteWithChildren } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/testdesign': { + id: '/testdesign' + path: '/testdesign' + fullPath: '/testdesign' + preLoaderRoute: typeof TestdesignRouteImport + parentRoute: typeof rootRouteImport + } '/_main': { id: '/_main' path: '' @@ -61,6 +87,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MainIndexRouteImport parentRoute: typeof MainRoute } + '/testdesign/h2': { + id: '/testdesign/h2' + path: '/h2' + fullPath: '/testdesign/h2' + preLoaderRoute: typeof TestdesignH2RouteImport + parentRoute: typeof TestdesignRoute + } } } @@ -74,8 +107,21 @@ const MainRouteChildren: MainRouteChildren = { const MainRouteWithChildren = MainRoute._addFileChildren(MainRouteChildren) +interface TestdesignRouteChildren { + TestdesignH2Route: typeof TestdesignH2Route +} + +const TestdesignRouteChildren: TestdesignRouteChildren = { + TestdesignH2Route: TestdesignH2Route, +} + +const TestdesignRouteWithChildren = TestdesignRoute._addFileChildren( + TestdesignRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { MainRoute: MainRouteWithChildren, + TestdesignRoute: TestdesignRouteWithChildren, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/packages/webapp/src/routes/__root.tsx b/packages/webapp/src/routes/__root.tsx index ad8d58a..543ac66 100644 --- a/packages/webapp/src/routes/__root.tsx +++ b/packages/webapp/src/routes/__root.tsx @@ -9,6 +9,6 @@ export const Route = createRootRoute({ function RootComponent() { return <> - {import.meta.env.DEV && } + } diff --git a/packages/webapp/src/routes/testdesign.h2.tsx b/packages/webapp/src/routes/testdesign.h2.tsx new file mode 100644 index 0000000..f84db76 --- /dev/null +++ b/packages/webapp/src/routes/testdesign.h2.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router" +import { HaloPauseMenu } from "@/components/testdesign/h2/HaloPauseMenu" +import "@/components/testdesign/h2/halo-pause-menu.css" + +export const Route = createFileRoute("/testdesign/h2")({ + component: HaloPauseMenu, +}) diff --git a/packages/webapp/src/routes/testdesign.tsx b/packages/webapp/src/routes/testdesign.tsx new file mode 100644 index 0000000..3f792ba --- /dev/null +++ b/packages/webapp/src/routes/testdesign.tsx @@ -0,0 +1,9 @@ +import { createFileRoute, Outlet } from "@tanstack/react-router" + +export const Route = createFileRoute("/testdesign")({ + component: TestDesignLayout, +}) + +function TestDesignLayout() { + return +}