Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions frontend/app/components/MapViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import {
} from "@/lib/map/api";
import { editorUrl } from "@/lib/map/editorAccess";
import { chronicleStudioHref } from "@/app/lib/map/chronicleDayRoute";
import { formatChronicleDay } from "@/app/lib/map/chronicleDayLabel";
import { useResponsiveFitMode } from "@/app/hooks/useResponsiveFitMode";
import { isArchivedMap, showReviewHistory } from "@/app/lib/map/archiveMaps";
import MapArchiveMenu from "./map/MapArchiveMenu";
Expand Down Expand Up @@ -793,7 +794,6 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
setHoveredFortZoc,
setSelectedRegionId: setHoveredRegionId,
getHoverRegion,
mapDisplayName: displayName,
mapObjects,
markers: mapMarkers,
labels: regionLabels,
Expand Down Expand Up @@ -1213,6 +1213,8 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
);
}

const dayLabel = day ? formatChronicleDay(day) : "this day";

/**
* Every mode is offered on a stored day, so an unclassified mode needs this
* answer when it has neither a static source nor a `CHRONICLE_MODE_SOURCE`.
Expand All @@ -1224,19 +1226,17 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
return (
<div className="flex min-h-[calc(100dvh-var(--tfmc-header-h))] flex-col items-center justify-center gap-3 bg-[var(--tfmc-forest-deep)] px-6 text-center">
<p className="font-[family-name:var(--font-fraunces)] text-2xl text-[var(--tfmc-cream)]">
Not recorded for {day}
Not recorded for {dayLabel}
</p>
<p className="max-w-md text-sm leading-snug text-[var(--tfmc-stone)]">
Nothing in this day&rsquo;s capture answers for this map mode, and it
is not one of the modes that stays the same on every day, so showing
today&rsquo;s would be a lie about the past.
This view wasn&rsquo;t recorded on this day.
</p>
<button
type="button"
onClick={() => handleMapTypeChange("nation")}
className={actionLinkClass}
>
Back to the nation map
Back to Realms
</button>
</div>
);
Expand All @@ -1260,19 +1260,17 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
return (
<div className="flex min-h-[calc(100dvh-var(--tfmc-header-h))] flex-col items-center justify-center gap-3 bg-[var(--tfmc-forest-deep)] px-6 text-center">
<p className="font-[family-name:var(--font-fraunces)] text-2xl text-[var(--tfmc-cream)]">
Missing from the {day} capture
Not available for {dayLabel}
</p>
<p className="max-w-md text-sm leading-snug text-[var(--tfmc-stone)]">
This day was recorded, but the file behind this map mode is not in
its snapshot. Nothing is shown rather than an empty world under a
real date. Another mode, or a nearby day, may still have it.
Try another view or a nearby day.
</p>
<button
type="button"
onClick={() => handleMapTypeChange("nation")}
className={actionLinkClass}
>
Back to the nation map
Back to Realms
</button>
</div>
);
Expand Down Expand Up @@ -1329,8 +1327,7 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>

const archiveFooter =
chronicle || archived ? null : (
<div className="flex items-center justify-between gap-2 text-sm text-[var(--tfmc-stone)]">
<span>Earlier chapters</span>
<div className="flex justify-end">
<MapArchiveMenu maps={maps} linkClass={actionLinkClass} />
</div>
);
Expand Down Expand Up @@ -1380,7 +1377,6 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
mapType={mapType}
regionId={selectedId}
regionData={regionData}
mapDisplayName={displayName}
sessionToken={authToken}
settlements={settlements}
onSelectRegion={(id) => selectRegion(id)}
Expand All @@ -1404,7 +1400,7 @@ const MapViewer = ({ mapId, day = null, dayBar, dayActions }: MapViewerProps) =>
banner={dayBar}
plaque={
<MapPlaque
eyebrow={archived ? "Archived chapter" : "World map"}
eyebrow={archived ? "Archived chapter" : undefined}
mapDisplayName={displayName}
actions={plaqueActions}
search={
Expand Down
32 changes: 13 additions & 19 deletions frontend/app/components/chronicle/ChronicleDayViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ function ChronicleDayBar({
<BackIcon size={18} />
</Link>
<div className="min-w-0 flex-1 text-center">
<p className="truncate text-xs text-[var(--tfmc-mist)]">
Stored day
{walk.total > 0 && walk.position > 0 ? ` · ${walk.position} of ${walk.total}` : ""}
</p>
{walk.total > 0 && walk.position > 0 ? (
<p className="truncate text-xs text-[var(--tfmc-mist)]">
Day {walk.position} of {walk.total}
</p>
) : null}
<p className="truncate font-[family-name:var(--font-fraunces)] text-xl leading-tight text-[var(--tfmc-cream)]">
{formatChronicleDay(day)}
</p>
Expand Down Expand Up @@ -177,14 +178,12 @@ function ChronicleDayBar({
</div>
{incomplete ? (
<p className="mt-1.5 px-1 text-xs leading-snug text-[var(--tfmc-accent)]">
Some sources were missing when this day was captured, so parts of this
map may be blank.
Parts of this day may be missing.
</p>
) : null}
{staleGeometry ? (
<p className="mt-1.5 px-1 text-xs leading-snug text-[var(--tfmc-accent)]">
The province map has been redrawn since this day was captured, so
borders may not line up.
Borders may not line up on this day.
</p>
) : null}
</div>
Expand Down Expand Up @@ -263,7 +262,7 @@ export default function ChronicleDayViewer({
if (typeof index !== "object" || index === null) {
setStatus({
kind: "error",
message: "The chronicle index came back in an unreadable shape.",
message: "Couldn't load the map history.",
});
return;
}
Expand All @@ -280,7 +279,7 @@ export default function ChronicleDayViewer({
message:
err instanceof Error
? err.message
: "Failed to load the chronicle index.",
: "Couldn't load the map history.",
});
});

Expand All @@ -299,7 +298,7 @@ export default function ChronicleDayViewer({
return (
<div className={shellClass}>
<p className="text-lg font-medium text-[var(--tfmc-cream)]">
Looking up {activeDay} in the chronicle…
Loading…
</p>
</div>
);
Expand All @@ -309,7 +308,7 @@ export default function ChronicleDayViewer({
return (
<div className={shellClass}>
<p className="font-[family-name:var(--font-fraunces)] text-2xl text-[var(--tfmc-cream)]">
Could not read the chronicle
Couldn&rsquo;t load this day
</p>
<p className="max-w-md text-sm leading-snug text-[var(--tfmc-accent)]">
{status.message}
Expand All @@ -323,15 +322,10 @@ export default function ChronicleDayViewer({
return (
<div className={shellClass}>
<p className="text-xs font-medium uppercase tracking-widest text-[var(--tfmc-mist)]">
{displayName} chronicle
{displayName} timelapse
</p>
<p className="font-[family-name:var(--font-fraunces)] text-2xl text-[var(--tfmc-cream)]">
No record of {activeDay}
</p>
<p className="max-w-md text-sm leading-snug text-[var(--tfmc-stone)]">
This map has no stored snapshot for that day — it was either never
captured or has since been removed. Showing nothing is deliberate: an
empty map under a real date would look like a real historical state.
No record of {formatChronicleDay(activeDay)}
</p>
<ChronicleDayFallbackLinks mapId={mapId} />
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/components/chronicle/ChroniclePanels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe("ChronicleTogglePanel", () => {
<ChronicleTogglePanel
toggles={{ ...CHRONICLE_TOGGLES_OFF, nationFill: true }}
onToggle={onToggle}
disabledReasons={{ nationNames: "No geometry." }}
disabledReasons={{ nationNames: "Nation names aren't available for this map." }}
notice={null}
focusOptions={[]}
focusNationId=""
Expand All @@ -105,7 +105,7 @@ describe("ChronicleTogglePanel", () => {
expect(screen.getByRole("switch", { name: "Nation names" }).hasAttribute("disabled")).toBe(
true
);
expect(screen.getByText(/Nation names: No geometry\./)).toBeDefined();
expect(screen.getByText("Nation names aren't available for this map.")).toBeDefined();
});
});

Expand Down Expand Up @@ -154,7 +154,7 @@ describe("ChronicleBuildPanel", () => {
it("shows how far the build has got", () => {
render(
<ChronicleBuildPanel
progress={{ completed: 5, total: 20, day: "2026-08-05", painted: 4, reused: 1, skipped: 0 }}
progress={{ completed: 5, total: 20, day: "2026-08-05" }}
error={null}
/>
);
Expand Down
39 changes: 11 additions & 28 deletions frontend/app/components/chronicle/ChroniclePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { CSSProperties, ReactNode } from "react";

import {
CHRONICLE_RENDER_SIZES,
formatChronicleBytes,
formatChronicleDuration,
type ChronicleBuildProgress,
type ChronicleEstimate,
Expand Down Expand Up @@ -328,11 +327,6 @@ export function ChronicleTogglePanel({
}) {
return (
<div className="space-y-5">
{/* Not on a phone, where the sheet's peek has room for the tiles only. */}
<p className="text-sm leading-snug text-[var(--tfmc-stone)] max-md:hidden">
The map starts bare. Pick what each day should show; the map previews the
latest day as you go.
</p>
{notice ? <ChronicleNotice>{notice}</ChronicleNotice> : null}

