You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
Right now if you create an "app" from your table which has multiple views attached, the views are displayed ontop one another. It makes the app look confusing, if there is much data in them: you have to fully scroll past one view to see another.
Describe the solution you'd like
Display each view on a separate "tab" (not browser tab, just an UI element).
Describe alternatives you've considered
Some template editor would be even better. A good example is FeePBX User Conlrol Panel.
I put together two quick design concepts and tested both live in the browser — this is pure DOM/CSS injected on top of the existing render, not a real Vue component yet, just a way to compare directions before committing to one.
Tab layout — tablesLayoutTabs()
Card layout — tablesLayoutCards()
You can try both yourself: open a context/application page with 2+ tables or views attached, paste the script below into your browser's dev console, and run tablesLayoutTabs(), tablesLayoutCards(), or tablesLayoutReset() to switch between them or revert to the current layout.
/** * Nextcloud Tables — Context page layout prototype * Targets: src/pages/Context.vue → .main-context-view .resources > div > .resource * * HOW TO USE * 1. Open a Tables "Application"/context page that has 2+ tables or views on it. * 2. Open DevTools console, paste this whole file, hit enter once. Nothing * happens yet — it just registers three functions on window. * 3. Try each layout: * tablesLayoutTabs() // tab strip, one resource visible at a time * tablesLayoutCards() // card picker grid, click a card to open it * tablesLayoutReset() // back to stock stacked layout * You can call them repeatedly / switch between them freely. * * Nothing here touches Vue state — it's pure DOM/CSS manipulation layered on * top of the rendered output, so a Vue re-render (e.g. switching context via * the nav, or a row/column mutating) will blow it away. Re-run the function * you want after that happens. This is meant purely to compare the two * visual directions before either gets built as a real Vue component. */;(()=>{constMARKER_ATTR='data-proto-nav'functiongetResources(){constcontainer=document.querySelector('.main-context-view .resources')if(!container){console.warn('[tables-layout-prototype] Could not find .main-context-view .resources — are you on a context/application page with resources on it?')returnnull}constresources=Array.from(container.querySelectorAll(':scope > div > .resource'))if(resources.length===0){console.warn('[tables-layout-prototype] Found the container but no .resource elements inside it.')returnnull}return{ container, resources }}functiongetMeta(resourceEl){consth1=resourceEl.querySelector('.row.first-row h1')consttitle=h1 ? h1.textContent.trim() : '(untitled)'constdescEl=resourceEl.querySelector('.element-description')constdescription=descEl ? descEl.textContent.trim() : ''return{ title, description }}constHIDDEN_ATTR='data-proto-hidden'functionclearInjectedNav(container){container.querySelectorAll(`[${MARKER_ATTR}]`).forEach(el=>el.remove())}functionhideEl(el){if(!el||el.hasAttribute(HIDDEN_ATTR))returnel.setAttribute(HIDDEN_ATTR,'1')el.dataset.protoPrevDisplay=el.style.display||''el.style.display='none'}functionrestoreHidden(root){root.querySelectorAll(`[${HIDDEN_ATTR}]`).forEach(el=>{el.style.display=el.dataset.protoPrevDisplay||''el.removeAttribute(HIDDEN_ATTR)deleteel.dataset.protoPrevDisplay})}functionresetVisibility(resources){resources.forEach(el=>{el.style.display=''el.style.margin=''el.classList.remove('proto-active-resource')})}window.tablesLayoutReset=functiontablesLayoutReset(){constfound=getResources()if(!found)returnclearInjectedNav(found.container)restoreHidden(found.container)resetVisibility(found.resources)console.info('[tables-layout-prototype] Reset to stock stacked layout.')}window.tablesLayoutTabs=functiontablesLayoutTabs(){constfound=getResources()if(!found)returnconst{ container, resources }=foundclearInjectedNav(container)restoreHidden(container)resetVisibility(resources)// title is now conveyed by the tab label itself, so drop the in-resource// title bar and keep only the description + the actual table/viewresources.forEach(el=>hideEl(el.querySelector('.row.first-row')))constnav=document.createElement('div')nav.setAttribute(MARKER_ATTR,'tabs')nav.style.cssText=` position: sticky; top: 0; z-index: 20; display: flex; gap: 4px; padding: 8px 20px 0; background: var(--color-main-background); border-bottom: 1px solid var(--color-border); overflow-x: auto; `constitems=resources.map((el,i)=>getMeta(el))functionactivate(index){resources.forEach((el,i)=>{el.style.display=i===index ? '' : 'none'})Array.from(nav.children).forEach((btn,i)=>{constactive=i===indexbtn.style.borderBottomColor=active ? 'var(--color-primary-element)' : 'transparent'btn.style.color=active ? 'var(--color-main-text)' : 'var(--color-text-maxcontrast)'btn.style.fontWeight=active ? '600' : '400'})}items.forEach((meta,i)=>{constbtn=document.createElement('button')btn.type='button'btn.textContent=meta.titlebtn.title=meta.description||meta.titlebtn.style.cssText=` appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; padding: 10px 14px; font-size: 14px; cursor: pointer; white-space: nowrap; border-radius: var(--border-radius) var(--border-radius) 0 0; `btn.addEventListener('mouseenter',()=>{if(btn.style.borderBottomColor==='transparent')btn.style.background='var(--color-background-hover)'})btn.addEventListener('mouseleave',()=>{btn.style.background='transparent'})btn.addEventListener('click',()=>activate(i))nav.appendChild(btn)})container.insertBefore(nav,container.firstChild)activate(0)console.info(`[tables-layout-prototype] Tab layout applied — ${items.length} tab(s).`)}window.tablesLayoutCards=functiontablesLayoutCards(){constfound=getResources()if(!found)returnconst{ container, resources }=foundclearInjectedNav(container)restoreHidden(container)resetVisibility(resources)// title + description are both already shown on the card, so the// opened resource should just be the table/view itselfresources.forEach(el=>{hideEl(el.querySelector('.row.first-row'))hideEl(el.querySelector('.element-description'))})constgrid=document.createElement('div')grid.setAttribute(MARKER_ATTR,'cards')grid.style.cssText=` position: sticky; top: 0; z-index: 20; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; padding: 16px 20px; background: var(--color-main-background); border-bottom: 1px solid var(--color-border); `constitems=resources.map(el=>getMeta(el))functionactivate(index){resources.forEach((el,i)=>{el.style.display=i===index ? '' : 'none'})Array.from(grid.children).forEach((card,i)=>{card.style.borderColor=i===index ? 'var(--color-primary-element)' : 'var(--color-border)'card.style.background=i===index ? 'var(--color-primary-element-light)' : 'var(--color-main-background)'})// scroll the opened resource into view since the grid pushes it downresources[index].scrollIntoView({behavior: 'smooth',block: 'nearest'})}items.forEach((meta,i)=>{constcard=document.createElement('div')card.style.cssText=` border: 2px solid var(--color-border); border-radius: var(--border-radius-large, 12px); padding: 14px; cursor: pointer; background: var(--color-main-background); transition: border-color .1s ease, background .1s ease; `consttitle=document.createElement('div')title.textContent=meta.titletitle.style.cssText='font-weight: 600; font-size: 15px; margin-bottom: 4px;'constdesc=document.createElement('div')desc.textContent=meta.description
? (meta.description.length>80 ? meta.description.slice(0,80)+'…' : meta.description)
: ''desc.style.cssText='font-size: 12px; color: var(--color-text-maxcontrast); line-height: 1.3;'card.appendChild(title)card.appendChild(desc)card.addEventListener('click',()=>activate(i))grid.appendChild(card)})container.insertBefore(grid,container.firstChild)activate(0)console.info(`[tables-layout-prototype] Card layout applied — ${items.length} card(s). Click a card to switch.`)}console.info('[tables-layout-prototype] Ready. Run tablesLayoutTabs(), tablesLayoutCards(), or tablesLayoutReset().')})()
I'm leaning toward the card layout — it scales better once there are more than a couple of resources on a context, since the tab bar can get crowded while cards give each resource more room to show a meaningful preview of its title/description.
Before I put together a Vibe coded PR: I would go for this to be a straight drop-in replacement for the current stacked layout (no setting, no toggle, just the new default).
If cards is the preferred direction, I think it'd be worth reusing an existing card component/style already used in the tables app Nextcloud rather than inventing a new one — for example something close to the style used for adding a new table
Happy to open a PR once there's agreement on direction and that this will be purely vibe coded.
Mm will try to submit something at the end of the week end. I went against my own advice to implement this as a new default and now i am trying to add this with an option. But this adds migration steps and stuff 😢 . @enjeck Could i submit something as a draft if i do not manage to get the migration working ?
Activity
Aveyron-RetD commentedon Apr 22, 2026
similar topic as #1913
RTechSn commentedon Apr 22, 2026
#1913 fits "Describe alternatives you've considered", but demands much more development time than suggested in "Describe the solution you'd like"
blizzz commentedon Apr 22, 2026
Thanks for your suggestion on the tabbed layout!
Aveyron-RetD commentedon Jul 16, 2026
I put together two quick design concepts and tested both live in the browser — this is pure DOM/CSS injected on top of the existing render, not a real Vue component yet, just a way to compare directions before committing to one.
Tab layout —

