@principal-ai/file-city-react 0.5.40 → 0.5.41
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/dist/components/FileCity3D/FileCity3D.d.ts +8 -2
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +129 -40
- package/dist/components/FileCityExplorer/AddToAreaModal.d.ts +14 -0
- package/dist/components/FileCityExplorer/AddToAreaModal.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/AddToAreaModal.js +140 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.d.ts +14 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.js +176 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.d.ts +30 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.js +1045 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts +10 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.js +73 -0
- package/dist/components/FileCityExplorer/index.d.ts +3 -0
- package/dist/components/FileCityExplorer/index.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/index.js +1 -0
- package/dist/components/FileCityExplorer/layers.d.ts +16 -0
- package/dist/components/FileCityExplorer/layers.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/layers.js +61 -0
- package/dist/components/FileCityExplorer/model.d.ts +32 -0
- package/dist/components/FileCityExplorer/model.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/model.js +14 -0
- package/dist/components/FileCityExplorer/pathConversion.d.ts +19 -0
- package/dist/components/FileCityExplorer/pathConversion.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/pathConversion.js +26 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.d.ts +21 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.js +42 -0
- package/dist/components/FileCityExplorer/styles.d.ts +9 -0
- package/dist/components/FileCityExplorer/styles.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/styles.js +28 -0
- package/dist/utils/folderElevatedPanels.d.ts +3 -1
- package/dist/utils/folderElevatedPanels.d.ts.map +1 -1
- package/dist/utils/folderElevatedPanels.js +5 -2
- package/package.json +2 -1
- package/src/components/FileCity3D/FileCity3D.tsx +200 -52
- package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
- package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
- package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
- package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
- package/src/components/FileCityExplorer/index.ts +2 -0
- package/src/components/FileCityExplorer/layers.ts +72 -0
- package/src/components/FileCityExplorer/model.ts +35 -0
- package/src/components/FileCityExplorer/pathConversion.ts +32 -0
- package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
- package/src/components/FileCityExplorer/styles.ts +34 -0
- package/src/stories/2D3DComparison.stories.tsx +13 -2
- package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
- package/src/stories/FileCity3D.stories.tsx +24 -3
- package/src/stories/FileCityExplorer.stories.tsx +2474 -0
- package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
- package/src/utils/folderElevatedPanels.ts +8 -2
- package/src/stories/ScopeOverlay.stories.tsx +0 -1610
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
3
|
+
import type { Event, Namespace, Scope } from './model';
|
|
4
|
+
import { makeOverlayStyle, makeSectionLabelStyle } from './styles';
|
|
5
|
+
|
|
6
|
+
export const ScopeInfoOverlay: React.FC<{
|
|
7
|
+
info: { scope: Scope; ns: Namespace | null; ev: Event | null };
|
|
8
|
+
}> = ({ info }) => {
|
|
9
|
+
const { theme } = useTheme();
|
|
10
|
+
const overlayStyle = makeOverlayStyle(theme);
|
|
11
|
+
const sectionLabelStyle = makeSectionLabelStyle(theme);
|
|
12
|
+
|
|
13
|
+
const severityBg: Record<NonNullable<Event['severity']>, string> = {
|
|
14
|
+
ERROR: theme.colors.error,
|
|
15
|
+
WARN: theme.colors.warning,
|
|
16
|
+
INFO: theme.colors.info,
|
|
17
|
+
};
|
|
18
|
+
const defaultSeverityBg = theme.colors.backgroundSecondary;
|
|
19
|
+
|
|
20
|
+
const sectionDivider = `1px solid ${theme.colors.backgroundSecondary}`;
|
|
21
|
+
const codeChipStyle: React.CSSProperties = {
|
|
22
|
+
fontSize: theme.fontSizes[0],
|
|
23
|
+
color: theme.colors.textSecondary,
|
|
24
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
25
|
+
padding: '4px 6px',
|
|
26
|
+
borderRadius: theme.radii[2],
|
|
27
|
+
wordBreak: 'break-all',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const { scope, ns, ev } = info;
|
|
31
|
+
|
|
32
|
+
// Event leaf selected — show event detail.
|
|
33
|
+
if (ns && ev) {
|
|
34
|
+
return (
|
|
35
|
+
<div style={overlayStyle}>
|
|
36
|
+
<div style={{ padding: '14px 16px', borderBottom: sectionDivider }}>
|
|
37
|
+
<div style={sectionLabelStyle}>Event</div>
|
|
38
|
+
<div style={{ fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1], marginTop: 6 }}>
|
|
39
|
+
{ev.name}
|
|
40
|
+
</div>
|
|
41
|
+
{ev.severity && (
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
display: 'inline-block',
|
|
45
|
+
fontSize: theme.fontSizes[0],
|
|
46
|
+
marginTop: theme.space[2],
|
|
47
|
+
padding: '2px 6px',
|
|
48
|
+
borderRadius: theme.radii[1],
|
|
49
|
+
background: severityBg[ev.severity] ?? defaultSeverityBg,
|
|
50
|
+
color: theme.colors.highlight,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{ev.severity}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{ev.description && (
|
|
57
|
+
<div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textSecondary, marginTop: 10, lineHeight: 1.5 }}>
|
|
58
|
+
{ev.description}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
<div style={{ padding: '14px 16px' }}>
|
|
63
|
+
<div style={sectionLabelStyle}>Owning namespace</div>
|
|
64
|
+
<div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: theme.space[2] }}>
|
|
65
|
+
<span
|
|
66
|
+
style={{ width: 12, height: 12, borderRadius: theme.radii[1], background: ns.color, flexShrink: 0 }}
|
|
67
|
+
/>
|
|
68
|
+
<span style={{ fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1] }}>{ns.name}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginTop: 14, fontStyle: 'italic' }}>
|
|
71
|
+
Files-per-event mapping not wired yet — for now the event highlights its parent
|
|
72
|
+
namespace's paths.
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Namespace selected — show namespace detail.
|
|
80
|
+
if (ns) {
|
|
81
|
+
return (
|
|
82
|
+
<div style={overlayStyle}>
|
|
83
|
+
<div style={{ padding: '14px 16px', borderBottom: sectionDivider }}>
|
|
84
|
+
<div style={sectionLabelStyle}>Namespace</div>
|
|
85
|
+
<div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: theme.space[2] }}>
|
|
86
|
+
<span
|
|
87
|
+
style={{ width: 12, height: 12, borderRadius: theme.radii[1], background: ns.color, flexShrink: 0 }}
|
|
88
|
+
/>
|
|
89
|
+
<span style={{ fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1] }}>{ns.name}</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textMuted, marginTop: theme.space[2], lineHeight: 1.5 }}>
|
|
92
|
+
{ns.description}
|
|
93
|
+
</div>
|
|
94
|
+
<div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginTop: theme.space[2] }}>
|
|
95
|
+
in <span style={{ fontFamily: theme.fonts.monospace }}>{scope.id}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div style={{ padding: '14px 16px', borderBottom: sectionDivider }}>
|
|
99
|
+
<div style={sectionLabelStyle}>Claimed paths ({ns.paths.length})</div>
|
|
100
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }}>
|
|
101
|
+
{ns.paths.map(p => (
|
|
102
|
+
<code key={p} style={codeChipStyle}>
|
|
103
|
+
{p}
|
|
104
|
+
</code>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div style={{ padding: '14px 16px' }}>
|
|
109
|
+
<div style={sectionLabelStyle}>Events ({ns.events.length})</div>
|
|
110
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }}>
|
|
111
|
+
{ns.events.map(e => (
|
|
112
|
+
<div
|
|
113
|
+
key={e.name}
|
|
114
|
+
style={{
|
|
115
|
+
display: 'flex',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
gap: 6,
|
|
118
|
+
padding: '4px 6px',
|
|
119
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
120
|
+
borderRadius: theme.radii[2],
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{e.severity && (
|
|
124
|
+
<span
|
|
125
|
+
style={{
|
|
126
|
+
fontSize: theme.fontSizes[0],
|
|
127
|
+
padding: '1px 4px',
|
|
128
|
+
borderRadius: theme.radii[1],
|
|
129
|
+
background: severityBg[e.severity] ?? defaultSeverityBg,
|
|
130
|
+
color: theme.colors.highlight,
|
|
131
|
+
flexShrink: 0,
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{e.severity}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
<code style={{ fontSize: theme.fontSizes[0], color: theme.colors.textSecondary }}>
|
|
138
|
+
{e.name}
|
|
139
|
+
</code>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Scope selected — show scope summary.
|
|
149
|
+
const totalEvents = scope.namespaces.reduce((n, x) => n + x.events.length, 0);
|
|
150
|
+
return (
|
|
151
|
+
<div style={overlayStyle}>
|
|
152
|
+
<div style={{ padding: '14px 16px', borderBottom: sectionDivider }}>
|
|
153
|
+
<div style={sectionLabelStyle}>Scope</div>
|
|
154
|
+
<div style={{ fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1], marginTop: 6 }}>{scope.id}</div>
|
|
155
|
+
<div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textMuted, marginTop: theme.space[2], lineHeight: 1.5 }}>
|
|
156
|
+
{scope.description}
|
|
157
|
+
</div>
|
|
158
|
+
<div style={{ display: 'flex', gap: theme.space[3], marginTop: 12, fontSize: theme.fontSizes[0], color: theme.colors.textTertiary }}>
|
|
159
|
+
<div>
|
|
160
|
+
<div>{scope.paths.length}</div>
|
|
161
|
+
<div style={sectionLabelStyle}>scope paths</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div>
|
|
164
|
+
<div>{scope.namespaces.length}</div>
|
|
165
|
+
<div style={sectionLabelStyle}>namespaces</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div>
|
|
168
|
+
<div>{totalEvents}</div>
|
|
169
|
+
<div style={sectionLabelStyle}>events</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
{scope.paths.length > 0 && (
|
|
174
|
+
<div style={{ padding: '14px 16px', borderBottom: sectionDivider }}>
|
|
175
|
+
<div style={sectionLabelStyle}>Scope-level paths ({scope.paths.length})</div>
|
|
176
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }}>
|
|
177
|
+
{scope.paths.map(p => (
|
|
178
|
+
<code key={p} style={codeChipStyle}>
|
|
179
|
+
{p}
|
|
180
|
+
</code>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
<div style={{ padding: '14px 16px' }}>
|
|
186
|
+
<div style={sectionLabelStyle}>Namespaces</div>
|
|
187
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: theme.space[2], marginTop: theme.space[2] }}>
|
|
188
|
+
{scope.namespaces.map(n => (
|
|
189
|
+
<div
|
|
190
|
+
key={n.name}
|
|
191
|
+
style={{
|
|
192
|
+
padding: theme.space[2],
|
|
193
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
194
|
+
borderRadius: theme.radii[3],
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: theme.space[2] }}>
|
|
198
|
+
<span
|
|
199
|
+
style={{
|
|
200
|
+
width: 10,
|
|
201
|
+
height: 10,
|
|
202
|
+
borderRadius: theme.radii[1],
|
|
203
|
+
background: n.color,
|
|
204
|
+
flexShrink: 0,
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
<span style={{ fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[0] }}>{n.name}</span>
|
|
208
|
+
<span style={{ fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginLeft: 'auto' }}>
|
|
209
|
+
{n.events.length} event{n.events.length === 1 ? '' : 's'}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div
|
|
213
|
+
style={{
|
|
214
|
+
fontSize: theme.fontSizes[0],
|
|
215
|
+
color: theme.colors.textTertiary,
|
|
216
|
+
fontFamily: theme.fonts.monospace,
|
|
217
|
+
marginTop: theme.space[1],
|
|
218
|
+
wordBreak: 'break-all',
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{n.paths.join(' · ')}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HighlightLayer } from '../FileCity3D';
|
|
2
|
+
import type { Scope } from './model';
|
|
3
|
+
|
|
4
|
+
export const NAMESPACE_PALETTE = [
|
|
5
|
+
'#22c55e',
|
|
6
|
+
'#3b82f6',
|
|
7
|
+
'#f59e0b',
|
|
8
|
+
'#ec4899',
|
|
9
|
+
'#8b5cf6',
|
|
10
|
+
'#06b6d4',
|
|
11
|
+
'#ef4444',
|
|
12
|
+
'#14b8a6',
|
|
13
|
+
'#a855f7',
|
|
14
|
+
'#eab308',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** Colour used for area umbrella tiles and their picker swatches. */
|
|
18
|
+
export const AREA_PANEL_COLOR = '#64748b';
|
|
19
|
+
|
|
20
|
+
export function pickNamespaceColor(scopes: readonly Scope[]): string {
|
|
21
|
+
const used = new Set(scopes.flatMap(s => s.namespaces.map(n => n.color)));
|
|
22
|
+
return (
|
|
23
|
+
NAMESPACE_PALETTE.find(c => !used.has(c)) ??
|
|
24
|
+
NAMESPACE_PALETTE[scopes.length % NAMESPACE_PALETTE.length]
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build highlight layers for a scope: one fill layer per namespace plus a
|
|
30
|
+
* border-only layer for scope-level paths. Priority is path depth (longest-
|
|
31
|
+
* prefix wins) per the partition convention in docs/scope-namespace-overlay.md.
|
|
32
|
+
*
|
|
33
|
+
* `toCityPath` translates scope-relative paths back to city paths so
|
|
34
|
+
* districts can be matched.
|
|
35
|
+
*/
|
|
36
|
+
export function buildLayersForScope(
|
|
37
|
+
scope: Scope,
|
|
38
|
+
toCityPath: (scopePath: string) => string,
|
|
39
|
+
): HighlightLayer[] {
|
|
40
|
+
const layers: HighlightLayer[] = scope.namespaces.map(ns => {
|
|
41
|
+
const maxDepth = Math.max(1, ...ns.paths.map(p => p.split('/').length));
|
|
42
|
+
return {
|
|
43
|
+
id: `${scope.id}::${ns.name}`,
|
|
44
|
+
name: ns.name,
|
|
45
|
+
enabled: true,
|
|
46
|
+
color: ns.color,
|
|
47
|
+
opacity: 0.55,
|
|
48
|
+
priority: maxDepth,
|
|
49
|
+
items: ns.paths.map(p => ({
|
|
50
|
+
path: toCityPath(p),
|
|
51
|
+
type: 'directory' as const,
|
|
52
|
+
renderStrategy: 'fill' as const,
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
if (scope.paths.length > 0) {
|
|
57
|
+
layers.push({
|
|
58
|
+
id: `${scope.id}::__scope__`,
|
|
59
|
+
name: `${scope.id} (scope-level)`,
|
|
60
|
+
enabled: true,
|
|
61
|
+
color: '#64748b',
|
|
62
|
+
opacity: 0.4,
|
|
63
|
+
priority: 0,
|
|
64
|
+
items: scope.paths.map(p => ({
|
|
65
|
+
path: toCityPath(p),
|
|
66
|
+
type: 'directory' as const,
|
|
67
|
+
renderStrategy: 'fill' as const,
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return layers;
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope / namespace / area model for the explorer.
|
|
3
|
+
*
|
|
4
|
+
* `Event` and `Namespace` are derived from the upstream
|
|
5
|
+
* `EventNamespaceNode['namespace']` shape in `@principal-ai/principal-view-core`.
|
|
6
|
+
* `Namespace` adds a UI-required `color` (palette pick).
|
|
7
|
+
*
|
|
8
|
+
* `Scope` stays local: it flattens namespaces inline (`scope.namespaces[]`),
|
|
9
|
+
* which the upstream canvas-node split (`OtelScopeNode` + per-scope
|
|
10
|
+
* `*.events.canvas`) doesn't model. Treat as an explorer view-model.
|
|
11
|
+
*
|
|
12
|
+
* `ProjectArea` is re-exported directly from upstream.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EventNamespaceNode } from '@principal-ai/principal-view-core';
|
|
16
|
+
export type { ProjectArea } from '@principal-ai/principal-view-core';
|
|
17
|
+
|
|
18
|
+
export type Event = EventNamespaceNode['namespace']['events'][number];
|
|
19
|
+
|
|
20
|
+
export type Namespace = EventNamespaceNode['namespace'] & {
|
|
21
|
+
/** UI palette pick — not part of the upstream canvas-node shape. */
|
|
22
|
+
color: string;
|
|
23
|
+
/** Required here even though upstream allows `paths?` — explorer always sets it. */
|
|
24
|
+
paths: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface Scope {
|
|
28
|
+
/** Maps to `OtelScopeNode.otel.scope` upstream. */
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
/** Scope-level paths — corresponds to optional `OtelScopeNode.paths` upstream. */
|
|
33
|
+
paths: string[];
|
|
34
|
+
namespaces: Namespace[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path converters for the explorer.
|
|
3
|
+
*
|
|
4
|
+
* City data is rooted at `packageRoot` (e.g. `electron-app/`). Scope/namespace
|
|
5
|
+
* paths are authored relative to the package root (matching how principal-view
|
|
6
|
+
* canvases are stored). These converters round-trip between the two
|
|
7
|
+
* representations.
|
|
8
|
+
*
|
|
9
|
+
* `packageRoot` should include a trailing slash. Pass `''` if the city data
|
|
10
|
+
* is already rooted at the project root.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface PathConverters {
|
|
14
|
+
/** City path → scope/namespace path (strip the package-root prefix). */
|
|
15
|
+
toScopePath: (cityPath: string) => string;
|
|
16
|
+
/** Scope/namespace path → city path (re-add the package-root prefix). */
|
|
17
|
+
toCityPath: (scopePath: string) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createPathConverters(packageRoot: string): PathConverters {
|
|
21
|
+
return {
|
|
22
|
+
toScopePath(cityPath: string): string {
|
|
23
|
+
let p = cityPath.endsWith('/') ? cityPath.slice(0, -1) : cityPath;
|
|
24
|
+
if (packageRoot && p.startsWith(packageRoot)) p = p.slice(packageRoot.length);
|
|
25
|
+
return p;
|
|
26
|
+
},
|
|
27
|
+
toCityPath(scopePath: string): string {
|
|
28
|
+
if (!packageRoot) return scopePath;
|
|
29
|
+
return scopePath.startsWith(packageRoot) ? scopePath : packageRoot + scopePath;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Scope } from './model';
|
|
2
|
+
|
|
3
|
+
export interface ScopeTreeSelection {
|
|
4
|
+
scopeId: string;
|
|
5
|
+
namespaceName?: string;
|
|
6
|
+
eventName?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sentinel leaves used when a scope has no namespaces or a namespace has no
|
|
11
|
+
* events — the trees library infers directories from paths, so empty branches
|
|
12
|
+
* need a placeholder leaf to render.
|
|
13
|
+
*/
|
|
14
|
+
export const EMPTY_NS_SENTINEL = '(no namespaces)';
|
|
15
|
+
export const EMPTY_EVENTS_SENTINEL = '(no events)';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build canonical paths for the scope tree: `<scope.id>/<namespace.name>/<event.name>`.
|
|
19
|
+
* Scopes are top-level directories, namespaces children, events leaves.
|
|
20
|
+
* Empty scopes/namespaces emit a sentinel leaf so they still appear.
|
|
21
|
+
*/
|
|
22
|
+
export function buildScopeTreePaths(scopes: readonly Scope[]): string[] {
|
|
23
|
+
const out: string[] = [];
|
|
24
|
+
for (const scope of scopes) {
|
|
25
|
+
if (scope.namespaces.length === 0) {
|
|
26
|
+
out.push(`${scope.id}/${EMPTY_NS_SENTINEL}`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const ns of scope.namespaces) {
|
|
30
|
+
if (ns.events.length === 0) {
|
|
31
|
+
out.push(`${scope.id}/${ns.name}/${EMPTY_EVENTS_SENTINEL}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
for (const ev of ns.events) {
|
|
35
|
+
out.push(`${scope.id}/${ns.name}/${ev.name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseScopeTreePath(path: string): ScopeTreeSelection {
|
|
43
|
+
const [scopeId, namespaceName, eventName] = path.split('/');
|
|
44
|
+
const result: ScopeTreeSelection = { scopeId };
|
|
45
|
+
if (namespaceName && namespaceName !== EMPTY_NS_SENTINEL) {
|
|
46
|
+
result.namespaceName = namespaceName;
|
|
47
|
+
}
|
|
48
|
+
if (eventName && eventName !== EMPTY_EVENTS_SENTINEL) {
|
|
49
|
+
result.eventName = eventName;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Theme } from '@principal-ade/industry-theme';
|
|
3
|
+
|
|
4
|
+
/** Translucent overlay using the theme's background colour. */
|
|
5
|
+
export const withAlpha = (color: string, percent: number): string =>
|
|
6
|
+
`color-mix(in oklab, ${color} ${percent}%, transparent)`;
|
|
7
|
+
|
|
8
|
+
/** Reused across overlays, modals, and the selected-folder card. */
|
|
9
|
+
export const makeSectionLabelStyle = (theme: Theme): React.CSSProperties => ({
|
|
10
|
+
fontSize: theme.fontSizes[0],
|
|
11
|
+
color: theme.colors.textTertiary,
|
|
12
|
+
textTransform: 'uppercase',
|
|
13
|
+
letterSpacing: 0.5,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Frame style for the right-side info overlay. */
|
|
17
|
+
export const makeOverlayStyle = (theme: Theme): React.CSSProperties => ({
|
|
18
|
+
position: 'absolute',
|
|
19
|
+
top: 16,
|
|
20
|
+
left: 16,
|
|
21
|
+
width: 360,
|
|
22
|
+
maxHeight: 'calc(100vh - 32px)',
|
|
23
|
+
overflowY: 'auto',
|
|
24
|
+
background: withAlpha(theme.colors.background, 72),
|
|
25
|
+
backdropFilter: 'blur(8px)',
|
|
26
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
27
|
+
border: `1px solid ${theme.colors.border}`,
|
|
28
|
+
borderRadius: theme.radii[4],
|
|
29
|
+
color: theme.colors.text,
|
|
30
|
+
fontFamily: theme.fonts.body,
|
|
31
|
+
fontSize: theme.fontSizes[1],
|
|
32
|
+
zIndex: 100,
|
|
33
|
+
boxShadow: theme.shadows[3],
|
|
34
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
|
|
4
4
|
import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
|
|
5
|
-
import { FileCity3D, type HighlightLayer, type
|
|
5
|
+
import { FileCity3D, type HighlightLayer, type CityData } from '../components/FileCity3D';
|
|
6
6
|
import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
|
|
7
7
|
import authServerCityData from '../../../../assets/auth-server-city-data.json';
|
|
8
8
|
|
|
@@ -53,6 +53,7 @@ export const ViewModeSwitch: StoryObj = {
|
|
|
53
53
|
setHideOverlay(false);
|
|
54
54
|
setOverlayOpacity(1);
|
|
55
55
|
}
|
|
56
|
+
return undefined;
|
|
56
57
|
}, [viewMode]);
|
|
57
58
|
|
|
58
59
|
return (
|
|
@@ -266,6 +267,7 @@ const testScenarios: TestScenario[] = [
|
|
|
266
267
|
id: 'api-layer',
|
|
267
268
|
name: 'API Routes',
|
|
268
269
|
enabled: true,
|
|
270
|
+
priority: 0,
|
|
269
271
|
color: '#22c55e',
|
|
270
272
|
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
271
273
|
},
|
|
@@ -282,6 +284,7 @@ const testScenarios: TestScenario[] = [
|
|
|
282
284
|
id: 'api-layer',
|
|
283
285
|
name: 'API Routes',
|
|
284
286
|
enabled: true,
|
|
287
|
+
priority: 0,
|
|
285
288
|
color: '#3b82f6',
|
|
286
289
|
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
287
290
|
},
|
|
@@ -298,6 +301,7 @@ const testScenarios: TestScenario[] = [
|
|
|
298
301
|
id: 'lib-layer',
|
|
299
302
|
name: 'Libraries',
|
|
300
303
|
enabled: true,
|
|
304
|
+
priority: 0,
|
|
301
305
|
color: '#8b5cf6',
|
|
302
306
|
items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
|
|
303
307
|
},
|
|
@@ -314,6 +318,7 @@ const testScenarios: TestScenario[] = [
|
|
|
314
318
|
id: 'api-layer',
|
|
315
319
|
name: 'API Routes',
|
|
316
320
|
enabled: true,
|
|
321
|
+
priority: 0,
|
|
317
322
|
color: '#22c55e',
|
|
318
323
|
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
319
324
|
},
|
|
@@ -321,6 +326,7 @@ const testScenarios: TestScenario[] = [
|
|
|
321
326
|
id: 'lib-layer',
|
|
322
327
|
name: 'Libraries',
|
|
323
328
|
enabled: true,
|
|
329
|
+
priority: 0,
|
|
324
330
|
color: '#f59e0b',
|
|
325
331
|
items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
|
|
326
332
|
},
|
|
@@ -336,6 +342,7 @@ const testScenarios: TestScenario[] = [
|
|
|
336
342
|
id: 'api-layer',
|
|
337
343
|
name: 'API Routes',
|
|
338
344
|
enabled: true,
|
|
345
|
+
priority: 0,
|
|
339
346
|
color: '#3b82f6',
|
|
340
347
|
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
341
348
|
},
|
|
@@ -343,6 +350,7 @@ const testScenarios: TestScenario[] = [
|
|
|
343
350
|
id: 'bruno-layer',
|
|
344
351
|
name: 'Bruno Tests',
|
|
345
352
|
enabled: true,
|
|
353
|
+
priority: 0,
|
|
346
354
|
color: '#ef4444',
|
|
347
355
|
items: [{ path: 'auth-server/bruno', type: 'directory' as const }],
|
|
348
356
|
},
|
|
@@ -358,6 +366,7 @@ const testScenarios: TestScenario[] = [
|
|
|
358
366
|
id: 'single-file-layer',
|
|
359
367
|
name: 'Single File',
|
|
360
368
|
enabled: true,
|
|
369
|
+
priority: 0,
|
|
361
370
|
color: '#ec4899',
|
|
362
371
|
items: [{ path: 'auth-server/src/app/api/auth/workos/callback/route.ts', type: 'file' as const }],
|
|
363
372
|
},
|
|
@@ -373,6 +382,7 @@ const testScenarios: TestScenario[] = [
|
|
|
373
382
|
id: 'files-layer',
|
|
374
383
|
name: 'Selected Files',
|
|
375
384
|
enabled: true,
|
|
385
|
+
priority: 0,
|
|
376
386
|
color: '#14b8a6',
|
|
377
387
|
items: [
|
|
378
388
|
{ path: 'auth-server/src/app/api/auth/workos/callback/route.ts', type: 'file' as const },
|
|
@@ -658,6 +668,7 @@ export const PanelTransitionTest: StoryObj = {
|
|
|
658
668
|
setOverlayOpacity(1);
|
|
659
669
|
setHideOverlay(true);
|
|
660
670
|
}
|
|
671
|
+
return undefined;
|
|
661
672
|
}, [viewMode]);
|
|
662
673
|
|
|
663
674
|
return (
|