symbiote-ui 0.3.0-alpha.4

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 (322) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +76 -0
  4. package/canvas/AutoLayout.js +731 -0
  5. package/canvas/Breadcrumb/Breadcrumb.css.js +75 -0
  6. package/canvas/Breadcrumb/Breadcrumb.js +96 -0
  7. package/canvas/Breadcrumb/Breadcrumb.tpl.js +7 -0
  8. package/canvas/CanvasConnectionRenderer.js +971 -0
  9. package/canvas/CanvasGraph/CanvasGraph.css.js +29 -0
  10. package/canvas/CanvasGraph/CanvasGraph.js +1697 -0
  11. package/canvas/CanvasGraph/CanvasGraphDrawState.js +280 -0
  12. package/canvas/CanvasGraph/CanvasGraphGeometry.js +194 -0
  13. package/canvas/CanvasViewport.js +550 -0
  14. package/canvas/ConnectionRenderer.js +1283 -0
  15. package/canvas/FlowSimulator.js +326 -0
  16. package/canvas/ForceLayout.js +226 -0
  17. package/canvas/ForceWorker.js +1303 -0
  18. package/canvas/FrameManager.js +223 -0
  19. package/canvas/GraphExplorerShell/GraphExplorerShell.css.js +136 -0
  20. package/canvas/GraphExplorerShell/GraphExplorerShell.js +129 -0
  21. package/canvas/GraphExplorerShell/GraphExplorerShell.tpl.js +12 -0
  22. package/canvas/GraphTabs/GraphTabs.css.js +101 -0
  23. package/canvas/GraphTabs/GraphTabs.js +189 -0
  24. package/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  25. package/canvas/LODManager.js +88 -0
  26. package/canvas/Minimap/Minimap.css.js +73 -0
  27. package/canvas/Minimap/Minimap.js +210 -0
  28. package/canvas/Minimap/Minimap.tpl.js +7 -0
  29. package/canvas/NodeCanvas/NodeCanvas.css.js +398 -0
  30. package/canvas/NodeCanvas/NodeCanvas.js +1499 -0
  31. package/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  32. package/canvas/NodeSearch/NodeSearch.css.js +97 -0
  33. package/canvas/NodeSearch/NodeSearch.js +140 -0
  34. package/canvas/NodeSearch/NodeSearch.tpl.js +25 -0
  35. package/canvas/NodeViewManager.js +748 -0
  36. package/canvas/PcbRouteDiagnostics.js +463 -0
  37. package/canvas/PcbRouter.js +1127 -0
  38. package/canvas/PinExpansion.js +134 -0
  39. package/canvas/PseudoConnection.js +84 -0
  40. package/canvas/SelectionSync.js +163 -0
  41. package/canvas/SubgraphManager.js +203 -0
  42. package/canvas/SubgraphRouter.js +452 -0
  43. package/canvas/ViewportActions.js +473 -0
  44. package/canvas/graph-explorer.js +339 -0
  45. package/canvas/graph-layout.js +148 -0
  46. package/canvas/graph-model.js +68 -0
  47. package/canvas/html-in-canvas.js +202 -0
  48. package/canvas/project-graph-builder.js +440 -0
  49. package/canvas/project-graph-model.js +183 -0
  50. package/chat/ChatComposer/ChatComposer.css.js +652 -0
  51. package/chat/ChatComposer/ChatComposer.js +304 -0
  52. package/chat/ChatList/ChatList.css.js +102 -0
  53. package/chat/ChatList/ChatList.js +99 -0
  54. package/chat/ChatList/ChatList.tpl.js +20 -0
  55. package/chat/ChatListItem/ChatListItem.css.js +117 -0
  56. package/chat/ChatListItem/ChatListItem.js +32 -0
  57. package/chat/ChatListItem/ChatListItem.tpl.js +17 -0
  58. package/chat/ChatMessageItem/ChatMessageItem.css.js +628 -0
  59. package/chat/ChatMessageItem/ChatMessageItem.js +156 -0
  60. package/chat/ChatSidebar/ChatSidebar.css.js +150 -0
  61. package/chat/ChatSidebar/ChatSidebar.js +230 -0
  62. package/chat/ChatSidebar/ChatSidebar.tpl.js +18 -0
  63. package/chat/ChatSidebar/constants.js +11 -0
  64. package/chat/ChatSidebarItem/ChatSidebarItem.css.js +445 -0
  65. package/chat/ChatSidebarItem/ChatSidebarItem.js +304 -0
  66. package/chat/ChatTranscript/ChatTranscript.css.js +90 -0
  67. package/chat/ChatTranscript/ChatTranscript.js +244 -0
  68. package/chat/chat-context.js +123 -0
  69. package/chat/message-model.js +156 -0
  70. package/cli.js +20 -0
  71. package/control/Button/Button.css.js +93 -0
  72. package/control/Button/Button.js +78 -0
  73. package/control/Button/Button.tpl.js +3 -0
  74. package/control/Field/Field.css.js +91 -0
  75. package/control/Field/Field.js +17 -0
  76. package/control/Field/Field.tpl.js +3 -0
  77. package/core/Connection.js +47 -0
  78. package/core/Editor.js +449 -0
  79. package/core/Frame.js +33 -0
  80. package/core/GraphMermaid.js +348 -0
  81. package/core/GraphText.js +228 -0
  82. package/core/Node.js +145 -0
  83. package/core/Portal.js +106 -0
  84. package/core/Socket.js +187 -0
  85. package/core/SubgraphNode.js +121 -0
  86. package/core/base-path.js +55 -0
  87. package/core/dom-utils.js +14 -0
  88. package/core/index.js +18 -0
  89. package/core/local-cache.js +26 -0
  90. package/core/state-sync.js +227 -0
  91. package/custom-elements.json +6380 -0
  92. package/discover.js +240 -0
  93. package/display/Badge/Badge.css.js +44 -0
  94. package/display/Badge/Badge.js +17 -0
  95. package/display/Badge/Badge.tpl.js +3 -0
  96. package/display/Banner/Banner.css.js +61 -0
  97. package/display/Banner/Banner.js +17 -0
  98. package/display/Banner/Banner.tpl.js +3 -0
  99. package/display/CodeBlock/CodeBlock.css.js +194 -0
  100. package/display/CodeBlock/CodeBlock.js +220 -0
  101. package/display/CodeBlock/CodeBlock.tpl.js +11 -0
  102. package/display/DataTable/DataTable.css.js +101 -0
  103. package/display/DataTable/DataTable.js +136 -0
  104. package/display/DataTable/DataTable.tpl.js +13 -0
  105. package/display/EmptyState/EmptyState.css.js +33 -0
  106. package/display/EmptyState/EmptyState.js +17 -0
  107. package/display/EmptyState/EmptyState.tpl.js +3 -0
  108. package/display/EventFeed/EventFeed.css.js +145 -0
  109. package/display/EventFeed/EventFeed.js +64 -0
  110. package/display/EventFeed/EventFeed.tpl.js +14 -0
  111. package/display/EventFeed/EventFeedItem.js +116 -0
  112. package/display/EventFeed/EventFeedItem.tpl.js +22 -0
  113. package/display/LoadingOverlay/LoadingOverlay.css.js +91 -0
  114. package/display/LoadingOverlay/LoadingOverlay.js +48 -0
  115. package/display/LoadingOverlay/LoadingOverlay.tpl.js +12 -0
  116. package/display/Metric/Metric.css.js +60 -0
  117. package/display/Metric/Metric.js +17 -0
  118. package/display/Metric/Metric.tpl.js +6 -0
  119. package/display/OutputGraphPreview/OutputGraphPreview.css.js +122 -0
  120. package/display/OutputGraphPreview/OutputGraphPreview.js +89 -0
  121. package/display/OutputGraphPreview/OutputGraphPreview.tpl.js +13 -0
  122. package/display/OutputListPreview/OutputListPreview.css.js +109 -0
  123. package/display/OutputListPreview/OutputListPreview.js +77 -0
  124. package/display/OutputListPreview/OutputListPreview.tpl.js +13 -0
  125. package/display/SourceEditor/SourceEditor.css.js +39 -0
  126. package/display/SourceEditor/SourceEditor.js +129 -0
  127. package/display/SourceEditor/SourceEditor.tpl.js +10 -0
  128. package/display/SourceViewer/SourceViewer.css.js +80 -0
  129. package/display/SourceViewer/SourceViewer.js +418 -0
  130. package/display/SourceViewer/SourceViewer.tpl.js +17 -0
  131. package/display/StatusRibbon/StatusRibbon.css.js +73 -0
  132. package/display/StatusRibbon/StatusRibbon.js +87 -0
  133. package/display/StatusRibbon/StatusRibbon.tpl.js +7 -0
  134. package/display/event-feed-adapter.js +72 -0
  135. package/display/format-utils.js +29 -0
  136. package/display/highlight.js +659 -0
  137. package/display/icons.js +37 -0
  138. package/display/markdown-formatter.js +60 -0
  139. package/display/network-approval-page.js +487 -0
  140. package/display/output-preview.js +261 -0
  141. package/effects/CellBg/CellBg.css.js +33 -0
  142. package/effects/CellBg/CellBg.js +410 -0
  143. package/effects/CellBg/CellBg.tpl.js +5 -0
  144. package/graph/canvas-adapter.js +223 -0
  145. package/graph/graph-algorithms.js +31 -0
  146. package/graph/index.js +46 -0
  147. package/graph/model.js +176 -0
  148. package/graph/project-graph-build.js +66 -0
  149. package/graph/project-graph-metadata.js +253 -0
  150. package/graph/project-package.js +128 -0
  151. package/graph/project-runtime.js +116 -0
  152. package/graph/project-transaction.js +284 -0
  153. package/graph/skeleton-utils.js +84 -0
  154. package/graph/theme-contract.js +36 -0
  155. package/graph/transaction-parser.js +56 -0
  156. package/icons/MaterialSymbols.js +69 -0
  157. package/icons/material-symbols-outlined-400.ttf +0 -0
  158. package/icons/material-symbols.css +24 -0
  159. package/index.js +95 -0
  160. package/inspector/InspectorPanel/InspectorPanel.css.js +375 -0
  161. package/inspector/InspectorPanel/InspectorPanel.js +368 -0
  162. package/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  163. package/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  164. package/inspector/TemplatePreview/TemplatePreview.js +145 -0
  165. package/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  166. package/interactions/ConnectFlow.js +304 -0
  167. package/interactions/Drag.js +104 -0
  168. package/interactions/Selector.js +133 -0
  169. package/interactions/SnapGrid.js +66 -0
  170. package/interactions/Zoom.js +139 -0
  171. package/layout/ActionZone/ActionZone.css.js +88 -0
  172. package/layout/ActionZone/ActionZone.js +261 -0
  173. package/layout/ActionZone/ActionZone.tpl.js +11 -0
  174. package/layout/CrossLayoutPortalBridge/CrossLayoutPortalBridge.js +255 -0
  175. package/layout/Layout/Layout.css.js +91 -0
  176. package/layout/Layout/Layout.js +637 -0
  177. package/layout/Layout/Layout.tpl.js +27 -0
  178. package/layout/LayoutNode/LayoutNode.css.js +302 -0
  179. package/layout/LayoutNode/LayoutNode.js +509 -0
  180. package/layout/LayoutNode/LayoutNode.tpl.js +39 -0
  181. package/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  182. package/layout/LayoutPreview/LayoutPreview.js +102 -0
  183. package/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  184. package/layout/LayoutRouter/LayoutRouter.js +274 -0
  185. package/layout/LayoutRouter/SectionRegistry.js +135 -0
  186. package/layout/LayoutRouter/routerSync.js +250 -0
  187. package/layout/LayoutSidebar/LayoutSidebar.css.js +411 -0
  188. package/layout/LayoutSidebar/LayoutSidebar.js +368 -0
  189. package/layout/LayoutSidebar/LayoutSidebar.tpl.js +26 -0
  190. package/layout/LayoutSidebar/SidebarSection.css.js +20 -0
  191. package/layout/LayoutSidebar/SidebarSection.js +184 -0
  192. package/layout/LayoutSidebar/SidebarSection.tpl.js +22 -0
  193. package/layout/LayoutTree.js +373 -0
  194. package/layout/PanelMenu/PanelMenu.css.js +43 -0
  195. package/layout/PanelMenu/PanelMenu.js +95 -0
  196. package/layout/PanelMenu/PanelMenu.tpl.js +17 -0
  197. package/layout/ProjectTabs/ProjectTabs.css.js +188 -0
  198. package/layout/ProjectTabs/ProjectTabs.js +77 -0
  199. package/layout/ProjectTabs/ProjectTabs.tpl.js +15 -0
  200. package/layout/index.js +40 -0
  201. package/list/ListDetailShell/ListDetailShell.css.js +128 -0
  202. package/list/ListDetailShell/ListDetailShell.js +72 -0
  203. package/list/ListDetailShell/ListDetailShell.tpl.js +36 -0
  204. package/list/ListItem/ListItem.css.js +111 -0
  205. package/list/ListItem/ListItem.js +66 -0
  206. package/list/ListItem/ListItem.tpl.js +18 -0
  207. package/locale/index.js +503 -0
  208. package/manifest/component-registry.js +2446 -0
  209. package/manifest/graph-schema.js +285 -0
  210. package/manifest/index.js +6 -0
  211. package/manifest/project-schema-catalog.js +246 -0
  212. package/manifest/rule-catalog.js +201 -0
  213. package/manifest/theme-catalog.js +2149 -0
  214. package/manifest/ui-schema-catalog.js +334 -0
  215. package/menu/ContextMenu/ContextMenu.css.js +61 -0
  216. package/menu/ContextMenu/ContextMenu.js +82 -0
  217. package/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  218. package/navigation/QuickOpen/QuickOpen.css.js +92 -0
  219. package/navigation/QuickOpen/QuickOpen.js +185 -0
  220. package/navigation/QuickOpen/QuickOpen.tpl.js +15 -0
  221. package/navigation/quick-open-utils.js +101 -0
  222. package/node/CtrlItem/CtrlItem.css.js +41 -0
  223. package/node/CtrlItem/CtrlItem.js +24 -0
  224. package/node/CtrlItem/CtrlItem.tpl.js +17 -0
  225. package/node/GraphFrame/GraphFrame.css.js +66 -0
  226. package/node/GraphFrame/GraphFrame.js +32 -0
  227. package/node/GraphFrame/GraphFrame.tpl.js +13 -0
  228. package/node/GraphNode/GraphNode.css.js +815 -0
  229. package/node/GraphNode/GraphNode.js +173 -0
  230. package/node/GraphNode/GraphNode.tpl.js +33 -0
  231. package/node/NodeCallout/NodeCallout.css.js +91 -0
  232. package/node/NodeCallout/NodeCallout.js +281 -0
  233. package/node/NodeCallout/NodeCallout.tpl.js +8 -0
  234. package/node/NodeSocket/NodeSocket.css.js +68 -0
  235. package/node/NodeSocket/NodeSocket.js +26 -0
  236. package/node/NodeSocket/NodeSocket.tpl.js +7 -0
  237. package/node/PortItem/PortItem.css.js +93 -0
  238. package/node/PortItem/PortItem.js +87 -0
  239. package/node/PortItem/PortItem.tpl.js +10 -0
  240. package/package.json +165 -0
  241. package/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  242. package/palette/PaletteBrowser/PaletteBrowser.js +152 -0
  243. package/palette/PaletteBrowser/PaletteBrowser.tpl.js +23 -0
  244. package/plugins/History.js +408 -0
  245. package/plugins/Readonly.js +60 -0
  246. package/rules/symbiote-3x.json +170 -0
  247. package/schemas/component-descriptor-v1.json +91 -0
  248. package/schemas/component-descriptor-v2.json +145 -0
  249. package/schemas/graph-model-v1.json +179 -0
  250. package/schemas/graph-v1.json +91 -0
  251. package/schemas/project-package-v1.json +102 -0
  252. package/schemas/project-transaction-v1.json +114 -0
  253. package/schemas/runtime-ui-v1.json +80 -0
  254. package/schemas/theme-rule-block-v1.json +73 -0
  255. package/shapes/CircleShape.js +79 -0
  256. package/shapes/CommentShape.js +35 -0
  257. package/shapes/DiamondShape.js +130 -0
  258. package/shapes/NodeShape.js +79 -0
  259. package/shapes/PillShape.js +91 -0
  260. package/shapes/RectShape.js +84 -0
  261. package/shapes/SVGShape.js +525 -0
  262. package/shapes/index.js +63 -0
  263. package/surface/Card/Card.css.js +57 -0
  264. package/surface/Card/Card.js +17 -0
  265. package/surface/Card/Card.tpl.js +3 -0
  266. package/themes/Palette.js +30 -0
  267. package/themes/Skin.js +113 -0
  268. package/themes/Theme.js +82 -0
  269. package/themes/carbon.js +135 -0
  270. package/themes/dark.js +140 -0
  271. package/themes/default-dark.js +714 -0
  272. package/themes/default-provider.css +635 -0
  273. package/themes/default-provider.js +718 -0
  274. package/themes/ebook.js +136 -0
  275. package/themes/grey.js +137 -0
  276. package/themes/light.js +139 -0
  277. package/themes/neon.js +138 -0
  278. package/themes/pcb.js +273 -0
  279. package/themes/synthwave.js +138 -0
  280. package/tokens/base.json +29 -0
  281. package/tokens/themes/carbon.json +11 -0
  282. package/tokens/themes/dark.json +12 -0
  283. package/tokens/themes/default-dark.json +1543 -0
  284. package/tokens/themes/default-provider.json +1543 -0
  285. package/tokens/themes/ebook.json +11 -0
  286. package/tokens/themes/grey.json +11 -0
  287. package/tokens/themes/light.json +12 -0
  288. package/tokens/themes/neon.json +11 -0
  289. package/tokens/themes/pcb.json +11 -0
  290. package/tokens/themes/synthwave.json +11 -0
  291. package/toolbar/QuickToolbar/QuickToolbar.css.js +152 -0
  292. package/toolbar/QuickToolbar/QuickToolbar.js +529 -0
  293. package/toolbar/QuickToolbar/QuickToolbar.tpl.js +34 -0
  294. package/tree/TreePanel/TreePanel.css.js +112 -0
  295. package/tree/TreePanel/TreePanel.js +147 -0
  296. package/tree/TreePanel/TreePanel.tpl.js +18 -0
  297. package/tree/TreeView/TreeView.css.js +122 -0
  298. package/tree/TreeView/TreeView.js +365 -0
  299. package/tree/TreeView/TreeView.tpl.js +10 -0
  300. package/ui/dialogs.js +221 -0
  301. package/ui/host-adapters.js +114 -0
  302. package/ui/index.js +660 -0
  303. package/ui/locale.js +50 -0
  304. package/ui/overlay-stack.js +89 -0
  305. package/ui/shared-styles.js +26 -0
  306. package/webmcp.js +37 -0
  307. package/xr/deep-graph.js +646 -0
  308. package/xr/emulation.js +198 -0
  309. package/xr/gesture.js +228 -0
  310. package/xr/html-canvas-renderer.js +472 -0
  311. package/xr/index.js +15 -0
  312. package/xr/layout-projection.js +1046 -0
  313. package/xr/panel-frame.js +128 -0
  314. package/xr/panel-host.js +267 -0
  315. package/xr/pointer.js +258 -0
  316. package/xr/scene-controller.js +242 -0
  317. package/xr/spatial-scene.js +212 -0
  318. package/xr/theme-bridge.js +105 -0
  319. package/xr/three-webxr-adapter.js +3439 -0
  320. package/xr/webgl-layer-renderer.js +419 -0
  321. package/xr/webxr.js +679 -0
  322. package/xr/workbench.js +516 -0
