diff --git a/README.md b/README.md index d847236..f2bec9b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Paste URL / Select Files -> See Architecture -> Make Better Decisions ## Features ### Interactive Dependency Graph -See how your files connect at a glance. Click any node to highlight its dependencies. Drag, zoom, and explore. The **Code** view keeps that map and shows the selected file plus its connected files beside it. +See how your files connect at a glance. Click any node to highlight its dependencies. Drag, zoom, and explore. The **Code** view keeps that map and opens the selected file plus its connected files as full-file cards on the canvas, grouped by directory. ### Blast Radius Analysis *"If I change this file, what breaks?"* — CodeFlow answers this instantly. Select any file and see exactly how many files would be affected by changes. @@ -257,7 +257,7 @@ strings and Pascal comment forms; unsupported language features fall back gracef | **Layer** | Color by architectural layer (UI, Services, Utils, etc.) | | **Churn** | Color by commit frequency (hot spots) | | **Blast** | Color by impact when a file is selected | -| **Code** | Same graph, with file source and cross-file symbol highlights beside it | +| **Code** | Same graph, with selected and connected files as full-file cards grouped by directory | --- diff --git a/index.html b/index.html index c34b722..c447aa2 100644 --- a/index.html +++ b/index.html @@ -69,9 +69,21 @@ .resize-handle:hover{background:var(--acc);opacity:0.3} .sidebar-section{padding:12px;border-bottom:1px solid var(--border)} .sidebar-title{font-size:9px;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px} -.sidebar-scroll{flex:1;overflow-y:auto;padding:8px} +.sidebar-scroll{flex:1;overflow-y:auto;padding:8px;min-height:0} .sidebar-scroll::-webkit-scrollbar{width:6px} .sidebar-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} +.sidebar-tabs{display:flex;flex-shrink:0;border-bottom:1px solid var(--border);background:var(--bg2)} +.sidebar-tab{flex:1;padding:9px 4px;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--t3);font-family:inherit;font-size:9px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;cursor:pointer} +.sidebar-tab:hover{color:var(--t1)} +.sidebar-tab.active{color:var(--acc);border-bottom-color:var(--acc)} +.sidebar-pane{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden} +.sidebar-filter{flex-shrink:0;padding:8px 10px;border-bottom:1px solid var(--border)} +.color-by{position:absolute;top:12px;right:12px;z-index:51;display:flex;align-items:center;gap:2px;background:var(--bg1);border:1px solid var(--border);border-radius:8px;padding:3px 3px 3px 6px} +.color-by-label{font-size:8px;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:0.4px;margin-right:2px;white-space:nowrap} +.color-by-btn{padding:3px 7px;background:transparent;border:1px solid transparent;border-radius:5px;font-size:9px;font-family:inherit;color:var(--t2);cursor:pointer} +.color-by-btn:hover{color:var(--t0)} +.color-by-btn.active{background:var(--accbg);border-color:var(--acc);color:var(--acc)} +.legend.with-color-by{top:50px} .view-modes{display:flex;flex-direction:column;gap:2px} .view-mode{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:11px;color:var(--t1)} .view-mode:hover{background:var(--hover)} @@ -126,10 +138,13 @@ .legend-color{width:12px;height:12px;border-radius:3px;flex-shrink:0} .right-panel{width:360px;min-width:280px;max-width:500px;background:var(--bg1);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;position:relative;min-height:0} .right-panel .resize-handle{right:auto;left:-4px} -.panel-tabs{display:flex;border-bottom:1px solid var(--border);background:var(--bg2)} -.panel-tab{flex:1;padding:8px;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--t3);font-family:inherit;font-size:9px;font-weight:600;cursor:pointer} +.panel-tabs{display:flex;flex-wrap:nowrap;align-items:stretch;border-bottom:1px solid var(--border);background:var(--bg2)} +.panel-tab{flex:1 1 0;min-width:0;display:inline-flex;flex-direction:row;flex-wrap:nowrap;align-items:center;justify-content:center;gap:4px;height:34px;padding:0 4px;line-height:16px;white-space:nowrap;overflow:hidden;background:transparent;border:none;border-bottom:2px solid transparent;color:var(--t3);font-family:inherit;font-size:9px;font-weight:600;cursor:pointer} .panel-tab:hover{color:var(--t1)} .panel-tab.active{color:var(--acc);border-bottom-color:var(--acc)} +.panel-tab .icon{flex-shrink:0} +.panel-tab-pill{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;height:14px;min-width:14px;padding:0 5px;margin:0;border-radius:8px;font-size:8px;font-weight:600;line-height:14px;background:var(--bg3);color:var(--t2)} +.panel-tab-pill.alert{background:var(--red);color:#fff} .panel-content{flex:1;overflow-y:auto;padding:12px} .panel-content::-webkit-scrollbar{width:6px} .panel-content::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} @@ -221,8 +236,12 @@ .export-options{display:grid;grid-template-columns:repeat(2,1fr);gap:8px} .export-option{padding:16px;background:var(--bg0);border:1px solid var(--border);border-radius:8px;cursor:pointer;text-align:center} .export-option:hover{border-color:var(--acc);background:var(--accbg)} +.export-option.disabled{opacity:0.55;cursor:not-allowed} +.export-option.disabled:hover{border-color:var(--border);background:var(--bg0)} .export-option-icon{margin-bottom:8px;color:var(--acc)} +.export-option.disabled .export-option-icon{color:var(--t3)} .export-option-label{font-size:11px;color:var(--t1)} +.export-option.disabled .export-option-label{color:var(--t3)} .pr-header{padding:12px;background:var(--bg0);border-radius:8px;margin-bottom:12px} .pr-title{font-size:12px;color:var(--t0);font-weight:500} .pr-stats{display:flex;gap:12px;margin-top:8px;font-size:11px} @@ -454,37 +473,51 @@ .light .syn-tag{color:#e45649} .light .syn-attr{color:#986801} .light .syn-punct{color:#383a42} -.code-split{display:flex;width:100%;height:100%;min-width:0;min-height:0} -.code-split-graph{flex:1 1 48%;min-width:180px;position:relative;overflow:hidden} -.code-split-graph svg{width:100%;height:100%;display:block} -.code-split-resize{width:8px;flex-shrink:0;cursor:col-resize;background:var(--border);position:relative;z-index:20;touch-action:none} -.code-split-resize:hover,.code-split-resize.active{background:var(--acc)} -.code-split-pane{flex:1 1 52%;min-width:220px;max-width:70%;display:flex;flex-direction:column;background:var(--bg1);border-left:1px solid var(--border);min-height:0} -.code-split-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-bottom:1px solid var(--border);flex-shrink:0} -.code-split-title{font-size:10px;font-weight:600;color:var(--t2);text-transform:uppercase;letter-spacing:0.6px} -.code-split-hint{font-size:9px;color:var(--t3)} -.code-sym-row{display:flex;flex-wrap:wrap;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border2);flex-shrink:0;max-height:72px;overflow:auto} -.code-sym-chip{font-size:9px;padding:3px 7px;border-radius:999px;border:1px solid var(--border);background:var(--bg0);color:var(--t2);cursor:pointer;font-family:inherit} +.code-canvas{position:relative;width:100%;height:100%;min-width:0;min-height:0;overflow:hidden} +.code-canvas svg{width:100%;height:100%;display:block} +.code-canvas-cards{position:absolute;inset:0;pointer-events:none;transform-origin:0 0;z-index:4} +.code-canvas-hud{position:absolute;top:56px;left:12px;z-index:51;display:flex;flex-direction:column;align-items:stretch;gap:6px;width:168px;max-width:calc(100% - 24px);pointer-events:none} +.code-canvas-hint{font-size:10px;color:var(--t3);background:var(--bg1);border:1px solid var(--border);border-radius:8px;padding:6px 8px;line-height:1.4} +.code-sym-list{display:flex;flex-direction:column;gap:2px;padding:6px;border:1px solid var(--border);border-radius:8px;background:var(--bg1);max-height:min(420px,calc(100% - 140px));overflow-x:hidden;overflow-y:auto;pointer-events:auto} +.code-sym-chip{display:block;width:100%;box-sizing:border-box;font-size:9px;padding:4px 7px;border-radius:6px;border:1px solid var(--border);background:var(--bg0);color:var(--t2);cursor:pointer;font-family:inherit;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .code-sym-chip:hover{border-color:var(--acc);color:var(--acc)} .code-sym-chip.active{background:var(--accbg);border-color:var(--acc);color:var(--acc)} .code-sym-chip.fn{box-shadow:inset 3px 0 0 #61afef} .code-sym-chip.import{box-shadow:inset 3px 0 0 #c678dd} .code-sym-chip.export{box-shadow:inset 3px 0 0 #98c379} .code-sym-chip.var{box-shadow:inset 3px 0 0 #e06c75} -.code-file-tabs{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--border2);overflow-x:auto;flex-shrink:0} -.code-file-tab{font-size:9px;padding:4px 8px;border-radius:6px;border:1px solid var(--border);background:var(--bg0);color:var(--t2);cursor:pointer;white-space:nowrap;font-family:inherit} -.code-file-tab:hover{color:var(--t0);border-color:var(--acc)} -.code-file-tab.active{background:var(--accbg);border-color:var(--acc);color:var(--acc)} -.code-file-tab.connected{opacity:0.95} -.code-cards{flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px;padding:8px;min-height:0} -.code-card{border:1px solid var(--border);border-radius:8px;background:var(--bg0);overflow:hidden;display:flex;flex-direction:column;min-height:120px} -.code-card.primary{flex:1 1 55%;min-height:180px} -.code-card.linked{flex:0 1 28%;min-height:110px;max-height:240px} -.code-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:var(--bg2);border-bottom:1px solid var(--border2);cursor:pointer} -.code-card-name{font-size:11px;font-weight:600;color:var(--t0)} -.code-card-path{font-size:9px;color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.code-card-body{flex:1;overflow:auto;min-height:0} +.code-card{position:absolute;border:1px solid var(--border);border-radius:8px;background:var(--bg0);overflow:visible;display:flex;flex-direction:column;pointer-events:auto;box-shadow:0 10px 28px rgba(0,0,0,0.28);visibility:hidden;user-select:none} +.code-line-pill{position:absolute;left:100%;margin-left:8px;transform:translateY(-50%);z-index:2;font-size:8px;line-height:14px;padding:0 6px;border-radius:8px;border:1px solid var(--border);background:var(--bg1);color:var(--t1);white-space:nowrap;cursor:pointer;font-family:inherit} +.code-line-pill:hover,.code-line-pill.active{border-color:var(--acc);color:var(--acc);background:var(--accbg)} +.code-card.primary{border-color:var(--acc);box-shadow:0 12px 32px rgba(0,0,0,0.34),0 0 0 1px var(--acc)} +.code-card.linked{opacity:0.98} +.code-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:var(--bg2);border-bottom:1px solid var(--border2);cursor:grab;touch-action:none;flex-shrink:0} +.code-card-head:active{cursor:grabbing} +.code-card-name{font-size:11px;font-weight:600;color:var(--t0);transform-origin:left center;display:inline-block;white-space:nowrap} +.code-card-path{font-size:9px;color:var(--t3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:240px} +.code-card-body{flex:1;overflow:hidden;min-height:0;user-select:text} +.code-card-source-status{padding:12px;display:flex;flex-direction:column;align-items:flex-start;gap:10px} +.code-card-retry{background:transparent;border:1px solid var(--border);color:var(--t2);padding:4px 8px;border-radius:4px;font-size:10px;cursor:pointer} +.code-card-retry:hover{border-color:var(--acc);color:var(--acc)} +.code-card.clipped .code-card-body{overflow:auto;touch-action:pan-x pan-y} +.code-card.expand:not(.wrap) .code-card-body{overflow-x:auto;overflow-y:hidden;touch-action:pan-x} +.code-card.expand.wrap .code-card-body{overflow:hidden} +.code-card.wrap:not(.expand) .code-card-body{overflow-x:hidden;overflow-y:auto} +.code-card.wrap .file-preview-line{align-items:flex-start;height:auto} +.code-card.wrap .file-preview-text{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;min-width:0} .code-card-body .file-preview-code{padding:8px 0;font-size:11px} +.color-by.code-view-prefs{flex-direction:row;align-items:center;gap:2px;padding:3px 3px 3px 6px} +.code-card-resize{position:absolute;z-index:3;background:transparent} +.code-card-resize-e{top:14px;right:-4px;width:10px;bottom:14px;cursor:ew-resize} +.code-card-resize-s{left:14px;right:14px;bottom:-4px;height:10px;cursor:ns-resize} +.code-card-resize-se{right:-3px;bottom:-3px;width:14px;height:14px;cursor:nwse-resize} +.code-card.code-far .code-card-body{overflow:hidden} +.code-card.code-far .file-preview-linenum{display:none} +.code-card.code-far .file-preview-line{min-height:3px;height:3px;overflow:hidden} +.code-card.code-far .file-preview-text{font-size:3px;line-height:3px;letter-spacing:-0.5px} +.code-card.code-far .code-line-pill{display:none} +.has-code-card{display:none!important} +.has-code-card .nc,.has-code-card .node-label{pointer-events:none} .sym-mark{border-radius:3px;padding:0 1px;cursor:pointer} .sym-mark.fn{background:rgba(97,175,239,0.16)} .sym-mark.import{background:rgba(198,120,221,0.16)} @@ -502,16 +535,14 @@ .recent-mini-btn{font-size:8px;padding:3px 6px;border-radius:4px;border:1px solid var(--border);background:var(--bg2);color:var(--t2);cursor:pointer;font-family:inherit} .recent-mini-btn:hover{border-color:var(--acc);color:var(--acc)} .recent-mini-btn.danger:hover{border-color:var(--red);color:var(--red)} +.recent-mini-btn.danger.armed{background:var(--red);border-color:var(--red);color:#fff} .recent-empty{font-size:10px;color:var(--t3);line-height:1.4;padding:4px 0} .stale-banner{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;background:var(--accbg);border:1px solid var(--acc);border-radius:6px;margin:0 0 8px;font-size:10px;color:var(--t1)} .github-hint{font-size:9px;color:var(--t3);line-height:1.45;margin-top:6px;max-width:420px} .github-hint button{background:none;border:none;color:var(--acc);cursor:pointer;font-family:inherit;font-size:9px;padding:0;text-decoration:underline} .cli-chip{padding:4px 8px;background:var(--accbg);border:1px solid var(--acc);border-radius:6px;font-size:9px;color:var(--acc)} @media(max-width:980px){ -.code-split{flex-direction:column} -.code-split-graph{flex:1 1 42%;min-height:180px} -.code-split-resize{width:100%;height:8px;cursor:row-resize} -.code-split-pane{flex:1 1 58%;max-width:none;min-height:200px;border-left:0;border-top:1px solid var(--border)} +.code-canvas-hud{top:56px;left:10px;width:140px} } .auth-select{padding:6px 10px;background:var(--bg0);border:1px solid var(--border);border-radius:6px;color:var(--t0);font-family:inherit;font-size:10px;cursor:pointer;min-width:100px;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b95' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:28px} .auth-select:focus{outline:none;border-color:var(--acc)} @@ -575,7 +606,9 @@ .canvas-toolbar{top:68px;left:12px;right:12px;gap:8px;overflow-x:auto;padding-bottom:2px} .tool-btn{width:40px;height:40px;font-size:15px;flex:0 0 auto} .graph-config{top:120px;left:12px;right:12px;width:auto;max-width:none} +.color-by{top:auto;bottom:118px;right:12px} .legend{top:auto;bottom:68px;right:12px;max-width:calc(100vw - 24px)} +.legend.with-color-by{top:auto;bottom:68px} .canvas-info{left:12px;right:12px;bottom:12px;overflow-x:auto;padding-bottom:calc(2px + env(safe-area-inset-bottom))} .info-chip{white-space:nowrap;flex:0 0 auto} .export-options,.pr-grid,.pr-impact-grid,.unused-summary{grid-template-columns:1fr} @@ -602,7 +635,9 @@ .health-score{align-items:flex-start} .canvas-toolbar{top:64px} .graph-config{top:116px} +.color-by{bottom:122px} .legend{bottom:72px;right:12px} +.legend.with-color-by{bottom:72px} .panel-tabs{position:sticky;top:0;z-index:2} .file-preview-modal{width:100vw;height:100dvh;border-radius:0;border:none} .file-preview-header{border-radius:0;padding:12px} @@ -658,7 +693,9 @@ .graph-config{top:52px;left:10px;right:10px;width:auto;max-width:none} .architecture-view{padding:0} .architecture-shell{min-width:0;padding:0} +.color-by{top:auto;bottom:118px;right:10px} .legend{top:auto;bottom:72px;right:10px;max-width:calc(100vw - 20px)} +.legend.with-color-by{top:auto;bottom:72px} .canvas-info{left:10px;right:10px;bottom:10px;overflow-x:auto;padding-bottom:0} .info-chip{white-space:nowrap;flex:0 0 auto} } @@ -668,7 +705,9 @@ .mobile-secondary-row .auth-select{width:106px} .mobile-secondary-row .top-btn,.mobile-secondary-row .refresh-btn,.mobile-secondary-row .reset-btn{padding:0 9px} .canvas-toolbar{bottom:70px} +.color-by{bottom:116px} .legend{bottom:70px} +.legend.with-color-by{bottom:70px} } @@ -686,7 +725,7 @@ * 4. Shared analysis pipeline plus inline Web Worker fallback. * 5. React UI, local folder/ZIP ingestion, visualization, and modals. */ -const{useState,useEffect,useRef,useMemo,useCallback}=React; +const{useState,useEffect,useLayoutEffect,useRef,useMemo,useCallback}=React; const COLORS=['#4d9fff','#a78bfa','#22d3ee','#00ff9d','#ff9f43','#ec4899','#ff5f5f','#84cc16']; const LAYER_COLORS={ui:'#4d9fff',components:'#22d3ee',services:'#a78bfa',utils:'#00ff9d',data:'#ff9f43',config:'#ec4899',test:'#f59e0b',modules:'#a78bfa',forms:'#22d3ee',classes:'#ff9f43',note:'#c084fc'}; const IGNORE=new Set(['node_modules','.git','vendor','dist','build','out','coverage','.next','.nuxt','.cache','.parcel-cache','.turbo','.vercel','.local','.artifacts','.playwright-cli','playwright-report','test-results','.claude','.codex','.idea','.vscode','.pnpm-store','.yarn','tmp','temp','target','bin','obj','__pycache__','.venv','venv','env','.env','.tox','.mypy_cache','.pytest_cache','.ruff_cache','__pypackages__','.eggs','__macosx']); @@ -5693,6 +5732,11 @@ return excl?base+'|excl:'+excl:base; } +function githubSourceKeyForLoadedAnalysis(owner,repo,data,pendingPatterns){ + var patterns=data&&data.excludePatterns!=null?data.excludePatterns:pendingPatterns; + return githubCacheSourceKey(owner,repo,patterns); +} + function cachedAnalysisMatchesExcludes(record,patterns){ if(!record)return false; var wanted=normalizeExcludeKey(patterns); @@ -5714,6 +5758,45 @@ return Math.min(8,1/zoom); } +function nodeReplacedByCard(path,cardPaths){ + return !!(cardPaths&&cardPaths.has(path)); +} + +function unburyNodesFromCards(nodes,cardPaths,sizesByPath,pad){ + pad=pad==null?36:pad; + var list=nodes||[]; + var cards=[]; + list.forEach(function(node){ + if(!node||!cardPaths||!cardPaths.has(node.id)||!isFinite(node.x)||!isFinite(node.y))return; + cards.push(node); + }); + if(!cards.length)return list; + var pass; + for(pass=0;pass<4;pass++){ + var moved=false; + list.forEach(function(node){ + if(!node||(cardPaths&&cardPaths.has(node.id))||!isFinite(node.x)||!isFinite(node.y))return; + cards.forEach(function(card){ + var size=(sizesByPath&&sizesByPath[card.id])||codeCardSize(null); + var hw=size.width/2+pad,hh=size.height/2+pad; + var dx=node.x-card.x,dy=node.y-card.y; + if(Math.abs(dx)>=hw||Math.abs(dy)>=hh)return; + var left=hw+dx,right=hw-dx,top=hh+dy,bottom=hh-dy; + var min=Math.min(left,right,top,bottom); + if(min===left)node.x=card.x-hw; + else if(min===right)node.x=card.x+hw; + else if(min===top)node.y=card.y-hh; + else node.y=card.y+hh; + if(node.fx!=null)node.fx=node.x; + if(node.fy!=null)node.fy=node.y; + moved=true; + }); + }); + if(!moved)break; + } + return list; +} + function githubZipDownloadUrl(owner,repo){ return 'https://github.com/'+encodeURIComponent(owner)+'/'+encodeURIComponent(repo)+'/archive/HEAD.zip'; } @@ -5743,9 +5826,78 @@ return Array.from(linked); } +var CODE_CARD_MAX=12; + +function defaultCodeViewSeed(data,folderFilter){ + if(!data||!data.files||!data.files.length)return null; + var filtered=folderFilter?data.files.filter(function(f){return f.folder===folderFilter||f.folder.startsWith(folderFilter+'/');}):data.files; + if(!filtered.length)return null; + var byPath=Object.create(null); + filtered.forEach(function(f){byPath[f.path]=f;}); + var counts=Object.create(null); + (data.connections||[]).forEach(function(c){ + var src=typeof c.source==='object'?c.source.id:c.source; + var tgt=typeof c.target==='object'?c.target.id:c.target; + if(byPath[src])counts[src]=(counts[src]||0)+1; + if(byPath[tgt])counts[tgt]=(counts[tgt]||0)+1; + }); + var best=filtered[0]; + var bestN=counts[best.path]||0; + filtered.forEach(function(file){ + var n=counts[file.path]||0; + if(n>bestN){best=file;bestN=n;} + }); + return best.path; +} + +function fileMatchesFolderFilter(file,folderFilter){ + if(!folderFilter)return true; + if(!file)return false; + return file.folder===folderFilter||(!!file.folder&&file.folder.startsWith(folderFilter+'/')); +} + +function pathMatchesFolderFilter(path,data,folderFilter){ + if(!folderFilter)return true; + if(!data||!data.files||!path)return false; + for(var i=0;ilimit + }; +} + +var CODE_CARD_MIN_WIDTH=320; +var CODE_CARD_MAX_WIDTH=720; +var CODE_CARD_MIN_HEIGHT=160; +var CODE_CARD_MAX_HEIGHT=1840; +var CODE_CARD_LINE_HEIGHT=19; +var CODE_CARD_CHAR_WIDTH=7; +var CODE_CARD_HEAD_HEIGHT=42; +var CODE_CARD_BODY_PAD=16; +var CODE_CARD_GUTTER=72; + +function codeCardContentMetrics(file){ + var content=file&&typeof file.content==='string'?file.content:''; + var lines=content?content.split('\n'):['']; + var maxLineChars=0; + var lineChars=[]; + for(var i=0;imaxLineChars)maxLineChars=n; + } + return{lines:Math.max(1,lines.length),maxLineChars:maxLineChars,lineChars:lineChars}; +} + +var CODE_CARD_WIDTH=440; + +function normalizeCodeCardPrefs(prefs){ + prefs=prefs||{}; + return{expand:!!prefs.expand,wrap:!!prefs.wrap}; +} + +function codeCardWrapColumns(){ + return Math.max(1,Math.floor((CODE_CARD_WIDTH-CODE_CARD_GUTTER-CODE_CARD_BODY_PAD)/CODE_CARD_CHAR_WIDTH)); +} + +function codeCardWrappedLineCount(metrics,prefs){ + metrics=metrics||codeCardContentMetrics(null); + prefs=normalizeCodeCardPrefs(prefs); + if(!prefs.wrap)return Math.max(1,metrics.lines||1); + var cols=codeCardWrapColumns(); + var chars=metrics.lineChars||[]; + var count=0; + if(!chars.length)return Math.max(1,metrics.lines||1); + for(var i=0;iCODE_CARD_MAX_HEIGHT; + var widthClipped=!prefs.wrap&&naturalWidth>width; + return{width:width,height:height,clipped:heightClipped||widthClipped,expand:prefs.expand,wrap:prefs.wrap,naturalHeight:naturalHeight,naturalWidth:naturalWidth}; +} + +var CODE_CARD_RESIZE_MAX_WIDTH=1200; + +function clampCodeCardResize(width,height,prefs){ + prefs=normalizeCodeCardPrefs(prefs); + var w=Math.max(CODE_CARD_MIN_WIDTH,Math.min(CODE_CARD_RESIZE_MAX_WIDTH,Number(width)||CODE_CARD_WIDTH)); + var h=Math.max(CODE_CARD_MIN_HEIGHT,Number(height)||CODE_CARD_MIN_HEIGHT); + if(!prefs.expand)h=Math.min(CODE_CARD_MAX_HEIGHT,h); + return{width:w,height:h}; +} + +function applyCodeCardUserSize(base,override){ + base=base||codeCardSize(null); + if(!override)return base; + var next=clampCodeCardResize(override.width!=null?override.width:base.width,override.height!=null?override.height:base.height,base); + var heightClipped=!base.expand&&(base.naturalHeight||0)>next.height; + var widthClipped=!base.wrap&&(base.naturalWidth||0)>next.width; + return Object.assign({},base,{width:next.width,height:next.height,clipped:heightClipped||widthClipped}); +} + +function codeCardResizeDelta(clientX,clientY,startX,startY,startW,startH,scale,edge){ + var k=Number(scale); + if(!isFinite(k)||k<=0)k=1; + var dx=((Number(clientX)||0)-(Number(startX)||0))/k; + var dy=((Number(clientY)||0)-(Number(startY)||0))/k; + var width=Number(startW)||CODE_CARD_WIDTH; + var height=Number(startH)||CODE_CARD_MIN_HEIGHT; + if(edge==='e'||edge==='se')width+=dx; + if(edge==='s'||edge==='se')height+=dy; + return{width:width,height:height,dx:dx,dy:dy}; +} + +function graphFolderCenters(folders,width,height,options){ + options=options||{}; + var list=[]; + (folders||[]).forEach(function(f){if(f!=null&&list.indexOf(f)<0)list.push(f);}); + var out=Object.create(null); + if(!list.length)return out; + var cols=Math.max(2,Math.ceil(Math.sqrt(list.length))); + var rows=Math.max(1,Math.ceil(list.length/cols)); + var minW=options.minCellW==null?0:Number(options.minCellW)||0; + var minH=options.minCellH==null?0:Number(options.minCellH)||0; + var cw=Math.max(minW,(Number(width)||800)/(cols+1)); + var ch=Math.max(minH,(Number(height)||600)/(rows+1)); + list.forEach(function(f,i){ + out[f]={x:(i%cols+1)*cw,y:(Math.floor(i/cols)+1)*ch}; + }); + return out; +} + +function parkLeftoverCodeNodes(nodes,cardPaths,centers){ + (nodes||[]).forEach(function(node){ + if(!node)return; + if(cardPaths&&cardPaths.has(node.id))return; + if(node.fx!=null&&isFinite(Number(node.fx)))return; + var c=centers&¢ers[node.folder||'root']; + if(!c||!isFinite(c.x)||!isFinite(c.y))return; + node.x=c.x; + node.y=c.y; + }); + return nodes; +} + +function boxesOverlap(a,b,gap){ + if(!a||!b)return false; + gap=gap==null?0:Number(gap)||0; + return !(a.x>b.x+b.width+gap||a.x+a.widthb.y+b.height+gap||a.y+a.heightoriginX&&gx+groupW>originX+maxRowWidth){ + gx=originX; + gy+=rowH+groupGapY; + rowH=0; + } + items.forEach(function(file,i){ + var size=(sizesByPath&&sizesByPath[file.path])||codeCardSize(file); + var col=i%cols,row=Math.floor(i/cols); + placed[file.path]={ + x:gx+colX[col]+size.width/2, + y:gy+rowY[row]+size.height/2, + folder:folder + }; + }); + gx+=groupW+groupGapX; + rowH=Math.max(rowH,groupH); + }); + return placed; +} + +function pathIsFlagged(map,path){ + if(!path||!map)return false; + if(typeof map.has==='function')return map.has(path); + return !!map[path]; +} + +function hiddenOpenedCodePaths(paths,data,folderFilter){ + var hidden=Object.create(null); + if(!folderFilter)return hidden; + var visible=Object.create(null); + filesForOpenedCodePaths(paths,data,folderFilter).forEach(function(file){ + if(file&&file.path)visible[file.path]=true; + }); + (paths||[]).forEach(function(path){ + if(path&&!visible[path])hidden[path]=true; + }); + return hidden; +} + +function codeCardPlacementKeepSet(openedPaths,visibleFiles){ + var keep=Object.create(null); + (openedPaths||[]).forEach(function(path){if(path)keep[path]=true;}); + (visibleFiles||[]).forEach(function(file){if(file&&file.path)keep[file.path]=true;}); + return keep; +} + +function pruneCodeCardPlacements(placements,keep){ + var departed=Object.create(null); + Object.keys(placements||{}).forEach(function(path){ + if(keep&&keep[path])return; + departed[path]=true; + delete placements[path]; + }); + return departed; +} + +function evictHiddenCodeCards(list,hidden,count){ + var need=Number(count); + if(!isFinite(need)||need<=0)return (list||[]).slice(); + var removed=0; + return (list||[]).filter(function(path){ + if(removed>=need)return true; + if(pathIsFlagged(hidden,path)){ + removed++; + return false; + } + return true; + }); +} + +function openCodeCardPaths(prev,path,limit,replace,hidden){ + var list=(prev||[]).slice(); + if(!path)return list; + if(list.indexOf(path)>=0)return list; + var max=limit==null?CODE_CARD_MAX:Number(limit); + if(isFinite(max)&&list.length>=max){ + var need=list.length-max+1; + if(hidden)list=evictHiddenCodeCards(list,hidden,need); + if(list.length>=max){ + if(!replace)return list; + list=list.slice(Math.max(0,list.length-max+1)); + } + } + list.push(path); + return list; +} + +function resolveOpenCodeCard(prev,path,limit,replace,hidden){ + var before=prev||[]; + var next=openCodeCardPaths(before,path,limit,replace,hidden); + var already=!!(path&&before.indexOf(path)>=0); + var inserted=!!(path&&!already&&next.indexOf(path)>=0); + return{paths:next,already:already,inserted:inserted,opened:already||inserted}; +} + +function ensureCodeViewOpenedPaths(openedPaths,selectedPath,data,folderFilter){ + var seed=codeViewSeedPath(selectedPath,data,folderFilter); + if(!seed)return{paths:openedPaths||[],seed:null,opened:false,inserted:false}; + var resolved=resolveOpenCodeCard(openedPaths,seed,null,true,hiddenOpenedCodePaths(openedPaths,data,folderFilter)); + return{paths:resolved.paths,seed:seed,opened:resolved.opened,inserted:resolved.inserted}; +} + +function filesForOpenedCodePaths(paths,data,folderFilter){ + if(!data||!data.files)return[]; + var filtered=folderFilter?data.files.filter(function(f){return f.folder===folderFilter||f.folder.startsWith(folderFilter+'/');}):data.files; + var byPath=Object.create(null); + filtered.forEach(function(file){byPath[file.path]=file;}); + return(paths||[]).map(function(path){return byPath[path];}).filter(Boolean); +} + +function appendCodeCardPlacement(placements,file,size,options){ + options=options||{}; + var gapX=options.gapX==null?88:options.gapX; + var gapY=options.gapY==null?36:options.gapY; + var originX=options.originX==null?80:options.originX; + var originY=options.originY==null?72:options.originY; + var next=Object.assign(Object.create(null),placements||{}); + if(!file||!file.path)return next; + size=size||codeCardSize(file); + var folder=file.folder||'root'; + var prev=next[file.path]; + if(prev){ + next[file.path]={ + left:prev.left, + top:prev.top, + x:prev.left+size.width/2, + y:prev.top+size.height/2, + width:size.width, + height:size.height, + folder:prev.folder||folder + }; + return reflowUnpinnedCodeCards(next,options.pinnedPaths,options); + } + var same=[]; + var folderLeft=null; + Object.keys(next).forEach(function(id){ + var item=next[id]; + if(!item||item.folder!==folder)return; + same.push(item); + folderLeft=folderLeft==null?item.left:Math.min(folderLeft,item.left); + }); + var left,top; + if(!same.length){ + var center=options.centers&&options.centers[folder]; + if(center&&isFinite(center.x)&&isFinite(center.y)){ + left=center.x-size.width/2; + top=center.y-size.height/2; + }else{ + var maxRight=originX; + Object.keys(next).forEach(function(id){ + var item=next[id]; + if(!item)return; + maxRight=Math.max(maxRight,item.left+item.width+gapX); + }); + left=Object.keys(next).length?maxRight:originX; + top=originY; + } + }else{ + left=folderLeft; + var maxBottom=originY; + same.forEach(function(item){maxBottom=Math.max(maxBottom,item.top+item.height+gapY);}); + top=maxBottom; + } + next[file.path]={ + left:left, + top:top, + x:left+size.width/2, + y:top+size.height/2, + width:size.width, + height:size.height, + folder:folder + }; + return reflowUnpinnedCodeCards(next,options.pinnedPaths,options); +} + +function codePathIsPinned(pinnedPaths,path){ + if(!path||!pinnedPaths)return false; + if(typeof pinnedPaths.has==='function')return pinnedPaths.has(path); + return !!pinnedPaths[path]; +} + +function reflowUnpinnedCodeCards(placements,pinnedPaths,options){ + options=options||{}; + var gapY=options.gapY==null?36:options.gapY; + var originY=options.originY==null?72:options.originY; + var next=placements||Object.create(null); + var byFolder=Object.create(null); + Object.keys(next).forEach(function(path){ + var item=next[path]; + if(!item)return; + var folder=item.folder||'root'; + if(!byFolder[folder])byFolder[folder]=[]; + byFolder[folder].push(path); + }); + Object.keys(byFolder).forEach(function(folder){ + var paths=byFolder[folder]; + paths.sort(function(a,b){ + var dy=(next[a].top||0)-(next[b].top||0); + if(dy)return dy; + return ab?1:0; + }); + var center=options.centers&&options.centers[folder]; + var startY=originY; + if(center&&isFinite(center.y)){ + var firstH=next[paths[0]]&&next[paths[0]].height; + startY=center.y-((firstH||CODE_CARD_MIN_HEIGHT)/2); + } + var cursor=startY; + paths.forEach(function(path){ + var item=Object.assign({},next[path]); + next[path]=item; + if(codePathIsPinned(pinnedPaths,path)){ + item.x=item.left+item.width/2; + item.y=item.top+item.height/2; + cursor=Math.max(cursor,item.top+item.height+gapY); + return; + } + item.top=cursor; + item.x=item.left+item.width/2; + item.y=item.top+item.height/2; + cursor=item.top+item.height+gapY; + }); + }); + return next; +} + +function codeCardSymbolLine(file,name){ + if(!file||!name)return null; + var fns=file.functions||[]; + var i; + for(i=0;i0){ + var vspan=Math.max(80,dyAbs*0.45); + var dy=(y2CODE_VIEW_MAX_FIT_SCALE)return CODE_VIEW_MAX_FIT_SCALE; + return value; +} + +function codeCardFitBounds(nodes,sizesByPath,cardPaths){ + var minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity; + (nodes||[]).forEach(function(node){ + if(!node||!cardPaths||!cardPaths.has(node.id)||!isFinite(node.x)||!isFinite(node.y))return; + var size=(sizesByPath&&sizesByPath[node.id])||codeCardSize(null); + minX=Math.min(minX,node.x-size.width/2); + minY=Math.min(minY,node.y-size.height/2); + maxX=Math.max(maxX,node.x+size.width/2); + maxY=Math.max(maxY,node.y+size.height/2); + }); + if(!isFinite(minX))return null; + return {minX:minX,minY:minY,maxX:maxX,maxY:maxY,cx:(minX+maxX)/2,cy:(minY+maxY)/2}; +} + +function applyCodeCardLayout(layer,nodesById,transform,sizesByPath){ + if(!layer)return{placed:0,titleScale:1}; + layer.style.transform=codeCanvasTransformStyle(transform); + var k=transform&&isFinite(Number(transform.k))?Number(transform.k):1; + var titleScale=readableLabelScale(k); + var cards=layer.querySelectorAll?layer.querySelectorAll('[data-code-card]'):[]; + var placed=0; + Array.prototype.forEach.call(cards,function(card){ + var path=card.getAttribute('data-code-card'); + var size=(sizesByPath&&sizesByPath[path])||codeCardSize(null); + var style=codeCardAnchorStyle(nodesById&&nodesById[path],size); + card.style.visibility=style.visibility; + card.style.left=style.left; + card.style.top=style.top; + card.style.width=size.width+'px'; + card.style.height=size.height+'px'; + if(card.classList){ + if(size.clipped&&card.classList.add)card.classList.add('clipped'); + else if(card.classList.remove)card.classList.remove('clipped'); + if(size.expand&&card.classList.add)card.classList.add('expand'); + else if(card.classList.remove)card.classList.remove('expand'); + if(size.wrap&&card.classList.add)card.classList.add('wrap'); + else if(card.classList.remove)card.classList.remove('wrap'); + } + var title=card.querySelector?card.querySelector('.code-card-name'):null; + if(title)title.style.transform='scale('+titleScale+')'; + if(card.classList){ + if(k<0.22&&card.classList.add)card.classList.add('code-far'); + else if(card.classList.remove)card.classList.remove('code-far'); + } + if(style.visibility==='visible')placed++; + }); + return{placed:placed,titleScale:titleScale}; +} + +function armRecentDelete(armedId,clickedId){ + clickedId=clickedId||null; + if(clickedId&&armedId===clickedId)return{confirm:true,armedId:null}; + return{confirm:false,armedId:clickedId}; } function extractFileSymbols(file,connections){ @@ -5816,6 +6804,55 @@ return symbols; } +function codeCardSymbolPills(file,connections,prefs){ + var size=codeCardSize(file,prefs); + return extractFileSymbols(file,connections).map(function(sym){ + var line=codeCardSymbolLine(file,sym.name); + if(!line)return null; + return{name:sym.name,kind:sym.kind,line:line,top:codeCardLineY(size,line,file,prefs)}; + }).filter(Boolean); +} + +function codeCardPillViewTop(lineTop,scrollTop,cardHeight,headHeight){ + var y=Number(lineTop)-(Number(scrollTop)||0); + var min=headHeight==null?CODE_CARD_HEAD_HEIGHT:Number(headHeight); + var max=Number(cardHeight); + if(!isFinite(y)||!isFinite(min)||!isFinite(max))return null; + if(ymax)return null; + return y; +} + +function readCodeCardBodyScroll(layer,path){ + if(!layer||!path)return 0; + var cards=layer.querySelectorAll?layer.querySelectorAll('[data-code-card]'):[]; + for(var i=0;i