Files
state-hub/dashboard/src/components/ref-cell.js
tegwick b28298a2ec feat(dashboard): entity list UX — REF column, name cells, detail pages (CUST-WP-0030)
- ref-cell.js: REF column component — click=copy deeplink, dblclick=open
- field-help.js: field registry + fieldRow helper with help-tip decoration;
  FK fields (task_id, workstream_id, repo_id) render as async-linked cells
  with entity-title bubble-help on hover
- GET /token-events/{id} endpoint + get-by-id tests
- GET /repos/by-id/{repo_id} UUID lookup endpoint
- Landing pages: /token-events/[id], /workstreams/[id], /repos/[slug], /tasks/[id]
- token-cost.md: REF + Name columns on all three tables; parallel fetch of
  workstreams/tasks for title resolution
- reference.md: entity detail page URL scheme documented

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 22:35:35 +02:00

102 lines
2.9 KiB
JavaScript

// refCell(index, recordType, id) → HTMLElement
//
// Renders a 1-based row number in a table cell.
// Single click — copies deep-link to clipboard and flashes "Copied!".
// Double click — opens deep-link in a new tab.
//
// Deep-link format: <origin>/data/<recordType>/<id>
//
// Usage:
// import {refCell} from "./components/ref-cell.js";
// // in an Inputs.table format callback:
// format: { id: (_, i) => refCell(i + 1, "token-events", row.id) }
const _STYLE_ID = "refcell-global-style";
if (!document.getElementById(_STYLE_ID)) {
const s = document.createElement("style");
s.id = _STYLE_ID;
s.textContent = `
.ref-cell {
display: inline-block;
font-family: var(--monospace, monospace);
font-size: 0.78rem;
color: var(--theme-foreground-focus, #3b82f6);
cursor: pointer;
user-select: none;
padding: 0 2px;
border-radius: 3px;
transition: background 0.1s;
}
.ref-cell:hover {
background: var(--theme-foreground-faint, #e8f0fe);
}
.ref-cell-toast {
position: fixed;
z-index: 10000;
background: var(--theme-background, #fff);
border: 1px solid var(--theme-foreground-faint, #ddd);
border-radius: 6px;
padding: 0.3rem 0.65rem;
font-size: 0.75rem;
color: var(--theme-foreground, #333);
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
opacity: 0;
transition: opacity 0.1s ease;
pointer-events: none;
}
.ref-cell-toast.ref-cell-toast-visible { opacity: 1; }
`;
document.head.appendChild(s);
}
function _showToast(anchorEl, text) {
const toast = document.createElement("div");
toast.className = "ref-cell-toast";
toast.textContent = text;
document.body.appendChild(toast);
const rect = anchorEl.getBoundingClientRect();
const gap = 6;
toast.style.left = `${rect.left}px`;
toast.style.top = `${rect.top - toast.offsetHeight - gap}px`;
requestAnimationFrame(() => toast.classList.add("ref-cell-toast-visible"));
setTimeout(() => {
toast.classList.remove("ref-cell-toast-visible");
toast.addEventListener("transitionend", () => toast.remove(), {once: true});
}, 1200);
}
export function refCell(index, recordType, id) {
const deepLink = `${location.origin}/${recordType}/${id}`;
const el = document.createElement("span");
el.className = "ref-cell";
el.title = `Click to copy link · Double-click to open\n${deepLink}`;
el.textContent = String(index);
let clickTimer = null;
el.addEventListener("click", (e) => {
e.stopPropagation();
// Use a short delay so a double-click cancels the single-click handler.
clickTimer = setTimeout(async () => {
try {
await navigator.clipboard.writeText(deepLink);
_showToast(el, "Copied!");
} catch {
// Fallback for environments where clipboard API is blocked.
_showToast(el, deepLink);
}
}, 180);
});
el.addEventListener("dblclick", (e) => {
e.stopPropagation();
clearTimeout(clickTimer);
window.open(deepLink, "_blank", "noopener,noreferrer");
});
return el;
}