@@ -0,0 +1,274 @@
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
+ * Two levels of query params:
10
+ * - **Global** (registered via registerGlobalParam): persist across section switches
11
+ * - **Section** (everything else): reset when navigating to a new section
12
+ *
13
+ * Usage in templates: {{ROUTER/panel}}, {{ROUTER/subpath}}, {{ROUTER/query}}
14
+ * Usage in code: this.$['ROUTER/panel'], this.sub('ROUTER/panel', cb)
15
+ * Global params: this.sub('ROUTER/globalParams', cb)
16
+ *
17
+ * @module symbiote-node/layout/LayoutRouter
18
+ */
19
+ import { PubSub } from '@symbiotejs/symbiote/core/PubSub.js';
20
+
21
+ const CTX = 'ROUTER';
22
+
23
+ /** @type {Set<string>} Keys that persist across section switches */
24
+ const _globalKeys = new Set();
25
+
26
+ const routerCtx = PubSub.registerCtx(
27
+ {
28
+ panel: 'default',
29
+ subpath: '',
30
+ query: '',
31
+ globalParams: {},
32
+ },
33
+ CTX
34
+ );
35
+
36
+ /**
37
+ * Parse query string into object
38
+ * @param {string} str - Query string (without leading ?)
39
+ * @returns {Object<string, string>}
40
+ */
41
+ export function parseQuery(str) {
42
+ if (!str) return {};
43
+ const result = {};
44
+ for (const pair of str.split('&')) {
45
+ const eqIdx = pair.indexOf('=');
46
+ if (eqIdx >= 0) {
47
+ result[decodeURIComponent(pair.substring(0, eqIdx))] = decodeURIComponent(
48
+ pair.substring(eqIdx + 1)
49
+ );
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Build query string from key-value object
57
+ * @param {Object<string, string>} params
58
+ * @returns {string}
59
+ */
60
+ export function buildQuery(params) {
61
+ const entries = Object.entries(params).filter(([, v]) => v !== '' && v != null);
62
+ if (entries.length === 0) return '';
63
+ return entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
64
+ }
65
+
66
+ /**
67
+ * Build full hash string from parts
68
+ * @param {string} panel
69
+ * @param {string} [subpath]
70
+ * @param {Object} [params]
71
+ * @returns {string}
72
+ */
73
+ export function buildHash(panel, subpath, params) {
74
+ let hash = panel;
75
+ if (subpath) hash += '/' + subpath;
76
+ const q = params ? buildQuery(params) : '';
77
+ if (q) hash += '?' + q;
78
+ return hash;
79
+ }
80
+
81
+ /**
82
+ * Navigate to a new route — updates URL and PubSub context.
83
+ * Global params (registered via registerGlobalParam) are automatically
84
+ * carried over unless explicitly overridden or set to null.
85
+ * @param {string} panel - Master panel section ID
86
+ * @param {string} [subpath] - Sub-path (entity ID, etc.)
87
+ * @param {Object} [params] - Query parameters (overrides globals if specified)
88
+ */
89
+ export function navigate(panel, subpath = '', params = {}) {
90
+ if (typeof location === 'undefined') return;
91
+
92
+ const currentQuery = parseQuery(routerCtx.read('query'));
93
+ const merged = {};
94
+ for (const key of _globalKeys) {
95
+ if (currentQuery[key] && params[key] === undefined) {
96
+ merged[key] = currentQuery[key];
97
+ }
98
+ }
99
+
100
+ for (const [k, v] of Object.entries(params)) {
101
+ if (v != null && v !== '') {
102
+ merged[k] = v;
103
+ } else {
104
+ delete merged[k];
105
+ }
106
+ }
107
+ const hash = buildHash(panel, subpath, merged);
108
+
109
+ history.pushState(null, '', location.pathname + '#' + hash);
110
+ syncFromHash();
111
+ if (typeof window !== 'undefined') {
112
+ window.dispatchEvent(new Event('hashchange'));
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Update only query params of current route (keeps panel/subpath)
118
+ * Uses replaceState to avoid cluttering browser history
119
+ * @param {Object} params - Params to merge
120
+ */
121
+ export function updateParams(params) {
122
+ if (typeof location === 'undefined') return;
123
+ const currentQuery = parseQuery(routerCtx.read('query'));
124
+ const merged = { ...currentQuery };
125
+ for (const [k, v] of Object.entries(params)) {
126
+ if (v === '' || v == null) {
127
+ delete merged[k];
128
+ } else {
129
+ merged[k] = v;
130
+ }
131
+ }
132
+ const query = buildQuery(merged);
133
+ const hash = buildHash(routerCtx.read('panel'), routerCtx.read('subpath'), merged);
134
+ history.replaceState(null, '', '#' + hash);
135
+ routerCtx.pub('query', query);
136
+ if (typeof window !== 'undefined') {
137
+ window.dispatchEvent(new Event('hashchange'));
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Sync PubSub context from current URL hash
143
+ */
144
+ function syncFromHash() {
145
+ const raw = location.hash.replace(/^#/, '') || 'default';
146
+
147
+ const qIdx = raw.indexOf('?');
148
+ const pathPart = qIdx >= 0 ? raw.substring(0, qIdx) : raw;
149
+ const queryPart = qIdx >= 0 ? raw.substring(qIdx + 1) : '';
150
+
151
+ const slashIdx = pathPart.indexOf('/');
152
+ const panel = slashIdx >= 0 ? pathPart.substring(0, slashIdx) : pathPart;
153
+ const subpath = slashIdx >= 0 ? pathPart.substring(slashIdx + 1) : '';
154
+
155
+ routerCtx.pub('panel', panel);
156
+ routerCtx.pub('subpath', subpath);
157
+ routerCtx.pub('query', queryPart);
158
+
159
+
160
+ if (_globalKeys.size > 0) {
161
+ const allParams = parseQuery(queryPart);
162
+ const globals = {};
163
+ for (const key of _globalKeys) {
164
+ if (allParams[key]) globals[key] = allParams[key];
165
+ }
166
+ routerCtx.pub('globalParams', globals);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get current route state
172
+ * @returns {{ panel: string, subpath: string, query: string }}
173
+ */
174
+ export function getRoute() {
175
+ return {
176
+ panel: routerCtx.read('panel'),
177
+ subpath: routerCtx.read('subpath'),
178
+ query: routerCtx.read('query'),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Set default panel (first section to show if hash is empty)
184
+ * @param {string} panel
185
+ */
186
+ export function setDefaultPanel(panel) {
187
+ if (typeof location === 'undefined') return;
188
+ if (!location.hash || location.hash === '#') {
189
+ navigate(panel);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Register one or more param keys as global (persistent across section switches).
195
+ * Global params are automatically carried in navigate() and published
196
+ * via ROUTER/globalParams PubSub context.
197
+ * @param {...string} keys - Param names to register as global
198
+ */
199
+ export function registerGlobalParam(...keys) {
200
+ keys.forEach((k) => _globalKeys.add(k));
201
+
202
+ if (typeof location !== 'undefined') {
203
+ const allParams = parseQuery(routerCtx.read('query'));
204
+ const globals = {};
205
+ for (const key of _globalKeys) {
206
+ if (allParams[key]) globals[key] = allParams[key];
207
+ }
208
+ routerCtx.pub('globalParams', globals);
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Set a single global param value.
214
+ * Shorthand for registerGlobalParam + updateParams.
215
+ * @param {string} key
216
+ * @param {string|null} value - null removes the param
217
+ */
218
+ export function setGlobalParam(key, value) {
219
+ _globalKeys.add(key);
220
+ updateParams({ [key]: value });
221
+ }
222
+
223
+
224
+ if (typeof location !== 'undefined' && typeof window !== 'undefined') {
225
+ syncFromHash();
226
+ window.addEventListener('hashchange', syncFromHash);
227
+ }
228
+
229
+ /**
230
+ * Generic search parameters builder
231
+ */
232
+ export function getGraphSearchString(locationObj = typeof window !== 'undefined' ? window.location : {}) {
233
+ if (!locationObj || !locationObj.hash) return ''
234
+ const params = new URLSearchParams(locationObj.search || '')
235
+ const hashQuery = locationObj.hash.includes('?') ? locationObj.hash.split('?')[1] : ''
236
+ const hashParams = new URLSearchParams(hashQuery)
237
+ for (let [key, value] of hashParams) {
238
+ params.set(key, value)
239
+ }
240
+ return params.toString()
241
+ }
242
+
243
+ export function getGraphUrlParams(locationObj = typeof window !== 'undefined' ? window.location : {}) {
244
+ return new URLSearchParams(getGraphSearchString(locationObj))
245
+ }
246
+
247
+ export function parseGraphHash(hash = typeof window !== 'undefined' ? window.location.hash : '') {
248
+ if (!hash) return { path: '', params: new URLSearchParams() }
249
+ const [hashBase, queryStr] = hash.replace('#', '').split('?')
250
+ const hashParams = hashBase.split('/')
251
+ if (hashParams[0] === 'graph') hashParams.shift()
252
+ return {
253
+ path: hashParams.join('/'),
254
+ params: new URLSearchParams(queryStr || ''),
255
+ }
256
+ }
257
+
258
+ export function updateHashParam(key, value, locationObj = typeof window !== 'undefined' ? window.location : {}, historyObj = typeof history !== 'undefined' ? history : {}) {
259
+ if (!locationObj || !locationObj.hash) return
260
+ const [basePath, queryStr] = locationObj.hash.split('?')
261
+ const params = new URLSearchParams(queryStr || '')
262
+ if (value === null || value === undefined) {
263
+ params.delete(key)
264
+ } else {
265
+ params.set(key, value)
266
+ }
267
+ const newQuery = params.toString()
268
+ const newHash = newQuery ? `${basePath}?${newQuery}` : basePath
269
+ if (locationObj.hash === newHash) return
270
+ if (historyObj && typeof historyObj.replaceState === 'function') {
271
+ historyObj.replaceState(null, '', newHash)
272
+ }
273
+ }
274
+
@@ -0,0 +1,135 @@
1
+ import * as LayoutTree from '../LayoutTree.js';
2
+
3
+ export const SECTION_SCOPES = Object.freeze({
4
+ HOME: 'home',
5
+ PROJECT: 'project',
6
+ BOTH: 'both',
7
+ });
8
+
9
+ export class SectionRegistry {
10
+ constructor() {
11
+ this.sections = new Map();
12
+ this.layouts = new Map();
13
+ }
14
+
15
+ registerSection(id, { icon, label, order = 100, scope = SECTION_SCOPES.BOTH, layout } = {}) {
16
+ if (!id) throw new TypeError('Section id is required');
17
+ let normalizedScope = normalizeSectionScope(scope);
18
+ this.sections.set(id, { id, icon, label, order, scope: normalizedScope });
19
+ if (layout) this.layouts.set(id, layout);
20
+ return this.getSection(id);
21
+ }
22
+
23
+ getSection(id) {
24
+ return this.sections.get(id) || null;
25
+ }
26
+
27
+ getSections(filter = {}) {
28
+ let result = [...this.sections.values()];
29
+ if (filter.scope) {
30
+ result = result.filter((section) => sectionMatchesScope(section, filter.scope));
31
+ }
32
+ return result.sort((a, b) => a.order - b.order);
33
+ }
34
+
35
+ getHomeSections() {
36
+ return this.getSections({ scope: SECTION_SCOPES.HOME });
37
+ }
38
+
39
+ getProjectSections() {
40
+ return this.getSections({ scope: SECTION_SCOPES.PROJECT });
41
+ }
42
+
43
+ getSectionsForScope(projectIdOrScope) {
44
+ let scope = projectIdOrScope === SECTION_SCOPES.HOME || projectIdOrScope === SECTION_SCOPES.PROJECT
45
+ ? projectIdOrScope
46
+ : projectIdOrScope
47
+ ? SECTION_SCOPES.PROJECT
48
+ : SECTION_SCOPES.HOME;
49
+ return this.getSections({ scope });
50
+ }
51
+
52
+ getLayout(id) {
53
+ let layout = this.layouts.get(id);
54
+ return typeof layout === 'function' ? layout() : null;
55
+ }
56
+
57
+ hasSection(id) {
58
+ return this.sections.has(id);
59
+ }
60
+
61
+ clear() {
62
+ this.sections.clear();
63
+ this.layouts.clear();
64
+ }
65
+ }
66
+
67
+ export function createSectionRegistry() {
68
+ return new SectionRegistry();
69
+ }
70
+
71
+ export function normalizeSectionScope(scope = SECTION_SCOPES.BOTH) {
72
+ if (scope === SECTION_SCOPES.HOME || scope === SECTION_SCOPES.PROJECT || scope === SECTION_SCOPES.BOTH) {
73
+ return scope;
74
+ }
75
+ return SECTION_SCOPES.BOTH;
76
+ }
77
+
78
+ export function sectionMatchesScope(section, scope) {
79
+ let normalized = normalizeSectionScope(scope);
80
+ return section?.scope === normalized || section?.scope === SECTION_SCOPES.BOTH;
81
+ }
82
+
83
+ export function withGlobalPanel(layoutFn, panelType, {
84
+ direction = 'horizontal',
85
+ ratio = 0.65,
86
+ collapsed = true,
87
+ global = true,
88
+ panelState = {},
89
+ } = {}) {
90
+ return () => {
91
+ let main = layoutFn();
92
+ let panel = LayoutTree.createPanel(panelType, panelState);
93
+ panel.global = global;
94
+ panel.collapsed = collapsed;
95
+ return LayoutTree.createSplit(direction, main, panel, ratio);
96
+ };
97
+ }
98
+
99
+ const defaultRegistry = createSectionRegistry();
100
+
101
+ export function registerSection(id, options) {
102
+ return defaultRegistry.registerSection(id, options);
103
+ }
104
+
105
+ export function getSection(id) {
106
+ return defaultRegistry.getSection(id);
107
+ }
108
+
109
+ export function getSections(filter) {
110
+ return defaultRegistry.getSections(filter);
111
+ }
112
+
113
+ export function getHomeSections() {
114
+ return defaultRegistry.getHomeSections();
115
+ }
116
+
117
+ export function getProjectSections() {
118
+ return defaultRegistry.getProjectSections();
119
+ }
120
+
121
+ export function getSectionsForScope(projectIdOrScope) {
122
+ return defaultRegistry.getSectionsForScope(projectIdOrScope);
123
+ }
124
+
125
+ export function getLayout(id) {
126
+ return defaultRegistry.getLayout(id);
127
+ }
128
+
129
+ export function hasSection(id) {
130
+ return defaultRegistry.hasSection(id);
131
+ }
132
+
133
+ export function clearSections() {
134
+ defaultRegistry.clear();
135
+ }
@@ -0,0 +1,250 @@
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
+
68
+ let 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
+ let query = parseQuery(component.$['ROUTER/query']);
80
+ for (const [prop, { param, defaultVal, type }] of Object.entries(normalizedMap)) {
81
+ let rawValue = query[param];
82
+ if (rawValue !== undefined) {
83
+ let val = castValue(rawValue, type);
84
+ if (component.$[prop] !== val) {
85
+ component.$[prop] = val;
86
+ }
87
+ } else if (defaultVal !== undefined) {
88
+
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
+ let { param, defaultVal } = normalizedMap[prop];
106
+ let value = component.$[prop];
107
+
108
+ if (value === defaultVal) {
109
+ updateParams({ [param]: '' });
110
+ } else {
111
+ updateParams({ [param]: String(value) });
112
+ }
113
+ syncing = false;
114
+ }
115
+
116
+
117
+ component.sub('ROUTER/panel', (panel) => {
118
+ if (panel === panelName) {
119
+ readFromURL();
120
+ }
121
+ });
122
+
123
+
124
+ component.sub('ROUTER/query', () => {
125
+ if (component.$['ROUTER/panel'] !== panelName) return;
126
+ readFromURL();
127
+ });
128
+
129
+
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
+
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
+ let { tabs, detail, onActivate, syncParams } = config;
181
+
182
+
183
+ if (tabs && tabs.length > 0) {
184
+ let 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
+ let subpath = component.$['ROUTER/subpath'];
200
+ let listWrap = component.ref?.listWrap;
201
+ let isDetail = !!(detail && subpath);
202
+
203
+
204
+ component.toggleAttribute('data-detail', isDetail);
205
+
206
+ if (isDetail) {
207
+
208
+ if (listWrap) listWrap.hidden = true;
209
+
210
+ let 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
+
219
+ if (listWrap) listWrap.hidden = false;
220
+
221
+ if (detail) {
222
+ let detailEl = component.querySelector(detail.component);
223
+ if (detailEl) detailEl.hidden = true;
224
+ }
225
+
226
+ if (onActivate) onActivate();
227
+ }
228
+ }
229
+
230
+
231
+ component.sub('ROUTER/panel', (panel) => {
232
+ if (panel === panelName) {
233
+ checkDetailMode();
234
+ }
235
+ });
236
+
237
+
238
+ if (detail) {
239
+ component.sub('ROUTER/subpath', () => {
240
+ if (component.$['ROUTER/panel'] === panelName) {
241
+ checkDetailMode();
242
+ }
243
+ });
244
+ }
245
+
246
+
247
+ if (component.$['ROUTER/panel'] === panelName) {
248
+ checkDetailMode();
249
+ }
250
+ }