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.
+97 -62
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,66 +1701,70 @@ function InstallsButton({ item }: { item: Artifact }) {
> >
{t("installs", { count: item.installs })} {t("installs", { count: item.installs })}
</button> </button>
{open && ( {open &&
<div createPortal(
role="dialog" <div
aria-label={t("installsTitle")} ref={pop}
data-testid="artifact-installs" role="dialog"
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" aria-label={t("installsTitle")}
> data-testid="artifact-installs"
<p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p> style={at ? { left: at.left, top: at.top } : { left: 0, top: 0, visibility: "hidden" }}
{data === null ? ( className="fixed z-[60] w-80 max-w-[80vw] rounded-lg border border-border bg-surface-1 p-2 shadow-xl"
<p className="px-1 text-xs text-fg-muted">{t("loading")}</p> >
) : ( <p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p>
<ul className="max-h-64 space-y-1 overflow-auto"> {data === null ? (
{data.items.map((i, n) => ( <p className="px-1 text-xs text-fg-muted">{t("loading")}</p>
<li key={n} className="rounded px-1.5 py-1 text-xs hover:bg-surface-2"> ) : (
<div className="flex items-center gap-2"> <ul className="max-h-64 space-y-1 overflow-auto">
<span className="min-w-0 flex-1 truncate text-fg-primary"> {data.items.map((i, n) => (
{i.mine ? t("installsYou") : (i.user ?? t("installsUnknown"))} <li key={n} className="rounded px-1.5 py-1 text-xs hover:bg-surface-2">
{i.replaced && ( <div className="flex items-center gap-2">
<span className="ml-1 text-fg-muted">· {t("installsReplaced")}</span> <span className="min-w-0 flex-1 truncate text-fg-primary">
)} {i.mine ? t("installsYou") : (i.user ?? t("installsUnknown"))}
</span> {i.replaced && (
<span className="shrink-0 text-[11px] text-fg-muted"> <span className="ml-1 text-fg-muted">· {t("installsReplaced")}</span>
{new Date(i.at).toLocaleString()} )}
</span> </span>
</div> <span className="shrink-0 text-[11px] text-fg-muted">
{i.paths && ( {new Date(i.at).toLocaleString()}
<div className="mt-0.5 space-y-0.5"> </span>
{i.paths.length ? (
i.paths.map((p) => (
<p
key={p}
className="truncate font-mono text-[10px] text-fg-muted"
title={p}
>
{p}
</p>
))
) : (
<p className="font-mono text-[10px] text-fg-muted">
{t("installsPluginWhere")}
</p>
)}
</div> </div>
)} {i.paths && (
</li> <div className="mt-0.5 space-y-0.5">
))} {i.paths.length ? (
{data.unrecorded > 0 && ( i.paths.map((p) => (
<li className="px-1.5 py-1 text-[11px] text-fg-muted"> <p
{t("installsUnrecorded", { count: data.unrecorded })} key={p}
</li> className="truncate font-mono text-[10px] text-fg-muted"
)} title={p}
{data.items.length === 0 && data.unrecorded === 0 && ( >
<li className="px-1.5 py-1 text-[11px] text-fg-muted"> {p}
{t("installsNoneOfYours")} </p>
</li> ))
)} ) : (
</ul> <p className="font-mono text-[10px] text-fg-muted">
)} {t("installsPluginWhere")}
</div> </p>
)} )}
</div>
)}
</li>
))}
{data.unrecorded > 0 && (
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
{t("installsUnrecorded", { count: data.unrecorded })}
</li>
)}
{data.items.length === 0 && data.unrecorded === 0 && (
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
{t("installsNoneOfYours")}
</li>
)}
</ul>
)}
</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"
}, },