generated from coulomb/repo-seed
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>
This commit is contained in:
101
dashboard/src/components/ref-cell.js
Normal file
101
dashboard/src/components/ref-cell.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user