tablesLayoutTabs()Card layout —

tablesLayoutCards()You can try both yourself: open a context/application page with 2+ tables or views attached, paste the script below into your browser's dev console, and run
tablesLayoutTabs(),tablesLayoutCards(), ortablesLayoutReset()to switch between them or revert to the current layout.I'm leaning toward the card layout — it scales better once there are more than a couple of resources on a context, since the tab bar can get crowded while cards give each resource more room to show a meaningful preview of its title/description.
Before I put together a Vibe coded PR: I would go for this to be a straight drop-in replacement for the current stacked layout (no setting, no toggle, just the new default).
If cards is the preferred direction, I think it'd be worth reusing an existing card component/style already used in the tables app Nextcloud rather than inventing a new one — for example something close to the style used for adding a new table
Happy to open a PR once there's agreement on direction and that this will be purely vibe coded.
Aveyron-RetD commentedon Jul 22, 2026
@enjeck ^^ and @marcoambrosini ^^
taiebot commentedon Sep 21, 2026
Ok this is not a mockup but a real implementation.
I am struggling with vue, scrolling and positioning.
Will try to submit this shortly
taiebot commentedon Sep 30, 2026
Mm will try to submit something at the end of the week end. I went against my own advice to implement this as a new default and now i am trying to add this with an option. But this adds migration steps and stuff 😢 . @enjeck Could i submit something as a draft if i do not manage to get the migration working ?
taiebot commentedon Oct 1, 2026
well here we go #3054