feat(tabs): close-all button with confirmation dialog
Adds a ✕ button next to the + tab button that closes all open rApp tabs and returns to the user dashboard. Shows a confirm() prompt with tab count before proceeding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de698a2aa3
commit
dd38dcb631
|
|
@ -1267,6 +1267,42 @@ export function renderShell(opts: ShellOptions): string {
|
|||
}
|
||||
});
|
||||
|
||||
tabBar.addEventListener('layers-close-all', () => {
|
||||
// Track all closed modules so server merge doesn't resurrect them
|
||||
layers.forEach(l => _closedModuleIds.add(l.moduleId));
|
||||
// Clean up space layer refs
|
||||
const token = document.cookie.match(/encryptid_token=([^;]+)/)?.[1];
|
||||
layers.forEach(l => {
|
||||
if (l.spaceSlug && l._spaceRefId && token) {
|
||||
fetch('/api/spaces/' + encodeURIComponent(spaceSlug) + '/nest/' + encodeURIComponent(l._spaceRefId), {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
// Remove all cached panes
|
||||
if (tabCache) {
|
||||
layers.forEach(l => tabCache.removePane(l.moduleId));
|
||||
tabCache.hideAllPanes();
|
||||
}
|
||||
layers = [];
|
||||
saveTabs();
|
||||
// Show the dashboard
|
||||
const dashboard = document.querySelector('rstack-user-dashboard');
|
||||
if (dashboard) {
|
||||
dashboard.style.display = '';
|
||||
if (dashboard.refresh) dashboard.refresh();
|
||||
if (dashboard.setOpenTabs) dashboard.setOpenTabs([]);
|
||||
}
|
||||
const app = document.getElementById('app');
|
||||
if (app) app.classList.remove('canvas-layout');
|
||||
tabBar.setAttribute('active', '');
|
||||
tabBar.setLayers([]);
|
||||
currentModuleId = '';
|
||||
var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug;
|
||||
history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl);
|
||||
});
|
||||
|
||||
tabBar.addEventListener('layer-reorder', (e) => {
|
||||
const { layerId, newIndex } = e.detail;
|
||||
const oldIdx = layers.findIndex(l => l.id === layerId);
|
||||
|
|
@ -1482,6 +1518,9 @@ export function renderShell(opts: ShellOptions): string {
|
|||
tabBar.addEventListener('layer-close', (e) => {
|
||||
sync.removeLayer(e.detail.layerId);
|
||||
});
|
||||
tabBar.addEventListener('layers-close-all', () => {
|
||||
sync.getLayers().forEach(l => sync.removeLayer(l.id));
|
||||
});
|
||||
tabBar.addEventListener('layer-reorder', (e) => {
|
||||
const { layerId, newIndex } = e.detail;
|
||||
const all = sync.getLayers(); // already sorted by order
|
||||
|
|
@ -1822,6 +1861,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
|
||||
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
|
||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||
tabBar.addEventListener('layers-close-all', () => { layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||
tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* layer-switch — fired when user clicks a tab { detail: { layerId, moduleId } }
|
||||
* layer-add — fired when user clicks + to add a layer
|
||||
* layer-close — fired when user closes a tab { detail: { layerId } }
|
||||
* layers-close-all — fired when user clicks close-all button (no detail)
|
||||
* layer-reorder — fired on drag reorder { detail: { layerId, newIndex } }
|
||||
* view-toggle — fired when switching flat/stack view { detail: { mode } }
|
||||
* flow-select — fired when a flow is clicked in stack view { detail: { flowId } }
|
||||
|
|
@ -426,6 +427,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
</div>
|
||||
<div class="tab-add-wrap">
|
||||
<button class="tab-add" id="add-btn" title="Add layer">+</button>
|
||||
${this.#layers.length > 1 ? `<button class="tab-close-all" id="close-all-btn" title="Close all tabs">✕</button>` : ""}
|
||||
${this.#addMenuOpen ? this.#renderAddMenu() : ""}
|
||||
</div>
|
||||
<div class="tab-actions">
|
||||
|
|
@ -1098,6 +1100,20 @@ export class RStackTabBar extends HTMLElement {
|
|||
addBtn?.addEventListener("click", openAppSwitcher);
|
||||
addBtn?.addEventListener("touchend", openAppSwitcher);
|
||||
|
||||
// Close-all button
|
||||
const closeAllBtn = this.#shadow.getElementById("close-all-btn");
|
||||
if (closeAllBtn) {
|
||||
const handleCloseAll = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const count = this.#layers.length;
|
||||
if (!confirm(`Close all ${count} rApp tabs?`)) return;
|
||||
this.dispatchEvent(new CustomEvent("layers-close-all", { bubbles: true }));
|
||||
};
|
||||
closeAllBtn.addEventListener("click", handleCloseAll);
|
||||
closeAllBtn.addEventListener("touchend", handleCloseAll);
|
||||
}
|
||||
|
||||
// Add menu items — click + touch support
|
||||
this.#shadow.querySelectorAll<HTMLElement>(".add-menu-item").forEach(item => {
|
||||
const handleSelect = (e: Event) => {
|
||||
|
|
@ -1613,6 +1629,8 @@ const STYLES = `
|
|||
/* ── Add button ── */
|
||||
|
||||
.tab-add-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
|
|
@ -1642,6 +1660,31 @@ const STYLES = `
|
|||
background: var(--rs-bg-hover);
|
||||
color: var(--rs-text-primary);
|
||||
}
|
||||
.tab-close-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
min-height: 44px;
|
||||
border: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background: transparent;
|
||||
color: var(--rs-text-muted);
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tab-close-all:hover, .tab-close-all:active {
|
||||
background: rgba(239,68,68,0.15);
|
||||
color: #ef4444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Add menu ── */
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue