Tuck the trajectory replay controls into a compact pill

The wide range scrubber under the map was the loudest element on the live
map. Keep replay, speed and 回到最新, drop the slider itself, and shrink the
bar to a right aligned pill so the map and the colour legend stay in focus.
This commit is contained in:
npc0-hue
2026-09-16 21:31:11 +08:00
parent bc45188be9
commit c5d8141d2a
5 changed files with 7 additions and 7 deletions
+1 -2
View File
@@ -787,8 +787,7 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
.map-ride-marker{position:absolute;z-index:4;display:grid;place-items:center;width:22px;height:22px;border:1px solid color-mix(in srgb,var(--gold) 70%,var(--line));border-radius:999px;background:color-mix(in srgb,var(--surface-solid) 76%,transparent);color:var(--gold);font:800 9px/1 var(--font-mono);transform:translate(-50%,-50%) scale(var(--map-inverse,1));pointer-events:none} .map-ride-marker{position:absolute;z-index:4;display:grid;place-items:center;width:22px;height:22px;border:1px solid color-mix(in srgb,var(--gold) 70%,var(--line));border-radius:999px;background:color-mix(in srgb,var(--surface-solid) 76%,transparent);color:var(--gold);font:800 9px/1 var(--font-mono);transform:translate(-50%,-50%) scale(var(--map-inverse,1));pointer-events:none}
.map-ride-marker img{width:100%;height:100%;object-fit:contain} .map-ride-marker img{width:100%;height:100%;object-fit:contain}
.map-track-swatch{display:inline-block;width:10px;height:10px;margin-right:6px;border-radius:999px;vertical-align:middle} .map-track-swatch{display:inline-block;width:10px;height:10px;margin-right:6px;border-radius:999px;vertical-align:middle}
.map-playback-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 70%,transparent);border-radius:10px;background:var(--control-surface)} .map-playback-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;width:max-content;max-width:100%;margin-left:auto;padding:5px 10px;border:1px solid color-mix(in srgb,var(--line) 70%,transparent);border-radius:999px;background:var(--control-surface)}
.map-playback-bar input[type=range]{flex:1 1 240px;min-width:120px;min-height:22px;margin:0;accent-color:var(--accent)}
.map-playback-bar .page-status{margin:0} .map-playback-bar .page-status{margin:0}
.scum-map-menu{position:relative;flex:0 0 auto} .scum-map-menu{position:relative;flex:0 0 auto}
.scum-map-menu>summary{display:inline-flex;align-items:center;min-height:26px;padding:0 9px;font-size:11px;list-style:none;cursor:pointer} .scum-map-menu>summary{display:inline-flex;align-items:center;min-height:26px;padding:0 9px;font-size:11px;list-style:none;cursor:pointer}
@@ -721,9 +721,8 @@ function persistMapLayers(view: ViewState, actions: SCUMWorkspaceActions | undef
function mapPlaybackBar(e: ReactLike["createElement"], view: ViewState, span: { from: number; to: number }, enabled: boolean) { function mapPlaybackBar(e: ReactLike["createElement"], view: ViewState, span: { from: number; to: number }, enabled: boolean) {
const cursor = view.mapPlayback.value >= 1 ? Infinity : span.from + (span.to - span.from) * view.mapPlayback.value; const cursor = view.mapPlayback.value >= 1 ? Infinity : span.from + (span.to - span.from) * view.mapPlayback.value;
return e("div", { className: "map-playback-bar" }, return e("div", { className: "map-playback-bar" },
e("button", { type: "button", className: view.mapPlayback.playing ? "primary-command" : "icon-command", disabled: !enabled, "aria-label": view.mapPlayback.playing ? "暂停轨迹回放" : "播放轨迹回放", onClick: () => view.setMapPlayback((previous) => ({ ...previous, playing: !previous.playing, value: !previous.playing && previous.value >= 1 ? 0 : previous.value })) }, view.mapPlayback.playing ? "暂停" : "回放"),
e("input", { type: "range", min: "0", max: "1000", step: "1", value: String(Math.round(view.mapPlayback.value * 1000)), "aria-label": "轨迹回放进度", disabled: !enabled, onChange: (event: InputEvent) => view.setMapPlayback((previous) => ({ ...previous, playing: false, value: Math.min(1, Math.max(0, Number(inputValue(event)) / 1000)) })) }),
e("span", { className: "page-status" }, Number.isFinite(cursor) ? `回放 ${timeLabel(cursor)}` : "回放 全部"), e("span", { className: "page-status" }, Number.isFinite(cursor) ? `回放 ${timeLabel(cursor)}` : "回放 全部"),
e("button", { type: "button", className: view.mapPlayback.playing ? "primary-command" : "icon-command", disabled: !enabled, "aria-label": view.mapPlayback.playing ? "暂停轨迹回放" : "播放轨迹回放", onClick: () => view.setMapPlayback((previous) => ({ ...previous, playing: !previous.playing, value: !previous.playing && previous.value >= 1 ? 0 : previous.value })) }, view.mapPlayback.playing ? "暂停" : "回放"),
labeledField(e, "速度", e("select", { value: String(view.mapPlayback.speed), "aria-label": "轨迹回放速度", onChange: (event: InputEvent) => view.setMapPlayback((previous) => ({ ...previous, speed: Number(inputValue(event)) || 1 })) }, [1, 2, 4].map((speed) => e("option", { key: speed, value: String(speed) }, `${speed}x`)))), labeledField(e, "速度", e("select", { value: String(view.mapPlayback.speed), "aria-label": "轨迹回放速度", onChange: (event: InputEvent) => view.setMapPlayback((previous) => ({ ...previous, speed: Number(inputValue(event)) || 1 })) }, [1, 2, 4].map((speed) => e("option", { key: speed, value: String(speed) }, `${speed}x`)))),
e("button", { type: "button", className: "icon-command", disabled: !enabled, onClick: () => view.setMapPlayback(scumMapPlaybackIdle) }, "回到最新") e("button", { type: "button", className: "icon-command", disabled: !enabled, onClick: () => view.setMapPlayback(scumMapPlaybackIdle) }, "回到最新")
); );
@@ -3,7 +3,7 @@
"id": "game.scum", "id": "game.scum",
"name": "SCUM Server", "name": "SCUM Server",
"description": "First-party SCUM game server operations plugin with platform-mediated lifecycle and plugin-owned RCON data flows.", "description": "First-party SCUM game server operations plugin with platform-mediated lifecycle and plugin-owned RCON data flows.",
"version": "0.1.23", "version": "0.1.24",
"kind": "game-plugin", "kind": "game-plugin",
"tags": [ "tags": [
"scum", "scum",
+1 -1
View File
@@ -357,7 +357,7 @@ describe("plugin manifest validation", () => {
const serialized = JSON.stringify(manifest).toLowerCase(); const serialized = JSON.stringify(manifest).toLowerCase();
expect(serialized).not.toContain("local-proof"); expect(serialized).not.toContain("local-proof");
expect(manifest.version).toBe("0.1.23"); expect(manifest.version).toBe("0.1.24");
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server"); expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"])); expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([ expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
+3 -1
View File
@@ -306,7 +306,9 @@ describe("SCUM plugin feature module", () => {
expect(view.texts.some((text) => /\d+ \/ \d+ 个点 · \d+ 条轨迹/.test(text))).toBe(true); expect(view.texts.some((text) => /\d+ \/ \d+ 个点 · \d+ 条轨迹/.test(text))).toBe(true);
for (const layer of ["地形", "等高线", "夜间", "建筑区"]) expect(view.texts).toContain(layer); for (const layer of ["地形", "等高线", "夜间", "建筑区"]) expect(view.texts).toContain(layer);
expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["地形", "等高线", "夜间"])); expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["地形", "等高线", "夜间"]));
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间", "地图底图", "轨迹回放进度"])); expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间", "地图底图"]));
expect(view.inputs.map((input) => input.label)).not.toContain("轨迹回放进度");
expect(pageSource).not.toContain('type: "range"');
expect(view.inputs.map((input) => input.label)).not.toContain("启用自定义地图"); expect(view.inputs.map((input) => input.label)).not.toContain("启用自定义地图");
expect(view.texts).not.toContain("地图范围与图层"); expect(view.texts).not.toContain("地图范围与图层");
expect(view.texts).not.toContain("自定义范围"); expect(view.texts).not.toContain("自定义范围");