symbiote-ui 0.3.0-alpha.21 → 0.3.0-alpha.22

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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +14 -2
  3. package/canvas/AutoLayout.js +0 -1
  4. package/canvas/CanvasGraph/CanvasGraph.js +195 -9
  5. package/canvas/CanvasGraph/CanvasGraphDrawState.js +9 -2
  6. package/canvas/ForceLayout.js +412 -15
  7. package/canvas/NodeCanvas/NodeCanvas.js +51 -7
  8. package/canvas/graph-explorer.js +81 -3
  9. package/canvas/graph-layout.js +20 -5
  10. package/control/Button/Button.css.js +79 -16
  11. package/control/Button/Button.js +132 -14
  12. package/control/Button/Button.tpl.js +6 -1
  13. package/control/Field/Field.css.js +77 -0
  14. package/control/Field/Field.js +161 -0
  15. package/control/Field/Field.tpl.js +12 -1
  16. package/control/Rating/Rating.css.js +58 -0
  17. package/control/Rating/Rating.js +206 -0
  18. package/control/Rating/Rating.tpl.js +6 -0
  19. package/control/SegmentedControl/SegmentedControl.css.js +60 -0
  20. package/control/SegmentedControl/SegmentedControl.js +208 -0
  21. package/control/SegmentedControl/SegmentedControl.tpl.js +5 -0
  22. package/control/SelectionControl/SelectionControl.css.js +250 -0
  23. package/control/SelectionControl/SelectionControl.js +333 -0
  24. package/control/SelectionControl/SelectionControl.tpl.js +17 -0
  25. package/control/Slider/Slider.css.js +77 -0
  26. package/control/Slider/Slider.js +208 -0
  27. package/control/Slider/Slider.tpl.js +11 -0
  28. package/custom-elements.json +5504 -3396
  29. package/display/Badge/Badge.js +5 -0
  30. package/display/Banner/Banner.js +22 -0
  31. package/display/DataTable/DataTable.css.js +97 -2
  32. package/display/DataTable/DataTable.js +129 -6
  33. package/display/DataTable/DataTable.tpl.js +9 -4
  34. package/display/EmptyState/EmptyState.js +5 -0
  35. package/display/LoadingOverlay/LoadingOverlay.js +16 -0
  36. package/display/Metric/Metric.css.js +13 -13
  37. package/display/Metric/Metric.js +19 -0
  38. package/display/Tooltip/Tooltip.css.js +35 -0
  39. package/display/Tooltip/Tooltip.js +169 -0
  40. package/display/Tooltip/Tooltip.tpl.js +8 -0
  41. package/layout/ProjectTabs/ProjectTabs.js +108 -2
  42. package/layout/ProjectTabs/ProjectTabs.tpl.js +1 -1
  43. package/list/ListItem/ListItem.css.js +56 -18
  44. package/list/ListItem/ListItem.js +99 -4
  45. package/list/ListItem/ListItem.tpl.js +11 -4
  46. package/llms.txt +2 -0
  47. package/manifest/component-registry.js +365 -1
  48. package/menu/ContextMenu/ContextMenu.css.js +67 -12
  49. package/menu/ContextMenu/ContextMenu.js +121 -23
  50. package/menu/ContextMenu/ContextMenu.tpl.js +8 -9
  51. package/navigation/QuickOpen/QuickOpen.css.js +1 -1
  52. package/navigation/QuickOpen/QuickOpen.js +29 -5
  53. package/navigation/QuickOpen/QuickOpen.tpl.js +10 -4
  54. package/package.json +3 -2
  55. package/skills/symbiote-adapter-ant-design/SKILL.md +72 -0
  56. package/skills/symbiote-adapter-chakra/SKILL.md +66 -0
  57. package/skills/symbiote-adapter-fluent-ui/SKILL.md +68 -0
  58. package/skills/symbiote-adapter-mantine/SKILL.md +69 -0
  59. package/skills/symbiote-adapter-material-web/SKILL.md +68 -0
  60. package/skills/symbiote-adapter-mui/SKILL.md +73 -0
  61. package/skills/symbiote-adapter-radix-zag/SKILL.md +69 -0
  62. package/skills/symbiote-adapter-shoelace/SKILL.md +76 -0
  63. package/skills/symbiote-adapter-spectrum/SKILL.md +71 -0
  64. package/skills/symbiote-library-adapter/SKILL.md +138 -0
  65. package/skills/symbiote-library-adapter/references/functional-comparison-checklist.md +174 -0
  66. package/skills/symbiote-ui/SKILL.md +21 -1
  67. package/surface/Card/Card.css.js +13 -6
  68. package/surface/Card/Card.js +27 -0
  69. package/surface/Card/Card.tpl.js +5 -1
  70. package/tree/TreePanel/TreePanel.js +8 -0
  71. package/tree/TreeView/TreeView.js +92 -4
  72. package/ui/index.js +29 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to `symbiote-ui` will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.3.0-alpha.22] - 2026-06-08
