servicemap 0.1.42 → 0.1.44

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "servicemap",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Generate an internal service map page or desktop app for a software project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,7 +85,7 @@ function pageCss() {
85
85
  .intro span { display: block; margin-top: 8px; color: #53665d; font-size: 14px; }
86
86
  .edges { position: absolute; inset: 0; z-index: 26; width: 100%; height: 100%; pointer-events: none; }
87
87
  .edge-line { transition: opacity 180ms ease, stroke 180ms ease; }
88
- .edge-line.is-active { stroke: #7ee2b8; stroke-width: 2.4px; stroke-dasharray: 5 5; animation: servicemapDash .5s linear infinite; filter: drop-shadow(0 0 5px rgba(126,226,184,.55)); opacity: 1; }
88
+ .edge-line.is-active { stroke: #7ee2b8; stroke-width: 2.4px; stroke-dasharray: 1 9; stroke-linecap: round; animation: servicemapDash 640ms linear infinite; filter: drop-shadow(0 0 5px rgba(126,226,184,.9)) drop-shadow(0 0 16px rgba(93,224,165,.42)); opacity: 1; }
89
89
  .edge-line.is-muted { opacity: 0.12; }
90
90
  .hub { position: absolute; left: 50%; top: 50%; z-index: 20; width: 132px; height: 132px; transform: translate(-50%, -50%); border-radius: 24px; border: 1px solid #315c47; background: #09120d; display: grid; place-items: center; box-shadow: 0 0 0 9px rgba(79,183,128,0.12), 0 0 42px rgba(100,255,174,0.16); }
91
91
  button.hub { cursor: pointer; }
@@ -95,10 +95,11 @@ function pageCss() {
95
95
  button.service-node { cursor: grab; touch-action: none; user-select: none; color: inherit; font: inherit; text-align: left; }
96
96
  button.service-node:active { cursor: grabbing; }
97
97
  .service-node img { pointer-events: none; }
98
- .service-node:hover, .service-node.is-selected { border-color: #66efae; box-shadow: 0 0 0 2px rgba(101,240,173,0.42), 0 18px 44px rgba(0,0,0,0.38); }
98
+ .service-node:hover, .service-node.is-selected, .service-node.is-highlighted-child { border-color: #66efae; box-shadow: 0 0 0 2px rgba(101,240,173,0.42), 0 18px 44px rgba(0,0,0,0.38); }
99
+ .service-node.is-highlighted-child { box-shadow: 0 0 0 1px rgba(126,226,184,.36), 0 18px 44px rgba(0,0,0,0.38), 0 0 34px rgba(126,226,184,.18); }
99
100
  .map.has-selection .service-node { opacity: 0.16; }
100
101
  .map.has-selection .service-node.is-related { opacity: 0.48; }
101
- .map.has-selection .service-node.is-selected { opacity: 1; }
102
+ .map.has-selection .service-node.is-selected, .map.has-selection .service-node.is-highlighted-child { opacity: 1; }
102
103
  .service-node.is-selected { z-index: 27; }
103
104
  .logo { width: 48px; height: 48px; flex: 0 0 auto; border-radius: 8px; display: grid; place-items: center; overflow: hidden; color: #fff; font-weight: 700; }
104
105
  .logo img { width: 28px; height: 28px; object-fit: contain; filter: invert(1); }
@@ -165,7 +166,7 @@ function pageCss() {
165
166
  .graph-3d__zoom button + button { border-top: 1px solid rgba(255,255,255,.08); }
166
167
  .graph-3d__zoom button:hover { color: #fff; background: rgba(255,255,255,.08); }
167
168
  .graph-3d__loading { position: absolute; inset: 0; display: grid; place-items: center; color: #75857c; font-size: 9px; }
168
- @keyframes servicemapDash { from { stroke-dashoffset: 10; } to { stroke-dashoffset: 0; } }
169
+ @keyframes servicemapDash { to { stroke-dashoffset: -10; } }
169
170
  @media (prefers-reduced-motion: reduce) { .edge-line.is-active { animation: none; } }
170
171
  @keyframes servicemapLinkFlow { to { background-position: 22px 0; } }
171
172
  @media (max-width: 920px) {
@@ -339,15 +340,27 @@ function collectDescendantIds(nodes: ServiceNode[], selectedId: string | null) {
339
340
  return ids;
340
341
  }
341
342
 
342
- function isActiveTreeEdge(sourceId: string, targetId: string, selectedId: string | null, descendantIds: Set<string>) {
343
+ function collectParentIds(nodes: ServiceNode[], selectedId: string | null) {
344
+ const ids = new Set<string>();
345
+ if (!selectedId) return ids;
346
+ nodes.forEach((node) => {
347
+ const parentId = node.parentId ?? 'project';
348
+ if (node.id === selectedId) ids.add(parentId);
349
+ });
350
+ return ids;
351
+ }
352
+
353
+ function isActiveTreeEdge(sourceId: string, targetId: string, selectedId: string | null, parentIds: Set<string>, descendantIds: Set<string>) {
343
354
  if (!selectedId) return false;
344
355
  if (sourceId === selectedId && descendantIds.has(targetId)) return true;
345
- return descendantIds.has(sourceId) && descendantIds.has(targetId);
356
+ if (descendantIds.has(sourceId) && descendantIds.has(targetId)) return true;
357
+ return parentIds.has(sourceId) && targetId === selectedId;
346
358
  }
347
359
 
348
360
  function Connections({ nodes, selectedId }: { nodes: ServiceNode[]; selectedId: string | null }) {
349
361
  const ref = useRef<SVGSVGElement>(null);
350
362
  const [size, setSize] = useState({ width: 0, height: 0, nodeHalfWidth: 125 });
363
+ const parentIds = useMemo(() => collectParentIds(nodes, selectedId), [nodes, selectedId]);
351
364
  const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
352
365
  useEffect(() => {
353
366
  if (!ref.current) return;
@@ -372,7 +385,7 @@ function Connections({ nodes, selectedId }: { nodes: ServiceNode[]; selectedId:
372
385
  const dy = Math.sign(y2 - y1);
373
386
  const radius = Math.min(5, Math.abs(x2 - x1) / 4, Math.abs(y2 - y1) / 2);
374
387
  const path = \`M \${x1} \${y1} H \${middle - dx * radius} Q \${middle} \${y1} \${middle} \${y1 + dy * radius} V \${y2 - dy * radius} Q \${middle} \${y2} \${middle + dx * radius} \${y2} H \${x2}\`;
375
- const active = isActiveTreeEdge(node.parentId ?? 'project', node.id, selectedId, descendantIds);
388
+ const active = isActiveTreeEdge(node.parentId ?? 'project', node.id, selectedId, parentIds, descendantIds);
376
389
  return <path key={node.id} d={path} fill="none" stroke="rgba(103,130,118,.48)" strokeWidth="1.2" className={\`edge-line \${active ? 'is-active' : selectedId ? 'is-muted' : ''}\`} />;
377
390
  })}
378
391
  </svg>;
@@ -649,7 +662,7 @@ function AddServiceModal({
649
662
  placeholder="billing, hosting, analytics"
650
663
  />
651
664
 
652
- <label className="field-label" htmlFor="new-service-parent">Parent node</label>
665
+ <label className="field-label" htmlFor="new-service-parent">Parent service</label>
653
666
  <select
654
667
  id="new-service-parent"
655
668
  className="details-input"
@@ -713,16 +726,17 @@ function ServiceGraph3D({
713
726
  label: node.confidence === 'high' ? relationshipLabel(node.parentId) : \`may \${relationshipLabel(node.parentId)}\`,
714
727
  })),
715
728
  }), [nodes]);
729
+ const parentIds = useMemo(() => collectParentIds(nodes, selectedId), [nodes, selectedId]);
716
730
  const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
717
731
  const activeTreeLinkIds = useMemo(() => {
718
732
  const ids = new Set<string>();
719
733
  graphData.links.forEach((link) => {
720
734
  const source = endpointId(link.source);
721
735
  const target = endpointId(link.target);
722
- if (isActiveTreeEdge(source, target, selectedId, descendantIds)) ids.add(link.id);
736
+ if (isActiveTreeEdge(source, target, selectedId, parentIds, descendantIds)) ids.add(link.id);
723
737
  });
724
738
  return ids;
725
- }, [descendantIds, graphData.links, selectedId]);
739
+ }, [descendantIds, graphData.links, parentIds, selectedId]);
726
740
 
727
741
  useEffect(() => {
728
742
  const element = containerRef.current;
@@ -852,7 +866,7 @@ function ServiceGraph3D({
852
866
  <button type="button" onClick={() => zoom(1.5)} title="Zoom out" aria-label="Zoom out">−</button>
853
867
  </div>
854
868
  <div className="graph-3d__help" aria-hidden="true">Drag to rotate <span /> Scroll to zoom <span /> Click a service</div>
855
- {selectedId && <div className="graph-3d__focus" aria-hidden="true">{activeTreeLinkIds.size} child connections highlighted</div>}
869
+ {selectedId && <div className="graph-3d__focus" aria-hidden="true">{activeTreeLinkIds.size} connections highlighted</div>}
856
870
  </div>
857
871
  );
858
872
  }
@@ -931,6 +945,7 @@ export default function InternalServiceMapPage() {
931
945
  ])));
932
946
  const selected = nodes.find((service) => service.id === selectedId) ?? null;
933
947
  const descendantIds = useMemo(() => collectDescendantIds(nodes, selectedId), [nodes, selectedId]);
948
+ const highlightedChildIds = useMemo(() => selectedId === 'project' ? descendantIds : new Set<string>(), [descendantIds, selectedId]);
934
949
  const zoomIn = () => setZoom((current) => Math.min(1.45, current * 1.5));
935
950
  const zoomOut = () => setZoom((current) => current / 1.5);
936
951
  const resetZoom = () => {
@@ -1014,7 +1029,10 @@ export default function InternalServiceMapPage() {
1014
1029
  <button type="button" className={\`mode-button \${mode === '3d' ? 'is-active' : ''}\`} onClick={() => setMode('3d')}>3D</button>
1015
1030
  </div>
1016
1031
  <button type="button" className="center-button" onClick={centerMap} aria-label="Center graph" title="Center graph"><span className="center-icon" aria-hidden="true" /></button>
1017
- <button type="button" className="add-service-button" onClick={() => setIsAddingService(true)}>+ Add service</button>
1032
+ <button type="button" className="add-service-button" onClick={() => {
1033
+ setNewService((current) => ({ ...current, parentId: selectedId && selectedId !== 'project' ? selectedId : 'product' }));
1034
+ setIsAddingService(true);
1035
+ }}>+ Add service</button>
1018
1036
  </div>
1019
1037
  </header>
1020
1038
 
@@ -1035,12 +1053,13 @@ export default function InternalServiceMapPage() {
1035
1053
  <Connections nodes={nodes} selectedId={selectedId} />
1036
1054
  <button type="button" className={\`hub \${selectedId === 'project' ? 'is-selected' : ''}\`} onClick={() => setSelectedId('project')} aria-label={\`Trace \${graph.project?.name ?? 'Project'} child services\`}><div className="hub-inner"><ProjectMark /></div></button>
1037
1055
  {nodes.map((service) => {
1038
- const isRelated = descendantIds.has(service.id) || selected?.parentId === service.id;
1056
+ const isHighlightedChild = highlightedChildIds.has(service.id);
1057
+ const isRelated = descendantIds.has(service.id) || selected?.parentId === service.id || isHighlightedChild;
1039
1058
  return (
1040
1059
  <button
1041
1060
  type="button"
1042
1061
  key={service.id}
1043
- className={\`service-node \${selectedId === service.id ? 'is-selected' : ''} \${isRelated ? 'is-related' : ''}\`}
1062
+ className={\`service-node \${selectedId === service.id ? 'is-selected' : ''} \${isRelated ? 'is-related' : ''} \${isHighlightedChild ? 'is-highlighted-child' : ''}\`}
1044
1063
  style={{ left: \`\${service.x}%\`, top: \`\${service.y}%\` }}
1045
1064
  onPointerDown={(event) => startNodeDrag(event, service)}
1046
1065
  onPointerMove={moveNodeDrag}