Compare commits

...

2 Commits

Author SHA1 Message Date
nntrivi2001 db5a7776cc chore: v2.8.37 — the Artifacts installs list is never cut off 2026-10-29 11:25:10 +07:00
nntrivi2001 6c2c094f64 fix(artifacts): the installs list floats over the page, never cut off
It was an absolute box inside the card list, whose overflow-hidden clipped it
at the last card, and it opened to the right off a button near the edge. It
is now a portal at the button: below it, above when there is no room, kept
inside the window, following scroll and resize.
2026-10-29 11:25:10 +07:00
6 changed files with 107 additions and 67 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Claude Code Working Guide # Claude Code Working Guide
_Last synced: v2.8.36 (2026-10-29) — an HTML page opens in a modal over the screen (Changes Preview, Chat)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. _Last synced: v2.8.37 (2026-10-29) — the Artifacts page's installs list floats over the page, never clipped._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
## Project mission ## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring. - Maintain a reliable local-first dashboard for Claude Code session monitoring.
+41 -6
View File
@@ -48,7 +48,8 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn> * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/ */
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ChevronRight, ChevronRight,
@@ -1629,12 +1630,41 @@ function DetailDialog({
); );
} }
/** "N installs" on a card; a click lists who installed it, when, and where. */ /** "N installs" on a card; a click lists who installed it, when, and where.
* The list floats over the page (a portal at the button, below it or above
* when there is no room, kept inside the window): inside the card list's
* `overflow-hidden` it was cut off at the last card. */
function InstallsButton({ item }: { item: Artifact }) { function InstallsButton({ item }: { item: Artifact }) {
const { t } = useTranslation("artifacts"); const { t } = useTranslation("artifacts");
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [data, setData] = useState<ArtifactInstalls | null>(null); const [data, setData] = useState<ArtifactInstalls | null>(null);
const box = useRef<HTMLSpanElement>(null); const box = useRef<HTMLSpanElement>(null);
const pop = useRef<HTMLDivElement>(null);
const [at, setAt] = useState<{ left: number; top: number } | null>(null);
useLayoutEffect(() => {
if (!open) return setAt(null);
const place = () => {
const b = box.current?.getBoundingClientRect();
const p = pop.current;
if (!b || !p) return;
const w = p.offsetWidth;
const h = p.offsetHeight;
const gap = 4;
const below = b.bottom + gap + h <= window.innerHeight - 8 || b.top - gap - h < 8;
setAt({
left: Math.max(8, Math.min(b.left, window.innerWidth - w - 8)),
top: below ? b.bottom + gap : b.top - gap - h,
});
};
place();
window.addEventListener("resize", place);
window.addEventListener("scroll", place, true);
return () => {
window.removeEventListener("resize", place);
window.removeEventListener("scroll", place, true);
};
}, [open, data]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
@@ -1647,7 +1677,8 @@ function InstallsButton({ item }: { item: Artifact }) {
toast.error(err); toast.error(err);
}); });
const onDown = (e: MouseEvent) => { const onDown = (e: MouseEvent) => {
if (!box.current?.contains(e.target as Node)) setOpen(false); const n = e.target as Node;
if (!box.current?.contains(n) && !pop.current?.contains(n)) setOpen(false);
}; };
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false); if (e.key === "Escape") setOpen(false);
@@ -1670,12 +1701,15 @@ function InstallsButton({ item }: { item: Artifact }) {
> >
{t("installs", { count: item.installs })} {t("installs", { count: item.installs })}
</button> </button>
{open && ( {open &&
createPortal(
<div <div
ref={pop}
role="dialog" role="dialog"
aria-label={t("installsTitle")} aria-label={t("installsTitle")}
data-testid="artifact-installs" data-testid="artifact-installs"
className="absolute left-0 z-30 mt-1 w-80 max-w-[80vw] rounded-lg border border-border bg-surface-1 p-2 shadow-xl" style={at ? { left: at.left, top: at.top } : { left: 0, top: 0, visibility: "hidden" }}
className="fixed z-[60] w-80 max-w-[80vw] rounded-lg border border-border bg-surface-1 p-2 shadow-xl"
> >
<p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p> <p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p>
{data === null ? ( {data === null ? (
@@ -1728,7 +1762,8 @@ function InstallsButton({ item }: { item: Artifact }) {
)} )}
</ul> </ul>
)} )}
</div> </div>,
document.body
)} )}
</span> </span>
); );
@@ -362,6 +362,11 @@ describe("Artifacts", () => {
expect(await within(box).findByText("/h/.claude/skills/deploy")).toBeInTheDocument(); expect(await within(box).findByText("/h/.claude/skills/deploy")).toBeInTheDocument();
expect(within(box).getByText(/khoa/)).toBeInTheDocument(); expect(within(box).getByText(/khoa/)).toBeInTheDocument();
expect(within(box).getByText(/1 earlier installs/)).toBeInTheDocument(); expect(within(box).getByText(/1 earlier installs/)).toBeInTheDocument();
// Over the page, outside the card list that clips its overflow — and a
// click inside it keeps it open.
expect(box.parentElement).toBe(document.body);
await user.click(within(box).getByText(/khoa/));
expect(screen.getByTestId("artifact-installs")).toBeInTheDocument();
await user.keyboard("{Escape}"); await user.keyboard("{Escape}");
expect(screen.queryByTestId("artifact-installs")).toBeNull(); expect(screen.queryByTestId("artifact-installs")).toBeNull();
}); });
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
<span <span
class="text-status-warning/90" class="text-status-warning/90"
> >
UI build v2.8.36 (rebuild client to match) UI build v2.8.37 (rebuild client to match)
</span> </span>
</p> </p>
</div> </div>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.36", "version": "2.8.37",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.8.36", "version": "2.8.37",
"hasInstallScript": true, "hasInstallScript": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.8.36", "version": "2.8.37",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },