Merge pull request #222 from Tencent-Hunyuan/fix/memory-detail-structure-and-stability

fix(memhub): stabilize memory detail navigation
This commit is contained in:
kristinhe1111 authored and GitHub committed 2026-10-28 20:44:13 +08:00
commit a621262fac
7 files changed
+434 -39

No files matched your search

+195 -7
View File
@@ -13,7 +13,6 @@ import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
@@ -259,6 +258,40 @@ it("OBS-15: switching memory shows a read in progress instead of the previous me
expect(screen.getByRole("status").textContent).toContain("正在读取这条记忆");
});
it("shows a stable loading surface when the selected memory is read again", async () => {
const view = render(<Host simplified />);
await act(async () => {});
expect(screen.getByRole("heading", { name: "第一条" })).toBeTruthy();
view.rerender(<Host simplified deferMemoryId="m0" />);
await userEvent.click(
within(screen.getByRole("list", { name: "地图记忆列表" })).getByRole(
"button",
{ name: /第一条/ },
),
);
expect(screen.queryByRole("heading", { name: "第一条" })).toBeNull();
expect(screen.getByRole("status").textContent).toContain("正在读取这条记忆");
expect(document.querySelector(".map-detail-loading")).toBeTruthy();
});
it("shows retry instead of stale content when the selected memory reread fails", async () => {
const view = render(<Host simplified />);
await act(async () => {});
expect(screen.getByRole("heading", { name: "第一条" })).toBeTruthy();
view.rerender(<Host simplified failMemoryId="m0" />);
await userEvent.click(
within(screen.getByRole("list", { name: "地图记忆列表" })).getByRole(
"button",
{ name: /第一条/ },
),
);
expect(screen.queryByRole("heading", { name: "第一条" })).toBeNull();
expect(screen.getByRole("alert").textContent).toContain(
"这条记忆的原文暂时无法读取",
);
expect(screen.getByRole("button", { name: "重试读取原文" })).toBeTruthy();
});
it("keeps the detail header outside the scrolling body and resets detail scroll on selection", async () => {
const { container } = render(
<Host simplified sources={[importedSources[0]]} />,
@@ -269,6 +302,14 @@ it("keeps the detail header outside the scrolling body and resets detail scroll
const body = container.querySelector<HTMLElement>(".map-inspector-body");
expect(heading?.nextElementSibling).toBe(body);
if (!body) throw new Error("Memory detail scroll body is missing");
body.scrollTop = 90;
await userEvent.click(
within(screen.getByRole("list", { name: "地图记忆列表" })).getByRole(
"button",
{ name: /第一条/ },
),
);
expect(body.scrollTop).toBe(0);
body.scrollTop = 180;
await userEvent.click(
within(screen.getByRole("list", { name: "地图记忆列表" })).getByRole(
@@ -348,6 +389,7 @@ function Host({
processing = null,
loaded = true,
deferMemoryId,
failMemoryId,
graphValue = graph,
onViewHistory,
onConfigureModel = () => {},
@@ -364,12 +406,17 @@ function Host({
processing?: MemoryProcessingSummary | null;
loaded?: boolean;
deferMemoryId?: string;
failMemoryId?: string;
graphValue?: MemoryGraphResponse;
onViewHistory?(): void;
onConfigureModel?(): void;
libraryView?: Parameters<typeof MemoryMap>[0]["libraryView"];
} = {}) {
const [detail, setDetail] = useState<MemoryDetail | null>(null);
const [detailLoad, setDetailLoad] = useState<{
memoryId: string;
status: "loading" | "failed";
} | null>(null);
return (
<MemoryMap
simplified={simplified}
@@ -391,14 +438,25 @@ function Host({
}
: null
}
detailLoad={detailLoad}
draft={detail?.memory.content ?? ""}
busy={false}
answer={null}
streaming={null}
requestIds={{ current: new Map() }}
onOpenMemory={(id) => {
if (failMemoryId && id === failMemoryId) {
setDetail(null);
setDetailLoad({ memoryId: id, status: "failed" });
return;
}
// Lets a test leave one read unanswered.
if (deferMemoryId && id === deferMemoryId) return;
if (deferMemoryId && id === deferMemoryId) {
setDetail(null);
setDetailLoad({ memoryId: id, status: "loading" });
return;
}
setDetailLoad(null);
setDetail({
memory: items.find((m) => m.memory_id === id) ?? items[0],
evidence: [
@@ -416,7 +474,10 @@ function Host({
index_sync: null,
});
}}
onCloseDetail={() => setDetail(null)}
onCloseDetail={() => {
setDetail(null);
setDetailLoad(null);
}}
onAsk={() => {}}
onDraft={() => {}}
onSave={() => {}}
@@ -745,6 +806,8 @@ it("keeps pagination and current detail stable while the next page is loading",
const libraryView = { ...pagedView(page), onPage };
const view = render(<Host simplified libraryView={libraryView} />);
const list = screen.getByRole("list", { name: "地图记忆列表" });
const statusSlot = document.querySelector(".map-list-status");
expect(statusSlot?.getAttribute("data-visible")).toBe("false");
list.scrollTop = 160;
const pager = screen.getByRole("navigation", { name: "地图记忆分页" });
await userEvent.click(
@@ -759,6 +822,8 @@ it("keeps pagination and current detail stable while the next page is loading",
.getByRole("navigation", { name: "地图记忆分页" })
.getAttribute("aria-busy"),
).toBe("true");
expect(document.querySelector(".map-list-status")).toBe(statusSlot);
expect(statusSlot?.getAttribute("data-visible")).toBe("true");
expect(
(
screen.getByRole("button", {
@@ -794,10 +859,131 @@ it("keeps pagination and current detail stable while the next page is loading",
/>,
);
expect(list.scrollTop).toBe(0);
expect(document.querySelector(".map-list-status")).toBe(statusSlot);
expect(statusSlot?.getAttribute("data-visible")).toBe("false");
expect(within(list).getByText("下一页第 1 条")).toBeTruthy();
await waitFor(() =>
expect(screen.queryByRole("heading", { name: "第一条" })).toBeNull(),
expect(
await screen.findByRole("heading", { name: "下一页第 1 条" }),
).toBeTruthy();
expect(
within(list)
.getByRole("button", { name: /下一页第 1 条/ })
.getAttribute("aria-pressed"),
).toBe("true");
expect(screen.queryByRole("heading", { name: "第一条" })).toBeNull();
});
it("keeps the page intent through a failed read and selects the first row after retry", async () => {
const onPage = vi.fn();
const onRetry = vi.fn();
const page = {
items: memories,
total: 202,
offset: 0,
limit: 100,
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
};
const base = { ...pagedView(page), onPage, onRetry };
const view = render(<Host simplified libraryView={base} />);
await act(async () => {});
await userEvent.click(screen.getByRole("button", { name: "下一页记忆" }));
expect(
within(screen.getByRole("list", { name: "地图记忆列表" }))
.getByRole("button", { name: /第一条/ })
.hasAttribute("disabled"),
).toBe(true);
view.rerender(
<Host
simplified
libraryView={{
...base,
loading: false,
failed: true,
requestedOffset: 100,
}}
/>,
);
expect(screen.getByRole("heading", { name: "第一条" })).toBeTruthy();
await userEvent.click(
screen.getByRole("button", { name: "重新读取地图记忆" }),
);
expect(onRetry).toHaveBeenCalledOnce();
expect(
within(screen.getByRole("list", { name: "地图记忆列表" }))
.getByRole("button", { name: /第一条/ })
.hasAttribute("disabled"),
).toBe(true);
expect(
(
screen.getByRole("button", {
name: "搜索",
}) as HTMLButtonElement
).disabled,
).toBe(true);
const nextItems = memories.map((memory, index) => ({
...memory,
memory_id: `retry-${index}`,
content: `重试页第 ${index + 1} 条`,
}));
view.rerender(
<Host
simplified
items={nextItems}
libraryView={{
...base,
requestedOffset: 100,
page: {
...page,
items: nextItems,
offset: 100,
source_ids_by_memory: {
"retry-0": ["codex"],
"retry-1": ["codex"],
},
},
loading: false,
failed: false,
}}
/>,
);
expect(
await screen.findByRole("heading", { name: "重试页第 1 条" }),
).toBeTruthy();
});
it("returns to the shown page when a failed page request is cancelled by a row click", async () => {
const onPage = vi.fn();
const page = {
items: memories,
total: 202,
offset: 0,
limit: 100,
source_ids_by_memory: { m0: ["codex"], m1: ["codex"] },
};
const base = { ...pagedView(page), onPage };
const view = render(<Host simplified libraryView={base} />);
await act(async () => {});
await userEvent.click(screen.getByRole("button", { name: "下一页记忆" }));
view.rerender(
<Host
simplified
libraryView={{
...base,
failed: true,
requestedOffset: 100,
}}
/>,
);
await userEvent.click(
within(screen.getByRole("list", { name: "地图记忆列表" })).getByRole(
"button",
{ name: /第二条/ },
),
);
expect(onPage.mock.calls).toEqual([[100], [0]]);
expect(await screen.findByRole("heading", { name: "第二条" })).toBeTruthy();
});
it("names the SDK's no-source bucket 未识别来源, apart from a source under review", () => {
@@ -877,8 +1063,10 @@ it("badges each listed memory with its scene and pages 20 at a time", () => {
expect((next as HTMLButtonElement).disabled).toBe(false);
fireEvent.click(next);
expect(view.onPage).toHaveBeenCalledWith(40);
fireEvent.click(screen.getByRole("button", { name: "上一页记忆" }));
expect(view.onPage).toHaveBeenCalledWith(0);
const previous = screen.getByRole("button", { name: "上一页记忆" });
expect((previous as HTMLButtonElement).disabled).toBe(true);
fireEvent.click(previous);
expect(view.onPage).not.toHaveBeenCalledWith(0);
});
it("says a filtered source is empty rather than that nothing matched", () => {
+136 -25
View File
@@ -101,6 +101,7 @@ type Props = {
stale: boolean;
settledSource?: string;
settledQuery?: string;
requestedOffset?: number;
/** The source id the SDK uses for "no source given" (shown as 未识别来源). */
unattributedSource?: string;
onQuery(value: string): void;
@@ -224,6 +225,15 @@ export function MemoryMap(props: Props) {
const [pendingSourceSelection, setPendingSourceSelection] = useState<
string | null
>(null);
const [pendingPageOffset, setPendingPageOffset] = useState<number | null>(
null,
);
const [pageRetrying, setPageRetrying] = useState(false);
const pageRetrySawLoading = useRef(false);
const [detailResetRevision, setDetailResetRevision] = useState(0);
const pageTransitioning =
!!library?.loading ||
(pendingPageOffset !== null && (!library?.failed || pageRetrying));
const [question, setQuestion] = useState("");
const [inspectorOpen, setInspectorOpen] = useState(false);
const [filterNotice, setFilterNotice] = useState("");
@@ -426,6 +436,7 @@ export function MemoryMap(props: Props) {
: selectedSource
? `source:${selectedSource.source_id}`
: (visibleGroupNode?.node_id ?? "empty");
const inspectorResetKey = `${inspectorIdentity}:${detailResetRevision}`;
const visibleDetail =
detail &&
(!activeId || activeId === detail.memory.memory_id) &&
@@ -437,10 +448,24 @@ export function MemoryMap(props: Props) {
const inspectorDetail =
visibleDetail ??
(activeId && lastDetail.current ? lastDetail.current : null);
const readingActiveDetail =
!!activeId &&
props.detailLoad?.memoryId === activeId &&
props.detailLoad.status === "loading";
const failedActiveDetail =
!!activeId &&
props.detailLoad?.memoryId === activeId &&
props.detailLoad.status === "failed";
const pick = useCallback(
(memoryId: string, scroll = true) => {
if (!active.some((memory) => memory.memory_id === memoryId)) return;
if (pendingPageOffset !== null && library?.failed && library.page)
library.onPage(library.page.offset);
setPendingSourceSelection(null);
setPendingPageOffset(null);
setPageRetrying(false);
pageRetrySawLoading.current = false;
setDetailResetRevision((current) => current + 1);
scrollSelection.current = scroll;
if (!filtered.some((memory) => memory.memory_id === memoryId)) {
setGroupKey("");
@@ -452,7 +477,7 @@ export function MemoryMap(props: Props) {
setInspectorOpen(true);
onOpenMemory(memoryId);
},
[active, filtered, onOpenMemory],
[active, filtered, library, onOpenMemory, pendingPageOffset],
);
useEffect(() => {
if (
@@ -480,6 +505,24 @@ export function MemoryMap(props: Props) {
pick,
selectedSource,
]);
useLayoutEffect(() => {
if (pendingPageOffset === null || !library || library.loading) return;
if (library.failed || library.stale) return;
const expectedOffset = library.requestedOffset ?? pendingPageOffset;
if (library.page?.offset !== expectedOffset) return;
setPendingPageOffset(null);
setPageRetrying(false);
pageRetrySawLoading.current = false;
const first = active[0];
if (first) {
pick(first.memory_id, false);
return;
}
setSelectedId(null);
setGroupNode(null);
setInspectorOpen(false);
onCloseDetail();
}, [active, library, onCloseDetail, pendingPageOffset, pick]);
useEffect(() => {
if (!props.simplified || openedDefault.current || overlaysSuspended) return;
if (library?.loading && filtered.length === 0) return;
@@ -497,6 +540,17 @@ export function MemoryMap(props: Props) {
detail,
pick,
]);
useEffect(() => {
if (!pageRetrying || !library) return;
if (library.loading) {
pageRetrySawLoading.current = true;
return;
}
if (pageRetrySawLoading.current && library.failed) {
pageRetrySawLoading.current = false;
setPageRetrying(false);
}
}, [library, pageRetrying]);
useEffect(() => {
if (!activeId || !scrollSelection.current) return;
scrollSelection.current = false;
@@ -516,9 +570,9 @@ export function MemoryMap(props: Props) {
useLayoutEffect(() => {
// Detail content owns its own scrollport. A newly selected memory must
// start at its heading instead of inheriting the previous inspector view.
if (inspectorIdentity === "empty") return;
if (inspectorResetKey.startsWith("empty:")) return;
if (inspectorBody.current) inspectorBody.current.scrollTop = 0;
}, [inspectorIdentity]);
}, [inspectorResetKey]);
const settledPageOffset = library?.page?.offset;
useLayoutEffect(() => {
// A settled page is a new reading surface, not a continuation of the
@@ -531,12 +585,16 @@ export function MemoryMap(props: Props) {
setSelectedId(null);
setGroupNode(null);
setPendingSourceSelection(null);
setPendingPageOffset(null);
setPageRetrying(false);
pageRetrySawLoading.current = false;
setInspectorOpen(false);
onCloseDetail();
}, [onCloseDetail]);
useEffect(() => {
// A background map does not own the shared inspector in another mode.
if (overlaysSuspended || library?.loading) return;
if (overlaysSuspended || library?.loading || pendingPageOffset !== null)
return;
if (
requestedId &&
!active.some((memory) => memory.memory_id === requestedId)
@@ -556,6 +614,7 @@ export function MemoryMap(props: Props) {
clear,
overlaysSuspended,
library?.loading,
pendingPageOffset,
]);
function closeInspector() {
if (!compact) {
@@ -565,6 +624,14 @@ export function MemoryMap(props: Props) {
setPendingSourceSelection(null);
setInspectorOpen(false);
}
function retryLibrary() {
if (!library) return;
if (pendingPageOffset !== null) {
pageRetrySawLoading.current = false;
setPageRetrying(true);
}
library.onRetry();
}
function chooseGroup(key: string) {
setGroupNode(null);
if (library) {
@@ -619,6 +686,17 @@ export function MemoryMap(props: Props) {
? text("未识别来源")
: null) ??
text("全部记忆");
const libraryStatus = !library
? ""
: library.failed
? text("读取失败")
: library.stale
? text("正在核对来源…")
: pageTransitioning
? refreshing && library.page?.total !== 0
? text("正在核对…")
: text("正在读取…")
: "";
if (
props.simplified &&
(!library ||
@@ -819,21 +897,20 @@ export function MemoryMap(props: Props) {
<button
type="submit"
className="map-search-submit"
disabled={library.loading || hidden}
disabled={pageTransitioning || hidden}
>
{text("搜索")}
</button>
) : null}
</form>
{library && (library.loading || library.stale || library.failed) ? (
<p className="map-list-status" aria-live="polite">
{library.failed
? text("读取失败")
: library.stale
? text("正在核对来源…")
: refreshing && library.page?.total !== 0
? text("正在核对…")
: text("正在读取…")}
{library ? (
<p
className="map-list-status"
aria-live="polite"
aria-hidden={!libraryStatus || undefined}
data-visible={libraryStatus ? "true" : "false"}
>
{libraryStatus || "\u00a0"}
</p>
) : null}
{!props.simplified && (
@@ -903,6 +980,7 @@ export function MemoryMap(props: Props) {
type="button"
className="map-memory-item"
aria-pressed={activeId === m.memory_id}
disabled={pageTransitioning}
onClick={() => pick(m.memory_id)}
>
<span className={`map-state ${m.status}`} aria-hidden="true" />
@@ -956,7 +1034,7 @@ export function MemoryMap(props: Props) {
{library?.failed && (
<div role="alert">
<p>{text("记忆读取失败,请重试。")}</p>
<R6Button onClick={library.onRetry}>
<R6Button onClick={retryLibrary}>
{text("重新读取地图记忆")}
</R6Button>
</div>
@@ -991,7 +1069,7 @@ export function MemoryMap(props: Props) {
!library.stale && (
<nav
aria-label={text("地图记忆分页")}
aria-busy={library.loading || undefined}
aria-busy={pageTransitioning || undefined}
className="map-pagination"
>
<button
@@ -999,11 +1077,16 @@ export function MemoryMap(props: Props) {
className="quiet map-tool"
aria-label={text("上一页记忆")}
title={text("上一页记忆")}
disabled={library.loading || library.page.offset === 0}
disabled={pageTransitioning || library.page.offset === 0}
onClick={() => {
if (axis === "topic") setGroupKey("");
if (library.page)
library.onPage(library.page.offset - library.page.limit);
if (library.page) {
const offset = library.page.offset - library.page.limit;
setPageRetrying(false);
pageRetrySawLoading.current = false;
setPendingPageOffset(offset);
library.onPage(offset);
}
}}
>
<ChevronLeft size={16} aria-hidden="true" />
@@ -1029,13 +1112,18 @@ export function MemoryMap(props: Props) {
aria-label={text("下一页记忆")}
title={text("下一页记忆")}
disabled={
library.loading ||
pageTransitioning ||
library.page.offset + library.page.limit >= library.page.total
}
onClick={() => {
if (axis === "topic") setGroupKey("");
if (library.page)
library.onPage(library.page.offset + library.page.limit);
if (library.page) {
const offset = library.page.offset + library.page.limit;
setPageRetrying(false);
pageRetrySawLoading.current = false;
setPendingPageOffset(offset);
library.onPage(offset);
}
}}
>
<ChevronRight size={16} aria-hidden="true" />
@@ -1220,7 +1308,7 @@ export function MemoryMap(props: Props) {
<MemoryEvidencePanel
open={inspectorOpen && !!(activeId || visibleGroupNode)}
suspended={overlaysSuspended}
scrollResetKey={activeId ?? visibleGroupNode?.node_id ?? "empty"}
scrollResetKey={inspectorResetKey}
onClose={closeInspector}
>
<header className="map-inspector-heading">
@@ -1255,7 +1343,7 @@ export function MemoryMap(props: Props) {
<button
type="button"
className="secondary"
onClick={library.onRetry}
onClick={retryLibrary}
>
{text("重试")}
</button>
@@ -1276,8 +1364,17 @@ export function MemoryMap(props: Props) {
</div>
) : props.simplified && inspectorDetail ? (
<div
key={`${inspectorDetail.memory.memory_id}:${detailResetRevision}:${
readingActiveDetail
? "loading"
: failedActiveDetail
? "failed"
: "ready"
}`}
className="map-detail-surface"
aria-busy={
refreshing ||
readingActiveDetail ||
inspectorDetail.memory.memory_id !== activeId ||
undefined
}
@@ -1302,7 +1399,21 @@ export function MemoryMap(props: Props) {
until the new one arrived, which read as "nothing happened"
for two or three seconds. A read is not a value: show that a
read is happening instead. */}
{activeId && inspectorDetail.memory.memory_id !== activeId ? (
{failedActiveDetail ? (
<div className="map-detail-loading">
<p role="alert">
{text("这条记忆的原文暂时无法读取,选择已保留。")}
</p>
<button
type="button"
className="secondary"
onClick={() => activeId && onOpenMemory(activeId)}
>
{text("重试读取原文")}
</button>
</div>
) : readingActiveDetail ||
(activeId && inspectorDetail.memory.memory_id !== activeId) ? (
<div className="map-detail-loading">
<div className="map-detail-skeleton" aria-hidden="true">
<i />
+1
View File
@@ -1049,6 +1049,7 @@ export function MemorySpace({
stale: false,
settledSource: sdkLibrary.settledSource,
settledQuery: sdkLibrary.settledQuery,
requestedOffset: sdkLibraryOffset,
unattributedSource: UNATTRIBUTED_SOURCE_ID,
onQuery: setSdkLibraryQuery,
onSource: setSdkLibrarySource,
@@ -136,6 +136,70 @@ it("splits long memory text into a bounded heading and readable body", () => {
"selected-memory-body",
);
});
it("renders an SDK experience from its structured task and solution", () => {
const task =
"review an uncommitted diff for privacy and correctness blockers";
const solution = `Reusable checklist:\n1. Run \`git diff --check\`.\n2. Inspect ${"/very-long-path".repeat(20)}.`;
render(
<SelectedMemoryDetail
detail={{
...detail,
memory: {
...detail.memory,
content: `${task}\n${solution}`,
revision: 0,
},
evidence: [],
sdk_status: "active",
sdk_ref: {
scene: "coding",
kind: "experience",
variant: "coding",
},
sdk_task: task,
sdk_solution: solution,
}}
sources={[]}
/>,
);
expect(screen.getByRole("heading", { name: task })).toBeTruthy();
const body = document.querySelector(".selected-memory-body");
expect(body?.textContent).toBe(solution);
expect(
screen
.getByRole("heading", { name: task })
.closest("[data-memory-kind]")
?.getAttribute("data-memory-kind"),
).toBe("experience");
});
it("does not apply experience fields to a fact memory", () => {
const content = `${"事实".repeat(40)}。补充说明`;
render(
<SelectedMemoryDetail
detail={{
...detail,
memory: { ...detail.memory, content, revision: 0 },
evidence: [],
sdk_status: "active",
sdk_ref: {
scene: "working",
kind: "fact",
variant: "working",
},
sdk_task: "不应显示的任务标题",
sdk_solution: "不应显示的经验正文",
}}
sources={[]}
/>,
);
expect(screen.queryByText("不应显示的任务标题")).toBeNull();
expect(screen.queryByText("不应显示的经验正文")).toBeNull();
expect(
document
.querySelector(".selected-memory-detail")
?.getAttribute("data-memory-kind"),
).toBe("memory");
});
it.each([
[
"a late sentence end",
+20 -5
View File
@@ -65,7 +65,7 @@ export function SelectedMemoryDetail({
// (CJK text has none, so it breaks at the limit itself).
const punctuation = memory.content.search(/[。!?!?\n]/);
const space = memory.content.lastIndexOf(" ", HEADING_LIMIT);
const split =
const fallbackSplit =
punctuation > 0 && punctuation < HEADING_LIMIT
? punctuation + 1
: memory.content.length > HEADING_LIMIT
@@ -73,11 +73,26 @@ export function SelectedMemoryDetail({
? space
: HEADING_LIMIT
: -1;
const heading =
split > 0 ? memory.content.slice(0, split).trim() : memory.content;
const body = split > 0 ? memory.content.slice(split).trim() : "";
const fallbackHeading =
fallbackSplit > 0
? memory.content.slice(0, fallbackSplit).trim()
: memory.content;
const fallbackBody =
fallbackSplit > 0 ? memory.content.slice(fallbackSplit).trim() : "";
const experience =
detail.sdk_ref?.kind === "experience" &&
Boolean(detail.sdk_task?.trim() || detail.sdk_solution?.trim());
const heading = experience
? detail.sdk_task?.trim() || fallbackHeading
: fallbackHeading;
const body = experience
? detail.sdk_solution?.trim() || (detail.sdk_task ? "" : fallbackBody)
: fallbackBody;
return (
<article className="selected-memory-detail">
<article
className="selected-memory-detail"
data-memory-kind={experience ? "experience" : "memory"}
>
<p className="selected-memory-status">
{statusLabel}
{!detail.sdk_status &&
+16 -2
View File
@@ -788,12 +788,16 @@ button.rail-button.active::before {
}
.map-list-status {
flex: none;
min-height: 28px;
margin: 0;
padding: 4px;
color: var(--tx3);
font-size: 12px;
line-height: 20px;
}
.map-list-status[data-visible="false"] {
visibility: hidden;
}
.map-filter-notice {
font-size: 12px;
line-height: 1.65;
@@ -879,6 +883,10 @@ button.rail-button.active::before {
background: #fdfefe;
border-color: #c7d3dc;
}
.map-memory-item:disabled {
opacity: 1;
cursor: default;
}
.map-state {
width: 5px;
height: 5px;
@@ -1142,6 +1150,7 @@ button.map-tool {
}
.map-detail-loading {
display: grid;
min-height: 280px;
gap: 10px;
padding: 4px 0;
}
@@ -1152,8 +1161,13 @@ button.map-tool {
}
@keyframes map-detail-enter {
from {
opacity: 0;
transform: translateY(4px);
opacity: 0.35;
}
}
@media (prefers-reduced-motion: no-preference) {
.map-detail-surface,
.map-detail-loading {
animation: map-detail-enter 140ms cubic-bezier(0, 0, 0.2, 1);
}
}
.map-inspector .wb-detail {
+2
View File
@@ -263,12 +263,14 @@
display: block;
flex: none;
min-width: 0;
max-width: 100%;
font-size: 17px;
line-height: 29px;
letter-spacing: normal;
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.selected-memory-source {
margin: 32px 0 22px;