Confirm dialog
A destructive confirmation shown open: what will be deleted, what happens next, and a typed workspace name that unlocks the red Delete button. Placement and the backdrop are the project's.
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<style>
.odl-action-menu { box-sizing: border-box; inline-size: 100%; max-inline-size: 18rem; margin-inline: auto; display: grid; justify-items: end; gap: var(--ds-space-4);
color: var(--ds-color-text-primary); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-action-menu *, .odl-action-menu *::before { box-sizing: border-box; }
.odl-action-menu .odl-trigger { display: inline-flex; align-items: center; gap: var(--ds-space-inline-xs); min-block-size: max(var(--ds-size-control-md), var(--ds-size-target-min));
padding: 0 var(--ds-space-inset-sm); border: var(--ds-border-width-default) solid var(--ds-color-border-default); border-radius: var(--ds-radius-control);
background: var(--ds-color-surface-raised); color: var(--ds-color-text-primary); font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-label); cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-hover); }
.odl-action-menu .odl-trigger:hover, .odl-action-menu .odl-trigger[aria-expanded="true"] { background: var(--ds-color-bg-neutral-subtle); }
.odl-action-menu .odl-trigger svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); color: var(--ds-color-icon-subtle); }
.odl-action-menu [role="menu"] { inline-size: 100%; margin: 0; padding: var(--ds-space-4); list-style: none; display: grid;
background: var(--ds-color-surface-overlay); border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-overlay);
box-shadow: var(--ds-elevation-floating); transform-origin: top right; animation: odl-action-menu-in var(--ds-motion-duration-short) var(--ds-motion-easing-enter) both; }
.odl-action-menu [role="menu"][hidden] { display: none; }
@keyframes odl-action-menu-in { from { opacity: 0; transform: scale(var(--ds-motion-scale-enter)); } }
.odl-action-menu .odl-group { padding: var(--ds-space-6) var(--ds-space-inset-xs) var(--ds-space-2); color: var(--ds-color-text-tertiary); font: var(--ds-text-label-sm); letter-spacing: var(--ds-text-label-sm-tracking); }
.odl-action-menu [role="separator"] { block-size: var(--ds-border-width-default); margin: var(--ds-space-4) calc(-1 * var(--ds-space-4)); background: var(--ds-color-border-subtle); }
.odl-action-menu [role="menuitem"] { display: grid; grid-template-columns: var(--ds-size-icon-sm) minmax(0, 1fr) auto; gap: var(--ds-space-inline-sm); align-items: center;
min-block-size: max(var(--ds-size-row-sm), var(--ds-size-target-min)); padding: 0 var(--ds-space-inset-xs); border-radius: var(--ds-radius-nested); cursor: pointer; outline: none; }
.odl-action-menu [role="menuitem"] > svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); color: var(--ds-color-icon-subtle); }
.odl-action-menu [role="menuitem"]:hover, .odl-action-menu [role="menuitem"]:focus { background: var(--ds-color-bg-neutral-subtle); }
.odl-action-menu [role="menuitem"]:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: calc(-1 * var(--ds-focus-ring-width)); }
.odl-action-menu [role="menuitem"]:hover > svg, .odl-action-menu [role="menuitem"]:focus > svg { color: var(--ds-color-icon-default); }
.odl-action-menu kbd { display: inline-flex; gap: var(--ds-space-2); font: var(--ds-text-body-sm); font-family: var(--ds-font-family-text); color: var(--ds-color-text-tertiary); white-space: nowrap; }
.odl-action-menu .odl-sub > span:last-child svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); color: var(--ds-color-icon-subtle); display: block; }
.odl-action-menu [aria-disabled="true"] { cursor: not-allowed; color: var(--ds-color-text-disabled); }
.odl-action-menu [aria-disabled="true"] > svg, .odl-action-menu [aria-disabled="true"] small { color: var(--ds-color-text-disabled); }
.odl-action-menu [aria-disabled="true"]:hover { background: transparent; }
.odl-action-menu small { font: var(--ds-text-body-sm); white-space: nowrap; }
.odl-action-menu .odl-danger, .odl-action-menu .odl-danger > svg { color: var(--ds-color-text-danger); }
.odl-action-menu .odl-danger:hover, .odl-action-menu .odl-danger:focus { background: var(--ds-color-bg-danger-subtle); }
.odl-action-menu .odl-danger:hover > svg, .odl-action-menu .odl-danger:focus > svg { color: var(--ds-color-text-danger); }
.odl-action-menu .odl-danger kbd { color: var(--ds-color-text-tertiary); }
.odl-action-menu .odl-trigger:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
@media (prefers-reduced-motion: reduce) { .odl-action-menu, .odl-action-menu *, .odl-action-menu *::before { transition: none !important; animation: none !important; } }
</style>
<div class="odl-action-menu">
<button type="button" class="odl-trigger" aria-haspopup="menu" aria-expanded="true" aria-controls="odl-am-menu">More<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="m4 6 4 4 4-4"/></svg></button>
<div role="menu" id="odl-am-menu" aria-label="Bill actions" tabindex="-1">
<div class="odl-group" role="presentation">Bill</div>
<div role="menuitem" tabindex="0"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M10.5 2.5 13.5 5.5 6 13H3v-3z"/></svg><span>Edit details</span><kbd>E</kbd></div>
<div role="menuitem" tabindex="-1"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5V3.5a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg><span>Duplicate</span><kbd><span>⌘</span><span>D</span></kbd></div>
<div role="menuitem" tabindex="-1" class="odl-sub" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M2.5 4.5a1 1 0 0 1 1-1h3l1.5 1.5h4.5a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1z"/></svg><span>Move to</span><span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="m6 4 4 4-4 4"/></svg></span></div>
<div role="menuitem" tabindex="-1"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M8 2.5v8M4.5 7 8 10.5 11.5 7M3 13.5h10"/></svg><span>Download PDF</span><kbd><span>⇧</span><span>D</span></kbd></div>
<div role="separator"></div>
<div class="odl-group" role="presentation">Payment</div>
<div role="menuitem" tabindex="-1"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M6.5 9.5 9.5 6.5M7 4.5l1.2-1.2a2.5 2.5 0 0 1 3.5 3.5L10.5 8M5.5 8 4.3 9.2a2.5 2.5 0 0 0 3.5 3.5L9 11.5"/></svg><span>Copy payment link</span><kbd><span>⌘</span><span>L</span></kbd></div>
<div role="menuitem" tabindex="-1" aria-disabled="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="8" cy="8" r="5.5"/><path d="m5.7 8.2 1.6 1.6 3-3.3"/></svg><span>Mark as paid</span><small>Needs approval</small></div>
<div role="separator"></div>
<div role="menuitem" tabindex="-1" class="odl-danger"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M2.5 4.5h11M6.5 4.5V3h3v1.5M4 4.5l.7 8.5h6.6l.7-8.5"/></svg><span>Delete bill</span><kbd><span>⌘</span><span>⌫</span></kbd></div>
</div>
</div>
<script>
document.querySelectorAll(".odl-action-menu").forEach((root, n) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
const btn = root.querySelector(".odl-trigger"), menu = root.querySelector('[role="menu"]');
menu.id = "odl-am-menu-" + n; btn.setAttribute("aria-controls", menu.id);
const items = [...menu.querySelectorAll('[role="menuitem"]')];
const focusAt = (i) => { const k = (i + items.length) % items.length; items.forEach((x, j) => { x.tabIndex = j === k ? 0 : -1; }); items[k].focus(); };
const setOpen = (open, toItem) => { menu.hidden = !open; btn.setAttribute("aria-expanded", String(open)); if (open && toItem != null) focusAt(toItem); };
btn.addEventListener("click", () => setOpen(menu.hidden, 0));
btn.addEventListener("keydown", (e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setOpen(true, e.key === "ArrowDown" ? 0 : items.length - 1); } });
menu.addEventListener("keydown", (e) => {
const i = items.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); focusAt(i + 1); }
else if (e.key === "ArrowUp") { e.preventDefault(); focusAt(i - 1); }
else if (e.key === "Home") { e.preventDefault(); focusAt(0); }
else if (e.key === "End") { e.preventDefault(); focusAt(items.length - 1); }
else if (e.key === "Escape" || e.key === "Tab") { if (e.key === "Escape") e.preventDefault(); setOpen(false); btn.focus(); }
else if ((e.key === "Enter" || e.key === " ") && i > -1) { e.preventDefault(); items[i].click(); }
});
items.forEach((it, j) => {
it.addEventListener("mousemove", () => { if (document.activeElement !== it && menu.contains(document.activeElement)) focusAt(j); });
it.addEventListener("click", () => { if (it.getAttribute("aria-disabled") === "true" || it.hasAttribute("aria-haspopup")) return; setOpen(false); btn.focus(); });
});
});
</script>
A destructive confirmation shown open: what will be deleted, what happens next, and a typed workspace name that unlocks the red Delete button. Placement and the backdrop are the project's.
A side sheet for one record shown open: name, status, previous and next, the key fields, an attachment and a sticky footer with the two decisions. Where it slides in from is the project's.
A dense table with row checkboxes, status pills and a bar of bulk actions that appears, with a live count, as soon as a row is picked.