From c842a7dbf3aaf684a35d6ab29b0b96de49f7de36 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 21 Aug 2026 10:18:53 -0400 Subject: [PATCH] feat(forces): the camo strip works signed out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was a prompt with nothing behind it. None and a roll need no account — the roll is made in the browser and kept nowhere — so the strip offers both and says that a saved camo is what signing in adds. The force pane says the same about saving and exporting. --- web/scripts/hangar.test.mjs | 28 ++++++++++++++++++++++++++++ web/src/screens/camo-picker.ts | 13 ++++++++++++- web/src/screens/hangar.ts | 15 +++++++-------- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/web/scripts/hangar.test.mjs b/web/scripts/hangar.test.mjs index 10a2de5..15cca4c 100644 --- a/web/scripts/hangar.test.mjs +++ b/web/scripts/hangar.test.mjs @@ -231,3 +231,31 @@ test("Hangar is a destination rather than a promise", async () => { /\{ id: "hangar", href: "\/#hangar", label: "Hangar" \}/, ); }); + +test("the camo strip works signed out, without offering saved camo", async () => { + // Signed out you still get None and a roll — a camo made in the browser and + // kept nowhere. What an account adds is the list of ones you saved, and the + // strip says so rather than standing empty behind a prompt. + const picker = await readFile(`${web}src/screens/camo-picker.ts`, "utf8"); + assert.match( + picker, + /export function camoPicker\(session: Session \| null\)/, + "the picker takes a session again, so a signed-out screen has no strip", + ); + assert.match( + picker, + /if \(!session\) \{[\s\S]{0,200}Sign in to fight in a camo you saved/, + ); + const signedOut = picker.indexOf("if (!session)"); + const list = picker.indexOf("repo.list(session.did)"); + assert.ok( + signedOut < list, + "the list is read before the signed-out case is handled", + ); +}); + +test("Hangar renders a camo strip whether or not anyone is signed in", async () => { + const hangar = await readFile(`${web}src/screens/hangar.ts`, "utf8"); + assert.match(hangar, /camoPicker\(null\)/, "the signed-out strip is gone"); + assert.match(hangar, /camoPicker\(session\)/, "the signed-in strip is gone"); +}); diff --git a/web/src/screens/camo-picker.ts b/web/src/screens/camo-picker.ts index b3615e2..25d3716 100644 --- a/web/src/screens/camo-picker.ts +++ b/web/src/screens/camo-picker.ts @@ -85,7 +85,7 @@ export type Picker = { * likely just finished. Either way the choice is one click to change and zero * to accept. */ -export function camoPicker(session: Session): Picker { +export function camoPicker(session: Session | null): Picker { const picker: Picker = { rkey: undefined, rolled: null, @@ -280,6 +280,17 @@ export function camoPicker(session: Session): Picker { strip.append(link); } + // Signed out, the strip is what can be had without an account: None, and a + // roll that is made here and kept nowhere. Saved camo is a read of somebody + // else's repository and there is nobody to read — so the strip says that + // rather than sitting empty, and Random is a real choice rather than a + // consolation. + if (!session) { + note.textContent = "Sign in to fight in a camo you saved."; + preselect(plain, null); + return picker; + } + // Lazily, and never fatally: this is the only thing on the screen that // needs another host, and a match must still be startable when it is down. void import("../camo/repo") diff --git a/web/src/screens/hangar.ts b/web/src/screens/hangar.ts index 0ef4e06..f7af688 100644 --- a/web/src/screens/hangar.ts +++ b/web/src/screens/hangar.ts @@ -430,12 +430,12 @@ export function hangarScreen(): Node[] { // Filled in when a session turns up, which is the one thing on this screen // that needs one. A signed-out reader still gets everything else, so the // read is a side errand rather than a gate on the route. + // + // Signed out it still offers None and a roll — a camo made in the browser + // and kept nowhere, which needs no account — and says what an account would + // add. A dead prompt where a control belongs is worse than either. const camo = el("section", { className: "card hangar-camo" }, [ - el("h2", { textContent: "Camo" }), - el("p", { - className: "hint", - textContent: "Sign in to fight in one of your own camo schemes.", - }), + camoPicker(null).node, ]); const screen = el("div", { className: "hangar" }, [ @@ -469,7 +469,7 @@ export function hangarScreen(): Node[] { el("p", { className: "hint hangar-unsaved", textContent: - "Nothing here is saved yet — the force lasts as long as this page does.", + "Nothing here is saved yet — the force lasts as long as this page does. Saving one, and exporting it as a .mul, will need an account.", }), ]), camo, @@ -482,8 +482,7 @@ export function hangarScreen(): Node[] { void currentSession() .then((session) => { if (!session) return; - const picker = camoPicker(session); - camo.replaceChildren(el("h2", { textContent: "Camo" }), picker.node); + camo.replaceChildren(camoPicker(session).node); }) .catch((error: unknown) => { // The API being down costs the camo strip and nothing else: the library -- 2.51.2