Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b369cfae3 | ||
|
|
d11a9dc65d | ||
|
|
26f1e29c23 | ||
|
|
af55f7eb6f | ||
|
|
505c45ffdb | ||
|
|
df9654fed9 | ||
|
|
239ae4ada2 | ||
|
|
33d50851b0 |
No files matched your search
@@ -66,10 +66,12 @@ Then read values where they're used:
|
||||
`hero-galaxy.tsx`).
|
||||
|
||||
The panel picks up new groups automatically — no panel changes needed.
|
||||
Registered groups: Intro (page-intro choreography beats; `src/lib/intro.ts`),
|
||||
Registered groups: Stage (fluid content-stage width;
|
||||
`src/components/stage-width.tsx`), Intro (page-intro choreography beats;
|
||||
`src/lib/intro.ts`),
|
||||
Wordmark (logo-A optics, blade tuck, contrast scrim;
|
||||
`src/components/landing/hero-wordmark.tsx`), Galaxy layout, Galaxy motion,
|
||||
Galaxy pointer, Galaxy look (hero galaxy;
|
||||
`src/components/landing/hero-wordmark.tsx`), Galaxy layout, Galaxy
|
||||
motion, Galaxy pointer, Galaxy look (hero galaxy;
|
||||
`src/components/landing/hero-galaxy-tunables.ts`) and Ruler layout, Ruler
|
||||
motion, Ruler look (scroll ruler;
|
||||
`src/components/ruler/ruler-tunables.ts`).
|
||||
|
||||
+52
-40
@@ -15,7 +15,7 @@
|
||||
"gsap": "3.15.0",
|
||||
"lenis": "1.3.23",
|
||||
"lucide-react": "^1.31.0",
|
||||
"next": "16.2.6",
|
||||
"next": "^16.2.12",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
@@ -608,15 +608,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
|
||||
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
|
||||
"integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
||||
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
|
||||
"integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -630,9 +630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
||||
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
|
||||
"integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -646,12 +646,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
|
||||
"integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -662,12 +665,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
|
||||
"integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -678,12 +684,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
|
||||
"integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -694,12 +703,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
|
||||
"integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -710,9 +722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
|
||||
"integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -726,9 +738,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
|
||||
"integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2209,12 +2221,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
|
||||
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
|
||||
"version": "16.2.12",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
|
||||
"integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.2.6",
|
||||
"@next/env": "16.2.12",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -2228,14 +2240,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.2.6",
|
||||
"@next/swc-darwin-x64": "16.2.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.6",
|
||||
"@next/swc-linux-arm64-musl": "16.2.6",
|
||||
"@next/swc-linux-x64-gnu": "16.2.6",
|
||||
"@next/swc-linux-x64-musl": "16.2.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.6",
|
||||
"@next/swc-win32-x64-msvc": "16.2.6",
|
||||
"@next/swc-darwin-arm64": "16.2.12",
|
||||
"@next/swc-darwin-x64": "16.2.12",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.12",
|
||||
"@next/swc-linux-arm64-musl": "16.2.12",
|
||||
"@next/swc-linux-x64-gnu": "16.2.12",
|
||||
"@next/swc-linux-x64-musl": "16.2.12",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.12",
|
||||
"@next/swc-win32-x64-msvc": "16.2.12",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"gsap": "3.15.0",
|
||||
"lenis": "1.3.23",
|
||||
"lucide-react": "^1.31.0",
|
||||
"next": "16.2.6",
|
||||
"next": "^16.2.12",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
|
||||
@@ -104,6 +104,11 @@
|
||||
|
||||
html {
|
||||
background: var(--bg);
|
||||
/* Content stage width: 1280 on laptops, growing with the viewport toward
|
||||
1600 on wide monitors — sections genuinely widen instead of donating
|
||||
everything past 1280px to margin. Live-tunable (Stage group) via the
|
||||
inline override written by stage-width.tsx. */
|
||||
--stage-max: clamp(1280px, 72vw, 1600px);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Geist_Mono,
|
||||
} from "next/font/google";
|
||||
import SiteNav from "@/components/site-nav";
|
||||
import StageWidth from "@/components/stage-width";
|
||||
import SiteFooter from "@/components/site-footer";
|
||||
import IntroConductor from "@/components/intro-conductor";
|
||||
import MotionProvider from "@/components/motion-provider";
|
||||
@@ -101,11 +102,13 @@ const JSON_LD = {
|
||||
// full word flashes for the frames between paint and hydration, then
|
||||
// snaps into the logo-only formation start.
|
||||
//
|
||||
// Also reloads on back/forward-cache restores: Firefox freezes the JS heap
|
||||
// and discards the WebGL context, so a restored page shows a dead galaxy
|
||||
// canvas and stalled instruments. The listener is attached here (pre-paint)
|
||||
// so it lives inside the frozen page and fires on the restore; the intro is
|
||||
// seen-flagged by then, so the reload comes back settled.
|
||||
// Also reloads on back/forward-cache restores (pageshow with persisted):
|
||||
// a bfcache-restored page thaws with its WebGL context discarded in some
|
||||
// browsers, leaving a dead galaxy canvas — a reload swaps it for a clean
|
||||
// load, and the intro is seen-flagged by then so it comes back settled.
|
||||
// (The Firefox back-navigation hydration failure was a different bug:
|
||||
// Next's router crashed on back_forward loads — "Router action dispatched
|
||||
// before initialization" — fixed by the Next 16.2.12 patch bump.)
|
||||
const THEME_SCRIPT = `(function(){try{var t=localStorage.getItem("artcraft-theme");if(t==="light"||t==="dark"){document.documentElement.setAttribute("data-theme",t);}}catch(e){}try{if(!matchMedia("(prefers-reduced-motion: reduce)").matches){document.documentElement.setAttribute("data-intro","");}}catch(e){}addEventListener("pageshow",function(e){if(e.persisted)location.reload();});})();`;
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -135,6 +138,7 @@ export default function RootLayout({
|
||||
<ScrollRuler />
|
||||
</MotionProvider>
|
||||
<TunerPanel />
|
||||
<StageWidth />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -47,9 +47,6 @@ export const galaxyLayoutTuner = defineTunables(
|
||||
min: 8,
|
||||
max: 96,
|
||||
step: 1,
|
||||
// 3× the 17-clip showcase pool: the round-robin deal lands every clip
|
||||
// on exactly three cards, and the neighbor-gap sizing grows the cards
|
||||
// to keep the same fill.
|
||||
default: 51,
|
||||
info: "Cards riding the arms at the reference viewport size; smaller viewports scale this down proportionally (see Tuned @ Mpx).",
|
||||
},
|
||||
@@ -362,9 +359,9 @@ export const galaxyPointerTuner = defineTunables(
|
||||
boostScale: {
|
||||
label: "Boost scale ×",
|
||||
min: 1.2,
|
||||
max: 3,
|
||||
max: 4,
|
||||
step: 0.1,
|
||||
default: 2,
|
||||
default: 3,
|
||||
info: "Scale multiplier a click toggles onto the held card. Allowed to overlap neighbors; scales back down on leaving the card, and the next hold needs a fresh click.",
|
||||
},
|
||||
boostTau: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Component, useEffect, useMemo, useRef, useState, type ReactNode } from
|
||||
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||
import * as THREE from "three";
|
||||
import { SEEDANCE_SHOWCASE } from "@/lib/landing-data";
|
||||
import { webglAvailable } from "@/lib/webgl-support";
|
||||
import { introClock, introTuner } from "@/lib/intro";
|
||||
import { watchThemeColors, type ThemeColors } from "@/lib/theme-colors";
|
||||
import { useTunerStore } from "@/lib/tuner";
|
||||
@@ -220,9 +221,12 @@ export default function HeroGalaxy() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Gate: motion allowed and the tab actually foregrounded (a canvas born in
|
||||
// a hidden tab can come up blank).
|
||||
// Gate: WebGL actually available (a failed context creation inside r3f is
|
||||
// an unhandled rejection CanvasBoundary can't catch), motion allowed, and
|
||||
// the tab actually foregrounded (a canvas born in a hidden tab can come up
|
||||
// blank).
|
||||
useEffect(() => {
|
||||
if (!webglAvailable()) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
@@ -1265,6 +1269,7 @@ function GalaxyScene({
|
||||
CULL_TICK_S,
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Component, useEffect, useRef, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import LazyVideo from "@/components/lazy-video";
|
||||
import { watchThemeColors, type ThemeColors } from "@/lib/theme-colors";
|
||||
import { webglAvailable } from "@/lib/webgl-support";
|
||||
|
||||
// Plain code-split dynamic import — NOT `ssr: false`. It never renders during
|
||||
// SSR (gated by `ready`, false on the server), so WebGL stays off the server
|
||||
@@ -40,9 +41,12 @@ export default function HeroViewport({
|
||||
const insideRef = useRef(false);
|
||||
const targetPctRef = useRef(IDLE_CENTER);
|
||||
|
||||
// Gate: capable device, motion allowed, tab actually foregrounded.
|
||||
// Gate: WebGL available (r3f's context-creation failure is an unhandled
|
||||
// rejection CanvasBoundary can't catch — the footage below is the
|
||||
// fallback), capable device, motion allowed, tab actually foregrounded.
|
||||
useEffect(() => {
|
||||
const capable =
|
||||
webglAvailable() &&
|
||||
window.matchMedia("(pointer: fine) and (min-width: 768px)").matches &&
|
||||
window.matchMedia("(prefers-reduced-motion: no-preference)").matches;
|
||||
if (!capable) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppleIcon, MonitorIcon } from "lucide-react";
|
||||
import { SOCIAL_LINKS, WEBAPP_URL } from "@/lib/links";
|
||||
import { HERO_VIDEO_URL } from "@/lib/landing-data";
|
||||
import { HERO_VIDEO_URL, RELEASE_SLATE } from "@/lib/landing-data";
|
||||
import { GitHubIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/ui";
|
||||
import HeroMasthead, { HeroScrim } from "./hero-wordmark";
|
||||
@@ -23,7 +23,7 @@ export default function Hero() {
|
||||
outrank the ruler's fixed z-39 contrast pools at root level.
|
||||
No border-x here: the poster viewport stays immersive edge to
|
||||
edge — the structural rails begin at the announce strip. */}
|
||||
<div className="pointer-events-none relative z-40 mx-auto flex w-full max-w-[1280px] flex-1 flex-col items-center justify-center px-6 py-10 md:px-10">
|
||||
<div className="pointer-events-none relative z-40 mx-auto flex w-full max-w-[var(--stage-max)] flex-1 flex-col items-center justify-center px-6 py-10 md:px-10">
|
||||
<div
|
||||
data-reveal-group
|
||||
className="relative flex w-full max-w-2xl flex-col items-center text-center"
|
||||
@@ -87,15 +87,31 @@ export default function Hero() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-[1280px] border-x border-line">
|
||||
{/* Announce strip — the fold line under the poster. */}
|
||||
<div className="relative flex items-center justify-between gap-4 border-y border-line px-6 py-3 md:px-10">
|
||||
<div className="relative mx-auto max-w-[var(--stage-max)] border-x border-line">
|
||||
{/* Release slate — the fold line under the poster is the "what's
|
||||
new" band: mono release stamp + model chips deep-linking into
|
||||
the webapp surface where each model actually runs. */}
|
||||
<div className="relative flex flex-wrap items-center justify-between gap-x-8 gap-y-2 border-y border-line px-6 py-2.5 md:px-10">
|
||||
<span aria-hidden className="tick -top-[6px] -left-[6px]" />
|
||||
<span aria-hidden className="tick -top-[6px] -right-[5px]" />
|
||||
<p className="hud-label text-muted">Open-source AI studio</p>
|
||||
<p className="hud-label text-accent-ink">
|
||||
Now with Seedance 2.5, Nano Banana 2 & more
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<p className="hud-label text-accent-ink">
|
||||
New · {RELEASE_SLATE.stamp}
|
||||
</p>
|
||||
{RELEASE_SLATE.models.map((m) => (
|
||||
<a
|
||||
key={m.name}
|
||||
href={m.href}
|
||||
className="invert-block hud-label border border-line-strong px-2.5 py-1 text-muted"
|
||||
>
|
||||
{m.name}
|
||||
</a>
|
||||
))}
|
||||
<a href={WEBAPP_URL} className="hud-label text-faint hover:text-ink">
|
||||
+ more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Viewport frame — the product-demo stage: blocking vs. AI render. */}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function SectionShell({
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className={`border-t border-line ${className}`}>
|
||||
<div className="relative mx-auto max-w-[1280px] border-x border-line">
|
||||
<div className="relative mx-auto max-w-[var(--stage-max)] border-x border-line">
|
||||
{ticks && (
|
||||
<>
|
||||
<span aria-hidden className="tick -top-[6px] -left-[6px]" />
|
||||
|
||||
@@ -38,7 +38,7 @@ const LINK_COLUMNS: {
|
||||
export default function SiteFooter() {
|
||||
return (
|
||||
<footer className="border-t border-line">
|
||||
<div className="mx-auto max-w-[1280px] border-x border-line">
|
||||
<div className="mx-auto max-w-[var(--stage-max)] border-x border-line">
|
||||
<div className="grid gap-px bg-line md:grid-cols-[2fr_1fr_1fr_1fr]">
|
||||
<div className="bg-bg p-6 md:p-10">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { defineTunables, useTunerStore } from "@/lib/tuner";
|
||||
|
||||
// Live control over the content stage's fluid width (see --stage-max in
|
||||
// globals.css). Writes the root variable only when the knobs change — CSS
|
||||
// does the actual responsive work.
|
||||
const stageTuner = defineTunables("stage", "Stage", {
|
||||
stageMax: {
|
||||
label: "Stage max px",
|
||||
min: 1280,
|
||||
max: 1920,
|
||||
step: 10,
|
||||
default: 1600,
|
||||
info: "Ceiling of the content stage's fluid width — reached on wide monitors.",
|
||||
},
|
||||
stageVw: {
|
||||
label: "Stage vw %",
|
||||
min: 50,
|
||||
max: 100,
|
||||
step: 1,
|
||||
default: 72,
|
||||
info: "How much of the viewport the stage tracks between the 1280px floor and the ceiling.",
|
||||
},
|
||||
});
|
||||
|
||||
export default function StageWidth() {
|
||||
useEffect(() => {
|
||||
const apply = () => {
|
||||
const t = stageTuner.read();
|
||||
document.documentElement.style.setProperty(
|
||||
"--stage-max",
|
||||
`clamp(1280px, ${t.stageVw}vw, ${t.stageMax}px)`,
|
||||
);
|
||||
};
|
||||
apply();
|
||||
return useTunerStore.subscribe(apply);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
import { cdnMediaUrl, mediaUrl } from "./links";
|
||||
import { cdnMediaUrl, mediaUrl, webappUrl } from "./links";
|
||||
|
||||
// The release slate under the hero: the "what's new" band. `stamp` is the
|
||||
// release period shown in mono; each model chip deep-links into the webapp
|
||||
// surface where that model actually runs.
|
||||
export const RELEASE_SLATE = {
|
||||
stamp: "2026.09",
|
||||
models: [
|
||||
{ name: "Seedance 2.5", href: webappUrl("/create-video") },
|
||||
{ name: "Nano Banana 2", href: webappUrl("/create-image") },
|
||||
],
|
||||
} as const;
|
||||
|
||||
// Product feature roster, ported from the shipping landing page. Copy is the
|
||||
// approved marketing copy — edit deliberately, not casually.
|
||||
@@ -77,12 +88,12 @@ export const HERO_VIDEO_URL =
|
||||
// wall adapts to any count and mix of aspects, so the count below is the
|
||||
// only thing to change when clips are added or dropped.
|
||||
//
|
||||
// Sources live on the FakeYou CDN as /videos/001.mp4 .. /videos/017.mp4 and
|
||||
// Sources live on the FakeYou CDN as /videos/001.mp4 .. /videos/035.mp4 and
|
||||
// reach the page through the same-origin /cdn-media proxy, since WebGL video
|
||||
// textures require CORS-clean sources. When more clips are uploaded, raising
|
||||
// this count is the only change needed — every card/clip ratio works (cards
|
||||
// share clip textures; repeats are spread by least-recently-shown rebirth).
|
||||
const SHOWCASE_CLIP_COUNT = 17;
|
||||
const SHOWCASE_CLIP_COUNT = 35;
|
||||
|
||||
export type SeedanceClip = {
|
||||
src: string;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Pre-flight WebGL probe, shared by every r3f canvas gate.
|
||||
//
|
||||
// When context creation fails (exhausted drivers, blocklisted GPU,
|
||||
// software-rendering-only environments), THREE.WebGLRenderer throws inside
|
||||
// r3f's async init — an unhandled REJECTION that render-phase error
|
||||
// boundaries (CanvasBoundary) never see. Probing before mounting the
|
||||
// <Canvas> keeps the failure on our terms: the canvas simply never mounts
|
||||
// and the page's static fallbacks stand. Result is cached — a probe is
|
||||
// cheap but not free, and the answer doesn't change within a page's life.
|
||||
let cached: boolean | null = null;
|
||||
|
||||
export function webglAvailable(): boolean {
|
||||
if (cached !== null) return cached;
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl =
|
||||
canvas.getContext("webgl2") ??
|
||||
canvas.getContext("webgl") ??
|
||||
canvas.getContext("experimental-webgl");
|
||||
cached = !!gl;
|
||||
// Release the probe context immediately rather than waiting for GC —
|
||||
// browsers cap live contexts, and the real canvases want the slots.
|
||||
if (gl && "getExtension" in (gl as WebGLRenderingContext)) {
|
||||
(gl as WebGLRenderingContext)
|
||||
.getExtension("WEBGL_lose_context")
|
||||
?.loseContext();
|
||||
}
|
||||
} catch {
|
||||
cached = false;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
Reference in new issue
Block a user