project-graph-mcp 2.3.1 → 2.4.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.
Files changed (226) hide show
  1. package/package.json +3 -2
  2. package/src/analysis/analysis-cache.ctx +9 -0
  3. package/src/analysis/analysis-cache.js +1 -1
  4. package/src/analysis/complexity.ctx +6 -0
  5. package/src/analysis/complexity.js +1 -1
  6. package/src/analysis/custom-rules.ctx +14 -0
  7. package/src/analysis/custom-rules.js +1 -1
  8. package/src/analysis/db-analysis.ctx +7 -0
  9. package/src/analysis/db-analysis.js +1 -1
  10. package/src/analysis/dead-code.ctx +6 -0
  11. package/src/analysis/dead-code.js +1 -1
  12. package/src/analysis/full-analysis.ctx +9 -0
  13. package/src/analysis/full-analysis.js +1 -1
  14. package/src/analysis/jsdoc-checker.ctx +10 -0
  15. package/src/analysis/jsdoc-checker.js +1 -1
  16. package/src/analysis/jsdoc-generator.ctx +9 -0
  17. package/src/analysis/jsdoc-generator.js +1 -1
  18. package/src/analysis/large-files.ctx +6 -0
  19. package/src/analysis/large-files.js +1 -1
  20. package/src/analysis/outdated-patterns.ctx +7 -0
  21. package/src/analysis/outdated-patterns.js +1 -1
  22. package/src/analysis/similar-functions.ctx +6 -0
  23. package/src/analysis/similar-functions.js +1 -1
  24. package/src/analysis/test-annotations.ctx +11 -0
  25. package/src/analysis/test-annotations.js +1 -1
  26. package/src/analysis/type-checker.ctx +6 -0
  27. package/src/analysis/type-checker.js +1 -1
  28. package/src/analysis/undocumented.ctx +8 -0
  29. package/src/analysis/undocumented.js +1 -1
  30. package/src/cli/cli-handlers.ctx +7 -0
  31. package/src/cli/cli-handlers.js +1 -1
  32. package/src/cli/cli.ctx +6 -0
  33. package/src/cli/cli.js +1 -1
  34. package/src/compact/ai-context.ctx +6 -0
  35. package/src/compact/ai-context.js +1 -1
  36. package/src/compact/compact-migrate.ctx +8 -0
  37. package/src/compact/compact-migrate.js +1 -1
  38. package/src/compact/compact.ctx +11 -0
  39. package/src/compact/compact.js +1 -1
  40. package/src/compact/compress.ctx +7 -0
  41. package/src/compact/compress.js +1 -1
  42. package/src/compact/ctx-resolver.ctx +2 -0
  43. package/src/compact/ctx-resolver.js +1 -1
  44. package/src/compact/ctx-to-jsdoc.ctx +11 -0
  45. package/src/compact/ctx-to-jsdoc.js +1 -1
  46. package/src/compact/doc-dialect.ctx +11 -0
  47. package/src/compact/doc-dialect.js +2 -2
  48. package/src/compact/expand.ctx +14 -0
  49. package/src/compact/expand.js +1 -1
  50. package/src/compact/framework-references.ctx +7 -0
  51. package/src/compact/framework-references.js +1 -1
  52. package/src/compact/instructions.ctx +6 -0
  53. package/src/compact/instructions.js +1 -1
  54. package/src/compact/jsdoc-builder.ctx +4 -0
  55. package/src/compact/jsdoc-builder.js +1 -1
  56. package/src/compact/mode-config.ctx +8 -0
  57. package/src/compact/mode-config.js +1 -1
  58. package/src/compact/split-declarations.ctx +6 -0
  59. package/src/compact/split-declarations.js +1 -1
  60. package/src/compact/validate-pipeline.ctx +12 -0
  61. package/src/compact/validate-pipeline.js +1 -1
  62. package/src/core/event-bus.ctx +9 -0
  63. package/src/core/event-bus.js +1 -1
  64. package/src/core/file-walker.ctx +1 -0
  65. package/src/core/file-walker.js +1 -1
  66. package/src/core/filters.ctx +12 -0
  67. package/src/core/filters.js +1 -1
  68. package/src/core/graph-builder.ctx +7 -0
  69. package/src/core/graph-builder.js +1 -1
  70. package/src/core/parser.ctx +12 -0
  71. package/src/core/parser.js +1 -1
  72. package/src/core/utils.ctx +1 -0
  73. package/src/core/utils.js +1 -1
  74. package/src/core/workspace.ctx +7 -0
  75. package/src/core/workspace.js +1 -1
  76. package/src/lang/lang-go.ctx +8 -0
  77. package/src/lang/lang-go.js +1 -1
  78. package/src/lang/lang-python.ctx +5 -0
  79. package/src/lang/lang-python.js +1 -1
  80. package/src/lang/lang-sql.ctx +10 -0
  81. package/src/lang/lang-sql.js +1 -1
  82. package/src/lang/lang-typescript.ctx +6 -0
  83. package/src/lang/lang-typescript.js +1 -1
  84. package/src/lang/lang-utils.ctx +5 -0
  85. package/src/lang/lang-utils.js +1 -1
  86. package/src/mcp/mcp-server.ctx +6 -0
  87. package/src/mcp/mcp-server.js +1 -1
  88. package/src/mcp/tool-defs.ctx +2 -0
  89. package/src/mcp/tool-defs.js +1 -1
  90. package/src/mcp/tools.ctx +13 -0
  91. package/src/mcp/tools.js +1 -1
  92. package/src/network/backend-lifecycle.ctx +10 -0
  93. package/src/network/backend-lifecycle.js +1 -1
  94. package/src/network/backend.ctx +5 -0
  95. package/src/network/backend.js +1 -1
  96. package/src/network/local-gateway.ctx +9 -0
  97. package/src/network/local-gateway.js +1 -1
  98. package/src/network/mdns.ctx +6 -0
  99. package/src/network/mdns.js +1 -1
  100. package/src/network/server.ctx +2 -0
  101. package/src/network/server.js +2 -2
  102. package/src/network/web-server.ctx +17 -0
  103. package/src/network/web-server.js +2 -2
  104. package/web/follow-controller.js +94 -25
  105. package/web/panels/dep-graph.js +207 -21
  106. package/project-graph-mcp-2.3.0.tgz +0 -0
  107. package/vendor/symbiote-node/CHANGELOG.md +0 -31
  108. package/vendor/symbiote-node/LICENSE +0 -21
  109. package/vendor/symbiote-node/README.md +0 -206
  110. package/vendor/symbiote-node/canvas/AutoLayout.js +0 -725
  111. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +0 -73
  112. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +0 -93
  113. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +0 -9
  114. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +0 -962
  115. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +0 -1468
  116. package/vendor/symbiote-node/canvas/FlowSimulator.js +0 -323
  117. package/vendor/symbiote-node/canvas/ForceLayout.js +0 -189
  118. package/vendor/symbiote-node/canvas/ForceWorker.js +0 -1325
  119. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +0 -97
  120. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +0 -176
  121. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +0 -12
  122. package/vendor/symbiote-node/canvas/LODManager.js +0 -88
  123. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +0 -71
  124. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +0 -207
  125. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +0 -9
  126. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +0 -261
  127. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +0 -1840
  128. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +0 -22
  129. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +0 -97
  130. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +0 -132
  131. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +0 -21
  132. package/vendor/symbiote-node/canvas/NodeViewManager.js +0 -584
  133. package/vendor/symbiote-node/canvas/PinExpansion.js +0 -131
  134. package/vendor/symbiote-node/canvas/PseudoConnection.js +0 -80
  135. package/vendor/symbiote-node/canvas/SubgraphManager.js +0 -201
  136. package/vendor/symbiote-node/canvas/SubgraphRouter.js +0 -443
  137. package/vendor/symbiote-node/canvas/ViewportActions.js +0 -446
  138. package/vendor/symbiote-node/core/Connection.js +0 -45
  139. package/vendor/symbiote-node/core/Editor.js +0 -451
  140. package/vendor/symbiote-node/core/Frame.js +0 -31
  141. package/vendor/symbiote-node/core/GraphMermaid.js +0 -348
  142. package/vendor/symbiote-node/core/GraphText.js +0 -210
  143. package/vendor/symbiote-node/core/Node.js +0 -143
  144. package/vendor/symbiote-node/core/Portal.js +0 -104
  145. package/vendor/symbiote-node/core/Socket.js +0 -185
  146. package/vendor/symbiote-node/core/SubgraphNode.js +0 -125
  147. package/vendor/symbiote-node/index.js +0 -103
  148. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +0 -361
  149. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +0 -332
  150. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +0 -96
  151. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +0 -104
  152. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +0 -133
  153. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +0 -33
  154. package/vendor/symbiote-node/interactions/ConnectFlow.js +0 -307
  155. package/vendor/symbiote-node/interactions/Drag.js +0 -102
  156. package/vendor/symbiote-node/interactions/Selector.js +0 -132
  157. package/vendor/symbiote-node/interactions/SnapGrid.js +0 -65
  158. package/vendor/symbiote-node/interactions/Zoom.js +0 -140
  159. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +0 -88
  160. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +0 -254
  161. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +0 -11
  162. package/vendor/symbiote-node/layout/Layout/Layout.css.js +0 -88
  163. package/vendor/symbiote-node/layout/Layout/Layout.js +0 -622
  164. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +0 -25
  165. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +0 -293
  166. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +0 -467
  167. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +0 -33
  168. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +0 -46
  169. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +0 -102
  170. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +0 -6
  171. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +0 -156
  172. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +0 -250
  173. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +0 -379
  174. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +0 -263
  175. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +0 -20
  176. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +0 -183
  177. package/vendor/symbiote-node/layout/LayoutTree.js +0 -246
  178. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +0 -43
  179. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +0 -89
  180. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +0 -14
  181. package/vendor/symbiote-node/layout/index.js +0 -16
  182. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +0 -61
  183. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +0 -79
  184. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +0 -19
  185. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +0 -41
  186. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +0 -24
  187. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +0 -16
  188. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +0 -65
  189. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +0 -29
  190. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +0 -13
  191. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +0 -683
  192. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +0 -92
  193. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +0 -17
  194. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +0 -25
  195. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +0 -7
  196. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +0 -90
  197. package/vendor/symbiote-node/node/PortItem/PortItem.js +0 -87
  198. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +0 -10
  199. package/vendor/symbiote-node/package.json +0 -59
  200. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +0 -143
  201. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +0 -131
  202. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +0 -16
  203. package/vendor/symbiote-node/plugins/History.js +0 -384
  204. package/vendor/symbiote-node/plugins/Readonly.js +0 -59
  205. package/vendor/symbiote-node/shapes/CircleShape.js +0 -80
  206. package/vendor/symbiote-node/shapes/CommentShape.js +0 -35
  207. package/vendor/symbiote-node/shapes/DiamondShape.js +0 -115
  208. package/vendor/symbiote-node/shapes/NodeShape.js +0 -80
  209. package/vendor/symbiote-node/shapes/PillShape.js +0 -91
  210. package/vendor/symbiote-node/shapes/RectShape.js +0 -72
  211. package/vendor/symbiote-node/shapes/SVGShape.js +0 -494
  212. package/vendor/symbiote-node/shapes/index.js +0 -53
  213. package/vendor/symbiote-node/themes/Palette.js +0 -32
  214. package/vendor/symbiote-node/themes/Skin.js +0 -113
  215. package/vendor/symbiote-node/themes/Theme.js +0 -84
  216. package/vendor/symbiote-node/themes/carbon.js +0 -137
  217. package/vendor/symbiote-node/themes/dark.js +0 -137
  218. package/vendor/symbiote-node/themes/ebook.js +0 -138
  219. package/vendor/symbiote-node/themes/grey.js +0 -137
  220. package/vendor/symbiote-node/themes/light.js +0 -137
  221. package/vendor/symbiote-node/themes/neon.js +0 -138
  222. package/vendor/symbiote-node/themes/pcb.js +0 -273
  223. package/vendor/symbiote-node/themes/synthwave.js +0 -137
  224. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +0 -86
  225. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +0 -128
  226. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +0 -29
@@ -1,156 +0,0 @@
1
- /**
2
- * LayoutRouter — universal hash-based router for layout system
3
- *
4
- * Uses Symbiote PubSub named data context (ROUTER) to provide
5
- * reactive routing across the application.
6
- *
7
- * URL format: #panel/subpath?param1=value&param2=value
8
- *
9
- * Usage in templates: {{ROUTER/panel}}, {{ROUTER/subpath}}, {{ROUTER/query}}
10
- * Usage in code: this.$['ROUTER/panel'], this.sub('ROUTER/panel', cb)
11
- *
12
- * @module symbiote-node/layout/LayoutRouter
13
- */
14
- import { PubSub } from '@symbiotejs/symbiote';
15
-
16
- const CTX = 'ROUTER';
17
-
18
- const routerCtx = PubSub.registerCtx({
19
- panel: 'default',
20
- subpath: '',
21
- query: '',
22
- }, CTX);
23
-
24
- /**
25
- * Parse query string into object
26
- * @param {string} str - Query string (without leading ?)
27
- * @returns {Object<string, string>}
28
- */
29
- export function parseQuery(str) {
30
- if (!str) return {};
31
- const result = {};
32
- for (const pair of str.split('&')) {
33
- const eqIdx = pair.indexOf('=');
34
- if (eqIdx >= 0) {
35
- result[decodeURIComponent(pair.substring(0, eqIdx))] = decodeURIComponent(pair.substring(eqIdx + 1));
36
- }
37
- }
38
- return result;
39
- }
40
-
41
- /**
42
- * Build query string from key-value object
43
- * @param {Object<string, string>} params
44
- * @returns {string}
45
- */
46
- export function buildQuery(params) {
47
- const entries = Object.entries(params).filter(([, v]) => v !== '' && v != null);
48
- if (entries.length === 0) return '';
49
- return entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
50
- }
51
-
52
- /**
53
- * Build full hash string from parts
54
- * @param {string} panel
55
- * @param {string} [subpath]
56
- * @param {Object} [params]
57
- * @returns {string}
58
- */
59
- export function buildHash(panel, subpath, params) {
60
- let hash = panel;
61
- if (subpath) hash += '/' + subpath;
62
- const q = params ? buildQuery(params) : '';
63
- if (q) hash += '?' + q;
64
- return hash;
65
- }
66
-
67
- /**
68
- * Navigate to a new route — updates URL and PubSub context
69
- * @param {string} panel - Master panel section ID
70
- * @param {string} [subpath] - Sub-path (entity ID, etc.)
71
- * @param {Object} [params] - Query parameters
72
- */
73
- export function navigate(panel, subpath = '', params = {}) {
74
- if (typeof location === 'undefined') return;
75
- const hash = buildHash(panel, subpath, params);
76
- // Use pushState instead of location.hash to ensure clean URL
77
- // (location.hash preserves stale query strings like ?monitoring)
78
- history.pushState(null, '', location.pathname + '#' + hash);
79
- syncFromHash();
80
- if (typeof window !== 'undefined') {
81
- window.dispatchEvent(new Event('hashchange'));
82
- }
83
- }
84
-
85
- /**
86
- * Update only query params of current route (keeps panel/subpath)
87
- * Uses replaceState to avoid cluttering browser history
88
- * @param {Object} params - Params to merge
89
- */
90
- export function updateParams(params) {
91
- if (typeof location === 'undefined') return;
92
- const currentQuery = parseQuery(routerCtx.read('query'));
93
- const merged = { ...currentQuery };
94
- for (const [k, v] of Object.entries(params)) {
95
- if (v === '' || v == null) {
96
- delete merged[k];
97
- } else {
98
- merged[k] = v;
99
- }
100
- }
101
- const query = buildQuery(merged);
102
- const hash = buildHash(routerCtx.read('panel'), routerCtx.read('subpath'), merged);
103
- history.replaceState(null, '', '#' + hash);
104
- routerCtx.pub('query', query);
105
- if (typeof window !== 'undefined') {
106
- window.dispatchEvent(new Event('hashchange'));
107
- }
108
- }
109
-
110
- /**
111
- * Sync PubSub context from current URL hash
112
- */
113
- function syncFromHash() {
114
- const raw = location.hash.replace(/^#/, '') || 'default';
115
-
116
- const qIdx = raw.indexOf('?');
117
- const pathPart = qIdx >= 0 ? raw.substring(0, qIdx) : raw;
118
- const queryPart = qIdx >= 0 ? raw.substring(qIdx + 1) : '';
119
-
120
- const slashIdx = pathPart.indexOf('/');
121
- const panel = slashIdx >= 0 ? pathPart.substring(0, slashIdx) : pathPart;
122
- const subpath = slashIdx >= 0 ? pathPart.substring(slashIdx + 1) : '';
123
-
124
- routerCtx.pub('panel', panel);
125
- routerCtx.pub('subpath', subpath);
126
- routerCtx.pub('query', queryPart);
127
- }
128
-
129
- /**
130
- * Get current route state
131
- * @returns {{ panel: string, subpath: string, query: string }}
132
- */
133
- export function getRoute() {
134
- return {
135
- panel: routerCtx.read('panel'),
136
- subpath: routerCtx.read('subpath'),
137
- query: routerCtx.read('query'),
138
- };
139
- }
140
-
141
- /**
142
- * Set default panel (first section to show if hash is empty)
143
- * @param {string} panel
144
- */
145
- export function setDefaultPanel(panel) {
146
- if (typeof location === 'undefined') return;
147
- if (!location.hash || location.hash === '#') {
148
- navigate(panel);
149
- }
150
- }
151
-
152
- // Initial sync + listen to hashchange (browser-only)
153
- if (typeof location !== 'undefined' && typeof window !== 'undefined') {
154
- syncFromHash();
155
- window.addEventListener('hashchange', syncFromHash);
156
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * routerSync — bidirectional URL ↔ component state sync
3
- *
4
- * Maps URL query params to component init$ properties and vice versa.
5
- * Only syncs when the component's panel is active.
6
- *
7
- * Supports two mapping formats:
8
- *
9
- * Simple: { componentProp: 'urlParam' }
10
- * Extended: { componentProp: { param: 'urlParam', default: 'all', type: 'number' } }
11
- *
12
- * @example
13
- * // Simple format:
14
- * syncWithRouter(this, 'jobs', {
15
- * filterStatus: 'status',
16
- * filterRegion: 'region',
17
- * });
18
- *
19
- * // Extended format:
20
- * syncWithRouter(this, 'jobs', {
21
- * filterStatus: { param: 'status', default: 'all' },
22
- * currentPage: { param: 'page', default: 1, type: 'number' },
23
- * });
24
- *
25
- * @module symbiote-node/layout/LayoutRouter/routerSync
26
- */
27
- import { parseQuery, updateParams } from './LayoutRouter.js';
28
-
29
- /**
30
- * Normalize mapping entry to { param, defaultVal, type }
31
- * @param {string | { param: string, default?: *, type?: string }} entry
32
- * @returns {{ param: string, defaultVal: *, type: string }}
33
- */
34
- function normalizeMapping(entry) {
35
- if (typeof entry === 'string') {
36
- return { param: entry, defaultVal: undefined, type: 'string' };
37
- }
38
- return {
39
- param: entry.param,
40
- defaultVal: entry.default,
41
- type: entry.type ?? 'string',
42
- };
43
- }
44
-
45
- /**
46
- * Cast value to the target type
47
- * @param {string} value
48
- * @param {string} type
49
- * @returns {*}
50
- */
51
- function castValue(value, type) {
52
- if (type === 'number') return Number(value);
53
- if (type === 'boolean') return value === 'true';
54
- return value;
55
- }
56
-
57
- /**
58
- * Sync component state with router URL params
59
- *
60
- * @param {import('@symbiotejs/symbiote').default} component - Symbiote component
61
- * @param {string} panelName - Panel this component belongs to
62
- * @param {Object<string, string | { param: string, default?: *, type?: string }>} mapping
63
- */
64
- export function syncWithRouter(component, panelName, mapping) {
65
- let syncing = false;
66
-
67
- // Pre-normalize all mapping entries
68
- const normalizedMap = {};
69
- for (const [prop, entry] of Object.entries(mapping)) {
70
- normalizedMap[prop] = normalizeMapping(entry);
71
- }
72
-
73
- /**
74
- * Read URL params into component state
75
- */
76
- function readFromURL() {
77
- if (syncing) return;
78
- syncing = true;
79
- const query = parseQuery(component.$['ROUTER/query']);
80
- for (const [prop, { param, defaultVal, type }] of Object.entries(normalizedMap)) {
81
- const rawValue = query[param];
82
- if (rawValue !== undefined) {
83
- const val = castValue(rawValue, type);
84
- if (component.$[prop] !== val) {
85
- component.$[prop] = val;
86
- }
87
- } else if (defaultVal !== undefined) {
88
- // Apply default when param missing from URL
89
- if (component.$[prop] !== defaultVal) {
90
- component.$[prop] = defaultVal;
91
- }
92
- }
93
- }
94
- syncing = false;
95
- }
96
-
97
- /**
98
- * Write component state to URL params
99
- * @param {string} prop - Changed property name
100
- */
101
- function writeToURL(prop) {
102
- if (syncing) return;
103
- if (component.$['ROUTER/panel'] !== panelName) return;
104
- syncing = true;
105
- const { param, defaultVal } = normalizedMap[prop];
106
- const value = component.$[prop];
107
- // Skip writing default values to keep URL clean
108
- if (value === defaultVal) {
109
- updateParams({ [param]: '' });
110
- } else {
111
- updateParams({ [param]: String(value) });
112
- }
113
- syncing = false;
114
- }
115
-
116
- // Subscribe to route changes — read URL when this panel becomes active
117
- component.sub('ROUTER/panel', (panel) => {
118
- if (panel === panelName) {
119
- readFromURL();
120
- }
121
- });
122
-
123
- // Subscribe to query changes — update component when URL params change
124
- component.sub('ROUTER/query', () => {
125
- if (component.$['ROUTER/panel'] !== panelName) return;
126
- readFromURL();
127
- });
128
-
129
- // Subscribe to component property changes — write to URL
130
- for (const prop of Object.keys(normalizedMap)) {
131
- component.sub(prop, () => {
132
- if (component.$['ROUTER/panel'] === panelName) {
133
- writeToURL(prop);
134
- }
135
- });
136
- }
137
-
138
- // Initial read if already on this panel
139
- if (component.$['ROUTER/panel'] === panelName) {
140
- readFromURL();
141
- }
142
- }
143
-
144
- /**
145
- * setupPanelRouting — high-level panel routing setup
146
- *
147
- * Centralizes all routing logic for a panel:
148
- * - Panel activation (onActivate callback)
149
- * - List/detail switching via ROUTER/subpath
150
- * - Tab sync via ?tab= query param
151
- *
152
- * Convention:
153
- * #panel → list view, default tab
154
- * #panel?tab=groups → list view, groups tab
155
- * #panel/{id} → detail view
156
- *
157
- * Component requirements:
158
- * - ref="listWrap" → container for list view (hidden when detail)
159
- * - <detail-component> → detail view element (hidden when list)
160
- * - $.activeTab → tab state property (if tabs configured)
161
- *
162
- * @param {import('@symbiotejs/symbiote').default} component
163
- * @param {string} panelName - Panel section ID (e.g. 'users')
164
- * @param {Object} config
165
- * @param {string[]} [config.tabs] - Tab names, first is default
166
- * @param {{ component: string, loadMethod: string }} [config.detail] - Detail view config
167
- * @param {Function} [config.onActivate] - Called when panel becomes active (list mode)
168
- * @param {Object} [config.syncParams] - Additional params to sync via syncWithRouter
169
- *
170
- * @example
171
- * renderCallback() {
172
- * setupPanelRouting(this, 'users', {
173
- * tabs: ['users', 'groups'],
174
- * detail: { component: 'user-detail-view', loadMethod: 'loadUser' },
175
- * onActivate: () => this.#loadData(),
176
- * });
177
- * }
178
- */
179
- export function setupPanelRouting(component, panelName, config = {}) {
180
- const { tabs, detail, onActivate, syncParams } = config;
181
-
182
- // --- Tab sync via ?tab= ---
183
- if (tabs && tabs.length > 0) {
184
- const defaultTab = tabs[0];
185
- syncWithRouter(component, panelName, {
186
- activeTab: { param: 'tab', default: defaultTab },
187
- ...(syncParams || {}),
188
- });
189
- } else if (syncParams) {
190
- syncWithRouter(component, panelName, syncParams);
191
- }
192
-
193
- /**
194
- * Check and apply list/detail mode based on ROUTER/subpath
195
- */
196
- function checkDetailMode() {
197
- if (component.$['ROUTER/panel'] !== panelName) return;
198
-
199
- const subpath = component.$['ROUTER/subpath'];
200
- const listWrap = component.ref?.listWrap;
201
- const isDetail = !!(detail && subpath);
202
-
203
- // Global signal — CSS can hide tabs/actions via [data-detail]
204
- component.toggleAttribute('data-detail', isDetail);
205
-
206
- if (isDetail) {
207
- // --- Detail mode ---
208
- if (listWrap) listWrap.hidden = true;
209
-
210
- const detailEl = component.querySelector(detail.component);
211
- if (detailEl) {
212
- detailEl.hidden = false;
213
- if (typeof detailEl[detail.loadMethod] === 'function') {
214
- detailEl[detail.loadMethod](subpath);
215
- }
216
- }
217
- } else {
218
- // --- List mode ---
219
- if (listWrap) listWrap.hidden = false;
220
-
221
- if (detail) {
222
- const detailEl = component.querySelector(detail.component);
223
- if (detailEl) detailEl.hidden = true;
224
- }
225
-
226
- if (onActivate) onActivate();
227
- }
228
- }
229
-
230
- // Subscribe to panel activation
231
- component.sub('ROUTER/panel', (panel) => {
232
- if (panel === panelName) {
233
- checkDetailMode();
234
- }
235
- });
236
-
237
- // Subscribe to subpath changes (list ↔ detail)
238
- if (detail) {
239
- component.sub('ROUTER/subpath', () => {
240
- if (component.$['ROUTER/panel'] === panelName) {
241
- checkDetailMode();
242
- }
243
- });
244
- }
245
-
246
- // Initial check if already on this panel
247
- if (component.$['ROUTER/panel'] === panelName) {
248
- checkDetailMode();
249
- }
250
- }