feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
81
src/components/Screenshot.astro
Normal file
81
src/components/Screenshot.astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { screenById, WEB, PHONE } from '../data/screens.mjs';
|
||||
|
||||
/**
|
||||
* One screenshot, as a figure with its caption. PLAN.md §13 phase 9, D4 / D44.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY THE PAGE PASSES AN ID AND NOTHING ELSE
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* A marketing page and a documentation page show the same administration screen for
|
||||
* different reasons, and the thing they must not do is describe it differently. The alt
|
||||
* text and the caption therefore live with the capture in `screens.mjs`, next to the route
|
||||
* they came from, and a page asks for `admin-shard` rather than restating what is in it.
|
||||
*
|
||||
* It also means a re-capture cannot silently invalidate a caption: the sentence and the
|
||||
* frame it describes are edited in the same file.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY IT FAILS THE BUILD ON AN UNKNOWN ID
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* The alternative is a page that renders a broken image, which looks like a deployment
|
||||
* problem rather than a typo and survives review. `checkScreens.mjs` covers the other
|
||||
* direction — a declared screen whose file is missing — so between them a screenshot is
|
||||
* either complete or the build stops.
|
||||
*/
|
||||
interface Props {
|
||||
/** An `id` from `src/data/screens.mjs`. */
|
||||
id: string;
|
||||
/** Suppress the caption where the surrounding prose already says it. */
|
||||
bare?: boolean;
|
||||
}
|
||||
|
||||
const { id, bare = false } = Astro.props;
|
||||
|
||||
const shot = screenById(id);
|
||||
|
||||
if (!shot) {
|
||||
throw new Error(`Screenshot "${id}" is not declared in src/data/screens.mjs`);
|
||||
}
|
||||
|
||||
const src = `/screens/${shot.id}.webp`;
|
||||
|
||||
// Intrinsic size comes from the family rather than the entry: every capture in a family is
|
||||
// taken at one geometry (see screens.mjs), and `checkScreens.mjs` asserts the files really
|
||||
// are that size, so these attributes cannot drift from the pixels.
|
||||
const { width, height } = shot.family === 'web' ? WEB : PHONE;
|
||||
---
|
||||
|
||||
<figure class="shot">
|
||||
<img
|
||||
src={src}
|
||||
alt={shot.alt}
|
||||
width={width}
|
||||
height={height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{!bare && <figcaption>{shot.caption}</figcaption>}
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.shot {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.shot img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.shot figcaption {
|
||||
margin: 0.85rem 0 0;
|
||||
color: var(--dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,36 @@
|
||||
---
|
||||
import { screensOf, PHONE } from '../../data/screens.mjs';
|
||||
|
||||
/**
|
||||
* The app's screenshot strip — defined now, empty until phase 9 (D26).
|
||||
* The app's screenshot strip. Reserved in phase 5 (D26), filled in phase 9.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHY A COMPONENT THAT RENDERS NOTHING IS WORTH COMMITTING
|
||||
* WHY THIS COMPONENT EXISTED FOR A PHASE WITH NOTHING IN IT
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* PLAN.md §10 says `/app/` shows "the 14 existing screenshots". They exist —
|
||||
* `docs/android/screenshots/` on the `docs` repository — and they are the wrong fourteen:
|
||||
* a trusted-device and recovery-code smoke test from 2026-07-22, captured against a
|
||||
* PLAN.md §10 said `/app/` shows "the 14 existing screenshots". They exist —
|
||||
* `docs/android/screenshots/` on the `docs` repository — and they are the wrong fourteen: a
|
||||
* trusted-device and recovery-code smoke test from 2026-07-22, captured against a
|
||||
* development instance with no seeded content, before the theming work that changed how
|
||||
* every screen looks. Five of them are two-factor prompts. The home shot is an empty page.
|
||||
*
|
||||
* Shipping them would break two things at once: D4, which says real screenshots from the
|
||||
* review stack rather than placeholders, and §1, because they would show an app that no
|
||||
* longer looks like that. D26 records the decision — the slot is reserved, phase 9 fills
|
||||
* it, and phase 9 is already the phase that stands up the review stack and seeds the
|
||||
* content the web screenshots need. Adding an emulator pass to a rig that is being built
|
||||
* anyway is most of the work already done, and it has the property that the phone shots
|
||||
* and the browser shots then show the same deployment on the same day.
|
||||
*
|
||||
* The component exists rather than the page carrying a `TODO` because a defined shape is
|
||||
* what makes phase 9 a data change instead of a design task: fill `shots`, and the section
|
||||
* appears with a heading, a caption line and a grid. Nothing else has to be decided then.
|
||||
* Shipping them would have broken D4 and §1 at once, so D26 reserved the slot for the phase
|
||||
* that stands up the review stack anyway. The shape was defined then and the data arrived
|
||||
* now, which is exactly what it was for: filling it was a data change.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHAT PHASE 9 SHOULD PUT HERE
|
||||
* WHY IT READS screens.mjs RATHER THAN HOLDING ITS OWN LIST
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* Portrait captures at the device's own pixel size, from an API 36 emulator pointed at the
|
||||
* seeded review stack, one per idea rather than one per screen: the shard hub with live
|
||||
* data, the marketplace, a character sheet, the news list, the notification settings, and
|
||||
* the drawer showing a deployment's own navigation. Six is plenty. Fourteen was never a
|
||||
* target — it was the number that happened to exist.
|
||||
* The draft carried its own `shots` array, written before there was anywhere else to put
|
||||
* one. There is now: `src/data/screens.mjs` holds every capture the site ships, web and
|
||||
* phone alike, and `scripts/checkScreens.mjs` proves each one exists at the size the markup
|
||||
* claims. A second list here would be the one nothing checks.
|
||||
*
|
||||
* They belong in `public/`, not `brand-default/`: these are editorial content shipped with
|
||||
* the image, not branding an operator overrides (§7).
|
||||
* The phone captures come from an emulator pointed at the same seeded deployment the web
|
||||
* screenshots were taken from, on the same day — which is the property D26 was really
|
||||
* after, since the app takes its colours, type and navigation from the site it connects to.
|
||||
*/
|
||||
|
||||
/**
|
||||
* One capture. `width` and `height` are the real pixel dimensions and are required rather
|
||||
* than optional: without them the page reflows as each image decodes, and a strip of six
|
||||
* phone screenshots is the worst possible place for that.
|
||||
*
|
||||
* Frontmatter is TypeScript, so this is an interface rather than the JSDoc typedef the
|
||||
* `.mjs` data files use — and it has to be typed explicitly, because an empty array
|
||||
* annotated by inference is `any[]` and `astro check` is right to refuse it.
|
||||
*/
|
||||
interface Shot {
|
||||
/** Site-absolute path under `/screens/`. */
|
||||
src: string;
|
||||
/** What the screen shows, for somebody who cannot see it. */
|
||||
alt: string;
|
||||
caption: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const shots: Shot[] = [];
|
||||
const shots = screensOf('phone');
|
||||
---
|
||||
|
||||
{
|
||||
@@ -63,8 +38,8 @@ const shots: Shot[] = [];
|
||||
<section class="page section shots">
|
||||
<h2>What it looks like</h2>
|
||||
<p class="prose shots__lede">
|
||||
Captured against a real deployment with real content, not mocked up. The app takes
|
||||
its colours, type and navigation from the site it is connected to, so these show one
|
||||
Captured against a real deployment with real content, not mocked up. The app takes its
|
||||
colours, type and navigation from the site it is connected to, so these show one
|
||||
community's app rather than a neutral one.
|
||||
</p>
|
||||
|
||||
@@ -72,10 +47,10 @@ const shots: Shot[] = [];
|
||||
{shots.map((shot) => (
|
||||
<li class="shots__item">
|
||||
<img
|
||||
src={shot.src}
|
||||
src={`/screens/${shot.id}.webp`}
|
||||
alt={shot.alt}
|
||||
width={shot.width}
|
||||
height={shot.height}
|
||||
width={PHONE.width}
|
||||
height={PHONE.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
|
||||
44
src/components/home/WhatItLooksLike.astro
Normal file
44
src/components/home/WhatItLooksLike.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import Screenshot from '../Screenshot.astro';
|
||||
|
||||
/**
|
||||
* The homepage's one screenshot. PLAN.md §13 phase 9, D4 / D44.
|
||||
*
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* WHY ONE, AND WHY THIS ONE
|
||||
* -----------------------------------------------------------------------------------------
|
||||
* `DataPath` above it draws the claim — a private game server, a sidecar, a public site —
|
||||
* and a diagram of a data path is a promise that the data arrives. This is the page where
|
||||
* it arrives, captured from a deployment wired to a running shard, so the section directly
|
||||
* under the diagram is the diagram's evidence.
|
||||
*
|
||||
* A gallery here would compete with `Capabilities` further down, which is the part of the
|
||||
* homepage that enumerates. So: one figure, the signature screen, and the rest of the set
|
||||
* on `/features/` where each one sits beside the claim it supports.
|
||||
*/
|
||||
---
|
||||
|
||||
<section class="page section looks">
|
||||
<p class="eyebrow">What it looks like</p>
|
||||
<h2>The other end of that diagram</h2>
|
||||
<p class="prose looks__lede">
|
||||
A demo deployment with a real shard behind it. The gold supply, the uptime and the online
|
||||
count are read from the game server over the bridge — none of it is typed in, and the page
|
||||
is honest about a quiet realm.
|
||||
</p>
|
||||
|
||||
<Screenshot id="shard-status" />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.looks h2 {
|
||||
margin: 0.35rem 0 0.75rem;
|
||||
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
||||
}
|
||||
|
||||
.looks__lede {
|
||||
margin: 0;
|
||||
max-width: 46rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ description: Colours, fonts and corners from the Appearance screen; logo, hero a
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import Screenshot from '../../../../components/Screenshot.astro';
|
||||
|
||||
One prebuilt image runs as any community's site. Nothing about your identity is compiled
|
||||
in — it is a theme row in the database, three image files on a mount, and a few environment
|
||||
@@ -31,6 +32,8 @@ Two things the screen tells you that are easy to miss:
|
||||
- **The accent reaches the mobile app and the Discord bot**, both of which theme themselves
|
||||
from this site's public branding. Changing it here changes them.
|
||||
|
||||
<Screenshot id="admin-appearance" />
|
||||
|
||||
## Brand assets
|
||||
|
||||
The same screen uploads three images, and each applies as soon as the upload finishes —
|
||||
|
||||
@@ -4,10 +4,13 @@ description: The five states a module can be in, installing and upgrading, disab
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import Screenshot from '../../../../components/Screenshot.astro';
|
||||
|
||||
Installing your first module is [Getting started](/docs/getting-started/install-a-game-module/).
|
||||
This is what the screen means afterwards.
|
||||
|
||||
<Screenshot id="admin-modules" />
|
||||
|
||||
## The five states
|
||||
|
||||
`installed → enabled → started`, with `disabled` and `startup_failed` as recoverable
|
||||
|
||||
@@ -4,6 +4,7 @@ description: The module's shard screen — connection settings, what the status
|
||||
---
|
||||
|
||||
import platform from '../../../../data/platform.json';
|
||||
import Screenshot from '../../../../components/Screenshot.astro';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
With the `uo` module installed, **Shard (uo-link)** appears in the admin sidebar at
|
||||
@@ -13,6 +14,8 @@ the controls that ride on it.
|
||||
Setting it up for the first time is
|
||||
[Connect a game server](/docs/getting-started/connect-a-game-server/).
|
||||
|
||||
<Screenshot id="admin-shard" />
|
||||
|
||||
## Connection
|
||||
|
||||
Four fields, all four printed by the installer, plus the switch that turns the integration
|
||||
|
||||
@@ -4,6 +4,7 @@ description: The four roles and what each one reaches, creating accounts, and in
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import Screenshot from '../../../../components/Screenshot.astro';
|
||||
|
||||
## The four roles
|
||||
|
||||
@@ -24,6 +25,8 @@ Admin routes are re-validated against the database on **every request**, not jus
|
||||
Demoting an account takes effect at once — the open session does not keep its access until
|
||||
it expires.
|
||||
|
||||
<Screenshot id="admin-users" />
|
||||
|
||||
## Creating an account
|
||||
|
||||
**Admin → Users → + Add user** creates one directly: username, password, role, and it is
|
||||
|
||||
@@ -4,6 +4,7 @@ description: Signing in as the first admin, what the site does before anyone vis
|
||||
---
|
||||
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
import Screenshot from '../../../../components/Screenshot.astro';
|
||||
|
||||
The site is up and nobody can see it yet. That is the intended state: a new deployment
|
||||
**starts in maintenance mode**, showing visitors a "coming soon" page while the admin panel
|
||||
@@ -33,6 +34,8 @@ minute, and see [Authentication](/docs/administration/authentication/) for what
|
||||
**Web Bot Activity** screen shows and how to lift a ban.
|
||||
</Aside>
|
||||
|
||||
<Screenshot id="admin-dashboard" />
|
||||
|
||||
## What is already there
|
||||
|
||||
The first boot seeds a working site rather than an empty one:
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"releases": {
|
||||
"link": "v2.0.0",
|
||||
"installer": "v0.1.1",
|
||||
"Module-uo": "v1.0.1",
|
||||
"Module-uo": "v1.0.2",
|
||||
"Android-app": "v0.5.0"
|
||||
},
|
||||
|
||||
|
||||
240
src/data/screens.mjs
Normal file
240
src/data/screens.mjs
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* screens.mjs — every screenshot the site ships, and where it came from.
|
||||
*
|
||||
* PLAN.md §13 phase 9, D4 / D42–D45.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* ONE LIST, THREE READERS
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* `scripts/captureScreens.mjs` reads this to know what to shoot and where to click before
|
||||
* it shoots; `src/components/Screenshot.astro` reads it to render one figure by id; and
|
||||
* `scripts/checkScreens.mjs` reads it to prove that every file exists at the declared size
|
||||
* and that nothing in `public/screens/` is orphaned. A screenshot is therefore a data
|
||||
* change: add an entry, re-run the capture, and the check tells you if you missed a step.
|
||||
*
|
||||
* That split is what makes a re-capture cheap. §1 says the site must not describe a
|
||||
* product that no longer looks like that, and the way a screenshot goes stale is that
|
||||
* nobody remembers how it was taken. The route, the viewport, the scroll offset and the
|
||||
* signed-in state are all here, so the answer to "how do I retake this" is one command.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHY EVERY WEB SHOT IS THE SAME SIZE
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* A 1280×800 viewport at 1.5× device pixels — 1920×1200 in the file. Uniform because the
|
||||
* marketing pages lay them out in a grid and a grid of mixed aspect ratios reads as an
|
||||
* accident, and because a check that asserts one pair of numbers cannot drift the way a
|
||||
* per-file table can. Where a page's interesting part is below the fold, `scrollY` moves
|
||||
* the frame rather than the size changing.
|
||||
*
|
||||
* The phone shots are the device's own portrait size and are declared per family for the
|
||||
* same reason (see `PHONE`).
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* WHAT IS IN THEM
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* A demo deployment of this platform, wired to a real ServUO shard over a real sidecar
|
||||
* (D42): the marketplace rows are player vendors the game actually holds, the atlas is
|
||||
* parsed from the shard's own spawn files, the guild rosters came over the bridge. The
|
||||
* deployment is branded "Runic Gateway Demo" rather than a real community's name (D43) —
|
||||
* the screenshots show the platform, not somebody's private shard.
|
||||
*
|
||||
* Nothing here is a mock-up, and nothing here was drawn.
|
||||
*/
|
||||
|
||||
/** Web capture geometry. The capture script and the check both read these. */
|
||||
export const WEB = { viewport: { width: 1280, height: 800 }, scale: 1.5, width: 1920, height: 1200 };
|
||||
|
||||
/**
|
||||
* Phone capture geometry — the emulator's own portrait pixels, unscaled.
|
||||
*
|
||||
* An API 35 device rather than the API 36 the plan named: the API 36 image on this machine
|
||||
* had 200 MB left on its data partition and refused the install, and wiping somebody's
|
||||
* development device to take a screenshot is not a trade worth making. The app targets both.
|
||||
*/
|
||||
export const PHONE = { width: 1440, height: 3088 };
|
||||
|
||||
/**
|
||||
* @typedef {object} Screen
|
||||
* @property {string} id File stem under `public/screens/`, and the handle a page uses.
|
||||
* @property {string} route Route on the demo deployment. The capture script's only input.
|
||||
* @property {boolean} admin Capture signed in as an administrator rather than anonymously.
|
||||
* @property {number} [scrollY] Pixels to scroll before the shot, when the subject is below the fold.
|
||||
* @property {string} alt What the screen shows, for somebody who cannot see it.
|
||||
* @property {string} caption The sentence printed under the figure.
|
||||
* @property {'web'|'phone'} family Which geometry the file follows.
|
||||
*/
|
||||
|
||||
/** @type {Screen[]} */
|
||||
export const screens = [
|
||||
// ── The product's public surfaces ───────────────────────────────────────────────────
|
||||
{
|
||||
id: 'shard-status',
|
||||
route: '/uo/shard',
|
||||
admin: false,
|
||||
alt: 'The shard page of a Runic Gateway site, showing the shard online, its gold supply, the state of the shard link and how many players are online.',
|
||||
caption:
|
||||
'The shard console. Every number on it came over the bridge from a running game server — nothing here is stored by hand.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'marketplace',
|
||||
route: '/uo/market',
|
||||
admin: false,
|
||||
scrollY: 470,
|
||||
alt: 'The marketplace page, listing items for sale by player vendors with their prices, shop names and locations, above a search box and price filters.',
|
||||
caption:
|
||||
'Player vendors, searchable from the website — the same index the in-game vendor search reads, honouring the same per-vendor opt-out.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'spawn-atlas',
|
||||
route: '/uo/atlas',
|
||||
admin: false,
|
||||
scrollY: 430,
|
||||
alt: 'The spawn atlas, listing creatures with how many of them spawn and on which facets, above a search box and facet filters.',
|
||||
caption:
|
||||
"The spawn atlas is parsed from the shard's own spawn files, so it stays accurate whether or not the server is up.",
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'guilds',
|
||||
route: '/uo/guilds',
|
||||
admin: false,
|
||||
alt: 'The guilds page, listing each guild on the shard with its abbreviation, how many members are online, and its leader.',
|
||||
caption:
|
||||
'Guilds arrive from the shard, not from a form — and a Team on the website is one of them, with its own forum and roster.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'houses',
|
||||
route: '/uo/houses',
|
||||
admin: false,
|
||||
alt: 'The houses page, listing homes that have entered their final decay stage with their owner and location.',
|
||||
caption:
|
||||
'Houses in danger, from the same decay data the game uses — a live process, not a nightly export.',
|
||||
family: 'web',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'news',
|
||||
route: '/site/news',
|
||||
admin: false,
|
||||
alt: 'The news page, listing posts with their category, date, headline and summary.',
|
||||
caption:
|
||||
'News, five-on-friday and the newsletter are one posting system with three categories — and none of it knows what game you run.',
|
||||
family: 'web',
|
||||
},
|
||||
|
||||
// ── The administration screens the documentation describes ──────────────────────────
|
||||
{
|
||||
id: 'admin-dashboard',
|
||||
route: '/admin',
|
||||
admin: true,
|
||||
alt: 'The administration dashboard, showing the site mode, counts of posts, wiki pages and users, and a feed of recent administrative activity.',
|
||||
caption: 'The dashboard on first sign-in: site mode, what the site holds, and who has done what.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'admin-users',
|
||||
route: '/admin/users',
|
||||
admin: true,
|
||||
alt: 'The users screen, listing accounts with their role and status and the controls to change them.',
|
||||
caption: 'Users and roles. A change of role takes effect on the next request, not the next login.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'admin-shard',
|
||||
route: '/admin/uo/link',
|
||||
admin: true,
|
||||
alt: 'The shard connection screen, showing the sidecar connected, the websocket ingest live, and fields for the sidecar URL, token and protocol version.',
|
||||
caption:
|
||||
'The shard connection, showing a live sidecar. The token is write-only: it is never sent back to any client, including this screen.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'admin-modules',
|
||||
route: '/admin/modules',
|
||||
admin: true,
|
||||
alt: 'The modules screen, showing the Ultima Online module running with the routes it mounts, and the list of hosts modules may be installed from.',
|
||||
caption:
|
||||
'A module is installed from a release URL and runs inside the server, so only listed hosts are allowed to serve one.',
|
||||
family: 'web',
|
||||
},
|
||||
{
|
||||
id: 'admin-appearance',
|
||||
route: '/admin/appearance',
|
||||
admin: true,
|
||||
alt: 'The appearance screen, showing the colour and typography controls a deployment uses to set its own theme.',
|
||||
caption: 'Branding is data. One image runs as any community, and the app takes its colours from here too.',
|
||||
family: 'web',
|
||||
},
|
||||
|
||||
// ── The Android app, against the same deployment on the same day (D26) ──────────────
|
||||
//
|
||||
// Captured from an emulator pointed at the demo stack through `adb reverse`, signed in as
|
||||
// an ordinary player. The app takes its name, colours and navigation from the site it is
|
||||
// connected to, so these are not a neutral app: they are one deployment's app.
|
||||
{
|
||||
id: 'app-home',
|
||||
route: '/',
|
||||
admin: false,
|
||||
alt: "The app's home screen, showing the deployment's emblem and name, a live online indicator and its description.",
|
||||
caption:
|
||||
"The app is one screen of setup: type the address of a site, and it becomes that community's app.",
|
||||
family: 'phone',
|
||||
},
|
||||
{
|
||||
id: 'app-shard',
|
||||
route: '/shard',
|
||||
admin: false,
|
||||
alt: "The app's shard screen, showing the shard online with its gold supply, links to champion spawns, guilds, governors and falling houses, and a live activity feed.",
|
||||
caption:
|
||||
'The same bridge feeds the phone. The activity list is live — those two houses entered their final decay stage while this was open.',
|
||||
family: 'phone',
|
||||
},
|
||||
{
|
||||
id: 'app-market',
|
||||
route: '/market',
|
||||
admin: false,
|
||||
alt: "The app's marketplace, listing weapons for sale with their price, the shop selling them and where it stands.",
|
||||
caption: 'Every player vendor on the shard, searchable from a phone.',
|
||||
family: 'phone',
|
||||
},
|
||||
{
|
||||
id: 'app-wiki',
|
||||
route: '/wiki',
|
||||
admin: false,
|
||||
alt: "The app's wiki index, listing pages with their category and summary above a search box.",
|
||||
caption:
|
||||
"The wiki, the news and the rules are the site's own content, rendered natively rather than in a web view.",
|
||||
family: 'phone',
|
||||
},
|
||||
{
|
||||
id: 'app-account',
|
||||
route: '/account',
|
||||
admin: false,
|
||||
alt: "The app's account screen for a signed-in player, with username and password controls, two-factor authentication, trusted devices and recovery codes.",
|
||||
caption:
|
||||
'Self-service, and the same session model as the website: two-factor, trusted devices and recovery codes all live here.',
|
||||
family: 'phone',
|
||||
},
|
||||
{
|
||||
id: 'app-drawer',
|
||||
route: '/',
|
||||
admin: false,
|
||||
alt: "The app's navigation drawer for a signed-in player, listing the deployment's own pages above the player's own account, characters, vendors and houses.",
|
||||
caption:
|
||||
"The drawer is the deployment's own navigation, not a fixed menu — a site that renames or reorders its pages renames and reorders them here.",
|
||||
family: 'phone',
|
||||
},
|
||||
];
|
||||
|
||||
/** One entry by id, or `undefined`. */
|
||||
export function screenById(id) {
|
||||
return screens.find((shot) => shot.id === id);
|
||||
}
|
||||
|
||||
/** Every entry in a family, in declaration order. */
|
||||
export function screensOf(family) {
|
||||
return screens.filter((shot) => shot.family === family);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
import NotBuilt from '../components/NotBuilt.astro';
|
||||
import Screenshot from '../components/Screenshot.astro';
|
||||
|
||||
import platform from '../data/platform.json';
|
||||
import {
|
||||
@@ -55,6 +56,26 @@ import {
|
||||
assertCapabilityCoverage(platform.moduleUoCapabilities);
|
||||
assertDetailCoverage();
|
||||
|
||||
/**
|
||||
* Which screenshots sit under which group (PLAN.md §13 phase 9, D44).
|
||||
*
|
||||
* Deliberately here and not in `capabilities.mjs`. That file is a contract — two build-time
|
||||
* assertions read it and `/modules/` and the homepage render from it — and a group is
|
||||
* defined by what the software does, not by what somebody has got round to photographing.
|
||||
* A group with no figure is the normal case, not an omission: Infrastructure is about where
|
||||
* the container runs and who owns the database, and a picture of a web page says nothing
|
||||
* true about either.
|
||||
*
|
||||
* `Screenshot` throws on an id that `screens.mjs` does not declare, so a typo here fails the
|
||||
* build rather than rendering a broken image.
|
||||
*/
|
||||
const groupScreens: Record<string, string[]> = {
|
||||
community: ['news'],
|
||||
'game-intelligence': ['marketplace', 'spawn-atlas', 'guilds', 'houses'],
|
||||
administration: ['admin-dashboard', 'admin-users'],
|
||||
integration: ['admin-modules'],
|
||||
};
|
||||
|
||||
const title = 'Features';
|
||||
const description =
|
||||
'What a Runic Gateway deployment does — core, and what the installed game module adds.';
|
||||
@@ -110,6 +131,14 @@ const description =
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{groupScreens[group.id] && (
|
||||
<div class="group__shots">
|
||||
{groupScreens[group.id].map((id: string) => (
|
||||
<Screenshot id={id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))
|
||||
}
|
||||
@@ -118,6 +147,16 @@ const description =
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
/* One up, at the column's full width, even where a group has four of them.
|
||||
Two-up was tried first and is the obvious layout for a set of figures — but these are
|
||||
screenshots of a dense interface, and halving the width puts the product's own type at
|
||||
around a third of its real size, which reads as a thumbnail of something rather than a
|
||||
picture of it. A long section of legible evidence beats a tidy grid of unreadable
|
||||
tiles. */
|
||||
.group__shots {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.group__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { brand } from '../lib/brand.mjs';
|
||||
|
||||
import Hero from '../components/home/Hero.astro';
|
||||
import DataPath from '../components/home/DataPath.astro';
|
||||
import WhatItLooksLike from '../components/home/WhatItLooksLike.astro';
|
||||
import SelfHosted from '../components/home/SelfHosted.astro';
|
||||
import Capabilities from '../components/home/Capabilities.astro';
|
||||
import GetStarted from '../components/home/GetStarted.astro';
|
||||
@@ -28,6 +29,7 @@ import GetStarted from '../components/home/GetStarted.astro';
|
||||
<Base title={`${brand.siteName} — ${brand.tagline}`} description={brand.tagline} bareTitle>
|
||||
<Hero />
|
||||
<DataPath />
|
||||
<WhatItLooksLike />
|
||||
<SelfHosted />
|
||||
<Capabilities />
|
||||
<GetStarted />
|
||||
|
||||
Reference in New Issue
Block a user