{TOGGLE_GROUPS.map((group) => (
Expand Down Expand Up @@ -372,9 +366,10 @@ export function ChronicleTogglePanel({
</section>
))}

{/* A tile's title tooltip is not reliable on touch or keyboard. */}
{Object.entries(disabledReasons).map(([key, reason]) => (
<ChronicleNotice key={key} tone="quiet">
{TOGGLE_DETAILS.get(key as ChronicleToggleKey)?.label}: {reason}
{reason}
</ChronicleNotice>
))}

Expand Down Expand Up @@ -404,8 +399,7 @@ export function ChronicleTogglePanel({
<section className={`${sectionRuleClass} pt-4`}>
<SectionHeading title="Sequence" />
<p className="mt-1.5 text-xs leading-snug text-[var(--tfmc-stone)]">
One look plays as the layers are now. Add this look to stitch several
into one timelapse, in the order you queue them.
Queue several looks to play one after another.
</p>
<button
type="button"
Expand Down Expand Up @@ -575,8 +569,7 @@ export function ChronicleRangePanel({
{selection.incompleteDays.length && !selection.error ? (
<p className="mt-2 text-xs text-[var(--tfmc-accent)]">
{selection.incompleteDays.length} day
{selection.incompleteDays.length === 1 ? " was" : "s were"} captured with
missing sources.
{selection.incompleteDays.length === 1 ? " is" : "s are"} incomplete.
</p>
) : null}
</section>
Expand All @@ -603,7 +596,7 @@ export function ChronicleRangePanel({
{selection.error ? (
<p className="text-sm text-[var(--tfmc-accent)]">{selection.error}</p>
) : (
<dl className="grid grid-cols-3 gap-2 text-center">
<dl className="grid grid-cols-2 gap-2 text-center">
<div>
<dt className="text-xs text-[var(--tfmc-mist)]">Frames</dt>
<dd className="mt-0.5 text-lg font-semibold text-[var(--tfmc-cream)]">
Expand All @@ -616,12 +609,6 @@ export function ChronicleRangePanel({
~{formatChronicleDuration(estimate.totalMs)}
</dd>
</div>
<div>
<dt className="text-xs text-[var(--tfmc-mist)]">Memory</dt>
<dd className="mt-0.5 text-lg font-semibold text-[var(--tfmc-cream)]">
~{formatChronicleBytes(estimate.memoryBytes)}
</dd>
</div>
</dl>
)}
{looks > 1 && !selection.error ? (
Expand All @@ -633,8 +620,7 @@ export function ChronicleRangePanel({

{estimate.overCeiling ? (
<ChronicleNotice>
{formatChronicleBytes(estimate.memoryBytes)} of frames is more than this
browser should hold at once.{" "}
That&rsquo;s too many frames to build at once.{" "}
{looks > 1
? "Shorten the range, pick a smaller frame size, or drop a look from the sequence."
: "Shorten the range or pick a smaller frame size."}
Expand Down Expand Up @@ -682,12 +668,10 @@ export function ChronicleBuildPanel({
style={{ width: `${pct}%` }}
/>
</div>
{progress ? (
{progress?.day ? (
<p className="text-xs text-[var(--tfmc-stone)]">
{progress.look ? `${progress.look} · ` : ""}
{progress.day ? `${formatChronicleDay(progress.day)} · ` : ""}
{progress.painted} painted, {progress.reused} reused
{progress.skipped ? `, ${progress.skipped} skipped` : ""}
{formatChronicleDay(progress.day)}
</p>
) : null}
{error ? <ChronicleNotice>{error}</ChronicleNotice> : null}
Expand Down Expand Up @@ -774,7 +758,7 @@ export function ChroniclePlayer({
<p className="truncate text-xs text-[var(--tfmc-mist)]">
{look ? `${look} · ` : null}
Day {Math.min(activeIndex + 1, total)} of {total}
{incomplete ? <span className="text-[var(--tfmc-accent)]"> · sources missing</span> : null}
{incomplete ? <span className="text-[var(--tfmc-accent)]"> · incomplete</span> : null}
</p>
</div>
);
Expand Down Expand Up @@ -961,7 +945,7 @@ function WatermarkCornerPicker({
<div className="min-w-0 text-sm">
<p className="text-[var(--tfmc-cream)]">Watermark</p>
<p className="text-xs text-[var(--tfmc-stone)]">
{label}. Where the logo and Discord link sit, when they are on.
{label}
</p>
</div>
</div>
Expand Down Expand Up @@ -1056,8 +1040,7 @@ export function ChroniclePlaybackPanel({

{skippedDays.length ? (
<ChronicleNotice tone="quiet">
{skippedDays.length} day{skippedDays.length === 1 ? " had" : "s had"} no stored
sources and {skippedDays.length === 1 ? "was" : "were"} left out.
{skippedDays.length} day{skippedDays.length === 1 ? "" : "s"} skipped (no data).
</ChronicleNotice>
) : null}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ afterEach(() => {
describe("ChronicleStaffConsole", () => {
it("mounts signed out and shows the login gate instead of the wipe form", async () => {
render(<ChronicleStaffConsole mapId="main" />);
await waitFor(() => expect(screen.getByText("Profile login required")).toBeDefined());
await waitFor(() => expect(screen.getByText("Sign in to view this map")).toBeDefined());
expect(screen.queryByText(/Wipe the main chronicle/)).toBeNull();
});

Expand Down Expand Up @@ -105,9 +105,9 @@ describe("ChronicleStaffConsole", () => {
}));
render(<ChronicleStaffConsole mapId="main" />);
await waitFor(() => expect(screen.getByText("Backups")).toBeDefined());
fireEvent.change(screen.getByLabelText("Dest id"), { target: { value: "chapter" } });
fireEvent.change(screen.getByLabelText("Destination id"), { target: { value: "chapter" } });
fireEvent.change(screen.getByLabelText("Display name"), { target: { value: "Chapter" } });
fireEvent.change(screen.getByLabelText("Type the dest id to confirm"), { target: { value: "chapter" } });
fireEvent.change(screen.getByLabelText("Type the destination id to confirm"), { target: { value: "chapter" } });
fireEvent.change(screen.getByPlaceholderText("why this live map is being copied to a frozen id"), { target: { value: "Archive fixture" } });
fireEvent.click(screen.getByRole("button", { name: "Archive as chapter" }));
await waitFor(() => expect(screen.getByText("Copied main → chapter.")).toBeDefined());
Expand Down
Loading