Refresh launcher release presentation
This commit is contained in:
parent
b632e98ad1
commit
c063041efc
4 changed files with 267 additions and 285 deletions
|
|
@ -1,10 +1,14 @@
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { openWorldshaperStudioWindow } from "./worldshaperStudio/windowing";
|
||||||
import {
|
import {
|
||||||
buildWorldshaperStudioUrl,
|
CHANGELOG_SECTIONS,
|
||||||
openWorldshaperStudioWindow,
|
CHANGELOG_SPLASH_FOOTNOTE,
|
||||||
} from "./worldshaperStudio/windowing";
|
CHANGELOG_SPLASH_KICKER,
|
||||||
import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./worldshaperStudio/changelogData";
|
CHANGELOG_SPLASH_TITLE,
|
||||||
|
CHANGELOG_SPLASH_VERSION,
|
||||||
|
} from "./worldshaperStudio/changelogData";
|
||||||
|
import type { ChangelogItem } from "./worldshaperStudio/changelogData";
|
||||||
import launcherBackground from "../background.png";
|
import launcherBackground from "../background.png";
|
||||||
|
|
||||||
type WorldDefaultPayload = {
|
type WorldDefaultPayload = {
|
||||||
|
|
@ -14,16 +18,10 @@ type WorldDefaultPayload = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type LaunchState = "resolving" | "opening" | "opened" | "blocked" | "error";
|
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
|
||||||
|
|
||||||
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
|
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
|
||||||
|
|
||||||
let didAttemptInitialLaunch = false;
|
|
||||||
let rememberedLaunchState: LaunchState | "" = "";
|
|
||||||
let rememberedStatus = "";
|
|
||||||
let rememberedError = "";
|
|
||||||
let rememberedWorldId = "";
|
|
||||||
|
|
||||||
async function resolveDefaultWorldId(): Promise<string> {
|
async function resolveDefaultWorldId(): Promise<string> {
|
||||||
const response = await fetch("/api/world-default");
|
const response = await fetch("/api/world-default");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -39,225 +37,139 @@ function openStudioPopup(worldId: string): boolean {
|
||||||
return Boolean(popup);
|
return Boolean(popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openStudioInCurrentTab(worldId: string): void {
|
|
||||||
window.location.assign(buildWorldshaperStudioUrl(worldId, window, { worldId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRepo(): void {
|
function openRepo(): void {
|
||||||
window.location.assign("https://repo.andraxion.net/");
|
window.location.assign("https://repo.andraxion.net/");
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorldshaperLauncher() {
|
function WorldshaperLauncher() {
|
||||||
const [launchState, setLaunchState] = useState<LaunchState>("resolving");
|
const [launchState, setLaunchState] = useState<LaunchState>("ready");
|
||||||
const [status, setStatus] = useState("Preparing Worldshaper Studio...");
|
const [status, setStatus] = useState("Launch Worldshaper Studio in its floating window.");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [worldId, setWorldId] = useState("");
|
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
void resolveDefaultWorldId()
|
||||||
async function attemptInitialLaunch() {
|
.then((resolvedWorldId) => {
|
||||||
if (didAttemptInitialLaunch) {
|
|
||||||
setWorldId(rememberedWorldId);
|
|
||||||
setLaunchState(rememberedLaunchState || "blocked");
|
|
||||||
setStatus(rememberedStatus || "Open Worldshaper Studio.");
|
|
||||||
setError(rememberedError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
didAttemptInitialLaunch = true;
|
|
||||||
setLaunchState("resolving");
|
|
||||||
setError("");
|
|
||||||
setStatus("Preparing Worldshaper Studio...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resolvedWorldId = await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setWorldId(resolvedWorldId);
|
setWorldId(resolvedWorldId);
|
||||||
rememberedWorldId = resolvedWorldId;
|
})
|
||||||
setLaunchState("opening");
|
.catch(() => {
|
||||||
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
|
|
||||||
if (openStudioPopup(resolvedWorldId)) {
|
|
||||||
setLaunchState("opened");
|
|
||||||
setStatus("Worldshaper Studio opened in a separate window.");
|
|
||||||
rememberedLaunchState = "opened";
|
|
||||||
rememberedStatus = "Worldshaper Studio opened in a separate window.";
|
|
||||||
rememberedError = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLaunchState("blocked");
|
|
||||||
setStatus("Your browser blocked the studio window.");
|
|
||||||
rememberedLaunchState = "blocked";
|
|
||||||
rememberedStatus = "Your browser blocked the studio window.";
|
|
||||||
rememberedError = "";
|
|
||||||
} catch (nextError: unknown) {
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
|
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
||||||
setLaunchState("error");
|
});
|
||||||
setError(nextErrorText);
|
|
||||||
setStatus("Worldshaper Studio unavailable.");
|
|
||||||
rememberedLaunchState = "error";
|
|
||||||
rememberedStatus = "Worldshaper Studio unavailable.";
|
|
||||||
rememberedError = nextErrorText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void attemptInitialLaunch();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
async function handleLaunch(): Promise<void> {
|
||||||
if (launchState !== "blocked" || !worldId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryLaunch() {
|
|
||||||
if (!worldId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLaunchState("opening");
|
|
||||||
setStatus(`Opening Worldshaper Studio for ${worldId}...`);
|
|
||||||
if (openStudioPopup(worldId)) {
|
|
||||||
setLaunchState("opened");
|
|
||||||
setStatus("Worldshaper Studio opened in a separate window.");
|
|
||||||
rememberedLaunchState = "opened";
|
|
||||||
rememberedStatus = "Worldshaper Studio opened in a separate window.";
|
|
||||||
rememberedError = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLaunchState("blocked");
|
|
||||||
setStatus("Your browser blocked the studio window.");
|
|
||||||
rememberedLaunchState = "blocked";
|
|
||||||
rememberedStatus = "Your browser blocked the studio window.";
|
|
||||||
rememberedError = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerDown() {
|
|
||||||
retryLaunch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown() {
|
|
||||||
retryLaunch();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("pointerdown", handlePointerDown, { once: true });
|
|
||||||
window.addEventListener("keydown", handleKeyDown, { once: true });
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("pointerdown", handlePointerDown);
|
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [launchState, worldId]);
|
|
||||||
|
|
||||||
async function handleRetry(): Promise<void> {
|
|
||||||
setError("");
|
setError("");
|
||||||
setLaunchState("resolving");
|
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
|
||||||
setStatus("Preparing Worldshaper Studio...");
|
setLaunchState("opening");
|
||||||
|
setStatus(`Opening Worldshaper Studio for ${nextWorldId}...`);
|
||||||
try {
|
try {
|
||||||
const resolvedWorldId = worldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
|
||||||
setWorldId(resolvedWorldId);
|
setWorldId(resolvedWorldId);
|
||||||
rememberedWorldId = resolvedWorldId;
|
|
||||||
setLaunchState("opening");
|
|
||||||
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
|
setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`);
|
||||||
if (openStudioPopup(resolvedWorldId)) {
|
if (openStudioPopup(resolvedWorldId)) {
|
||||||
setLaunchState("opened");
|
setLaunchState("opened");
|
||||||
setStatus("Worldshaper Studio opened in a separate window.");
|
setStatus("Worldshaper Studio opened in a separate window.");
|
||||||
rememberedLaunchState = "opened";
|
|
||||||
rememberedStatus = "Worldshaper Studio opened in a separate window.";
|
|
||||||
rememberedError = "";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLaunchState("blocked");
|
setLaunchState("blocked");
|
||||||
setStatus("Your browser blocked the studio window.");
|
setStatus("Your browser blocked the studio window. Use the launch button again after allowing popups.");
|
||||||
rememberedLaunchState = "blocked";
|
|
||||||
rememberedStatus = "Your browser blocked the studio window.";
|
|
||||||
rememberedError = "";
|
|
||||||
} catch (nextError: unknown) {
|
} catch (nextError: unknown) {
|
||||||
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
|
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
|
||||||
setLaunchState("error");
|
setLaunchState("error");
|
||||||
setError(nextErrorText);
|
setError(nextErrorText);
|
||||||
setStatus("Worldshaper Studio unavailable.");
|
setStatus("Worldshaper Studio unavailable.");
|
||||||
rememberedLaunchState = "error";
|
|
||||||
rememberedStatus = "Worldshaper Studio unavailable.";
|
|
||||||
rememberedError = nextErrorText;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenHere(): void {
|
const isBusy = launchState === "opening";
|
||||||
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
|
|
||||||
openStudioInCurrentTab(nextWorldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBusy = launchState === "resolving" || launchState === "opening";
|
|
||||||
const showFallbackActions = launchState === "blocked" || launchState === "opened" || launchState === "error";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
className="launcher-shell"
|
className="launcher-shell"
|
||||||
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
|
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
|
||||||
>
|
>
|
||||||
<section className="launcher-card">
|
<div className="launcher-stack">
|
||||||
<p className="launcher-eyebrow">New RPG</p>
|
<section className="launcher-card">
|
||||||
<h1 className="launcher-title">Worldshaper Studio</h1>
|
<p className="launcher-eyebrow">New RPG</p>
|
||||||
<p className="launcher-status">{status}</p>
|
<h1 className="launcher-title">Worldshaper Studio</h1>
|
||||||
{launchState === "blocked" ? (
|
<p className="launcher-status">{status}</p>
|
||||||
<p className="launcher-hint">
|
{launchState === "blocked" ? (
|
||||||
Click anywhere or use the button below if your browser asks for permission to open the studio window.
|
<p className="launcher-hint">
|
||||||
</p>
|
Allow the popup, then use the studio button again to launch the floating editor window.
|
||||||
) : null}
|
</p>
|
||||||
{launchState === "opened" ? (
|
) : null}
|
||||||
<p className="launcher-hint">
|
{launchState === "opened" ? (
|
||||||
The editor is running in its own window. You can leave this tab open as a quick relaunch point, or close it.
|
<p className="launcher-hint">
|
||||||
</p>
|
The studio is open in its own slim window. This page stays behind as your release board and relaunch point.
|
||||||
) : null}
|
</p>
|
||||||
{error ? <p className="launcher-error">{error}</p> : null}
|
) : null}
|
||||||
{showFallbackActions ? (
|
{launchState === "ready" ? (
|
||||||
|
<p className="launcher-hint">
|
||||||
|
The editor is designed to live in its own floating window, so the launcher keeps the first step clean.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{error ? <p className="launcher-error">{error}</p> : null}
|
||||||
<div className="launcher-actions">
|
<div className="launcher-actions">
|
||||||
<button type="button" className="launcher-primary-btn" onClick={() => void handleRetry()} disabled={isBusy}>
|
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
|
||||||
Open Floating Studio
|
Open Floating Studio
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="launcher-secondary-btn" onClick={handleOpenHere} disabled={isBusy}>
|
|
||||||
Open In This Tab
|
|
||||||
</button>
|
|
||||||
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
|
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
|
||||||
Open Repo
|
Open Repo
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</section>
|
||||||
<section className="launcher-whats-new">
|
<section className="launcher-changelog-window" aria-labelledby="launcher-whats-new-title">
|
||||||
<div className="launcher-whats-new-head">
|
<div className="launcher-changelog-titlebar">
|
||||||
<h2 className="launcher-whats-new-title">What's New</h2>
|
<div className="launcher-changelog-title">What's New</div>
|
||||||
<p className="launcher-whats-new-version">{CHANGELOG_SPLASH_VERSION}</p>
|
<div className="launcher-changelog-hint">Current release highlights</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="launcher-whats-new-list">
|
<div className="launcher-changelog-body">
|
||||||
{CHANGELOG_SECTIONS.map((section) => (
|
<div className="changelog-splash-card">
|
||||||
<section key={section.title} className="launcher-whats-new-section">
|
<div className="changelog-splash-hero">
|
||||||
<h3 className="launcher-whats-new-section-title">{section.title}</h3>
|
<div className="changelog-splash-kicker">{CHANGELOG_SPLASH_KICKER}</div>
|
||||||
<ul className="launcher-whats-new-bullets">
|
<div className="changelog-splash-title" id="launcher-whats-new-title">{CHANGELOG_SPLASH_TITLE}</div>
|
||||||
{section.items.map((item, index) => {
|
<div className="changelog-splash-meta">Release {CHANGELOG_SPLASH_VERSION}</div>
|
||||||
const key = `${section.title}-${index}`;
|
</div>
|
||||||
if (typeof item === "string") {
|
<div className="changelog-splash-list">
|
||||||
return <li key={key}>{item}</li>;
|
{CHANGELOG_SECTIONS.map((section) => (
|
||||||
}
|
<section key={section.title} className="changelog-splash-section">
|
||||||
return (
|
<h3 className="changelog-splash-section-title">{section.title}</h3>
|
||||||
<li key={key}>
|
<ul className="changelog-splash-bullets">
|
||||||
<div>{item.text}</div>
|
{section.items.map((item, index) => {
|
||||||
{item.note ? <div className="launcher-whats-new-note">{item.note}</div> : null}
|
const key = `${section.title}-${index}`;
|
||||||
</li>
|
const normalizedItem: ChangelogItem = item;
|
||||||
);
|
if (typeof normalizedItem === "string") {
|
||||||
})}
|
return <li key={key}>{normalizedItem}</li>;
|
||||||
</ul>
|
}
|
||||||
</section>
|
return (
|
||||||
))}
|
<li key={key}>
|
||||||
|
<div>{normalizedItem.text}</div>
|
||||||
|
{normalizedItem.note ? <div className="changelog-splash-bullet-note">{normalizedItem.note}</div> : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="changelog-splash-footer">
|
||||||
|
<div className="changelog-splash-footnote">{CHANGELOG_SPLASH_FOOTNOTE}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
190
src/index.css
190
src/index.css
|
|
@ -32,8 +32,8 @@ body {
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
display: grid;
|
display: flex;
|
||||||
place-items: center;
|
justify-content: center;
|
||||||
padding: 48px 24px;
|
padding: 48px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +63,12 @@ body {
|
||||||
linear-gradient(180deg, rgba(8, 11, 17, 0.36) 0%, rgba(8, 11, 17, 0.52) 36%, rgba(7, 10, 15, 0.84) 72%, rgba(5, 7, 11, 0.96) 100%);
|
linear-gradient(180deg, rgba(8, 11, 17, 0.36) 0%, rgba(8, 11, 17, 0.52) 36%, rgba(7, 10, 15, 0.84) 72%, rgba(5, 7, 11, 0.96) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.launcher-stack {
|
||||||
|
width: min(760px, 100%);
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.launcher-card {
|
.launcher-card {
|
||||||
width: min(620px, 100%);
|
width: min(620px, 100%);
|
||||||
padding: 28px 30px;
|
padding: 28px 30px;
|
||||||
|
|
@ -119,66 +125,143 @@ body {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new {
|
.launcher-changelog-window {
|
||||||
margin-top: 24px;
|
width: min(760px, 100%);
|
||||||
padding-top: 18px;
|
border: 1px solid #4f79af;
|
||||||
border-top: 1px solid rgba(120, 170, 230, 0.16);
|
border-radius: 12px;
|
||||||
|
background: color-mix(in srgb, #0a1020 88%, transparent);
|
||||||
|
color: #d8e8ff;
|
||||||
|
box-shadow: 0 16px 34px rgba(3, 8, 18, 0.46);
|
||||||
|
overflow: hidden;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 34px minmax(0, 1fr);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-head {
|
.launcher-changelog-titlebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
align-items: baseline;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
padding: 0 10px;
|
||||||
gap: 10px;
|
background: linear-gradient(180deg, #1b365e 0%, #122743 100%);
|
||||||
margin-bottom: 14px;
|
border-bottom: 1px solid #365782;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-title {
|
.launcher-changelog-title {
|
||||||
margin: 0;
|
min-width: 0;
|
||||||
font-size: 1.1rem;
|
flex: 1 1 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #eef6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-version {
|
.launcher-changelog-hint {
|
||||||
margin: 0;
|
font-size: 11px;
|
||||||
color: #9fd8ff;
|
color: #a9c2ec;
|
||||||
font-size: 0.78rem;
|
white-space: nowrap;
|
||||||
letter-spacing: 0.08em;
|
}
|
||||||
|
|
||||||
|
.launcher-changelog-body {
|
||||||
|
min-height: 0;
|
||||||
|
max-height: min(52dvh, 720px);
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-card {
|
||||||
|
min-height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-hero {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #4f79af;
|
||||||
|
border-radius: 12px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(100, 170, 248, 0.24) 0%, transparent 48%),
|
||||||
|
linear-gradient(180deg, rgba(27, 54, 94, 0.64) 0%, rgba(17, 32, 63, 0.82) 100%);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(10, 16, 32, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-kicker {
|
||||||
|
color: #ffd166;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-list {
|
.changelog-splash-title {
|
||||||
display: grid;
|
color: #eef6ff;
|
||||||
gap: 14px;
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-section {
|
.changelog-splash-meta {
|
||||||
display: grid;
|
color: #a9c2ec;
|
||||||
gap: 8px;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-whats-new-section-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
color: #dff2ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-whats-new-bullets {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 18px;
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
color: #d5dfec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-whats-new-bullets li {
|
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-note {
|
.changelog-splash-list {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-section {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid #365782;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(17, 32, 63, 0.84);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(10, 16, 32, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-section-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #eef6ff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-bullets {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
color: #d7e7ff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-bullet-note {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: #9bacbe;
|
color: #a9c2ec;
|
||||||
font-size: 0.88rem;
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-footer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-splash-footnote {
|
||||||
|
color: #9fb8e5;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-primary-btn,
|
.launcher-primary-btn,
|
||||||
|
|
@ -1306,6 +1389,12 @@ button.danger:not(:disabled):hover {
|
||||||
filter: saturate(0.82) blur(1.5px);
|
filter: saturate(0.82) blur(1.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.launcher-stack,
|
||||||
|
.launcher-card,
|
||||||
|
.launcher-changelog-window {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.launcher-card {
|
.launcher-card {
|
||||||
padding: 22px 20px;
|
padding: 22px 20px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
@ -1315,16 +1404,15 @@ button.danger:not(:disabled):hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.launcher-whats-new-head {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-primary-btn,
|
.launcher-primary-btn,
|
||||||
.launcher-secondary-btn {
|
.launcher-secondary-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.launcher-changelog-body {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.page-shell {
|
.page-shell {
|
||||||
width: 96vw;
|
width: 96vw;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
|
|
|
||||||
|
|
@ -3,61 +3,37 @@ export type ChangelogItem = string | {
|
||||||
note?: string;
|
note?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG_SPLASH_VERSION = "2026-06-22-world-editor-release-v6";
|
export type ChangelogSection = {
|
||||||
|
|
||||||
export const CHANGELOG_SECTIONS = [
|
|
||||||
{
|
|
||||||
title: "World Rendering",
|
|
||||||
items: [
|
|
||||||
"Added live image opacity support in the renderer for both tiles and entity sprites.",
|
|
||||||
{
|
|
||||||
text: "Fixed world painting placement drift on very wide displays.",
|
|
||||||
note: "For the many of you out there using this on superultrawide monitors.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Graphics & Animation",
|
|
||||||
items: [
|
|
||||||
"Added animation frame timelines to the Graphic Painter.",
|
|
||||||
"Added frame duplication, enable/disable, delete, drag reorder, and default-frame selection.",
|
|
||||||
"Added animation speed and playback settings to graphics data.",
|
|
||||||
"Added animation preview support from the Graphic Painter.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "World Overview",
|
|
||||||
items: [
|
|
||||||
"Added chunk move, duplicate, rotate, flip, and delete workflows from the world overview.",
|
|
||||||
"Added safer chunk management feedback and confirmation flows.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Editor Stability",
|
|
||||||
items: [
|
|
||||||
"Fixed unified graphics conversion so saved image properties flow correctly into runtime tiles and sprites.",
|
|
||||||
"Improved live refresh behavior for renderer-facing graphic updates.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "TBI (To Be Implemented [Or thought about really hard])",
|
|
||||||
items: [
|
|
||||||
"Image animations on renderer with hotkey toggle and engine override toggle.",
|
|
||||||
"Add premade frame duplication effects, like duplicate and shift by direction.",
|
|
||||||
"Add animation effects that change opacity over time.",
|
|
||||||
"Add animation effects that shift saturation over time.",
|
|
||||||
"Work on a system to use an image with animation frames as a tile strip, placing the instance once but specifying which subimage to use.",
|
|
||||||
"Add an elevation system where some tiles can appear at different z-indexes and show a different subframe.",
|
|
||||||
"Allow unsnapped tile placement, possibly via hotkey, writing into a chunk patch instead of chunk rows.",
|
|
||||||
"Explore whether unsnapped placement should be true free placement or an additional sub-layer drawn over an existing layer.",
|
|
||||||
"Add custom prompts for text input and confirmation dialogs.",
|
|
||||||
"Prototype terrain painters for meta chunk painting and tile replacement, like rivers, mountains, and woods.",
|
|
||||||
"Talk to Justin more about zone and subzone music regions via chunk painting.",
|
|
||||||
"Autotilers.",
|
|
||||||
"Prefab stamps.",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as const satisfies ReadonlyArray<{
|
|
||||||
title: string;
|
title: string;
|
||||||
items: ReadonlyArray<ChangelogItem>;
|
items: ReadonlyArray<ChangelogItem>;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
export const CHANGELOG_SPLASH_VERSION = "2026-06-26-launcher-presentation-update";
|
||||||
|
export const CHANGELOG_SPLASH_KICKER = "Launch Experience Update";
|
||||||
|
export const CHANGELOG_SPLASH_TITLE = "What's New";
|
||||||
|
export const CHANGELOG_SPLASH_FOOTNOTE = "This release focuses on presentation, access, and a cleaner studio handoff.";
|
||||||
|
|
||||||
|
export const CHANGELOG_SECTIONS: ReadonlyArray<ChangelogSection> = [
|
||||||
|
{
|
||||||
|
title: "Studio Launch Experience",
|
||||||
|
items: [
|
||||||
|
"Worldshaper now opens from a dedicated launch page built to frame the studio instead of burying it behind a utility screen.",
|
||||||
|
"The editor now launches only in its slim floating window, keeping the first impression focused on the intended workspace.",
|
||||||
|
"The launch page now opens with an editor showcase backdrop that sets the tone before you step inside.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Project Access",
|
||||||
|
items: [
|
||||||
|
"Added a direct Repo destination from the launcher, making project browsing and source access part of the front door.",
|
||||||
|
"Release highlights now live on the main page, so returning creators can catch up before jumping back into the world.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Presentation & Structure",
|
||||||
|
items: [
|
||||||
|
"The launcher now gives the studio controls and release notes their own stage, mirroring the feel of the in-editor update window.",
|
||||||
|
"The entry flow has been tightened into a cleaner, more cinematic handoff from main page to creation space.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { clampFloatingWindowRect } from "./floatingWindowUtils";
|
import { clampFloatingWindowRect } from "./floatingWindowUtils";
|
||||||
import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./changelogData";
|
import {
|
||||||
|
CHANGELOG_SECTIONS,
|
||||||
|
CHANGELOG_SPLASH_FOOTNOTE,
|
||||||
|
CHANGELOG_SPLASH_KICKER,
|
||||||
|
CHANGELOG_SPLASH_TITLE,
|
||||||
|
CHANGELOG_SPLASH_VERSION,
|
||||||
|
} from "./changelogData";
|
||||||
import type { ChangelogItem } from "./changelogData";
|
import type { ChangelogItem } from "./changelogData";
|
||||||
|
|
||||||
const CHANGELOG_SPLASH_WINDOW_KEY = "changelogSplash";
|
const CHANGELOG_SPLASH_WINDOW_KEY = "changelogSplash";
|
||||||
|
|
@ -160,7 +166,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (state.titleEl) {
|
if (state.titleEl) {
|
||||||
state.titleEl.textContent = "What's New";
|
state.titleEl.textContent = CHANGELOG_SPLASH_TITLE;
|
||||||
}
|
}
|
||||||
if (state.metaEl) {
|
if (state.metaEl) {
|
||||||
state.metaEl.textContent = `Release ${CHANGELOG_SPLASH_VERSION}`;
|
state.metaEl.textContent = `Release ${CHANGELOG_SPLASH_VERSION}`;
|
||||||
|
|
@ -221,7 +227,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
|
||||||
heroEl.className = "changelog-splash-hero";
|
heroEl.className = "changelog-splash-hero";
|
||||||
const kickerEl = document.createElement("div");
|
const kickerEl = document.createElement("div");
|
||||||
kickerEl.className = "changelog-splash-kicker";
|
kickerEl.className = "changelog-splash-kicker";
|
||||||
kickerEl.textContent = "Mid-release update";
|
kickerEl.textContent = CHANGELOG_SPLASH_KICKER;
|
||||||
const titleEl = document.createElement("div");
|
const titleEl = document.createElement("div");
|
||||||
titleEl.className = "changelog-splash-title";
|
titleEl.className = "changelog-splash-title";
|
||||||
const metaEl = document.createElement("div");
|
const metaEl = document.createElement("div");
|
||||||
|
|
@ -235,7 +241,7 @@ export function createChangelogSplashWindowController(scope: ControllerScope) {
|
||||||
footerEl.className = "changelog-splash-footer";
|
footerEl.className = "changelog-splash-footer";
|
||||||
const footnoteEl = document.createElement("div");
|
const footnoteEl = document.createElement("div");
|
||||||
footnoteEl.className = "changelog-splash-footnote";
|
footnoteEl.className = "changelog-splash-footnote";
|
||||||
footnoteEl.textContent = "Major features and fixes only. Removed experiments are intentionally omitted.";
|
footnoteEl.textContent = CHANGELOG_SPLASH_FOOTNOTE;
|
||||||
const actionBtnEl = document.createElement("button");
|
const actionBtnEl = document.createElement("button");
|
||||||
actionBtnEl.type = "button";
|
actionBtnEl.type = "button";
|
||||||
actionBtnEl.className = "mini-btn";
|
actionBtnEl.className = "mini-btn";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue