@wtdlee/repomap 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let r=[],u=n?.envResult,t=n?.railsAnalysis,o=n?.activeTab||"pages",e=s.repositories[0]?.displayName||s.repositories[0]?.name||"Repository";for(let p of s.repositories){this.graphqlOps.push(...p.analysis?.graphqlOperations||[]),this.apiCalls.push(...p.analysis?.apiCalls||[]);let f=p.analysis?.components||[];for(let a of f)this.components.push({name:a.name,filePath:a.filePath,type:a.type,dependencies:a.dependencies||[],hooks:a.hooks||[]});}for(let p of s.repositories){let f=p.analysis?.pages||[];for(let a of f)r.push({...a,repo:p.name,children:[],parent:null,depth:0});}let{rootPages:i,relations:l}=this.buildHierarchy(r);return this.renderPageMapHtml(r,i,l,e,{envResult:u,railsAnalysis:t,activeTab:o})}buildHierarchy(s){let n=new Map,r=[];for(let t of s)n.set(t.path,t);for(let t of s){let o=t.path.split("/").filter(Boolean);for(let e=o.length-1;e>=1;e--){let i="/"+o.slice(0,e).join("/"),l=n.get(i);if(l){t.parent=i,t.depth=l.depth+1,l.children.includes(t.path)||l.children.push(t.path),r.push({from:i,to:t.path,type:"parent-child",description:`Sub-page of ${i}`});break}}if(t.parent||(t.depth=Math.max(0,o.length-1)),t.layout)for(let e of s)e.path!==t.path&&e.layout===t.layout&&(r.find(l=>l.type==="same-layout"&&(l.from===t.path&&l.to===e.path||l.from===e.path&&l.to===t.path))||r.push({from:t.path,to:e.path,type:"same-layout",description:`Both use ${t.layout}`}));}return {rootPages:s.filter(t=>!t.parent).sort((t,o)=>t.path.localeCompare(o.path)),relations:r}}renderPageMapHtml(s,n,r,u,t){let o=t?.envResult,e=t?.railsAnalysis,i=t?.activeTab||"pages",l=JSON.stringify(this.graphqlOps.map(c=>({name:c.name,type:c.type,variables:c.variables,fields:c.fields,returnType:c.returnType,usedIn:c.usedIn}))),p=JSON.stringify(this.components),f=e?JSON.stringify(e.routes.routes):"[]",a=e?JSON.stringify(e.controllers.controllers):"[]",h=e?JSON.stringify(e.models.models):"[]",v=e?JSON.stringify(e.views):'{ "views": [], "pages": [], "summary": {} }',y=e?JSON.stringify(e.react):'{ "components": [], "entryPoints": [], "summary": {} }',d=e?JSON.stringify(e.grpc):'{ "services": [] }',C=e?JSON.stringify(e.summary):"null",m=o?.hasRails||false,b=o?.hasNextjs||false,w=o?.hasReact||false,x=new Map;for(let c of s){let g=c.path.split("/").filter(Boolean)[0]||"root";x.has(g)||x.set(g,[]),x.get(g)?.push(c);}return `<!DOCTYPE html>
1
+ var E=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(i,n){let r=[],u=n?.envResult,t=n?.railsAnalysis,o=n?.activeTab||"pages",e=i.repositories[0]?.displayName||i.repositories[0]?.name||"Repository";for(let p of i.repositories){this.graphqlOps.push(...p.analysis?.graphqlOperations||[]),this.apiCalls.push(...p.analysis?.apiCalls||[]);let g=p.analysis?.components||[];for(let a of g)this.components.push({name:a.name,filePath:a.filePath,type:a.type,dependencies:a.dependencies||[],hooks:a.hooks||[]});}for(let p of i.repositories){let g=p.analysis?.pages||[];for(let a of g)r.push({...a,repo:p.name,children:[],parent:null,depth:0});}let{rootPages:l,relations:s}=this.buildHierarchy(r);return this.renderPageMapHtml(r,l,s,e,{envResult:u,railsAnalysis:t,activeTab:o})}buildHierarchy(i){let n=new Map,r=[];for(let t of i)n.set(t.path,t);for(let t of i){let o=t.path.split("/").filter(Boolean);for(let e=o.length-1;e>=1;e--){let l="/"+o.slice(0,e).join("/"),s=n.get(l);if(s){t.parent=l,t.depth=s.depth+1,s.children.includes(t.path)||s.children.push(t.path),r.push({from:l,to:t.path,type:"parent-child",description:`Sub-page of ${l}`});break}}if(t.parent||(t.depth=Math.max(0,o.length-1)),t.layout)for(let e of i)e.path!==t.path&&e.layout===t.layout&&(r.find(s=>s.type==="same-layout"&&(s.from===t.path&&s.to===e.path||s.from===e.path&&s.to===t.path))||r.push({from:t.path,to:e.path,type:"same-layout",description:`Both use ${t.layout}`}));}return {rootPages:i.filter(t=>!t.parent).sort((t,o)=>t.path.localeCompare(o.path)),relations:r}}renderPageMapHtml(i,n,r,u,t){let o=t?.envResult,e=t?.railsAnalysis,l=t?.activeTab||"pages",s=c=>JSON.stringify(c).replace(/</g,"\\u003c"),p=s(this.graphqlOps.map(c=>({name:c.name,type:c.type,variables:c.variables,fields:c.fields,returnType:c.returnType,usedIn:c.usedIn}))),g=s(this.components),a=s(i),h=s(r),v=s(this.apiCalls),y=e?s(e.routes.routes):"[]",d=e?s(e.controllers.controllers):"[]",w=e?s(e.models.models):"[]",C=e?s(e.views):'{ "views": [], "pages": [], "summary": {} }',P=e?s(e.react):'{ "components": [], "entryPoints": [], "summary": {} }',x=e?s(e.grpc):'{ "services": [] }',S=e?s(e.summary):"null",m=o?.hasRails||false,f=o?.hasNextjs||false,R=o?.hasReact||false,b=new Map;for(let c of i){let k=c.path.split("/").filter(Boolean)[0]||"root";b.has(k)||b.set(k,[]),b.get(k)?.push(c);}return `<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -16,15 +16,15 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
16
16
  <div style="display:flex;align-items:center;gap:24px">
17
17
  <h1 style="cursor:pointer" onclick="location.href='/'">\u{1F4CA} ${u}</h1>
18
18
  <nav style="display:flex;gap:4px">
19
- <a href="/page-map" class="nav-link ${i==="pages"?"active":""}">Page Map</a>
20
- ${m?`<a href="/rails-map" class="nav-link ${i==="rails"?"active":""}">Rails Map</a>`:""}
19
+ <a href="/page-map" class="nav-link ${l==="pages"?"active":""}">Page Map</a>
20
+ ${m?`<a href="/rails-map" class="nav-link ${l==="rails"?"active":""}">Rails Map</a>`:""}
21
21
  <a href="/docs" class="nav-link">Docs</a>
22
22
  <a href="/api/report" class="nav-link" target="_blank">API</a>
23
23
  </nav>
24
24
  </div>
25
25
  <div style="display:flex;gap:12px;align-items:center">
26
26
  <!-- Environment filter badges -->
27
- ${m&&b?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
27
+ ${m&&f?`<div class="env-filters" style="display:flex;gap:4px;margin-right:8px">
28
28
  <button class="env-badge env-badge-active" data-env="all" onclick="filterByEnv('all')">All</button>
29
29
  <button class="env-badge" data-env="nextjs" onclick="filterByEnv('nextjs')">\u269B\uFE0F Next.js</button>
30
30
  <button class="env-badge" data-env="rails" onclick="filterByEnv('rails')">\u{1F6E4}\uFE0F Rails</button>
@@ -63,7 +63,7 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
63
63
  <!-- Frontend Stats -->
64
64
  <h3 style="margin-top:16px;font-size:10px;text-transform:uppercase;color:var(--text2);letter-spacing:1px">Frontend</h3>
65
65
  <div class="stats" id="stats-container">
66
- <div class="stat" data-filter="pages"><div class="stat-val">${s.length}</div><div class="stat-label">Pages</div></div>
66
+ <div class="stat" data-filter="pages"><div class="stat-val">${i.length}</div><div class="stat-label">Pages</div></div>
67
67
  <div class="stat" data-filter="hierarchies"><div class="stat-val">${r.filter(c=>c.type==="parent-child").length}</div><div class="stat-label">Hierarchies</div></div>
68
68
  <div class="stat" data-filter="graphql"><div class="stat-val">${this.graphqlOps.length}</div><div class="stat-label">GraphQL</div></div>
69
69
  <div class="stat" data-filter="restapi"><div class="stat-val">${this.apiCalls.length}</div><div class="stat-label">REST API</div></div>
@@ -84,17 +84,17 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
84
84
 
85
85
  <div class="content">
86
86
  <!-- Pages Tree View (for all screens - Next.js/React/Rails) -->
87
- <div class="tree-view ${i==="pages"?"active":""}" id="tree-view" data-tab="pages">
88
- ${s.length>0?this.buildTreeHtml(x,s):""}
87
+ <div class="tree-view ${l==="pages"?"active":""}" id="tree-view" data-tab="pages">
88
+ ${i.length>0?this.buildTreeHtml(b,i):""}
89
89
  <div id="page-map-react-components-section" style="${m?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
90
90
  </div>
91
- <div id="page-map-rails-section" style="${s.length>0&&m?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
92
- ${m&&s.length===0?'<div class="empty-state-sm">Loading screens...</div>':""}
91
+ <div id="page-map-rails-section" style="${i.length>0&&m?"margin-top:20px;border-top:1px solid var(--bg3);padding-top:20px":""}">
92
+ ${m&&i.length===0?'<div class="empty-state-sm">Loading screens...</div>':""}
93
93
  </div>
94
94
  </div>
95
95
 
96
96
  <!-- Rails Routes View (dedicated) -->
97
- <div class="tree-view ${i==="rails"?"active":""}" id="rails-tree-view" data-tab="rails">
97
+ <div class="tree-view ${l==="rails"?"active":""}" id="rails-tree-view" data-tab="rails">
98
98
  <div id="rails-routes-container">
99
99
  ${m?'<div class="empty-state-sm">Loading Rails routes...</div>':'<div class="empty-state">No Rails environment detected</div>'}
100
100
  </div>
@@ -139,32 +139,34 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
139
139
  // Environment detection results
140
140
  const envInfo = {
141
141
  hasRails: ${m},
142
- hasNextjs: ${b},
143
- hasReact: ${w}
142
+ hasNextjs: ${f},
143
+ hasReact: ${R}
144
144
  };
145
145
 
146
146
  // Frontend data
147
- const pages = ${JSON.stringify(s)};
148
- const relations = ${JSON.stringify(r)};
149
- const graphqlOps = ${l};
150
- const components = ${p};
151
- const apiCallsData = ${JSON.stringify(this.apiCalls)};
147
+ const pages = ${a};
148
+ const relations = ${h};
149
+ const graphqlOps = ${p};
150
+ const components = ${g};
151
+ const apiCallsData = ${v};
152
152
  window.apiCalls = apiCallsData;
153
153
  const pageMap = new Map(pages.map(p => [p.path, p]));
154
154
  const gqlMap = new Map(graphqlOps.map(op => [op.name, op]));
155
155
  const compMap = new Map(components.map(c => [c.name, c]));
156
+ // Mapping metadata for UI debugging (key -> {confidence, evidence})
157
+ const opMetaMap = new Map();
156
158
 
157
159
  // Rails data (if available)
158
- const railsRoutes = ${f};
159
- const railsControllers = ${a};
160
- const railsModels = ${h};
161
- const railsViews = ${v};
162
- const railsReact = ${y};
163
- const railsGrpc = ${d};
164
- const railsSummary = ${C};
160
+ const railsRoutes = ${y};
161
+ const railsControllers = ${d};
162
+ const railsModels = ${w};
163
+ const railsViews = ${C};
164
+ const railsReact = ${P};
165
+ const railsGrpc = ${x};
166
+ const railsSummary = ${S};
165
167
 
166
168
  // Current active tab state
167
- let currentMainTab = '${i}';
169
+ let currentMainTab = '${l}';
168
170
 
169
171
  // Modal history stack for back navigation
170
172
  const modalHistory = [];
@@ -629,12 +631,18 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
629
631
  html += '<div class="route-stat" data-filter="grpc"><div class="route-stat-val cyan">' + totalWithGrpc + '</div><div class="route-stat-label">gRPC</div></div>';
630
632
  html += '</div>';
631
633
  // Analysis coverage indicator
632
- if (totalWithActionInfo > 0) {
633
- const coverage = Math.round((totalWithActionInfo / combinedData.length) * 100);
634
- const coverageTooltip = 'Percentage of routes successfully matched with controller actions to extract details (JSON/HTML rendering, redirects, etc). This is a tool analysis metric, not a code quality indicator.';
635
- const coverageClass = coverage > 70 ? 'coverage-high' : coverage > 40 ? 'coverage-mid' : 'coverage-low';
634
+ {
635
+ const totalRoutesAnalyzed = combinedData.length;
636
+ const totalWithControllerMatch = combinedData.filter(r => r.controllerInfo).length;
637
+ const overallCoverage = totalRoutesAnalyzed > 0 ? Math.round((totalWithActionInfo / totalRoutesAnalyzed) * 100) : 0;
638
+ const matchedCoverage = totalWithControllerMatch > 0 ? Math.round((totalWithActionInfo / totalWithControllerMatch) * 100) : 0;
639
+ const overallTooltip = 'Percentage of routes that were successfully matched with controller actions to extract details. This includes routes pointing to external controllers (gems/engines) which are not analyzable from app/controllers.';
640
+ const matchedTooltip = 'Percentage of routes with a matched app controller (app/controllers) where the target action method was found and parsed.';
641
+ const overallClass = overallCoverage > 70 ? 'coverage-high' : overallCoverage > 40 ? 'coverage-mid' : 'coverage-low';
642
+ const matchedClass = matchedCoverage > 70 ? 'coverage-high' : matchedCoverage > 40 ? 'coverage-mid' : 'coverage-low';
636
643
  html += '<div class="coverage-info">';
637
- html += '<div class="coverage-text" title="' + coverageTooltip + '">Action Details Coverage: <span class="' + coverageClass + '">' + coverage + '%</span> (' + totalWithActionInfo + '/' + combinedData.length + ' routes analyzed) \u2139\uFE0F</div>';
644
+ html += '<div class="coverage-text" title="' + overallTooltip + '">Action Details Coverage (overall): <span class="' + overallClass + '">' + overallCoverage + '%</span> (' + totalWithActionInfo + '/' + totalRoutesAnalyzed + ' routes analyzed) \u2139\uFE0F</div>';
645
+ html += '<div class="coverage-text" title="' + matchedTooltip + '">Action Details Coverage (matched controllers): <span class="' + matchedClass + '">' + matchedCoverage + '%</span> (' + totalWithActionInfo + '/' + totalWithControllerMatch + ' matched) \u2139\uFE0F</div>';
638
646
  html += '</div>';
639
647
  }
640
648
  html += '</div>';
@@ -1930,61 +1938,12 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
1930
1938
  stepsHtml += '</div></div>';
1931
1939
  }
1932
1940
 
1933
- // Data operations - show grouped by source path, sorted by depth
1934
- // Also include GraphQL from related components (including their dependencies)
1941
+ // Data operations - use page.dataFetching from engine.ts only
1942
+ // (engine.ts already enriches pages with GraphQL from components via enrichPagesWithHookGraphQL)
1935
1943
  let dataHtml = '';
1936
1944
 
1937
- // Recursively extract GraphQL from component and its dependencies
1938
- function extractComponentGraphQL(comp, visited = new Set(), depth = 0) {
1939
- const results = [];
1940
- if (!comp || visited.has(comp.name) || depth > 10) return results;
1941
- visited.add(comp.name);
1942
-
1943
- // Extract GraphQL from this component's hooks
1944
- if (comp.hooks) {
1945
- comp.hooks.forEach(hook => {
1946
- // Only match hooks with "Query: " or "Mutation: " prefix (from dataflow analyzer)
1947
- // This avoids matching unrelated hooks like useQueryParams
1948
- if (hook.startsWith('Query: ') || hook.startsWith('Mutation: ') || hook.startsWith('Subscription: ')) {
1949
- let queryName = hook.replace('Query: ', '').replace('Mutation: ', '').replace('Subscription: ', '').trim();
1950
- // Skip empty names or hooks without actual operation names
1951
- if (!queryName) {
1952
- return;
1953
- }
1954
- const isM = hook.includes('Mutation');
1955
- const depthArrows = '\u2192 '.repeat(depth + 1);
1956
- results.push({
1957
- type: isM ? 'useMutation' : 'useQuery',
1958
- operationName: depthArrows + queryName + ' (via ' + comp.name + ')',
1959
- variables: []
1960
- });
1961
- }
1962
- });
1963
- }
1964
-
1965
- // Recursively check dependencies
1966
- if (comp.dependencies) {
1967
- comp.dependencies.forEach(depName => {
1968
- const depComp = componentByName.get(depName);
1969
- if (depComp) {
1970
- results.push(...extractComponentGraphQL(depComp, visited, depth + 1));
1971
- }
1972
- });
1973
- }
1974
-
1975
- return results;
1976
- }
1977
-
1978
- // Get GraphQL from page's components (including dependencies)
1979
- const pageComps = getPageComponents(page);
1980
- const componentGraphQL = [];
1981
- const visited = new Set();
1982
- pageComps.forEach(comp => {
1983
- componentGraphQL.push(...extractComponentGraphQL(comp, visited, 0));
1984
- });
1985
-
1986
- // Combine direct dataFetching with component GraphQL
1987
- const allDataFetching = [...(page.dataFetching || []), ...componentGraphQL];
1945
+ // Use dataFetching directly from engine.ts analysis
1946
+ const allDataFetching = [...(page.dataFetching || [])];
1988
1947
 
1989
1948
  if (allDataFetching.length > 0) {
1990
1949
  // Separate actual GraphQL operations from component references
@@ -2002,6 +1961,7 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2002
1961
  // Extract query name - remove arrows and (via xxx) pattern
2003
1962
  let queryName = rawName.replace(/^[\u2192\\s]+/, '').trim();
2004
1963
  let sourcePath = 'Direct';
1964
+ let sourceDetail = '';
2005
1965
  let depth = 0;
2006
1966
 
2007
1967
  // Method 1: Extract from (via xxx) pattern in operationName (from extractComponentGraphQL)
@@ -2011,13 +1971,35 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2011
1971
  queryName = queryName.replace(viaMatch[0], '').trim();
2012
1972
  depth = arrowCount || 1;
2013
1973
  }
2014
- // Method 2: Use df.source field (from engine.ts enrichPagesWithHookGraphQL)
1974
+ // Method 2: Use df.source field
2015
1975
  else if (source.startsWith('component:')) {
2016
1976
  sourcePath = source.replace('component:', '');
2017
1977
  depth = 1;
2018
1978
  } else if (source.startsWith('hook:')) {
2019
1979
  sourcePath = source.replace('hook:', '');
2020
1980
  depth = 1;
1981
+ } else if (source.startsWith('usedIn:')) {
1982
+ // Evidence-based source (file where the operation reference was found)
1983
+ sourcePath = 'Indirect';
1984
+ // Keep detail for modal
1985
+ sourceDetail = source.replace('usedIn:', '');
1986
+ depth = 1;
1987
+ } else if (source.startsWith('import:')) {
1988
+ sourcePath = 'Import';
1989
+ sourceDetail = source.replace('import:', '');
1990
+ depth = 1;
1991
+ } else if (source.startsWith('common:')) {
1992
+ sourcePath = 'Common (shared)';
1993
+ sourceDetail = source.replace('common:', '');
1994
+ depth = 1;
1995
+ } else if (source.startsWith('close:')) {
1996
+ sourcePath = 'Close (related)';
1997
+ sourceDetail = source.replace('close:', '');
1998
+ depth = 1;
1999
+ } else if (source.startsWith('indirect:')) {
2000
+ sourcePath = 'Indirect';
2001
+ sourceDetail = source.replace('indirect:', '');
2002
+ depth = 1;
2021
2003
  }
2022
2004
  // "import:xxx" or no source stays as Direct
2023
2005
 
@@ -2025,15 +2007,27 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2025
2007
  ...df,
2026
2008
  queryName,
2027
2009
  sourcePath,
2028
- depth
2010
+ sourceDetail: sourceDetail || undefined,
2011
+ depth,
2029
2012
  };
2030
2013
  });
2031
2014
 
2032
- // Sort by depth (lower first) then by source path
2015
+ // Sort by depth (lower first) then by source category priority
2016
+ const sourcePriority = (src) => {
2017
+ if (src === 'Direct') return 0;
2018
+ if (src === 'Close (related)') return 1;
2019
+ if (src === 'Import') return 2;
2020
+ if (src === 'Indirect') return 3;
2021
+ if (src === 'Common (shared)') return 4;
2022
+ return 5;
2023
+ };
2033
2024
  parsedOps.sort((a, b) => {
2034
2025
  if (a.depth !== b.depth) return a.depth - b.depth;
2035
2026
  if (a.sourcePath === 'Direct') return -1;
2036
2027
  if (b.sourcePath === 'Direct') return 1;
2028
+ const ap = sourcePriority(a.sourcePath);
2029
+ const bp = sourcePriority(b.sourcePath);
2030
+ if (ap !== bp) return ap - bp;
2037
2031
  return a.sourcePath.localeCompare(b.sourcePath);
2038
2032
  });
2039
2033
 
@@ -2104,7 +2098,7 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2104
2098
  sortedPaths.forEach(pathName => {
2105
2099
  const ops = groupedByPath.get(pathName);
2106
2100
  const isDirect = pathName === 'Direct';
2107
- const depthIndicator = isDirect ? '' : '\u21B3 ';
2101
+ // Remove the "\u21B3" prefix (UI becomes cleaner with <details>/<summary>)
2108
2102
  const pathLabel = isDirect ? 'Direct (this page)' : pathName;
2109
2103
  // UI indent: 4px per level added to base padding (10px)
2110
2104
  const uiLevel = pathToUiLevel.get(pathName) || 0;
@@ -2112,20 +2106,68 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2112
2106
  const totalPadding = 10 + uiIndent;
2113
2107
 
2114
2108
  // Group container, header aligned with detail-item content
2115
- dataHtml += '<div class="data-path-group" style="margin:8px 0">' +
2116
- '<div class="data-path-header" style="font-size:11px;color:var(--text2);margin-bottom:4px;padding-left:'+totalPadding+'px">' +
2117
- depthIndicator + '<span class="text-accent">' + pathLabel + '</span> (' + ops.length + ')' +
2118
- '</div>';
2119
-
2109
+ // Non-direct groups are collapsed by default to reduce noise from shared/common operations.
2110
+ const isClose = pathName === 'Close (related)';
2111
+ const isCollapsedByDefault = !(isDirect || isClose);
2112
+ const detailsOpenAttr = isCollapsedByDefault ? '' : ' open';
2113
+
2114
+ dataHtml += '<details class="data-path-group" style="margin:8px 0"' + detailsOpenAttr + '>' +
2115
+ '<summary class="data-path-header" style="--pad-left:'+totalPadding+'px">' +
2116
+ '<span class="text-accent">' + pathLabel + '</span> (' + ops.length + ')' +
2117
+ '</summary>';
2118
+
2119
+ // Secondary grouping inside the group by concrete source file (sourceDetail).
2120
+ // This reduces noise when a group contains operations from many files.
2121
+ const bySource = new Map();
2120
2122
  ops.forEach(op => {
2121
- const isQ = !op.type?.includes('Mutation');
2122
- const srcArg = op.sourcePath !== 'Direct' ? ",\\'"+op.sourcePath.replace(/'/g, "\\\\'")+"\\'": '';
2123
- // detail-item keeps base padding, adds indent
2124
- dataHtml += '<div class="detail-item data-op" style="padding:8px 10px 8px '+totalPadding+'px" onclick="showDataDetail(\\''+op.queryName.replace(/'/g, "\\\\'")+"\\'"+srcArg+')">' +
2125
- '<span class="tag '+(isQ?'tag-query':'tag-mutation')+'" style="font-size:10px">'+(isQ?'Q':'M')+'</span> '+op.queryName+'</div>';
2123
+ const key = op.sourceDetail || '';
2124
+ if (!bySource.has(key)) bySource.set(key, []);
2125
+ bySource.get(key).push(op);
2126
2126
  });
2127
2127
 
2128
- dataHtml += '</div>';
2128
+ const sourceKeys = Array.from(bySource.keys()).sort((a, b) => {
2129
+ // Put "unknown/empty" at the end
2130
+ if (!a && b) return 1;
2131
+ if (a && !b) return -1;
2132
+ return String(a).localeCompare(String(b));
2133
+ });
2134
+
2135
+ sourceKeys.forEach(sourceKey => {
2136
+ const sourceOps = bySource.get(sourceKey) || [];
2137
+ const hasSourceHeader = !!sourceKey && sourceKeys.length > 1;
2138
+
2139
+ if (hasSourceHeader) {
2140
+ const sourceFileName = String(sourceKey).split(/[\\/]/).pop() || String(sourceKey);
2141
+ dataHtml += '<div class="data-source-group" style="margin:6px 0">' +
2142
+ '<div class="data-source-header" style="padding-left:'+totalPadding+'px">' +
2143
+ '<span style="opacity:0.9">' + sourceFileName + '</span> (' + sourceOps.length + ')' +
2144
+ '</div>';
2145
+ }
2146
+
2147
+ sourceOps.forEach(op => {
2148
+ const isQ = !op.type?.includes('Mutation');
2149
+ const sourceForModal = op.sourceDetail || op.sourcePath;
2150
+ const srcArg = op.sourcePath !== 'Direct' && sourceForModal
2151
+ ? "\\'"+sourceForModal.replace(/'/g, "\\\\'")+"\\'"
2152
+ : 'null';
2153
+
2154
+ // Store mapping metadata in a global map and pass only a key to onclick.
2155
+ const metaKey = op.queryName + '|' + (sourceForModal || '') + '|' + (op.type || '');
2156
+ if (op.confidence || (op.evidence && op.evidence.length)) {
2157
+ opMetaMap.set(metaKey, { confidence: op.confidence, evidence: op.evidence });
2158
+ }
2159
+ const metaArg = ",\\'"+metaKey.replace(/'/g, "\\\\'")+"\\'";
2160
+ // detail-item keeps base padding, adds indent
2161
+ dataHtml += '<div class="detail-item data-op" style="padding:8px 10px 8px '+totalPadding+'px" onclick="showDataDetail(\\''+op.queryName.replace(/'/g, "\\\\'")+"\\',"+srcArg+metaArg+')">' +
2162
+ '<span class="tag '+(isQ?'tag-query':'tag-mutation')+'" style="font-size:10px">'+(isQ?'Q':'M')+'</span> '+op.queryName+'</div>';
2163
+ });
2164
+
2165
+ if (hasSourceHeader) {
2166
+ dataHtml += '</div>';
2167
+ }
2168
+ });
2169
+
2170
+ dataHtml += '</details>';
2129
2171
  });
2130
2172
 
2131
2173
  dataHtml += '</div>';
@@ -2202,75 +2244,7 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2202
2244
  if (c.name) componentByName.set(c.name, c);
2203
2245
  });
2204
2246
 
2205
- // Check if a component (or its dependencies) uses GraphQL
2206
- function componentUsesGraphQL(comp, visited = new Set()) {
2207
- if (!comp || visited.has(comp.name)) return false;
2208
- visited.add(comp.name);
2209
-
2210
- // Check hooks for GraphQL queries (only match "Query: X" or "Mutation: X" format)
2211
- if (comp.hooks && comp.hooks.some(h =>
2212
- h.startsWith('Query: ') || h.startsWith('Mutation: ') || h.startsWith('Subscription: ')
2213
- )) {
2214
- return true;
2215
- }
2216
-
2217
- // Check dependencies recursively (limit depth to avoid infinite loops)
2218
- if (comp.dependencies && visited.size < 20) {
2219
- for (const dep of comp.dependencies) {
2220
- const depComp = componentByName.get(dep);
2221
- if (depComp && componentUsesGraphQL(depComp, visited)) {
2222
- return true;
2223
- }
2224
- }
2225
- }
2226
-
2227
- return false;
2228
- }
2229
-
2230
- // Find components related to a page (strict matching only)
2231
- function getPageComponents(page) {
2232
- const relatedComps = [];
2233
-
2234
- // 1. Find by page file path (exact match)
2235
- if (page.filePath) {
2236
- const pageComp = componentByFile.get(page.filePath);
2237
- if (pageComp) relatedComps.push(pageComp);
2238
-
2239
- // Check for PageContainer pattern based on file name
2240
- const baseName = page.filePath.split('/').pop()?.replace(/\\.(tsx?|jsx?)$/, '') || '';
2241
- const containerName = baseName.charAt(0).toUpperCase() + baseName.slice(1) + 'PageContainer';
2242
- const container = componentByName.get(containerName);
2243
- if (container) relatedComps.push(container);
2244
-
2245
- // Check for feature-based container: /pages/app/agencies \u2192 AgenciesPageContainer
2246
- const pathParts = page.filePath.split('/');
2247
- const pageIndex = pathParts.indexOf('pages');
2248
- if (pageIndex >= 0 && pathParts.length > pageIndex + 2) {
2249
- // Get the main feature segment (e.g., 'agencies' from '/pages/app/agencies/...')
2250
- const featureSegment = pathParts[pageIndex + 2];
2251
- if (featureSegment && !featureSegment.startsWith('[')) {
2252
- const featurePascal = featureSegment.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
2253
- const featureContainer = componentByName.get(featurePascal + 'PageContainer');
2254
- if (featureContainer) relatedComps.push(featureContainer);
2255
- }
2256
- }
2257
- }
2258
-
2259
- // 2. Find by component name from page data
2260
- if (page.component) {
2261
- const comp = componentByName.get(page.component);
2262
- if (comp) relatedComps.push(comp);
2263
-
2264
- // Also check for Container pattern
2265
- const containerName = page.component + 'Container';
2266
- const container = componentByName.get(containerName);
2267
- if (container) relatedComps.push(container);
2268
- }
2269
-
2270
- return relatedComps;
2271
- }
2272
-
2273
- // Include pages with direct GraphQL usage OR pages whose components use GraphQL
2247
+ // Include pages with direct GraphQL usage (from engine.ts enrichPagesWithHookGraphQL)
2274
2248
  const pagesWithGraphQL = new Set([
2275
2249
  // Pages with direct dataFetching
2276
2250
  ...pages.filter(p =>
@@ -2279,11 +2253,6 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2279
2253
  df.type === 'getServerSideProps' || df.type === 'getStaticProps'
2280
2254
  )
2281
2255
  ).map(p => p.path),
2282
- // Pages whose components (or dependencies) use GraphQL
2283
- ...pages.filter(p => {
2284
- const pageComps = getPageComponents(p);
2285
- return pageComps.some(comp => componentUsesGraphQL(comp));
2286
- }).map(p => p.path),
2287
2256
  // Pages whose files are referenced in GraphQL operation usedIn
2288
2257
  ...pages.filter(p => {
2289
2258
  if (!p.filePath) return false;
@@ -2695,12 +2664,28 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2695
2664
  btn.remove();
2696
2665
  };
2697
2666
 
2698
- function showDataDetail(rawName, sourcePath) {
2667
+ function showDataDetail(rawName, sourcePath, metaKeyOrJson) {
2699
2668
  // Clean up name: remove "\u2192 " prefix and " (ComponentName)" suffix
2700
2669
  const name = rawName
2701
2670
  .replace(/^[\u2192\\->\\s]+/, '')
2702
2671
  .replace(/\\s*\\([^)]+\\)\\s*$/, '');
2703
2672
 
2673
+ let meta = null;
2674
+ try {
2675
+ // New: lookup by meta key
2676
+ if (metaKeyOrJson && typeof metaKeyOrJson === 'string' && opMetaMap.has(metaKeyOrJson)) {
2677
+ meta = opMetaMap.get(metaKeyOrJson);
2678
+ }
2679
+ // Backward compatibility: if someone passes raw JSON string
2680
+ else if (metaKeyOrJson && typeof metaKeyOrJson === 'string' && metaKeyOrJson.trim().startsWith('{')) {
2681
+ meta = JSON.parse(metaKeyOrJson);
2682
+ } else if (metaKeyOrJson && typeof metaKeyOrJson === 'object') {
2683
+ meta = metaKeyOrJson;
2684
+ }
2685
+ } catch {
2686
+ meta = null;
2687
+ }
2688
+
2704
2689
  // Convert SCREAMING_CASE to PascalCase (e.g., COMPANY_QUERY \u2192 CompanyQuery)
2705
2690
  const toPascalCase = (str) => {
2706
2691
  if (!/^[A-Z][A-Z0-9_]*$/.test(str)) return str;
@@ -2772,12 +2757,110 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2772
2757
 
2773
2758
  if (op) {
2774
2759
  // Found GraphQL operation
2775
- html = '<div class="detail-section"><h4>Type</h4><span class="tag '+(op.type==='mutation'?'tag-mutation':'tag-query')+'">'+op.type.toUpperCase()+'</span></div>';
2760
+ const confidence = meta?.confidence || '';
2761
+ const confidenceTheme = (level) => {
2762
+ if (level === 'certain') {
2763
+ return {
2764
+ label: 'CERTAIN',
2765
+ bg: '#22c55e',
2766
+ title: 'Certain: reached via a very close import path from the page (0\u20132 steps)',
2767
+ };
2768
+ }
2769
+ if (level === 'likely') {
2770
+ return {
2771
+ label: 'LIKELY',
2772
+ bg: '#f59e0b',
2773
+ title: 'Likely: reachable via the import graph, but indirect (3+ steps)',
2774
+ };
2775
+ }
2776
+ if (level === 'unknown') {
2777
+ return {
2778
+ label: 'COMMON',
2779
+ bg: '#64748b',
2780
+ title: 'Common: reached via widely shared modules across many pages',
2781
+ };
2782
+ }
2783
+ return null;
2784
+ };
2785
+
2786
+ const conf = confidenceTheme(confidence);
2787
+ const confidenceBadge =
2788
+ confidence === 'likely' && conf
2789
+ ? '<span class="tag" title="' +
2790
+ conf.title +
2791
+ '" style="cursor:help;font-size:10px;display:inline-flex;align-items:center;justify-content:center;height:18px;line-height:18px;padding:0 8px;border-radius:999px;background:' +
2792
+ conf.bg +
2793
+ ';color:white;opacity:0.95">' +
2794
+ conf.label +
2795
+ '</span>'
2796
+ : '';
2797
+
2798
+ html =
2799
+ '<div class="detail-section"><h4>Type</h4><div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">' +
2800
+ '<span class="tag ' +
2801
+ (op.type === 'mutation' ? 'tag-mutation' : 'tag-query') +
2802
+ '" style="display:inline-flex;align-items:center;justify-content:center;height:18px;line-height:18px;padding:0 8px;border-radius:999px">' +
2803
+ op.type.toUpperCase() +
2804
+ '</span>' +
2805
+ confidenceBadge +
2806
+ '</div></div>';
2807
+
2808
+ const escapeHtml = (s) => String(s ?? '')
2809
+ .replace(/&/g, '&amp;')
2810
+ .replace(/</g, '&lt;')
2811
+ .replace(/>/g, '&gt;')
2812
+ .replace(/"/g, '&quot;')
2813
+ .replace(/'/g, '&#39;');
2814
+
2815
+ // Prepare Evidence section HTML (render later, after GraphQL section)
2816
+ let evidenceSectionHtml = '';
2817
+ if (meta && meta.evidence && Array.isArray(meta.evidence) && meta.evidence.length > 0) {
2818
+ evidenceSectionHtml += '<div class="detail-section"><h4>Evidence</h4>';
2819
+ meta.evidence.slice(0, 12).forEach(ev => {
2820
+ const file = ev.file || '';
2821
+ const line = ev.line ? ':' + ev.line : '';
2822
+ const detailRaw = ev.detail ? String(ev.detail) : '';
2823
+
2824
+ // Escape then enhance arrows for readability
2825
+ const detailEsc = escapeHtml(detailRaw);
2826
+ const detailPretty = detailEsc.replace(/-&gt;|->/g, '<span style="color:#60a5fa;font-weight:700;padding:0 4px">\u2192</span>');
2827
+
2828
+ evidenceSectionHtml += '<div class="detail-item" style="font-size:11px;display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%;overflow:hidden">' +
2829
+ '<code style="background:#0f172a;color:#93c5fd;padding:2px 6px;border-radius:3px;font-size:10px;display:block;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' +
2830
+ escapeHtml(file + line) +
2831
+ '</code>' +
2832
+ (detailRaw
2833
+ ? '<div style="opacity:0.92;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' + detailPretty + '</div>'
2834
+ : '') +
2835
+ '</div>';
2836
+ });
2837
+ if (meta.evidence.length > 12) {
2838
+ evidenceSectionHtml += '<div class="detail-item" style="font-size:11px;opacity:0.8">... '+(meta.evidence.length - 12)+' more</div>';
2839
+ }
2840
+ evidenceSectionHtml += '</div>';
2841
+ }
2842
+
2843
+ // Confidence is shown as a small badge next to Type to avoid taking extra vertical space.
2776
2844
 
2777
2845
  // Source info
2778
2846
  if (sourcePath) {
2779
- const isHook = sourcePath.startsWith('use');
2780
- html += '<div class="detail-section"><h4>Source</h4><div class="detail-item" style="font-size:12px">via '+(isHook?'Hook':'Component')+': <span class="text-accent">'+sourcePath+'</span></div></div>';
2847
+ const looksLikeFile =
2848
+ sourcePath.includes('/') ||
2849
+ sourcePath.endsWith('.ts') ||
2850
+ sourcePath.endsWith('.tsx') ||
2851
+ sourcePath.endsWith('.js') ||
2852
+ sourcePath.endsWith('.jsx');
2853
+ const isHook = !looksLikeFile && sourcePath.startsWith('use');
2854
+ const label = looksLikeFile ? 'File' : isHook ? 'Hook' : 'Component';
2855
+ html += '<div class="detail-section">' +
2856
+ '<h4 style="display:flex;justify-content:space-between;align-items:center">' +
2857
+ 'Source' +
2858
+ '<span class="tag tag-default" style="font-size:10px">via ' + label + '</span>' +
2859
+ '</h4>' +
2860
+ '<code style="background:#0f172a;color:#93c5fd;padding:8px 10px;border-radius:6px;font-family:monospace;font-size:11px;display:block;max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word">' +
2861
+ escapeHtml(sourcePath) +
2862
+ '</code>' +
2863
+ '</div>';
2781
2864
  }
2782
2865
 
2783
2866
  // Operation Name with copy button
@@ -2802,10 +2885,14 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
2802
2885
 
2803
2886
  html += '<div class="detail-section"><h4 style="display:flex;justify-content:space-between;align-items:center">GraphQL<button class="copy-btn" onclick="copyGqlCode(this)" data-code="'+gqlCodeEscaped+'" title="Copy GraphQL">\u{1F4CB}</button></h4>';
2804
2887
  html += '<pre style="background:#0f172a;color:#e2e8f0;padding:12px;border-radius:6px;font-size:11px;overflow-x:auto;white-space:pre;max-height:300px;overflow-y:auto">' + gqlCode + '</pre></div>';
2888
+ // Evidence should appear right after GraphQL section
2889
+ if (evidenceSectionHtml) html += evidenceSectionHtml;
2805
2890
  } else if (op.variables?.length) {
2806
2891
  html += '<div class="detail-section"><h4>Variables</h4>';
2807
2892
  op.variables.forEach(v => { html += '<div class="detail-item">'+v.name+': <code style="background:#0f172a;color:#93c5fd;padding:2px 6px;border-radius:3px;font-family:monospace">'+v.type+'</code>'+(v.required?' (required)':'')+'</div>'; });
2808
2893
  html += '</div>';
2894
+ // Evidence should appear right after Variables section (when GraphQL block is absent)
2895
+ if (evidenceSectionHtml) html += evidenceSectionHtml;
2809
2896
  }
2810
2897
  if (op.usedIn?.length) {
2811
2898
  html += '<div class="detail-section"><h4>Used In ('+op.usedIn.length+' files)</h4>';
@@ -3568,13 +3655,10 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3568
3655
  const page = pageMap.get(pagePath);
3569
3656
  if (!page) return;
3570
3657
 
3571
- // Get page's components and calculate total GraphQL
3572
- const pageComps = getPageComponents(page);
3573
- const visited = new Set();
3658
+ // Count GraphQL from dataFetching only (already enriched by engine.ts)
3574
3659
  let queries = 0;
3575
3660
  let mutations = 0;
3576
3661
 
3577
- // Count from direct dataFetching
3578
3662
  (page.dataFetching || []).forEach(df => {
3579
3663
  if (df.type?.includes('Mutation')) {
3580
3664
  mutations++;
@@ -3583,16 +3667,6 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3583
3667
  }
3584
3668
  });
3585
3669
 
3586
- // Count from component hooks (simplified - just count hooks)
3587
- pageComps.forEach(comp => {
3588
- if (!comp || visited.has(comp.name)) return;
3589
- visited.add(comp.name);
3590
- (comp.hooks || []).forEach(hook => {
3591
- if (hook.includes('Mutation:')) mutations++;
3592
- else if (hook.includes('Query:')) queries++;
3593
- });
3594
- });
3595
-
3596
3670
  // Update Q tag
3597
3671
  const qTag = item.querySelector('.tag-query');
3598
3672
  if (qTag) {
@@ -3621,12 +3695,12 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3621
3695
  setTimeout(updatePageGqlCounts, 100);
3622
3696
  </script>
3623
3697
  </body>
3624
- </html>`}buildTreeHtml(s,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],u=0;return Array.from(s.entries()).sort((t,o)=>t[0].localeCompare(o[0])).map(([t,o])=>{let e=r[u++%r.length],i=o.sort((a,h)=>a.path.localeCompare(h.path)),l=new Set(i.map(a=>a.path)),p=new Map;for(let a of i){let h=a.path.split("/").filter(Boolean),v=0;for(let y=h.length-1;y>=1;y--){let d="/"+h.slice(0,y).join("/");if(l.has(d)){v=(p.get(d)??0)+1;break}}p.set(a.path,v);}let f=i.map(a=>{let h=this.getPageType(a.path),v=p.get(a.path)??0,d=a.repo||"",C=n.some(g=>g.repo&&g.repo!==d),m=d.split("/").pop()?.split("-").map(g=>g.substring(0,4)).join("-")||d.substring(0,8),b=C&&d?`<span class="tag tag-repo" title="${d}">${m}</span>`:"",w=/^\/[A-Z]/.test(a.path)||a.filePath&&a.filePath.includes("components/pages"),x=w&&a.filePath?a.filePath.replace(/\.tsx?$/,"").replace(/^(frontend\/src\/|src\/)/,""):a.path,c=w?'<span class="tag tag-info" title="SPA Component Page">SPA</span>':"";return `<div class="page-item" data-path="${a.path}" data-repo="${d}" onclick="selectPage('${a.path}')" style="--depth:${v}">
3698
+ </html>`}buildTreeHtml(i,n){let r=["#ef4444","#f97316","#eab308","#22c55e","#14b8a6","#3b82f6","#8b5cf6","#ec4899"],u=0;return Array.from(i.entries()).sort((t,o)=>t[0].localeCompare(o[0])).map(([t,o])=>{let e=r[u++%r.length],l=o.sort((a,h)=>a.path.localeCompare(h.path)),s=new Set(l.map(a=>a.path)),p=new Map;for(let a of l){let h=a.path.split("/").filter(Boolean),v=0;for(let y=h.length-1;y>=1;y--){let d="/"+h.slice(0,y).join("/");if(s.has(d)){v=(p.get(d)??0)+1;break}}p.set(a.path,v);}let g=l.map(a=>{let h=this.getPageType(a.path),v=p.get(a.path)??0,d=a.repo||"",w=n.some(f=>f.repo&&f.repo!==d),C=d.split("/").pop()?.split("-").map(f=>f.substring(0,4)).join("-")||d.substring(0,8),P=w&&d?`<span class="tag tag-repo" title="${d}">${C}</span>`:"",x=/^\/[A-Z]/.test(a.path)||a.filePath&&a.filePath.includes("components/pages"),S=x&&a.filePath?a.filePath.replace(/\.tsx?$/,"").replace(/^(frontend\/src\/|src\/)/,""):a.path,m=x?'<span class="tag tag-info" title="SPA Component Page">SPA</span>':"";return `<div class="page-item" data-path="${a.path}" data-repo="${d}" onclick="selectPage('${a.path}')" style="--depth:${v}">
3625
3699
  <span class="page-type" style="--type-color:${h.color}">${h.label}</span>
3626
- <span class="page-path">${x}</span>
3700
+ <span class="page-path">${S}</span>
3627
3701
  <div class="page-tags">
3628
- ${b}
3629
- ${c}
3702
+ ${P}
3703
+ ${m}
3630
3704
  ${a.authentication?.required?'<span class="tag tag-auth">AUTH</span>':""}
3631
3705
  <span class="tag tag-query gql-count" data-page-path="${a.path}" style="display:none">Q:0</span>
3632
3706
  <span class="tag tag-mutation gql-count-m" data-page-path="${a.path}" style="display:none">M:0</span>
@@ -3637,5 +3711,5 @@ var P=class{graphqlOps=[];apiCalls=[];components=[];generatePageMapHtml(s,n){let
3637
3711
  <span class="group-name">/${t}</span>
3638
3712
  <span class="group-count">${o.length}</span>
3639
3713
  </div>
3640
- <div class="group-content">${f}</div>
3641
- </div>`}).join("")}getPageType(s){let n=s.split("/").filter(Boolean).pop()||"";return n==="new"||s.endsWith("/new")?{label:"CREATE",color:"#22c55e"}:n==="edit"||s.includes("/edit")?{label:"EDIT",color:"#f59e0b"}:n.startsWith("[")||n.startsWith(":")?{label:"DETAIL",color:"#3b82f6"}:s.includes("setting")?{label:"SETTINGS",color:"#6b7280"}:{label:"LIST",color:"#06b6d4"}}};export{P as a};
3714
+ <div class="group-content">${g}</div>
3715
+ </div>`}).join("")}getPageType(i){let n=i.split("/").filter(Boolean).pop()||"";return n==="new"||i.endsWith("/new")?{label:"CREATE",color:"#22c55e"}:n==="edit"||i.includes("/edit")?{label:"EDIT",color:"#f59e0b"}:n.startsWith("[")||n.startsWith(":")?{label:"DETAIL",color:"#3b82f6"}:i.includes("setting")?{label:"SETTINGS",color:"#6b7280"}:{label:"LIST",color:"#06b6d4"}}};export{E as a};