@principal-ai/file-city-react 0.5.39 → 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utils/folderElevatedPanels.d.ts +62 -0
- package/dist/utils/folderElevatedPanels.d.ts.map +1 -0
- package/dist/utils/folderElevatedPanels.js +130 -0
- 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/index.ts +8 -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 +176 -0
- package/src/stories/ScopeOverlay.stories.tsx +0 -1687
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Event, Namespace, Scope } from './model';
|
|
3
|
+
export declare const ScopeInfoOverlay: React.FC<{
|
|
4
|
+
info: {
|
|
5
|
+
scope: Scope;
|
|
6
|
+
ns: Namespace | null;
|
|
7
|
+
ev: Event | null;
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=ScopeInfoOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScopeInfoOverlay.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/ScopeInfoOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGvD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC;IACtC,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE,CAAC;CAChE,CA6NA,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
3
|
+
import { makeOverlayStyle, makeSectionLabelStyle } from './styles';
|
|
4
|
+
export const ScopeInfoOverlay = ({ info }) => {
|
|
5
|
+
const { theme } = useTheme();
|
|
6
|
+
const overlayStyle = makeOverlayStyle(theme);
|
|
7
|
+
const sectionLabelStyle = makeSectionLabelStyle(theme);
|
|
8
|
+
const severityBg = {
|
|
9
|
+
ERROR: theme.colors.error,
|
|
10
|
+
WARN: theme.colors.warning,
|
|
11
|
+
INFO: theme.colors.info,
|
|
12
|
+
};
|
|
13
|
+
const defaultSeverityBg = theme.colors.backgroundSecondary;
|
|
14
|
+
const sectionDivider = `1px solid ${theme.colors.backgroundSecondary}`;
|
|
15
|
+
const codeChipStyle = {
|
|
16
|
+
fontSize: theme.fontSizes[0],
|
|
17
|
+
color: theme.colors.textSecondary,
|
|
18
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
19
|
+
padding: '4px 6px',
|
|
20
|
+
borderRadius: theme.radii[2],
|
|
21
|
+
wordBreak: 'break-all',
|
|
22
|
+
};
|
|
23
|
+
const { scope, ns, ev } = info;
|
|
24
|
+
// Event leaf selected — show event detail.
|
|
25
|
+
if (ns && ev) {
|
|
26
|
+
return (_jsxs("div", { style: overlayStyle, children: [_jsxs("div", { style: { padding: '14px 16px', borderBottom: sectionDivider }, children: [_jsx("div", { style: sectionLabelStyle, children: "Event" }), _jsx("div", { style: { fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1], marginTop: 6 }, children: ev.name }), ev.severity && (_jsx("div", { style: {
|
|
27
|
+
display: 'inline-block',
|
|
28
|
+
fontSize: theme.fontSizes[0],
|
|
29
|
+
marginTop: theme.space[2],
|
|
30
|
+
padding: '2px 6px',
|
|
31
|
+
borderRadius: theme.radii[1],
|
|
32
|
+
background: severityBg[ev.severity] ?? defaultSeverityBg,
|
|
33
|
+
color: theme.colors.highlight,
|
|
34
|
+
}, children: ev.severity })), ev.description && (_jsx("div", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textSecondary, marginTop: 10, lineHeight: 1.5 }, children: ev.description }))] }), _jsxs("div", { style: { padding: '14px 16px' }, children: [_jsx("div", { style: sectionLabelStyle, children: "Owning namespace" }), _jsxs("div", { style: { marginTop: 6, display: 'flex', alignItems: 'center', gap: theme.space[2] }, children: [_jsx("span", { style: { width: 12, height: 12, borderRadius: theme.radii[1], background: ns.color, flexShrink: 0 } }), _jsx("span", { style: { fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1] }, children: ns.name })] }), _jsx("div", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginTop: 14, fontStyle: 'italic' }, children: "Files-per-event mapping not wired yet \u2014 for now the event highlights its parent namespace's paths." })] })] }));
|
|
35
|
+
}
|
|
36
|
+
// Namespace selected — show namespace detail.
|
|
37
|
+
if (ns) {
|
|
38
|
+
return (_jsxs("div", { style: overlayStyle, children: [_jsxs("div", { style: { padding: '14px 16px', borderBottom: sectionDivider }, children: [_jsx("div", { style: sectionLabelStyle, children: "Namespace" }), _jsxs("div", { style: { marginTop: 6, display: 'flex', alignItems: 'center', gap: theme.space[2] }, children: [_jsx("span", { style: { width: 12, height: 12, borderRadius: theme.radii[1], background: ns.color, flexShrink: 0 } }), _jsx("span", { style: { fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1] }, children: ns.name })] }), _jsx("div", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textMuted, marginTop: theme.space[2], lineHeight: 1.5 }, children: ns.description }), _jsxs("div", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginTop: theme.space[2] }, children: ["in ", _jsx("span", { style: { fontFamily: theme.fonts.monospace }, children: scope.id })] })] }), _jsxs("div", { style: { padding: '14px 16px', borderBottom: sectionDivider }, children: [_jsxs("div", { style: sectionLabelStyle, children: ["Claimed paths (", ns.paths.length, ")"] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }, children: ns.paths.map(p => (_jsx("code", { style: codeChipStyle, children: p }, p))) })] }), _jsxs("div", { style: { padding: '14px 16px' }, children: [_jsxs("div", { style: sectionLabelStyle, children: ["Events (", ns.events.length, ")"] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }, children: ns.events.map(e => (_jsxs("div", { style: {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: 6,
|
|
42
|
+
padding: '4px 6px',
|
|
43
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
44
|
+
borderRadius: theme.radii[2],
|
|
45
|
+
}, children: [e.severity && (_jsx("span", { style: {
|
|
46
|
+
fontSize: theme.fontSizes[0],
|
|
47
|
+
padding: '1px 4px',
|
|
48
|
+
borderRadius: theme.radii[1],
|
|
49
|
+
background: severityBg[e.severity] ?? defaultSeverityBg,
|
|
50
|
+
color: theme.colors.highlight,
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
}, children: e.severity })), _jsx("code", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textSecondary }, children: e.name })] }, e.name))) })] })] }));
|
|
53
|
+
}
|
|
54
|
+
// Scope selected — show scope summary.
|
|
55
|
+
const totalEvents = scope.namespaces.reduce((n, x) => n + x.events.length, 0);
|
|
56
|
+
return (_jsxs("div", { style: overlayStyle, children: [_jsxs("div", { style: { padding: '14px 16px', borderBottom: sectionDivider }, children: [_jsx("div", { style: sectionLabelStyle, children: "Scope" }), _jsx("div", { style: { fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[1], marginTop: 6 }, children: scope.id }), _jsx("div", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textMuted, marginTop: theme.space[2], lineHeight: 1.5 }, children: scope.description }), _jsxs("div", { style: { display: 'flex', gap: theme.space[3], marginTop: 12, fontSize: theme.fontSizes[0], color: theme.colors.textTertiary }, children: [_jsxs("div", { children: [_jsx("div", { children: scope.paths.length }), _jsx("div", { style: sectionLabelStyle, children: "scope paths" })] }), _jsxs("div", { children: [_jsx("div", { children: scope.namespaces.length }), _jsx("div", { style: sectionLabelStyle, children: "namespaces" })] }), _jsxs("div", { children: [_jsx("div", { children: totalEvents }), _jsx("div", { style: sectionLabelStyle, children: "events" })] })] })] }), scope.paths.length > 0 && (_jsxs("div", { style: { padding: '14px 16px', borderBottom: sectionDivider }, children: [_jsxs("div", { style: sectionLabelStyle, children: ["Scope-level paths (", scope.paths.length, ")"] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: theme.space[1], marginTop: 6 }, children: scope.paths.map(p => (_jsx("code", { style: codeChipStyle, children: p }, p))) })] })), _jsxs("div", { style: { padding: '14px 16px' }, children: [_jsx("div", { style: sectionLabelStyle, children: "Namespaces" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: theme.space[2], marginTop: theme.space[2] }, children: scope.namespaces.map(n => (_jsxs("div", { style: {
|
|
57
|
+
padding: theme.space[2],
|
|
58
|
+
background: theme.colors.backgroundDark ?? theme.colors.background,
|
|
59
|
+
borderRadius: theme.radii[3],
|
|
60
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: theme.space[2] }, children: [_jsx("span", { style: {
|
|
61
|
+
width: 10,
|
|
62
|
+
height: 10,
|
|
63
|
+
borderRadius: theme.radii[1],
|
|
64
|
+
background: n.color,
|
|
65
|
+
flexShrink: 0,
|
|
66
|
+
} }), _jsx("span", { style: { fontFamily: theme.fonts.monospace, fontSize: theme.fontSizes[0] }, children: n.name }), _jsxs("span", { style: { fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, marginLeft: 'auto' }, children: [n.events.length, " event", n.events.length === 1 ? '' : 's'] })] }), _jsx("div", { style: {
|
|
67
|
+
fontSize: theme.fontSizes[0],
|
|
68
|
+
color: theme.colors.textTertiary,
|
|
69
|
+
fontFamily: theme.fonts.monospace,
|
|
70
|
+
marginTop: theme.space[1],
|
|
71
|
+
wordBreak: 'break-all',
|
|
72
|
+
}, children: n.paths.join(' · ') })] }, n.name))) })] })] }));
|
|
73
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FileCityExplorer } from './FileCityExplorer';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HighlightLayer } from '../FileCity3D';
|
|
2
|
+
import type { Scope } from './model';
|
|
3
|
+
export declare const NAMESPACE_PALETTE: string[];
|
|
4
|
+
/** Colour used for area umbrella tiles and their picker swatches. */
|
|
5
|
+
export declare const AREA_PANEL_COLOR = "#64748b";
|
|
6
|
+
export declare function pickNamespaceColor(scopes: readonly Scope[]): string;
|
|
7
|
+
/**
|
|
8
|
+
* Build highlight layers for a scope: one fill layer per namespace plus a
|
|
9
|
+
* border-only layer for scope-level paths. Priority is path depth (longest-
|
|
10
|
+
* prefix wins) per the partition convention in docs/scope-namespace-overlay.md.
|
|
11
|
+
*
|
|
12
|
+
* `toCityPath` translates scope-relative paths back to city paths so
|
|
13
|
+
* districts can be matched.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildLayersForScope(scope: Scope, toCityPath: (scopePath: string) => string): HighlightLayer[];
|
|
16
|
+
//# sourceMappingURL=layers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layers.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/layers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,eAAO,MAAM,iBAAiB,UAW7B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAMnE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GACxC,cAAc,EAAE,CAiClB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const NAMESPACE_PALETTE = [
|
|
2
|
+
'#22c55e',
|
|
3
|
+
'#3b82f6',
|
|
4
|
+
'#f59e0b',
|
|
5
|
+
'#ec4899',
|
|
6
|
+
'#8b5cf6',
|
|
7
|
+
'#06b6d4',
|
|
8
|
+
'#ef4444',
|
|
9
|
+
'#14b8a6',
|
|
10
|
+
'#a855f7',
|
|
11
|
+
'#eab308',
|
|
12
|
+
];
|
|
13
|
+
/** Colour used for area umbrella tiles and their picker swatches. */
|
|
14
|
+
export const AREA_PANEL_COLOR = '#64748b';
|
|
15
|
+
export function pickNamespaceColor(scopes) {
|
|
16
|
+
const used = new Set(scopes.flatMap(s => s.namespaces.map(n => n.color)));
|
|
17
|
+
return (NAMESPACE_PALETTE.find(c => !used.has(c)) ??
|
|
18
|
+
NAMESPACE_PALETTE[scopes.length % NAMESPACE_PALETTE.length]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build highlight layers for a scope: one fill layer per namespace plus a
|
|
22
|
+
* border-only layer for scope-level paths. Priority is path depth (longest-
|
|
23
|
+
* prefix wins) per the partition convention in docs/scope-namespace-overlay.md.
|
|
24
|
+
*
|
|
25
|
+
* `toCityPath` translates scope-relative paths back to city paths so
|
|
26
|
+
* districts can be matched.
|
|
27
|
+
*/
|
|
28
|
+
export function buildLayersForScope(scope, toCityPath) {
|
|
29
|
+
const layers = scope.namespaces.map(ns => {
|
|
30
|
+
const maxDepth = Math.max(1, ...ns.paths.map(p => p.split('/').length));
|
|
31
|
+
return {
|
|
32
|
+
id: `${scope.id}::${ns.name}`,
|
|
33
|
+
name: ns.name,
|
|
34
|
+
enabled: true,
|
|
35
|
+
color: ns.color,
|
|
36
|
+
opacity: 0.55,
|
|
37
|
+
priority: maxDepth,
|
|
38
|
+
items: ns.paths.map(p => ({
|
|
39
|
+
path: toCityPath(p),
|
|
40
|
+
type: 'directory',
|
|
41
|
+
renderStrategy: 'fill',
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
if (scope.paths.length > 0) {
|
|
46
|
+
layers.push({
|
|
47
|
+
id: `${scope.id}::__scope__`,
|
|
48
|
+
name: `${scope.id} (scope-level)`,
|
|
49
|
+
enabled: true,
|
|
50
|
+
color: '#64748b',
|
|
51
|
+
opacity: 0.4,
|
|
52
|
+
priority: 0,
|
|
53
|
+
items: scope.paths.map(p => ({
|
|
54
|
+
path: toCityPath(p),
|
|
55
|
+
type: 'directory',
|
|
56
|
+
renderStrategy: 'fill',
|
|
57
|
+
})),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return layers;
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import type { EventNamespaceNode } from '@principal-ai/principal-view-core';
|
|
15
|
+
export type { ProjectArea } from '@principal-ai/principal-view-core';
|
|
16
|
+
export type Event = EventNamespaceNode['namespace']['events'][number];
|
|
17
|
+
export type Namespace = EventNamespaceNode['namespace'] & {
|
|
18
|
+
/** UI palette pick — not part of the upstream canvas-node shape. */
|
|
19
|
+
color: string;
|
|
20
|
+
/** Required here even though upstream allows `paths?` — explorer always sets it. */
|
|
21
|
+
paths: string[];
|
|
22
|
+
};
|
|
23
|
+
export interface Scope {
|
|
24
|
+
/** Maps to `OtelScopeNode.otel.scope` upstream. */
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
/** Scope-level paths — corresponds to optional `OtelScopeNode.paths` upstream. */
|
|
29
|
+
paths: string[];
|
|
30
|
+
namespaces: Namespace[];
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,YAAY,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,MAAM,MAAM,KAAK,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG,kBAAkB,CAAC,WAAW,CAAC,GAAG;IACxD,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,WAAW,KAAK;IACpB,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,kFAAkF;IAClF,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export interface PathConverters {
|
|
13
|
+
/** City path → scope/namespace path (strip the package-root prefix). */
|
|
14
|
+
toScopePath: (cityPath: string) => string;
|
|
15
|
+
/** Scope/namespace path → city path (re-add the package-root prefix). */
|
|
16
|
+
toCityPath: (scopePath: string) => string;
|
|
17
|
+
}
|
|
18
|
+
export declare function createPathConverters(packageRoot: string): PathConverters;
|
|
19
|
+
//# sourceMappingURL=pathConversion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pathConversion.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/pathConversion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,yEAAyE;IACzE,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAYxE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
export function createPathConverters(packageRoot) {
|
|
13
|
+
return {
|
|
14
|
+
toScopePath(cityPath) {
|
|
15
|
+
let p = cityPath.endsWith('/') ? cityPath.slice(0, -1) : cityPath;
|
|
16
|
+
if (packageRoot && p.startsWith(packageRoot))
|
|
17
|
+
p = p.slice(packageRoot.length);
|
|
18
|
+
return p;
|
|
19
|
+
},
|
|
20
|
+
toCityPath(scopePath) {
|
|
21
|
+
if (!packageRoot)
|
|
22
|
+
return scopePath;
|
|
23
|
+
return scopePath.startsWith(packageRoot) ? scopePath : packageRoot + scopePath;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Scope } from './model';
|
|
2
|
+
export interface ScopeTreeSelection {
|
|
3
|
+
scopeId: string;
|
|
4
|
+
namespaceName?: string;
|
|
5
|
+
eventName?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel leaves used when a scope has no namespaces or a namespace has no
|
|
9
|
+
* events — the trees library infers directories from paths, so empty branches
|
|
10
|
+
* need a placeholder leaf to render.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EMPTY_NS_SENTINEL = "(no namespaces)";
|
|
13
|
+
export declare const EMPTY_EVENTS_SENTINEL = "(no events)";
|
|
14
|
+
/**
|
|
15
|
+
* Build canonical paths for the scope tree: `<scope.id>/<namespace.name>/<event.name>`.
|
|
16
|
+
* Scopes are top-level directories, namespaces children, events leaves.
|
|
17
|
+
* Empty scopes/namespaces emit a sentinel leaf so they still appear.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildScopeTreePaths(scopes: readonly Scope[]): string[];
|
|
20
|
+
export declare function parseScopeTreePath(path: string): ScopeTreeSelection;
|
|
21
|
+
//# sourceMappingURL=scopeTreePaths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopeTreePaths.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/scopeTreePaths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,qBAAqB,gBAAgB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,EAAE,CAkBtE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAUnE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel leaves used when a scope has no namespaces or a namespace has no
|
|
3
|
+
* events — the trees library infers directories from paths, so empty branches
|
|
4
|
+
* need a placeholder leaf to render.
|
|
5
|
+
*/
|
|
6
|
+
export const EMPTY_NS_SENTINEL = '(no namespaces)';
|
|
7
|
+
export const EMPTY_EVENTS_SENTINEL = '(no events)';
|
|
8
|
+
/**
|
|
9
|
+
* Build canonical paths for the scope tree: `<scope.id>/<namespace.name>/<event.name>`.
|
|
10
|
+
* Scopes are top-level directories, namespaces children, events leaves.
|
|
11
|
+
* Empty scopes/namespaces emit a sentinel leaf so they still appear.
|
|
12
|
+
*/
|
|
13
|
+
export function buildScopeTreePaths(scopes) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const scope of scopes) {
|
|
16
|
+
if (scope.namespaces.length === 0) {
|
|
17
|
+
out.push(`${scope.id}/${EMPTY_NS_SENTINEL}`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
for (const ns of scope.namespaces) {
|
|
21
|
+
if (ns.events.length === 0) {
|
|
22
|
+
out.push(`${scope.id}/${ns.name}/${EMPTY_EVENTS_SENTINEL}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const ev of ns.events) {
|
|
26
|
+
out.push(`${scope.id}/${ns.name}/${ev.name}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
export function parseScopeTreePath(path) {
|
|
33
|
+
const [scopeId, namespaceName, eventName] = path.split('/');
|
|
34
|
+
const result = { scopeId };
|
|
35
|
+
if (namespaceName && namespaceName !== EMPTY_NS_SENTINEL) {
|
|
36
|
+
result.namespaceName = namespaceName;
|
|
37
|
+
}
|
|
38
|
+
if (eventName && eventName !== EMPTY_EVENTS_SENTINEL) {
|
|
39
|
+
result.eventName = eventName;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { Theme } from '@principal-ade/industry-theme';
|
|
3
|
+
/** Translucent overlay using the theme's background colour. */
|
|
4
|
+
export declare const withAlpha: (color: string, percent: number) => string;
|
|
5
|
+
/** Reused across overlays, modals, and the selected-folder card. */
|
|
6
|
+
export declare const makeSectionLabelStyle: (theme: Theme) => React.CSSProperties;
|
|
7
|
+
/** Frame style for the right-side info overlay. */
|
|
8
|
+
export declare const makeOverlayStyle: (theme: Theme) => React.CSSProperties;
|
|
9
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/components/FileCityExplorer/styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAE3D,+DAA+D;AAC/D,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,SAAS,MAAM,KAAG,MACD,CAAC;AAE3D,oEAAoE;AACpE,eAAO,MAAM,qBAAqB,GAAI,OAAO,KAAK,KAAG,KAAK,CAAC,aAKzD,CAAC;AAEH,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,KAAG,KAAK,CAAC,aAiBpD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Translucent overlay using the theme's background colour. */
|
|
2
|
+
export const withAlpha = (color, percent) => `color-mix(in oklab, ${color} ${percent}%, transparent)`;
|
|
3
|
+
/** Reused across overlays, modals, and the selected-folder card. */
|
|
4
|
+
export const makeSectionLabelStyle = (theme) => ({
|
|
5
|
+
fontSize: theme.fontSizes[0],
|
|
6
|
+
color: theme.colors.textTertiary,
|
|
7
|
+
textTransform: 'uppercase',
|
|
8
|
+
letterSpacing: 0.5,
|
|
9
|
+
});
|
|
10
|
+
/** Frame style for the right-side info overlay. */
|
|
11
|
+
export const makeOverlayStyle = (theme) => ({
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
top: 16,
|
|
14
|
+
left: 16,
|
|
15
|
+
width: 360,
|
|
16
|
+
maxHeight: 'calc(100vh - 32px)',
|
|
17
|
+
overflowY: 'auto',
|
|
18
|
+
background: withAlpha(theme.colors.background, 72),
|
|
19
|
+
backdropFilter: 'blur(8px)',
|
|
20
|
+
WebkitBackdropFilter: 'blur(8px)',
|
|
21
|
+
border: `1px solid ${theme.colors.border}`,
|
|
22
|
+
borderRadius: theme.radii[4],
|
|
23
|
+
color: theme.colors.text,
|
|
24
|
+
fontFamily: theme.fonts.body,
|
|
25
|
+
fontSize: theme.fontSizes[1],
|
|
26
|
+
zIndex: 100,
|
|
27
|
+
boxShadow: theme.shadows[3],
|
|
28
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
|
|
|
16
16
|
export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
|
|
17
17
|
export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, IsolationMode, HeightScaling, FlatPattern, ElevatedScopePanel, } from './components/FileCity3D';
|
|
18
18
|
export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
|
|
19
|
+
export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
|
|
20
|
+
export type { BuildFolderElevatedPanelsOptions } from './utils/folderElevatedPanels';
|
|
19
21
|
export { resolveVisualizationIntent } from './utils/visualizationResolution';
|
|
20
22
|
export type { VisualizationIntent, ResolvedVisualizationState, } from './utils/visualizationResolution';
|
|
21
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,GAChB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAIrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export { CityViewWithReactFlow, } from './components/CityViewWithReactFlow';
|
|
|
20
20
|
export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
|
|
21
21
|
// 3D visualization component
|
|
22
22
|
export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
|
|
23
|
+
// Folder-driven elevated panels (file-tree expansion → 3D umbrella tiles)
|
|
24
|
+
export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
|
|
23
25
|
// Visualization resolution utilities
|
|
24
26
|
// See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
|
|
25
27
|
export { resolveVisualizationIntent } from './utils/visualizationResolution';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CityData } from '@principal-ai/file-city-builder';
|
|
2
|
+
import type { ElevatedScopePanel } from '../components/FileCity3D';
|
|
3
|
+
/**
|
|
4
|
+
* Stable color for a folder path, picked from a small palette via a string
|
|
5
|
+
* hash. Two folders at the same depth get visibly different colors; the
|
|
6
|
+
* same folder always gets the same color across renders.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hashFolderColor(path: string): string;
|
|
9
|
+
interface Bounds {
|
|
10
|
+
minX: number;
|
|
11
|
+
maxX: number;
|
|
12
|
+
minZ: number;
|
|
13
|
+
maxZ: number;
|
|
14
|
+
}
|
|
15
|
+
interface FolderIndex {
|
|
16
|
+
/** Parent directory → list of immediate child directories. Top-level folders live under '' (empty string). */
|
|
17
|
+
children: Map<string, string[]>;
|
|
18
|
+
/** Folder path → world bounds spanning every descendant district. */
|
|
19
|
+
bounds: Map<string, Bounds>;
|
|
20
|
+
/** Folder path → number of descendant files. */
|
|
21
|
+
fileCounts: Map<string, number>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Precompute the data structures `buildFolderElevatedPanels` needs from a
|
|
25
|
+
* `CityData`. Cache this when the city data is stable to avoid redoing the
|
|
26
|
+
* O(districts × depth) walk on every render.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildFolderIndex(cityData: CityData): FolderIndex;
|
|
29
|
+
export interface BuildFolderElevatedPanelsOptions {
|
|
30
|
+
cityData: CityData;
|
|
31
|
+
/**
|
|
32
|
+
* Set of folder paths that are currently expanded in the file tree. A folder
|
|
33
|
+
* not in this set is treated as collapsed.
|
|
34
|
+
*/
|
|
35
|
+
expandedFolders: ReadonlySet<string>;
|
|
36
|
+
/** Toggle handler invoked when an umbrella tile is clicked. */
|
|
37
|
+
onToggleFolder?: (folderPath: string, event: MouseEvent) => void;
|
|
38
|
+
/** Double-click handler for an umbrella tile. */
|
|
39
|
+
onDoubleClickFolder?: (folderPath: string, event: MouseEvent) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Scale label font size by descendant file count. Default true. When false,
|
|
42
|
+
* the renderer's auto-sized label is used (size derived from tile footprint).
|
|
43
|
+
*/
|
|
44
|
+
scaleLabelByFileCount?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
|
|
47
|
+
* city's index across renders to avoid recomputing it.
|
|
48
|
+
*/
|
|
49
|
+
index?: FolderIndex;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Walks the folder hierarchy from the top-level folders of a `CityData`. For
|
|
53
|
+
* each folder:
|
|
54
|
+
* - if expanded → recurse into child folders
|
|
55
|
+
* - if collapsed → emit one elevated panel covering the union of every
|
|
56
|
+
* descendant district's world bounds, colored via `hashFolderColor`.
|
|
57
|
+
*
|
|
58
|
+
* Mirror of the scope-tree expansion behavior, applied to file-tree folders.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildFolderElevatedPanels(options: BuildFolderElevatedPanelsOptions): ElevatedScopePanel[];
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=folderElevatedPanels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folderElevatedPanels.d.ts","sourceRoot":"","sources":["../../src/utils/folderElevatedPanels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAenE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,WAAW;IACnB,8GAA8G;IAC9G,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,gDAAgD;IAChD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,CA+ChE;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;OAGG;IACH,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACjE,iDAAiD;IACjD,mBAAmB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtE;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,kBAAkB,EAAE,CA0CtB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const FOLDER_PALETTE = [
|
|
2
|
+
'#3b82f6',
|
|
3
|
+
'#22c55e',
|
|
4
|
+
'#f59e0b',
|
|
5
|
+
'#ec4899',
|
|
6
|
+
'#8b5cf6',
|
|
7
|
+
'#06b6d4',
|
|
8
|
+
'#ef4444',
|
|
9
|
+
'#14b8a6',
|
|
10
|
+
'#a855f7',
|
|
11
|
+
'#eab308',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Stable color for a folder path, picked from a small palette via a string
|
|
15
|
+
* hash. Two folders at the same depth get visibly different colors; the
|
|
16
|
+
* same folder always gets the same color across renders.
|
|
17
|
+
*/
|
|
18
|
+
export function hashFolderColor(path) {
|
|
19
|
+
let h = 0;
|
|
20
|
+
for (let i = 0; i < path.length; i++) {
|
|
21
|
+
h = (h * 31 + path.charCodeAt(i)) | 0;
|
|
22
|
+
}
|
|
23
|
+
return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Precompute the data structures `buildFolderElevatedPanels` needs from a
|
|
27
|
+
* `CityData`. Cache this when the city data is stable to avoid redoing the
|
|
28
|
+
* O(districts × depth) walk on every render.
|
|
29
|
+
*/
|
|
30
|
+
export function buildFolderIndex(cityData) {
|
|
31
|
+
const children = new Map();
|
|
32
|
+
const directorySet = new Set();
|
|
33
|
+
for (const d of cityData.districts)
|
|
34
|
+
directorySet.add(d.path);
|
|
35
|
+
const dirs = Array.from(directorySet).sort();
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
const slash = dir.lastIndexOf('/');
|
|
38
|
+
const parent = slash >= 0 ? dir.slice(0, slash) : '';
|
|
39
|
+
const arr = children.get(parent);
|
|
40
|
+
if (arr)
|
|
41
|
+
arr.push(dir);
|
|
42
|
+
else
|
|
43
|
+
children.set(parent, [dir]);
|
|
44
|
+
}
|
|
45
|
+
const bounds = new Map();
|
|
46
|
+
for (const district of cityData.districts) {
|
|
47
|
+
const b = district.worldBounds;
|
|
48
|
+
let path = district.path;
|
|
49
|
+
while (path) {
|
|
50
|
+
const cur = bounds.get(path);
|
|
51
|
+
if (!cur) {
|
|
52
|
+
bounds.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (b.minX < cur.minX)
|
|
56
|
+
cur.minX = b.minX;
|
|
57
|
+
if (b.maxX > cur.maxX)
|
|
58
|
+
cur.maxX = b.maxX;
|
|
59
|
+
if (b.minZ < cur.minZ)
|
|
60
|
+
cur.minZ = b.minZ;
|
|
61
|
+
if (b.maxZ > cur.maxZ)
|
|
62
|
+
cur.maxZ = b.maxZ;
|
|
63
|
+
}
|
|
64
|
+
const slash = path.lastIndexOf('/');
|
|
65
|
+
if (slash < 0)
|
|
66
|
+
break;
|
|
67
|
+
path = path.slice(0, slash);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const fileCounts = new Map();
|
|
71
|
+
for (const b of cityData.buildings) {
|
|
72
|
+
let path = b.path;
|
|
73
|
+
const slash = path.lastIndexOf('/');
|
|
74
|
+
path = slash >= 0 ? path.slice(0, slash) : '';
|
|
75
|
+
while (path) {
|
|
76
|
+
fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
|
|
77
|
+
const s = path.lastIndexOf('/');
|
|
78
|
+
if (s < 0)
|
|
79
|
+
break;
|
|
80
|
+
path = path.slice(0, s);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { children, bounds, fileCounts };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Walks the folder hierarchy from the top-level folders of a `CityData`. For
|
|
87
|
+
* each folder:
|
|
88
|
+
* - if expanded → recurse into child folders
|
|
89
|
+
* - if collapsed → emit one elevated panel covering the union of every
|
|
90
|
+
* descendant district's world bounds, colored via `hashFolderColor`.
|
|
91
|
+
*
|
|
92
|
+
* Mirror of the scope-tree expansion behavior, applied to file-tree folders.
|
|
93
|
+
*/
|
|
94
|
+
export function buildFolderElevatedPanels(options) {
|
|
95
|
+
const { cityData, expandedFolders, onToggleFolder, onDoubleClickFolder, scaleLabelByFileCount = true, } = options;
|
|
96
|
+
const index = options.index ?? buildFolderIndex(cityData);
|
|
97
|
+
const panels = [];
|
|
98
|
+
const walk = (folderPath) => {
|
|
99
|
+
if (expandedFolders.has(folderPath)) {
|
|
100
|
+
const kids = index.children.get(folderPath) ?? [];
|
|
101
|
+
for (const child of kids)
|
|
102
|
+
walk(child);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const bounds = index.bounds.get(folderPath);
|
|
106
|
+
if (!bounds)
|
|
107
|
+
return;
|
|
108
|
+
const label = folderPath.split('/').pop() ?? folderPath;
|
|
109
|
+
const fileCount = index.fileCounts.get(folderPath) ?? 0;
|
|
110
|
+
const labelSize = scaleLabelByFileCount
|
|
111
|
+
? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
|
|
112
|
+
: undefined;
|
|
113
|
+
panels.push({
|
|
114
|
+
id: `folder::${folderPath}`,
|
|
115
|
+
color: hashFolderColor(folderPath),
|
|
116
|
+
height: 4,
|
|
117
|
+
thickness: 2,
|
|
118
|
+
bounds,
|
|
119
|
+
label,
|
|
120
|
+
labelSize,
|
|
121
|
+
onClick: onToggleFolder ? (event) => onToggleFolder(folderPath, event) : undefined,
|
|
122
|
+
onDoubleClick: onDoubleClickFolder
|
|
123
|
+
? (event) => onDoubleClickFolder(folderPath, event)
|
|
124
|
+
: undefined,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
for (const top of index.children.get('') ?? [])
|
|
128
|
+
walk(top);
|
|
129
|
+
return panels;
|
|
130
|
+
}
|