8
+
9
+ ### Added
10
+
11
+ - Added `canvas-graph` multi-node focus via `fitNodes()`, `flyToNodes()`,
12
+ and `focusNodes()`, including WebMCP/discovery metadata for
13
+ `canvas_graph_focus_nodes`.
14
+ - Extended `createGraphViewModeController().focusNode()` with `flatNodeIds` so
15
+ flat graph demos can fit several visible nodes without drilling into a parent
16
+ group.
17
+ - Added a main-thread `ForceLayout` fallback so bundled hosts still render
18
+ `canvas-graph` when the standalone worker file is not served.
19
+
20
+ ### Fixed
21
+
22
+ - Kept flat graph selection emphasis scoped to the selected node by default so
23
+ multi-node focus does not fan out repeated pulse waves across every fitted
24
+ neighbor.
25
+ - Refreshed cached `canvas-graph` drawing colors on `cascade-theme-change` and
26
+ root/component theme mutations so flat canvas nodes, edges, and backgrounds
27
+ inherit the active cascade theme without host-local redraw hacks.
28
+
7
29
  ## [0.3.0-alpha.20] - 2026-06-08
8
30
 
9
31
  ### Changed
package/README.md CHANGED
@@ -130,7 +130,7 @@ editor.addNode(node);
130
130
  let canvas = document.querySelector('node-canvas');
131
131
  canvas.setEditor(editor);
132
132
  canvas.setPathStyle('pcb');
133
- canvas.setFlowLayout({ nodeIds: [node.id], direction: 'vertical', scroll: true });
133
+ canvas.applyLayout({ algorithm: 'flow', nodeIds: [node.id], direction: 'vertical', scroll: true });
134
134
 
