FIX - stabilize homescreen dragging across scaling (#18)

* FIX - stabilize homescreen dragging across scaling

Render the home drag preview in an unzoomed viewport portal and normalize FiveM CEF DOM geometry before hit testing and drop settling. Cover Chrome 103, fractional scaling, 4K, ultrawide, and non-16:9 viewport behavior.

* FIX - Implement LB guest app layout and lifecycle bridge

Ensure custom LB apps are properly sized and visible by applying the required CSS contract on initialization and upon receiving the 'componentsLoaded' signal.

* FIX - match uppercase script tags in LB bridge test
This commit is contained in:
DerEchteAlec
2026-08-20 19:14:00 +02:00
committed by GitHub
parent 36b859cabb
commit dca0868753
13 changed files with 814 additions and 57 deletions
@@ -94,36 +94,99 @@ describe('Springboard page swipe contract', () => {
expect(folderIconSource).toContain(':style="dragPointerStyle"')
})
it('uses the same scale-aware in-phone drag path as widgets', () => {
expect(viewSource).not.toContain('source.cloneNode(true)')
expect(viewSource).not.toContain('<Teleport to="body">')
expect(viewSource).not.toContain(':external-drag-visual')
it('renders the home drag visual through the unzoomed phone portal', () => {
expect(viewSource).toContain('source.cloneNode(true)')
expect(viewSource).toContain(
"'springboard--home-dragging': draggingHomeApp !== null",
'<Teleport defer to="#phone-home-drag-portal">',
)
expect(viewSource).toContain('draggedElement?.getBoundingClientRect()')
expect(appIconSource).not.toContain('externalDragVisual')
expect(folderIconSource).not.toContain('externalDragVisual')
expect(mainCss).not.toContain('.home-drag-layer')
expect(mainCss).not.toContain('.home-drag-ghost')
expect(mainCss).not.toContain('.app-icon-item--drag-source')
expect(viewSource).toContain(
'<div ref="homeDragLayer" class="home-drag-layer" aria-hidden="true"></div>',
)
expect(viewSource).toContain('readPhoneViewportGeometry')
expect(viewSource).toMatch(/const sourceBounds = \w+\.rect\(source\)/)
expect(viewSource).toMatch(/const clipBounds = \w+\.rect\(\w+\)/)
expect(viewSource).not.toContain('homeDragLocalPoint')
expect(viewSource).not.toContain('springboardViewportToLocal')
expect(viewSource).toContain("position.className = 'home-drag-position'")
expect(viewSource).toMatch(
/homeDragGrip = \{\s*x: event\.clientX - sourceBounds\.left,\s*y: event\.clientY - sourceBounds\.top,/,
)
expect(viewSource).toMatch(
/ghost\.style\.transform = `scale\(\$\{\w+\.scaleX\}, \$\{\w+\.scaleY\}\)`/,
)
expect(
viewSource.match(/:external-drag-visual="homeDragVisualActive"/g),
).toHaveLength(4)
expect(viewSource).toContain('updateHomeDragGhost(event)')
expect(viewSource).toContain('const dropGhost = homeDragGhost')
expect(viewSource).toContain('const dropOrigin = homeDragPreviewBounds')
expect(viewSource).not.toContain('dropGhost?.getBoundingClientRect()')
expect(viewSource).toContain('clearHomeDragGhost(dropGhost)')
expect(viewSource).not.toContain('draggedElement?.getBoundingClientRect()')
expect(viewSource).not.toContain('springboard--home-dragging')
expect(appIconSource).toContain('externalDragVisual?: boolean')
expect(folderIconSource).toContain('externalDragVisual?: boolean')
expect(appIconSource).toContain('app-icon-item--drag-source')
expect(folderIconSource).toContain('app-icon-item--drag-source')
expect(mainCss).toMatch(
/\.springboard--home-dragging \.springboard-page--apps\s*\{[^}]*overflow:\s*visible;/s,
/\.phone-home-drag-portal\s*\{[^}]*pointer-events:\s*none;/s,
)
expect(mainCss).toContain('.home-drag-position')
expect(mainCss).toContain('.home-drag-ghost')
expect(mainCss).toContain('.app-icon-item--drag-source')
expect(mainCss).toMatch(/\.springboard\s*\{[\s\S]*?overflow:\s*hidden;/)
expect(mainCss).toMatch(
/\.springboard-page--apps\s*\{[^}]*overflow:\s*hidden;/s,
)
})
it('cleans up the external drag visual on every terminal path', () => {
expect(
viewSource.match(
/@dragstart="startHomeDrag\('(grid|dock)', [^,]+, \$event\)"/g,
),
).toHaveLength(4)
expect(viewSource.match(/@dragcancel="stopHomeDrag"/g)).toHaveLength(4)
expect(viewSource).toContain(
'if (expectedGhost && homeDragGhost !== expectedGhost) return',
)
expect(viewSource).toMatch(
/if \(!dragged\) \{\s*clearHomeDragGhost\(\)\s*return/,
)
expect(viewSource).toMatch(
/animateHomeItemDrop\(\s*draggedItem,\s*dropArea,\s*dropIndex,\s*dropOrigin,?\s*\)\.finally\(/,
)
expect(viewSource).toMatch(
/function stopHomeDrag[\s\S]*?clearHomeDragGhost\(\)/,
)
expect(viewSource).toMatch(
/if \(folderId\) \{[\s\S]*?clearHomeDragGhost\(\)/,
)
expect(viewSource).toMatch(
/onBeforeUnmount\(\(\) => \{[\s\S]*?clearHomeDragGhost\(\)/,
)
})
it('targets the active visual page and lets grid or dock drags turn pages', () => {
expect(viewSource).toContain(':data-home-page="page.page"')
expect(viewSource).toContain(':data-home-target-offset="cell.targetOffset"')
expect(viewSource).toContain('nearestGridDropTarget')
expect(viewSource).toContain('moveHomeAppToGridPage')
expect(viewSource).not.toContain("draggingHomeApp.value?.area === 'grid'")
expect(viewSource).toContain('event.clientX < pageBounds.left')
expect(viewSource).toContain('event.clientY > pageBounds.bottom')
expect(viewSource).toContain('event.clientX < springboardBounds.left')
expect(viewSource).toContain('event.clientY > springboardBounds.bottom')
expect(viewSource).toContain('function homeDragViewportRect(')
expect(viewSource).toContain('if (geometry) return geometry.rect(element)')
expect(viewSource).toContain('homeDragViewportRect(pageElement, geometry)')
expect(viewSource).toContain('homeDragViewportRect(springboard, geometry)')
expect(viewSource).toContain('homeDragViewportRect(slot, geometry)')
expect(viewSource).toContain('homeDragViewportRect(dock, geometry)')
expect(viewSource).toMatch(
/homeDragViewportRect\(\s*appElement,\s*readPhoneViewportGeometry\(appElement\),?\s*\)/,
)
expect(viewSource).toContain(
'springboardBounds.left + (bounds.left - pageBounds.left)',
)
expect(viewSource).toContain('nearestSpringboardRectIndex')
expect(viewSource).toContain("queueEdgePageTurn(lastHomePointer, 'app')")
expect(viewSource).toContain("edgePageLocked = dragType === 'widget'")
+273 -35
View File
@@ -41,6 +41,11 @@ import {
type HomeItem,
} from '@/utils/homeLayout'
import type { ReorderDirection } from '@/utils/keyboard'
import {
readPhoneViewportGeometry,
type PhoneViewportGeometry,
type PhoneViewportRect,
} from '@/utils/phoneViewportGeometry'
import { layoutSpringboardHomePages } from '@/utils/springboardLayout'
import {
maximumRenderedWidgetPage,
@@ -106,6 +111,8 @@ const draggingHomeApp = ref<{
area: HomeArea
index: number
} | null>(null)
const homeDragLayer = ref<HTMLElement | null>(null)
const homeDragVisualActive = ref(false)
const temporaryHomePage = ref<number | null>(null)
const openedFolderId = ref<string | null>(null)
const folderDraggingOutside = ref(false)
@@ -125,6 +132,11 @@ let edgePageDirection = 0
let edgePageLocked = false
let folderHoverTimer: number | undefined
let lastHomePointer: { clientX: number; clientY: number } | null = null
let homeDragGhost: HTMLElement | null = null
let homeDragGrip: { x: number; y: number } | null = null
let homeDragGeometry: PhoneViewportGeometry | null = null
let homeDragClipBounds: PhoneViewportRect | null = null
let homeDragPreviewBounds: PhoneViewportRect | null = null
const installedApps = computed(() =>
PHONE_APPS.filter((app) => appStore.isInstalled(app.id)),
@@ -453,7 +465,7 @@ function startWidgetDrag(id: string, event: PointerEvent): void {
).find((candidate) => candidate.dataset.widgetId === id)
if (!widget) return
pageTransitioning.value = false
const bounds = widget.getBoundingClientRect()
const bounds = homeDragViewportRect(widget)
draggingWidgetId.value = id
widgetDragGrip.value = {
x: event.clientX - bounds.left,
@@ -474,7 +486,7 @@ function updateWidgetDragPreview(event: {
)
const pageElement = grid?.closest<HTMLElement>('.springboard-page')
if (!grid || !pageElement) return
const renderedGridBounds = grid.getBoundingClientRect()
const renderedGridBounds = homeDragViewportRect(grid)
const gridStyle = getComputedStyle(grid)
const scaleX =
grid.offsetWidth > 0 ? renderedGridBounds.width / grid.offsetWidth : 1
@@ -529,7 +541,7 @@ function clearTemporaryHomePage(keepCurrentPage = false): void {
function resolveHomeEdgeTurn(event: { clientX: number }) {
const springboard = document.querySelector<HTMLElement>('.springboard')
if (!springboard) return null
const bounds = springboard.getBoundingClientRect()
const bounds = homeDragViewportRect(springboard)
const renderedLastPage = appPages.value.length
return resolveSpringboardHomeEdgeTurn(
event.clientX,
@@ -548,7 +560,7 @@ function queueEdgePageTurn(
): void {
const springboard = document.querySelector<HTMLElement>('.springboard')
if (!springboard) return
const bounds = springboard.getBoundingClientRect()
const bounds = homeDragViewportRect(springboard)
const turn =
dragType === 'app'
? resolveHomeEdgeTurn(event)
@@ -733,10 +745,166 @@ async function saveWidgetConfig(
if (configured) changePage(configured.page)
}
function startHomeDrag(area: HomeArea, index: number): void {
function homeDragViewportRect(
element: Element,
geometry = homeDragGeometry ?? readPhoneViewportGeometry(element),
): PhoneViewportRect {
if (geometry) return geometry.rect(element)
const bounds = element.getBoundingClientRect()
return {
bottom: bounds.bottom,
height: bounds.height,
left: bounds.left,
right: bounds.right,
top: bounds.top,
width: bounds.width,
}
}
function viewportRectAt(
left: number,
top: number,
width: number,
height: number,
): PhoneViewportRect {
return {
bottom: top + height,
height,
left,
right: left + width,
top,
width,
}
}
function clearHomeDragGhost(expectedGhost?: HTMLElement | null): void {
if (expectedGhost && homeDragGhost !== expectedGhost) return
homeDragGhost?.remove()
homeDragGhost = null
homeDragGrip = null
homeDragGeometry = null
homeDragClipBounds = null
homeDragPreviewBounds = null
homeDragVisualActive.value = false
const layer = homeDragLayer.value
layer?.style.removeProperty('left')
layer?.style.removeProperty('top')
layer?.style.removeProperty('width')
layer?.style.removeProperty('height')
}
function updateHomeDragGhost(event: {
clientX: number
clientY: number
}): void {
const ghost = homeDragGhost
const grip = homeDragGrip
const clipBounds = homeDragClipBounds
const previewBounds = homeDragPreviewBounds
if (!ghost || !grip || !clipBounds || !previewBounds) return
const left = event.clientX - grip.x
const top = event.clientY - grip.y
ghost.style.transform = `translate3d(${left - clipBounds.left}px, ${top - clipBounds.top}px, 0)`
homeDragPreviewBounds = viewportRectAt(
left,
top,
previewBounds.width,
previewBounds.height,
)
}
function createHomeDragGhost(event: PointerEvent): void {
clearHomeDragGhost()
const eventTarget =
event.currentTarget instanceof Element
? event.currentTarget
: event.target instanceof Element
? event.target
: null
const source = eventTarget?.closest<HTMLElement>('.app-icon-item')
const layer = homeDragLayer.value
const springboard = source?.closest<HTMLElement>('.springboard')
const portal = layer?.closest<HTMLElement>('.phone-home-drag-portal')
const geometry = readPhoneViewportGeometry(source ?? null)
if (!source || !layer || !springboard || !portal || !geometry) return
const portalBounds = portal.getBoundingClientRect()
const clipBounds = geometry.rect(springboard)
const sourceBounds = geometry.rect(source)
if (
clipBounds.width <= 0 ||
clipBounds.height <= 0 ||
sourceBounds.width <= 0 ||
sourceBounds.height <= 0
) {
return
}
layer.style.left = `${clipBounds.left - portalBounds.left}px`
layer.style.top = `${clipBounds.top - portalBounds.top}px`
layer.style.width = `${clipBounds.width}px`
layer.style.height = `${clipBounds.height}px`
const ghost = source.cloneNode(true) as HTMLElement
ghost.classList.remove(
'app-icon-item--dragging',
'app-icon-item--editing',
'app-icon-item--drag-source',
'app-icon-item--drop-settling',
'home-folder-item--dragging',
'home-item--folder-hover',
)
ghost.classList.add('home-drag-ghost')
ghost.removeAttribute('style')
ghost
.querySelector<HTMLElement>('.app-icon-button')
?.style.removeProperty('transform')
ghost.setAttribute('aria-hidden', 'true')
ghost.querySelector('.app-icon-remove')?.remove()
for (const element of [ghost, ...Array.from(ghost.querySelectorAll('*'))]) {
element.removeAttribute('id')
for (const attribute of element.getAttributeNames()) {
if (attribute.startsWith('data-home-')) element.removeAttribute(attribute)
}
}
for (const control of ghost.querySelectorAll<HTMLElement>(
'a, button, input, select, textarea, [tabindex]',
)) {
control.tabIndex = -1
}
const position = document.createElement('div')
position.className = 'home-drag-position'
position.style.width = `${sourceBounds.width}px`
position.style.height = `${sourceBounds.height}px`
ghost.style.width = `${source.offsetWidth}px`
ghost.style.height = `${source.offsetHeight}px`
ghost.style.transform = `scale(${geometry.scaleX}, ${geometry.scaleY})`
position.appendChild(ghost)
homeDragGhost = position
homeDragGrip = {
x: event.clientX - sourceBounds.left,
y: event.clientY - sourceBounds.top,
}
homeDragGeometry = geometry
homeDragClipBounds = clipBounds
homeDragPreviewBounds = sourceBounds
layer.appendChild(position)
homeDragVisualActive.value = true
updateHomeDragGhost(event)
}
function startHomeDrag(
area: HomeArea,
index: number,
event: PointerEvent,
): void {
clearTemporaryHomePage()
lastHomePointer = null
draggingHomeApp.value = { area, index }
createHomeDragGhost(event)
}
function clearFolderHover(): void {
@@ -754,10 +922,40 @@ function queueFolderHover(event: PointerEvent): void {
return
}
const targetElement = document
.elementsFromPoint(event.clientX, event.clientY)
.map((element) => element.closest<HTMLElement>('[data-home-index]'))
.find((element) => element && !element.closest('.app-icon-item--dragging'))
const targetElement = Array.from(
document.querySelectorAll<HTMLElement>('[data-home-area][data-home-index]'),
)
.filter((element) => {
const area = element.dataset.homeArea as HomeArea | undefined
const index = Number(element.dataset.homeIndex)
if (
(area !== 'grid' && area !== 'dock') ||
!Number.isInteger(index) ||
(area === dragged.area && index === dragged.index)
) {
return false
}
if (
area === 'grid' &&
Number(
element.closest<HTMLElement>('[data-home-page]')?.dataset.homePage,
) !== phone.currentPage
) {
return false
}
const bounds = homeDragViewportRect(element)
return (
event.clientX >= bounds.left &&
event.clientX <= bounds.right &&
event.clientY >= bounds.top &&
event.clientY <= bounds.bottom
)
})
.sort((left, right) => {
const leftIsDock = left.dataset.homeArea === 'dock'
const rightIsDock = right.dataset.homeArea === 'dock'
return Number(rightIsDock) - Number(leftIsDock)
})[0]
const area = targetElement?.dataset.homeArea as HomeArea | undefined
const targetIndex = Number(targetElement?.dataset.homeIndex)
if (
@@ -822,6 +1020,7 @@ function queueFolderHover(event: PointerEvent): void {
if (folderId) {
draggingHomeApp.value = null
lastHomePointer = null
clearHomeDragGhost()
clearTemporaryHomePage()
openedFolderId.value = folderId
}
@@ -832,6 +1031,7 @@ function queueFolderHover(event: PointerEvent): void {
function moveHomeDrag(event: PointerEvent): void {
if (!draggingHomeApp.value) return
lastHomePointer = { clientX: event.clientX, clientY: event.clientY }
updateHomeDragGhost(event)
if (resolveHomeEdgeTurn(event)) {
clearFolderHover()
queueEdgePageTurn(event, 'app')
@@ -845,7 +1045,7 @@ async function animateHomeItemDrop(
item: HomeItem,
area: HomeArea,
index: number,
from: DOMRect,
from: PhoneViewportRect,
): Promise<void> {
await nextTick()
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
@@ -861,7 +1061,10 @@ async function animateHomeItemDrop(
)
if (!appElement) return
const target = appElement.getBoundingClientRect()
const target = homeDragViewportRect(
appElement,
readPhoneViewportGeometry(appElement),
)
const { x: offsetX, y: offsetY } = springboardViewportDeltaToLocal(
from.left - target.left,
from.top - target.top,
@@ -924,13 +1127,16 @@ function nearestGridDropTarget(event: {
`.springboard-page--apps[data-home-page="${page}"] .app-grid`,
)
const pageElement = grid?.closest<HTMLElement>('.springboard-page')
if (!grid || !pageElement) return null
const pageBounds = pageElement.getBoundingClientRect()
const springboard = grid?.closest<HTMLElement>('.springboard')
if (!grid || !pageElement || !springboard) return null
const geometry = homeDragGeometry ?? readPhoneViewportGeometry(springboard)
const pageBounds = homeDragViewportRect(pageElement, geometry)
const springboardBounds = homeDragViewportRect(springboard, geometry)
if (
event.clientX < pageBounds.left ||
event.clientX > pageBounds.right ||
event.clientY < pageBounds.top ||
event.clientY > pageBounds.bottom
event.clientX < springboardBounds.left ||
event.clientX > springboardBounds.right ||
event.clientY < springboardBounds.top ||
event.clientY > springboardBounds.bottom
) {
return null
}
@@ -940,7 +1146,15 @@ function nearestGridDropTarget(event: {
const closestIndex = nearestSpringboardRectIndex(
event.clientX,
event.clientY,
slots.map((slot) => slot.getBoundingClientRect()),
slots.map((slot) => {
const bounds = homeDragViewportRect(slot, geometry)
return {
height: bounds.height,
left: springboardBounds.left + (bounds.left - pageBounds.left),
top: springboardBounds.top + (bounds.top - pageBounds.top),
width: bounds.width,
}
}),
)
const closest = closestIndex === null ? null : slots[closestIndex]
const targetOffset = Number(closest?.dataset.homeTargetOffset)
@@ -953,7 +1167,8 @@ function nearestDockDropTarget(event: {
}): HTMLElement | null {
const dock = document.querySelector<HTMLElement>('.app-dock')
if (!dock) return null
const dockBounds = dock.getBoundingClientRect()
const geometry = homeDragGeometry ?? readPhoneViewportGeometry(dock)
const dockBounds = homeDragViewportRect(dock, geometry)
if (
event.clientX < dockBounds.left ||
event.clientX > dockBounds.right ||
@@ -970,7 +1185,7 @@ function nearestDockDropTarget(event: {
const closestIndex = nearestSpringboardRectIndex(
event.clientX,
event.clientY,
slots.map((slot) => slot.getBoundingClientRect()),
slots.map((slot) => homeDragViewportRect(slot, geometry)),
)
return closestIndex === null ? null : slots[closestIndex]
}
@@ -979,12 +1194,14 @@ function finishHomeDrag(event: PointerEvent): void {
clearEdgePageTurn()
clearFolderHover()
const dragged = draggingHomeApp.value
if (!dragged) return
if (!dragged) {
clearHomeDragGhost()
return
}
updateHomeDragGhost(event)
const draggedItem = appStore.homeLayout[dragged.area][dragged.index]
const draggedElement = document.querySelector<HTMLElement>(
`[data-home-area="${dragged.area}"][data-home-index="${dragged.index}"]`,
)
const dropOrigin = draggedElement?.getBoundingClientRect()
const dropGhost = homeDragGhost
const dropOrigin = homeDragPreviewBounds ? { ...homeDragPreviewBounds } : null
const dockTarget = nearestDockDropTarget(event)
const gridTarget = dockTarget ? null : nearestGridDropTarget(event)
let dropArea = dragged.area
@@ -1022,7 +1239,14 @@ function finishHomeDrag(event: PointerEvent): void {
lastHomePointer = null
clearTemporaryHomePage(moved && gridTarget !== null)
if (moved && draggedItem && dropOrigin) {
void animateHomeItemDrop(draggedItem, dropArea, dropIndex, dropOrigin)
void animateHomeItemDrop(
draggedItem,
dropArea,
dropIndex,
dropOrigin,
).finally(() => clearHomeDragGhost(dropGhost))
} else {
clearHomeDragGhost()
}
}
@@ -1031,6 +1255,7 @@ function stopHomeDrag(): void {
clearFolderHover()
draggingHomeApp.value = null
lastHomePointer = null
clearHomeDragGhost()
clearTemporaryHomePage()
}
@@ -1043,7 +1268,9 @@ function reorderHomeApp(
const appElement = document.querySelector<HTMLElement>(
`[data-home-area="${area}"][data-home-index="${sourceIndex}"]`,
)
const moveOrigin = appElement?.getBoundingClientRect()
const moveOrigin = appElement
? homeDragViewportRect(appElement, readPhoneViewportGeometry(appElement))
: null
const targetIndex = homeKeyboardTarget(
appStore.homeLayout,
area,
@@ -1131,7 +1358,10 @@ function extractOpenedFolderApp(
`.home-folder-panel [data-folder-app-index="${sourceIndex}"]`,
)
?.closest<HTMLElement>('.app-icon-item')
const sourceBounds = sourceElement?.getBoundingClientRect()
const geometry = readPhoneViewportGeometry(sourceElement ?? null)
const sourceBounds = sourceElement
? homeDragViewportRect(sourceElement, geometry)
: null
const candidates = Array.from(
document.querySelectorAll<HTMLElement>('[data-home-index][data-home-area]'),
).filter((element) => {
@@ -1145,8 +1375,8 @@ function extractOpenedFolderApp(
})
const target = candidates.reduce<HTMLElement | null>((closest, candidate) => {
if (!closest) return candidate
const bounds = candidate.getBoundingClientRect()
const closestBounds = closest.getBoundingClientRect()
const bounds = homeDragViewportRect(candidate, geometry)
const closestBounds = homeDragViewportRect(closest, geometry)
const distance = Math.hypot(
event.clientX - (bounds.left + bounds.width / 2),
event.clientY - (bounds.top + bounds.height / 2),
@@ -1212,6 +1442,7 @@ onBeforeUnmount(() => {
pendingWidgetPointer = null
lastWidgetPointer = null
lastHomePointer = null
clearHomeDragGhost()
temporaryHomePage.value = null
draggingHomeApp.value = null
draggingWidgetId.value = null
@@ -1227,7 +1458,6 @@ onBeforeUnmount(() => {
'springboard--dragging': dragging,
'springboard--editing': editMode,
'springboard--folder-open': folderOverlayVisible,
'springboard--home-dragging': draggingHomeApp !== null,
'springboard--widget-dragging': draggingWidgetId !== null,
},
]"
@@ -1303,6 +1533,7 @@ onBeforeUnmount(() => {
:data-home-index="cell.sourceIndex"
:data-home-target-offset="cell.targetOffset"
:edit-mode="editMode"
:external-drag-visual="homeDragVisualActive"
:class="{
'home-item--folder-hover':
folderHoverTarget === `grid:${cell.sourceIndex}`,
@@ -1310,7 +1541,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
@dragstart="startHomeDrag('grid', cell.sourceIndex)"
@dragstart="startHomeDrag('grid', cell.sourceIndex, $event)"
@edit="enterEditMode"
@remove="removeHomeApp(cell.app.id)"
@reorder="reorderHomeApp('grid', cell.sourceIndex, $event)"
@@ -1325,6 +1556,7 @@ onBeforeUnmount(() => {
:data-home-index="cell.sourceIndex"
:data-home-target-offset="cell.targetOffset"
:edit-mode="editMode"
:external-drag-visual="homeDragVisualActive"
:folder="cell.folder"
:class="{
'home-item--folder-hover':
@@ -1333,7 +1565,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
@dragstart="startHomeDrag('grid', cell.sourceIndex)"
@dragstart="startHomeDrag('grid', cell.sourceIndex, $event)"
@edit="enterEditMode"
@open="openFolder(cell.folder.id)"
@reorder="reorderHomeApp('grid', cell.sourceIndex, $event)"
@@ -1484,6 +1716,10 @@ onBeforeUnmount(() => {
</section>
</div>
<Teleport defer to="#phone-home-drag-portal">
<div ref="homeDragLayer" class="home-drag-layer" aria-hidden="true"></div>
</Teleport>
<Transition name="edit-done">
<k-glass
v-if="editMode && isEditablePage && !folderOverlayVisible"
@@ -1530,6 +1766,7 @@ onBeforeUnmount(() => {
:data-home-item-key="`app-${slot.app.id}`"
:data-home-index="slot.index"
:edit-mode="editMode"
:external-drag-visual="homeDragVisualActive"
:show-label="false"
:class="{
'home-item--folder-hover':
@@ -1538,7 +1775,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
@dragstart="startHomeDrag('dock', slot.index)"
@dragstart="startHomeDrag('dock', slot.index, $event)"
@edit="enterEditMode"
@remove="removeHomeApp(slot.app.id)"
@reorder="reorderHomeApp('dock', slot.index, $event)"
@@ -1552,6 +1789,7 @@ onBeforeUnmount(() => {
:data-home-item-key="slot.folder.id"
:data-home-index="slot.index"
:edit-mode="editMode"
:external-drag-visual="homeDragVisualActive"
:folder="slot.folder"
:show-label="false"
:class="{
@@ -1561,7 +1799,7 @@ onBeforeUnmount(() => {
@dragcancel="stopHomeDrag"
@dragend="finishHomeDrag"
@dragmove="moveHomeDrag"
@dragstart="startHomeDrag('dock', slot.index)"
@dragstart="startHomeDrag('dock', slot.index, $event)"
@edit="enterEditMode"
@open="openFolder(slot.folder.id)"
@reorder="reorderHomeApp('dock', slot.index, $event)"