@@ -6,120 +6,113 @@ import { registry } from "./registry.js";
66import { VirtualGrid } from "./grid/virtualGrid.js" ;
77
88// -----------------------------
9- // GRID RENDERING (Reconciliation + FLIP Animation )
9+ // GRID RENDERING (View Transitions API )
1010// -----------------------------
1111
12+ /**
13+ * @param {Object } dragInfo - { id: number, dropType: 'success' | 'fail' }
14+ */
1215export async function renderGrid ( dragInfo = null ) {
1316 const dashboard = qs ( '#dashboard' ) ;
1417 if ( ! dashboard ) return ;
1518
1619 renderGridLines ( ) ;
1720
18- const apps = state . apps ;
19- const domMap = new Map ( ) ;
20- const prevRects = new Map ( ) ;
21-
22- // 1. Snapshot OLD Positions (First)
23- qsa ( '.app-card' , dashboard ) . forEach ( el => {
24- const id = parseInt ( el . dataset . id ) ;
25- if ( id ) {
26- domMap . set ( id , el ) ;
27- prevRects . set ( id , el . getBoundingClientRect ( ) ) ;
21+ // Helper to perform the actual DOM updates
22+ const updateDOM = async ( ) => {
23+
24+ // --- 1. FORCE CLEANUP (Moved outside the loop for safety) ---
25+ // This guarantees the floating card is reset, even if the state loop is busy.
26+ if ( dragInfo && dragInfo . id ) {
27+ const draggedEl = document . getElementById ( `app-${ dragInfo . id } ` ) ;
28+ if ( draggedEl ) {
29+ draggedEl . classList . remove ( 'moving' ) ;
30+ draggedEl . style . position = '' ;
31+ draggedEl . style . width = '' ;
32+ draggedEl . style . height = '' ;
33+ draggedEl . style . left = '' ;
34+ draggedEl . style . top = '' ;
35+ draggedEl . style . zIndex = '' ;
36+ }
2837 }
29- } ) ;
3038
31- // Override the dragged item's "Old" position with its "Floating" position
32- if ( dragInfo && dragInfo . id && dragInfo . rect ) {
33- prevRects . set ( dragInfo . id , dragInfo . rect ) ;
34- }
39+ const apps = state . apps ;
40+ const domMap = new Map ( ) ;
3541
36- // 2. Update DOM (Last)
37- for ( const app of apps ) {
38- let el = domMap . get ( app . id ) ;
42+ qsa ( '.app-card' , dashboard ) . forEach ( el => {
43+ const id = parseInt ( el . dataset . id ) ;
44+ if ( id ) domMap . set ( id , el ) ;
45+ } ) ;
3946
40- if ( el ) {
41- // Check for position/size changes
42- const currentX = parseInt ( el . dataset . x ) ;
43- const currentY = parseInt ( el . dataset . y ) ;
44- const currentW = parseInt ( el . dataset . cols ) ;
45- const currentH = parseInt ( el . dataset . rows ) ;
47+ for ( const app of apps ) {
48+ let el = domMap . get ( app . id ) ;
4649
47- if ( currentX !== app . x || currentY !== app . y || currentW !== app . cols || currentH !== app . rows ) {
48- applyGridPosition ( el , app . x , app . y , app . cols , app . rows ) ;
49- }
50+ if ( el ) {
51+ // UPDATE EXISTING
52+ // Ensure Transition Name is set
53+ if ( ! el . style . viewTransitionName ) {
54+ el . style . viewTransitionName = `app-${ app . id } ` ;
55+ }
5056
51- // Check for content changes
52- const dataHash = JSON . stringify ( app . data || { } ) + app . name ;
53- const currentHash = el . dataset . contentHash ;
57+ const currentX = parseInt ( el . dataset . x ) ;
58+ const currentY = parseInt ( el . dataset . y ) ;
59+ const currentW = parseInt ( el . dataset . cols ) ;
60+ const currentH = parseInt ( el . dataset . rows ) ;
5461
55- if ( app . data ?. bgColor ) el . style . backgroundColor = app . data . bgColor ;
56- if ( app . data ?. textColor ) el . style . color = app . data . textColor ;
62+ if ( currentX !== app . x || currentY !== app . y || currentW !== app . cols || currentH !== app . rows ) {
63+ applyGridPosition ( el , app . x , app . y , app . cols , app . rows ) ;
64+ }
5765
58- if ( dataHash !== currentHash ) {
59- await mountAppContent ( el , app ) ;
60- el . dataset . contentHash = dataHash ;
61- }
66+ // Content Check
67+ const dataHash = JSON . stringify ( app . data || { } ) + app . name ;
68+ const currentHash = el . dataset . contentHash ;
6269
63- domMap . delete ( app . id ) ;
64- } else {
65- // Create New
66- el = await createAppElement ( app ) ;
67- dashboard . appendChild ( el ) ;
68- }
69- }
70+ if ( app . data ?. bgColor ) el . style . backgroundColor = app . data . bgColor ;
71+ if ( app . data ?. textColor ) el . style . color = app . data . textColor ;
7072
71- // Cleanup removed apps
72- domMap . forEach ( el => el . remove ( ) ) ;
73+ if ( dataHash !== currentHash ) {
74+ await mountAppContent ( el , app ) ;
75+ el . dataset . contentHash = dataHash ;
76+ }
7377
74- // 3. Invert & Play (Animate)
75- // Double RAF ensures the DOM update is fully processed before we calculate deltas
76- requestAnimationFrame ( ( ) => {
77- const animations = [ ] ;
78+ domMap . delete ( app . id ) ;
79+ } else {
80+ // CREATE NEW
81+ el = await createAppElement ( app ) ;
82+ dashboard . appendChild ( el ) ;
83+ }
84+ }
7885
79- apps . forEach ( app => {
80- const el = document . getElementById ( `app- ${ app . id } ` ) ;
81- const oldRect = prevRects . get ( app . id ) ;
86+ // Cleanup removed apps
87+ domMap . forEach ( el => el . remove ( ) ) ;
88+ } ;
8289
83- if ( el && oldRect ) {
84- const newRect = el . getBoundingClientRect ( ) ;
90+ // --- THE ANIMATION TRIGGER ---
91+ if ( document . startViewTransition ) {
92+ let styleTag = null ;
8593
86- // Calculate Delta
87- const dX = oldRect . left - newRect . left ;
88- const dY = oldRect . top - newRect . top ;
94+ // Success: Snap instantly. Fail: Animate back.
95+ if ( dragInfo && dragInfo . id && dragInfo . dropType === 'success' ) {
96+ styleTag = document . createElement ( 'style' ) ;
97+ styleTag . innerHTML = `
98+ ::view-transition-group(app-${ dragInfo . id } ) {
99+ animation-duration: 0s !important;
100+ }
101+ ` ;
102+ document . head . appendChild ( styleTag ) ;
103+ }
89104
90- // Only animate significant moves
91- if ( Math . abs ( dX ) > 1 || Math . abs ( dY ) > 1 ) {
92- // INVERT (Start State): Move back to old position instantly
93- el . style . transition = 'none' ;
94- el . style . transform = `translate(${ dX } px, ${ dY } px)` ;
95- el . style . zIndex = '100' ; // Float above others
105+ const transition = document . startViewTransition ( ( ) => updateDOM ( ) ) ;
96106
97- animations . push ( el ) ;
98- }
99- }
107+ transition . finished . finally ( ( ) => {
108+ if ( styleTag ) styleTag . remove ( ) ;
100109 } ) ;
101110
102- // PLAY (End State): Remove transform smoothly
103- // Nested RAF forces the browser to paint the 'Invert' state first
104- requestAnimationFrame ( ( ) => {
105- animations . forEach ( el => {
106- el . style . transition = 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)' ;
107- el . style . transform = '' ;
108-
109- // Cleanup Z-Index
110- const cleanup = ( ) => {
111- el . style . zIndex = '' ;
112- el . style . transition = '' ;
113- el . removeEventListener ( 'transitionend' , cleanup ) ;
114- } ;
115- el . addEventListener ( 'transitionend' , cleanup ) ;
116- } ) ;
117- } ) ;
118- } ) ;
111+ } else {
112+ updateDOM ( ) ;
113+ }
119114}
120115
121- // ... (Rest of helpers remain identical) ...
122-
123116async function createAppElement ( app ) {
124117 const el = createEl ( 'div' , {
125118 class : 'app-card' ,
@@ -129,6 +122,8 @@ async function createAppElement(app) {
129122 }
130123 } ) ;
131124
125+ el . style . viewTransitionName = `app-${ app . id } ` ;
126+
132127 applyGridPosition ( el , app . x , app . y , app . cols , app . rows ) ;
133128
134129 if ( app . data ?. bgColor ) el . style . backgroundColor = app . data . bgColor ;
@@ -141,6 +136,8 @@ async function createAppElement(app) {
141136 return el ;
142137}
143138
139+ // ... (Rest of helpers remain unchanged) ...
140+
144141async function mountAppContent ( el , app ) {
145142 const appDef = registry . get ( app . subtype ) ;
146143 let innerHTML = 'Unknown App' ;
0 commit comments