135
135
  let graphView = createGraphViewModeController({
136
136
  shell: document.querySelector('graph-explorer-shell'),
@@ -144,9 +144,21 @@ let graphView = createGraphViewModeController({
144
144
  });
145
145
 
146
146
  graphView.setMode(new URLSearchParams(location.search).get('mode'));
147
- graphView.focusNode({ nodeId: 'generated-view' });
147
+ graphView.focusNode({ nodeId: 'generated-view', flatNodeIds: ['generated-view'] });
148
148
  ```
149
149
 
150
+ For flat overview demos, keep the nodes that should be visible together at the
151
+ root of the `canvas-graph` model and pass `flatNodeIds` to
152
+ `focusNode()`. The older `flatNodeId` field focuses a single node and may enter
153
+ that node's parent group when the model is hierarchical.
154
+
155
+ `node-canvas.applyLayout()` is the agent-facing layout entrypoint. Use
156
+ `algorithm: 'auto'` for grouped graph auto-layout, `algorithm: 'tree'` for
157
+ structured directory/tree layouts, and `algorithm: 'flow'` for document-like
158
+ previews. Flat graph seed positions from `computeInitialGraphPositions()` are
159
+ stable by default; pass `random: Math.random` only when a non-deterministic demo
160
+ is intentional.
161
+
150
162
  `panel-layout` owns reusable split and panel behavior only. Product routes,
151
163
  transport, persistence, and permission checks remain host policy:
152
164
 
@@ -414,7 +414,6 @@ export function computeAutoLayout(editor, options = {}) {
414
414
  for (let r = 0; r < maxR; r += step) {
415
415
  for (let delta = 0; delta <= M_PI; delta += angularStep) {
416
416
  for (const sign of [1, -1]) {
417
- cycleCount++;
418
417
  let a = prefAngle + delta * sign;
419
418
  let x = Math.round(Math.cos(a) * r);
420
419
  let y = Math.round(Math.sin(a) * r);
@@ -16,6 +16,7 @@ import {
16
16
  } from './CanvasGraphGeometry.js';
17
17
  import { GRAPH_TYPE_COLOR_TOKENS } from '../../graph/theme-contract.js';
18
18
  import {
19
+ CANVAS_GRAPH_LAYER_TARGETS,
19
20
  getDepthGroupsFrame,
20
21
  getLayerAnimationFrame,
21
22
  getNextPulseQueue,
@@ -48,6 +49,19 @@ const DEFAULT_MENU_ITEMS = Object.freeze([
48
49
  { action: 'view-code', label: 'View Code', path: 'M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0L19.2 12l-4.6-4.6L16 6l6 6-6 6-1.4-1.4z' },
49
50
  ]);
50
51
 
52
+ function normalizeFocusNodeIds(nodeIds) {
53
+ let ids = Array.isArray(nodeIds) ? nodeIds : [nodeIds];
54
+ let normalized = [];
55
+ let seen = new Set();
56
+ for (let id of ids) {
57
+ let normalizedId = String(id || '').trim();
58
+ if (!normalizedId || seen.has(normalizedId)) continue;
59
+ seen.add(normalizedId);
60
+ normalized.push(normalizedId);
61
+ }
62
+ return normalized;
63
+ }
64
+
51
65
  function toRgba(rgb, alpha = 1) {
52
66
  return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
53
67
  }
@@ -94,6 +108,15 @@ function readThemeRgb(source, token, fallback) {
94
108
  return resolveCanvasColor(value, fallback);
95
109
  }
96
110
 
111
+ function scheduleFrame(callback) {
112
+ if (typeof globalThis.requestAnimationFrame === 'function') {
113
+ let id = globalThis.requestAnimationFrame(callback);
114
+ return () => globalThis.cancelAnimationFrame?.(id);
115
+ }
116
+ let id = setTimeout(callback, 0);
117
+ return () => clearTimeout(id);
118
+ }
119
+
97
120
  export class CanvasGraph extends Symbiote {
98
121
  init$ = {
99
122
  // These defaults will be updated from external controller if needed
@@ -185,6 +208,8 @@ export class CanvasGraph extends Symbiote {
185
208
  this._visualDragDeltaY = 0;
186
209
  this._dragWorldTransform = null;
187
210
  this._layoutSnapshot = null;
211
+ this._themeSyncQueued = false;
212
+ this._cancelThemeSync = null;
188
213
 
189
214
  // Info panel state (typewriter HUD to the right of active node)
190
215
  this._infoPanel = {
@@ -218,12 +243,7 @@ export class CanvasGraph extends Symbiote {
218
243
  4: { scale: 1, opacity: 1, parallax: 0 }
219
244
  };
220
245
 
221
- this.LAYER_TARGETS = {
222
- scale: [1.12, 1.0, 0.95, 0.88, 0.78],
223
- opacity: [1.0, 0.9, 0.55, 0.06, 0.03],
224
- blur: [0, 0, 1, 3, 5],
225
- parallax: [0, 0, 0.02, 0.04, 0.07]
226
- };
246
+ this.LAYER_TARGETS = CANVAS_GRAPH_LAYER_TARGETS;
227
247
 
228
248
  this.depthGroups = {
229
249
  0: { edges: [], nodes: [] },
@@ -238,6 +258,7 @@ export class CanvasGraph extends Symbiote {
238
258
  this.resizeCanvas();
239
259
 
240
260
  this.bindEvents();
261
+ this._bindThemeSync();
241
262
 
242
263
  this._wakeLoop();
243
264
 
@@ -249,12 +270,16 @@ export class CanvasGraph extends Symbiote {
249
270
  });
250
271
  }
251
272
 
252
- setTimeout(() => this.syncCanvasTheme(), 100);
273
+ this._scheduleCanvasThemeSync();
253
274
  }
254
275
 
255
276
  disconnectedCallback() {
256
277
  this._loopRunning = false;
257
278
  if (this._animationFrame) cancelAnimationFrame(this._animationFrame);
279
+ this._cancelThemeSync?.();
280
+ this._cancelThemeSync = null;
281
+ this.ownerDocument?.removeEventListener?.('cascade-theme-change', this._themeChangeHandler);
282
+ this._themeObserver?.disconnect();
258
283
  if (this.worker) this.worker.stop();
259
284
  }
260
285
 
@@ -284,6 +309,53 @@ export class CanvasGraph extends Symbiote {
284
309
  this.fitView();
285
310
  }
286
311
 
312
+ _getVisibleFocusFrame(nodeId, { fallbackToParent = true } = {}) {
313
+ let id = String(nodeId || '').trim();
314
+ if (!id) return null;
315
+
316
+ let node = this.nodeMap?.get(id);
317
+ if (!node && fallbackToParent) {
318
+ let graphNode = this.graphDB?.nodes?.get(id);
319
+ let parentId = graphNode?.parentId;
320
+ while (parentId && !node) {
321
+ node = this.nodeMap?.get(parentId);
322
+ if (node) {
323
+ id = parentId;
324
+ break;
325
+ }
326
+ parentId = this.graphDB?.nodes?.get(parentId)?.parentId;
327
+ }
328
+ }
329
+ if (!node) return null;
330
+
331
+ let pos = this.getSmooth(id) || this.nodePositions.get(id);
332
+ if (!pos) return null;
333
+
334
+ if (this.renderMode === 'dots') {
335
+ let connections = this.adjMap?.get(id)?.size || 0;
336
+ let radius = getNodeRadius(node, connections);
337
+ return {
338
+ id,
339
+ node,
340
+ minX: pos.x - radius,
341
+ minY: pos.y - radius,
342
+ maxX: pos.x + radius,
343
+ maxY: pos.y + radius,
344
+ };
345
+ }
346
+
347
+ let width = Number.isFinite(node.w) ? node.w : 160;
348
+ let height = Number.isFinite(node.h) ? node.h : 40;
349
+ return {
350
+ id,
351
+ node,
352
+ minX: pos.x,
353
+ minY: pos.y,
354
+ maxX: pos.x + width,
355
+ maxY: pos.y + height,
356
+ };
357
+ }
358
+
287
359
  fitView(padding = 60, animate = true) {
288
360
  if (!this.nodePositions.size) return;
289
361
  const rect = this.canvas.getBoundingClientRect();
@@ -327,12 +399,90 @@ export class CanvasGraph extends Symbiote {
327
399
  this._wakeLoop();
328
400
  }
329
401
 
330
- pulseNode(nodeId, durationMs = 1500) {
402
+ fitNodes(nodeIds, options = {}) {
403
+ let ids = normalizeFocusNodeIds(nodeIds);
404
+ if (ids.length === 0) return false;
405
+
406
+ const rect = this.canvas.getBoundingClientRect();
407
+ if (rect.width === 0 || rect.height === 0) return false;
408
+
409
+ let frames = ids
410
+ .map((id) => this._getVisibleFocusFrame(id, { fallbackToParent: options.fallbackToParent !== false }))
411
+ .filter(Boolean);
412
+ if (frames.length === 0) return false;
413
+
414
+ let minX = Math.min(...frames.map((frame) => frame.minX));
415
+ let minY = Math.min(...frames.map((frame) => frame.minY));
416
+ let maxX = Math.max(...frames.map((frame) => frame.maxX));
417
+ let maxY = Math.max(...frames.map((frame) => frame.maxY));
418
+ let graphW = maxX - minX || 1;
419
+ let graphH = maxY - minY || 1;
420
+ let cx = (minX + maxX) / 2;
421
+ let cy = (minY + maxY) / 2;
422
+ let padding = Number.isFinite(options.padding) ? options.padding : 80;
423
+ let minZoom = Number.isFinite(options.minZoom) ? options.minZoom : 0.02;
424
+ let maxZoom = Number.isFinite(options.maxZoom) ? options.maxZoom : 2.0;
425
+ let newZoom = Math.max(minZoom, Math.min(
426
+ (rect.width - padding * 2) / graphW,
427
+ (rect.height - padding * 2) / graphH,
428
+ maxZoom
429
+ ));
430
+ let newPanX = rect.width / 2 - cx * newZoom;
431
+ let newPanY = rect.height / 2 - cy * newZoom;
432
+
433
+ let animate = options.animate !== false;
434
+ this._zoomAnchor = null;
435
+ if (animate) {
436
+ this._targetZoom = newZoom;
437
+ this._targetPanX = newPanX;
438
+ this._targetPanY = newPanY;
439
+ } else {
440
+ this.zoom = newZoom;
441
+ this._targetZoom = newZoom;
442
+ this.panX = newPanX;
443
+ this.panY = newPanY;
444
+ this._targetPanX = null;
445
+ this._targetPanY = null;
446
+ }
447
+
448
+ let selectedId = null;
449
+ if (typeof options.select === 'string') {
450
+ selectedId = options.select;
451
+ } else if (options.select === true) {
452
+ selectedId = frames[0]?.id || null;
453
+ }
454
+ if (selectedId && this.nodeMap?.has(selectedId)) {
455
+ this.activeNode = this.nodeMap.get(selectedId);
456
+ this.deactivating = false;
457
+ this.updateInteractionDepths();
458
+ }
459
+
460
+ this.needsDraw = true;
461
+ this._wakeLoop();
462
+ return true;
463
+ }
464
+
465
+ flyToNodes(nodeIds, options = {}) {
466
+ return this.fitNodes(nodeIds, options);
467
+ }
468
+
469
+ focusNodes(nodeIds, options = {}) {
470
+ let ids = normalizeFocusNodeIds(nodeIds);
471
+ if (ids.length === 0) return false;
472
+ if (!Array.isArray(nodeIds) && ids.length === 1 && options.fit !== true) {
473
+ this.flyToNode(ids[0], options);
474
+ return true;
475
+ }
476
+ return this.fitNodes(ids, options);
477
+ }
478
+
479
+ pulseNode(nodeId, durationMs = 1500, options = {}) {
331
480
  this._pulses = getNextPulseQueue({
332
481
  pulses: this._pulses || [],
333
482
  nodeId,
334
483
  startTime: performance.now(),
335
484
  duration: durationMs,
485
+ waves: Number.isFinite(options.waves) ? options.waves : 1,
336
486
  });
337
487
  this.needsDraw = true;
338
488
  this._wakeLoop();
@@ -756,6 +906,38 @@ export class CanvasGraph extends Symbiote {
756
906
  return `rgb(${rr},${gg},${bbb})`;
757
907
  }
758
908
 
909
+ _bindThemeSync() {
910
+ this._themeChangeHandler = () => this._scheduleCanvasThemeSync();
911
+ this.ownerDocument?.addEventListener?.('cascade-theme-change', this._themeChangeHandler);
912
+
913
+ if (typeof globalThis.MutationObserver !== 'function') return;
914
+ this._themeObserver = new MutationObserver(() => this._scheduleCanvasThemeSync());
915
+ let themeSources = [
916
+ this.ownerDocument?.documentElement,
917
+ this.ownerDocument?.body,
918
+ this.parentElement,
919
+ this,
920
+ ].filter(Boolean);
921
+ let seen = new Set();
922
+ for (let source of themeSources) {
923
+ if (seen.has(source)) continue;
924
+ seen.add(source);
925
+ this._themeObserver.observe(source, { attributes: true, attributeFilter: ['class', 'style'] });
926
+ }
927
+ }
928
+
929
+ _scheduleCanvasThemeSync() {
930
+ if (this._themeSyncQueued) return;
931
+ this._themeSyncQueued = true;
932
+ this._cancelThemeSync = scheduleFrame(() => {
933
+ this._themeSyncQueued = false;
934
+ this._cancelThemeSync = null;
935
+ this.syncCanvasTheme();
936
+ this.needsDraw = true;
937
+ this._wakeLoop();
938
+ });
939
+ }
940
+
759
941
  syncCanvasTheme() {
760
942
  this._bgRgb = readThemeRgb(this, '--sn-bg', this._bgRgb);
761
943
  this._edgeRgb = readThemeRgb(this, '--sn-conn-color', this._edgeRgb);
@@ -1004,6 +1186,8 @@ export class CanvasGraph extends Symbiote {
1004
1186
  fillStyle = this.blendBg(fromTC[0], fromTC[1], fromTC[2], 0.35);
1005
1187
  }
1006
1188
 
1189
+ currentCtx.save();
1190
+ currentCtx.globalAlpha *= edge.aAlpha;
1007
1191
  currentCtx.fillStyle = fillStyle;
1008
1192
  currentCtx.beginPath();
1009
1193
  const midX = from.x + dx * 0.5, midY = from.y + dy * 0.5;
@@ -1018,6 +1202,7 @@ export class CanvasGraph extends Symbiote {
1018
1202
  currentCtx.arc(from.x, from.y, wFrom, ang - Math.PI/2, ang - Math.PI * 1.5, true);
1019
1203
  currentCtx.closePath();
1020
1204
  currentCtx.fill();
1205
+ currentCtx.restore();
1021
1206
  }
1022
1207
 
1023
1208
  // Nodes
@@ -1133,7 +1318,8 @@ export class CanvasGraph extends Symbiote {
1133
1318
  const pos = this.getSmooth(p.id) || this.nodePositions.get(p.id);
1134
1319
  if (!pos) return false;
1135
1320
  const progress = elapsed / p.duration;
1136
- const pulsePhase = (progress * 3) % 1;
1321
+ const waves = Number.isFinite(p.waves) ? Math.max(1, p.waves) : 1;
1322
+ const pulsePhase = (progress * waves) % 1;
1137
1323
  const r = 20 + (pulsePhase * 80);
1138
1324
  const opacity = 1 - pulsePhase;
1139
1325
  mainCtx.beginPath();
@@ -45,13 +45,20 @@ export function resolveViewportAnimation(options) {
45
45
  return next;
46
46
  }
47
47
 
48
- export function getNextPulseQueue({ pulses = [], nodeId, startTime, duration }) {
48
+ export function getNextPulseQueue({ pulses = [], nodeId, startTime, duration, waves = 1 }) {
49
49
  return [
50
50
  ...pulses.filter((pulse) => pulse.id !== nodeId),
51
- { id: nodeId, startTime, duration },
51
+ { id: nodeId, startTime, duration, waves },
52
52
  ];
53
53
  }
54
54
 
55
+ export const CANVAS_GRAPH_LAYER_TARGETS = Object.freeze({
56
+ scale: Object.freeze([1.14, 0.96, 0.88, 0.78, 0.68]),
57
+ opacity: Object.freeze([1, 0.52, 0.24, 0.07, 0.02]),
58
+ blur: Object.freeze([0, 0.4, 1.6, 3.5, 6]),
59
+ parallax: Object.freeze([0, 0.02, 0.045, 0.075, 0.11]),
60
+ });
61
+
55
62
  export function resolveGroupOrbitRotationFrame(options) {
56
63
  let {
57
64
  rotation = 0,