diff --git a/src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts b/src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts index ec7560dd3..37b9ca7c6 100644 --- a/src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts +++ b/src/uix/soma/components/drag-drop/drag-drop-provider.svelte.ts @@ -159,6 +159,9 @@ export class DragDropProvider { }; this.keyboard = params.keyboard; this.announceTemplate(DRAG_DROP_LANGS.DRAG_STARTED, { item: params.label }); + // Morfo: handle-pick on `draggable`. The source element is the + // draggable that just initiated the drag — pass it as fallback. + void this.runtime.trigger('handle-pick', { fallbackTarget: params.source }); if (params.keyboard) { this.setupKeyboardNav(); @@ -213,6 +216,9 @@ export class DragDropProvider { item: drag.label, target: targetLabel.trim() }); + // Morfo: handle-drop on `droppable`. The target is the droppable + // that received the dropped item. + void this.runtime.trigger('handle-drop', { fallbackTarget: target }); this.finish(); } @@ -225,6 +231,9 @@ export class DragDropProvider { outcome: 'cancel' }); this.announce(DRAG_DROP_LANGS.DRAG_CANCELLED); + // Morfo: commit-cancel on `draggable`. The source still holds + // the dragged-from element; use it as fallback target. + void this.runtime.trigger('commit-cancel', { fallbackTarget: drag.source }); // Return focus to the source for keyboard drags. const source = drag.source; this.finish(); diff --git a/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts b/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts index e0bd88b93..a1d600322 100644 --- a/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts +++ b/src/uix/soma/components/virtual-grid/virtual-grid-provider.svelte.ts @@ -183,6 +183,12 @@ export class VirtualGridProvider { left: left ?? this.scrollLeft, behavior: opts.behavior ?? 'auto' }); + // Morfo: shift-navigate-to-cell on `viewport`. Programmatic scroll- + // to-cell is the discrete-navigation moment. `handle-scroll-row` / + // `handle-scroll-column` are intentionally NOT emitted here for + // the same fatigue reason as virtual-list (haptic-only family, + // pixel-frequency events would buzz nonstop). + void this.runtime.trigger('shift-navigate-to-cell', { fallbackTarget: vp }); }; readonly snippetProps = $derived.by(() => ({ @@ -230,9 +236,22 @@ export class VirtualGridViewportProvider { if (!el) return; const updateSize = () => { + const prevH = this.provider.viewportHeight; + const prevW = this.provider.viewportWidth; this.provider.viewportHeight = el.clientHeight; this.provider.viewportWidth = el.clientWidth; + // Morfo: commit-set-resize on `provider`. Only emit on real + // dimension change (ResizeObserver fires on every layout); + // skip the initial measurement (prevH === prevW === 0). + if ((prevH !== el.clientHeight || prevW !== el.clientWidth) && (prevH !== 0 || prevW !== 0)) { + void this.provider.runtime.trigger('commit-set-resize'); + } }; + // NOTE: `handle-scroll-row` / `handle-scroll-column` declared in + // the morfo but NOT emitted here. See virtual-list note — + // pixel-frequency scroll with haptic-only family = device buzz. + // Apps that want scroll-driven sema should wire their own + // throttled emit. const updateScroll = () => { this.provider.scrollTop = el.scrollTop; this.provider.scrollLeft = el.scrollLeft; diff --git a/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts b/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts index a21712140..0e640f8a6 100644 --- a/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts +++ b/src/uix/soma/components/virtual-list/virtual-list-provider.svelte.ts @@ -261,6 +261,16 @@ export class VirtualListProvider { target = Math.max(0, Math.min(this.totalSize - vpSize, target)); + // Morfo: shift-navigate-to-index on `viewport`. Programmatic + // scroll-to is a discrete navigation moment (vs the continuous + // `handle-scroll` which is intentionally NOT emitted from this + // provider — see note in `VirtualListViewportProvider`). + const vpForEmit = this.viewportRef; + void this.runtime.trigger( + 'shift-navigate-to-index', + vpForEmit ? { fallbackTarget: vpForEmit } : undefined + ); + // Internal viewport scroll. const vp = this.viewportRef; if (vp) { @@ -337,8 +347,21 @@ export class VirtualListViewportProvider { const horizontal = this.provider.isHorizontal; const updateSize = () => { - this.provider.viewportSize = horizontal ? el.clientWidth : el.clientHeight; + const prev = this.provider.viewportSize; + const next = horizontal ? el.clientWidth : el.clientHeight; + this.provider.viewportSize = next; + // Morfo: commit-set-resize on `provider`. Only emit on real + // size change (ResizeObserver fires on every layout pass). + if (prev !== next && prev !== 0) { + void this.provider.runtime.trigger('commit-set-resize'); + } }; + // NOTE: `handle-scroll` is declared in the morfo but NOT emitted + // here by default. The scroll listener below fires on every + // pixel; family.handle.activeChannels=['haptic'] so emitting on + // each event would buzz nonstop on mobile. Apps that want + // scroll-driven sema feedback should wire it themselves with + // their own throttle / debounce policy. const updateScroll = () => { this.provider.scrollOffset = horizontal ? el.scrollLeft : el.scrollTop; };