Refresh launcher release presentation

This commit is contained in:
Andraxion 2026-06-26 21:36:41 -04:00
parent b632e98ad1
commit c063041efc
4 changed files with 267 additions and 285 deletions

View file

@ -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&apos;s New</h2> <div className="launcher-changelog-title">What&apos;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>
); );
} }

View file

@ -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;

View file

@ -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.",
],
},
];

View file

@ -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";