@ltm-blueverse/alpha-semantic-hub 0.7.0
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/LICENSE +21 -0
- package/README.md +1674 -0
- package/explorer/README.md +282 -0
- package/explorer/src/App.css +0 -0
- package/explorer/src/App.tsx +2035 -0
- package/explorer/src/ErrorBoundary.tsx +112 -0
- package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
- package/explorer/src/assets/hero.png +0 -0
- package/explorer/src/assets/react.svg +1 -0
- package/explorer/src/assets/vite.svg +1 -0
- package/explorer/src/explorerCapabilities.ts +24 -0
- package/explorer/src/index.css +71 -0
- package/explorer/src/main.tsx +7 -0
- package/explorer/src/store/edgePairKeys.d.ts +2 -0
- package/explorer/src/store/edgePairKeys.js +7 -0
- package/explorer/src/store/graphStore.ts +183 -0
- package/explorer/src/store/registryStore.ts +78 -0
- package/explorer/src/types.d.ts +16 -0
- package/explorer/src/ui/primitives.css +414 -0
- package/explorer/src/ui/primitives.tsx +170 -0
- package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
- package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
- package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
- package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
- package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
- package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
- package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
- package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
- package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
- package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
- package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
- package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
- package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
- package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
- package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
- package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
- package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
- package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
- package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
- package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
- package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
- package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
- package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
- package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
- package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
- package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
- package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
- package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
- package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
- package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
- package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
- package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
- package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
- package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
- package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
- package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
- package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
- package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
- package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
- package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
- package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
- package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
- package/package.json +31 -0
- package/plugins/.claude-plugin/README.md +262 -0
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/.claude-plugin/plugin.json +33 -0
- package/plugins/.cline-plugin/README.md +32 -0
- package/plugins/.cline-plugin/marketplace.json +17 -0
- package/plugins/.cline-plugin/plugin.json +35 -0
- package/plugins/.codex-plugin/marketplace.json +21 -0
- package/plugins/.codex-plugin/plugin.json +35 -0
- package/plugins/.continue-plugin/README.md +34 -0
- package/plugins/.continue-plugin/marketplace.json +17 -0
- package/plugins/.continue-plugin/plugin.json +33 -0
- package/plugins/.cursor-plugin/marketplace.json +18 -0
- package/plugins/.cursor-plugin/plugin.json +32 -0
- package/plugins/.openclaw-plugin/README.md +62 -0
- package/plugins/.openclaw-plugin/marketplace.json +18 -0
- package/plugins/.openclaw-plugin/plugin.json +46 -0
- package/plugins/.pi-plugin/README.md +35 -0
- package/plugins/.vscode-plugin/README.md +48 -0
- package/plugins/.vscode-plugin/marketplace.json +17 -0
- package/plugins/.vscode-plugin/plugin.json +35 -0
- package/plugins/.windsurf-plugin/README.md +39 -0
- package/plugins/.windsurf-plugin/marketplace.json +17 -0
- package/plugins/.windsurf-plugin/plugin.json +35 -0
- package/plugins/agents/decision-advisor.md +126 -0
- package/plugins/agents/explainability.md +129 -0
- package/plugins/agents/kg-assistant.md +73 -0
- package/plugins/hooks/hooks.json +12 -0
- package/plugins/skills/causal/SKILL.md +74 -0
- package/plugins/skills/change/SKILL.md +80 -0
- package/plugins/skills/decision/SKILL.md +197 -0
- package/plugins/skills/deduplicate/SKILL.md +41 -0
- package/plugins/skills/embed/SKILL.md +230 -0
- package/plugins/skills/explain/SKILL.md +44 -0
- package/plugins/skills/export/SKILL.md +67 -0
- package/plugins/skills/extract/SKILL.md +93 -0
- package/plugins/skills/ingest/SKILL.md +38 -0
- package/plugins/skills/ontology/SKILL.md +89 -0
- package/plugins/skills/policy/SKILL.md +66 -0
- package/plugins/skills/provenance/SKILL.md +70 -0
- package/plugins/skills/query/SKILL.md +84 -0
- package/plugins/skills/reason/SKILL.md +201 -0
- package/plugins/skills/temporal/SKILL.md +164 -0
- package/plugins/skills/validate/SKILL.md +228 -0
- package/plugins/skills/visualize/SKILL.md +249 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/workspaces/ManageWorkspace/KGOverviewTab.tsx
|
|
3
|
+
*
|
|
4
|
+
* Quick-view dashboard for the Knowledge Graph: node/edge counts,
|
|
5
|
+
* type distributions, and top connected nodes.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useCallback } from "react";
|
|
8
|
+
import { Network, RefreshCw, Loader2 } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
interface KGStats {
|
|
11
|
+
node_count: number;
|
|
12
|
+
edge_count: number;
|
|
13
|
+
node_types?: Record<string, number>;
|
|
14
|
+
edge_types?: Record<string, number>;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NodeItem {
|
|
19
|
+
id: string;
|
|
20
|
+
type: string;
|
|
21
|
+
content: string;
|
|
22
|
+
properties?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface NodeListResponse {
|
|
26
|
+
nodes: NodeItem[];
|
|
27
|
+
total: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TypeBar({ label, count, total, color }: { label: string; count: number; total: number; color: string }) {
|
|
31
|
+
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, padding: "5px 0" }}>
|
|
34
|
+
<div style={{ width: 120, flexShrink: 0, color: "#f0cdd3", fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={label}>
|
|
35
|
+
{label}
|
|
36
|
+
</div>
|
|
37
|
+
<div style={{ flex: 1, height: 6, borderRadius: 999, background: "rgba(255,255,255,0.06)", overflow: "hidden" }}>
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
width: `${pct}%`,
|
|
41
|
+
height: "100%",
|
|
42
|
+
borderRadius: 999,
|
|
43
|
+
background: color,
|
|
44
|
+
transition: "width 400ms ease",
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div style={{ width: 52, textAlign: "right", flexShrink: 0, display: "flex", gap: 6, justifyContent: "flex-end" }}>
|
|
49
|
+
<span style={{ color: "#b08b93", fontSize: 11 }}>{count.toLocaleString()}</span>
|
|
50
|
+
<span style={{ color: "#9c7a82", fontSize: 11 }}>{pct}%</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const NODE_COLORS = ["#3E79F2", "#149287", "#2F9F61", "#555FD6", "#8A56D8", "#B65473", "#C9922E", "#ff2d4b", "#ff7a45"];
|
|
57
|
+
const EDGE_COLORS = ["#4cc38a", "#ff8fa0", "#d2a8ff", "#ff7a45", "#ff7b72", "#ff2d4b", "#ff2d4b", "#8A56D8"];
|
|
58
|
+
|
|
59
|
+
function buildTypeMap(nodes: NodeItem[], key: keyof NodeItem): Record<string, number> {
|
|
60
|
+
const map: Record<string, number> = {};
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
const val = String(node[key] ?? "unknown");
|
|
63
|
+
map[val] = (map[val] ?? 0) + 1;
|
|
64
|
+
}
|
|
65
|
+
return map;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function KGOverviewTab() {
|
|
69
|
+
const [stats, setStats] = useState<KGStats | null>(null);
|
|
70
|
+
const [loading, setLoading] = useState(false);
|
|
71
|
+
const [error, setError] = useState("");
|
|
72
|
+
const [topNodes, setTopNodes] = useState<{ node: NodeItem; neighborCount: number }[]>([]);
|
|
73
|
+
const [nodeTypeMap, setNodeTypeMap] = useState<Record<string, number>>({});
|
|
74
|
+
|
|
75
|
+
const fetchOverview = useCallback(async () => {
|
|
76
|
+
setLoading(true);
|
|
77
|
+
setError("");
|
|
78
|
+
try {
|
|
79
|
+
const [statsRes, nodesRes] = await Promise.all([
|
|
80
|
+
fetch("/api/graph/stats"),
|
|
81
|
+
fetch("/api/graph/nodes?limit=500"),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
if (!statsRes.ok) throw new Error(`Stats fetch failed (${statsRes.status})`);
|
|
85
|
+
if (!nodesRes.ok) throw new Error(`Nodes fetch failed (${nodesRes.status})`);
|
|
86
|
+
|
|
87
|
+
const statsData: KGStats = await statsRes.json();
|
|
88
|
+
setStats(statsData);
|
|
89
|
+
if (statsRes.status === 207) {
|
|
90
|
+
setError((statsData as any).message || "Warning: Partial success loading stats.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const nodesData: NodeListResponse = await nodesRes.json();
|
|
94
|
+
const nodes = nodesData.nodes ?? [];
|
|
95
|
+
setNodeTypeMap(buildTypeMap(nodes, "type"));
|
|
96
|
+
if (nodesRes.status === 207) {
|
|
97
|
+
const nodesMessage = (nodesData as any).message || "Warning: Partial success loading nodes.";
|
|
98
|
+
setError((prev) => (prev ? `${prev} ${nodesMessage}` : nodesMessage));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Simulate neighbor counts via edges fetch for top-N
|
|
102
|
+
const edgesRes = await fetch("/api/graph/edges?limit=2000");
|
|
103
|
+
if (edgesRes.ok) {
|
|
104
|
+
const edgesData = await edgesRes.json();
|
|
105
|
+
const edges: { source: string; target: string }[] = edgesData.edges ?? [];
|
|
106
|
+
const degreeMap: Record<string, number> = {};
|
|
107
|
+
for (const edge of edges) {
|
|
108
|
+
degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1;
|
|
109
|
+
degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
const sorted = nodes
|
|
112
|
+
.map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 }))
|
|
113
|
+
.sort((a, b) => b.neighborCount - a.neighborCount)
|
|
114
|
+
.slice(0, 10);
|
|
115
|
+
setTopNodes(sorted);
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
setError(err instanceof Error ? err.message : "Failed to load graph overview. Ensure the server is running.");
|
|
119
|
+
} finally {
|
|
120
|
+
setLoading(false);
|
|
121
|
+
}
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
let ignore = false;
|
|
126
|
+
async function fetchInitial() {
|
|
127
|
+
if (!ignore) {
|
|
128
|
+
setLoading(true);
|
|
129
|
+
setError("");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const [statsRes, nodesRes] = await Promise.all([
|
|
133
|
+
fetch("/api/graph/stats"),
|
|
134
|
+
fetch("/api/graph/nodes?limit=500"),
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
if (!statsRes.ok) throw new Error(`Stats fetch failed (${statsRes.status})`);
|
|
138
|
+
if (!nodesRes.ok) throw new Error(`Nodes fetch failed (${nodesRes.status})`);
|
|
139
|
+
|
|
140
|
+
const statsData: KGStats = await statsRes.json();
|
|
141
|
+
if (!ignore) {
|
|
142
|
+
setStats(statsData);
|
|
143
|
+
if (statsRes.status === 207) {
|
|
144
|
+
setError((statsData as { message?: string }).message || "Warning: Partial success loading stats.");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const nodesData: NodeListResponse = await nodesRes.json();
|
|
149
|
+
const nodes = nodesData.nodes ?? [];
|
|
150
|
+
if (!ignore) {
|
|
151
|
+
setNodeTypeMap(buildTypeMap(nodes, "type"));
|
|
152
|
+
if (nodesRes.status === 207) {
|
|
153
|
+
const nodesMessage = (nodesData as { message?: string }).message || "Warning: Partial success loading nodes.";
|
|
154
|
+
setError((prev) => (prev ? `${prev} ${nodesMessage}` : nodesMessage));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Simulate neighbor counts via edges fetch for top-N
|
|
159
|
+
const edgesRes = await fetch("/api/graph/edges?limit=2000");
|
|
160
|
+
if (edgesRes.ok) {
|
|
161
|
+
const edgesData = await edgesRes.json();
|
|
162
|
+
const edges: { source: string; target: string }[] = edgesData.edges ?? [];
|
|
163
|
+
const degreeMap: Record<string, number> = {};
|
|
164
|
+
for (const edge of edges) {
|
|
165
|
+
degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1;
|
|
166
|
+
degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1;
|
|
167
|
+
}
|
|
168
|
+
const sorted = nodes
|
|
169
|
+
.map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 }))
|
|
170
|
+
.sort((a, b) => b.neighborCount - a.neighborCount)
|
|
171
|
+
.slice(0, 10);
|
|
172
|
+
if (!ignore) setTopNodes(sorted);
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (!ignore) setError(err instanceof Error ? err.message : "Failed to load graph overview. Ensure the server is running.");
|
|
176
|
+
} finally {
|
|
177
|
+
if (!ignore) setLoading(false);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
void fetchInitial();
|
|
181
|
+
return () => { ignore = true; };
|
|
182
|
+
}, []);
|
|
183
|
+
|
|
184
|
+
const nodeTypeEntries = Object.entries(nodeTypeMap).sort((a, b) => b[1] - a[1]);
|
|
185
|
+
const edgeTypeEntries = stats?.edge_types
|
|
186
|
+
? Object.entries(stats.edge_types).sort((a, b) => b[1] - a[1])
|
|
187
|
+
: [];
|
|
188
|
+
|
|
189
|
+
const totalNodes = stats?.node_count ?? 0;
|
|
190
|
+
const totalEdges = stats?.edge_count ?? 0;
|
|
191
|
+
|
|
192
|
+
const statCards = [
|
|
193
|
+
{ label: "Nodes", value: totalNodes.toLocaleString(), color: "var(--ws-accent)", sub: `${nodeTypeEntries.length} types` },
|
|
194
|
+
{ label: "Edges", value: totalEdges.toLocaleString(), color: "var(--ws-green)", sub: `${edgeTypeEntries.length} rel. types` },
|
|
195
|
+
{ label: "Density", value: totalNodes > 1 ? ((totalEdges / (totalNodes * (totalNodes - 1))) * 100).toFixed(3) + "%" : "—", color: "var(--ws-purple)", sub: "graph density" },
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="ws-page">
|
|
200
|
+
{error ? (
|
|
201
|
+
<div style={{ margin: "16px 22px 0 22px", padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)" }}>
|
|
202
|
+
{error}
|
|
203
|
+
</div>
|
|
204
|
+
) : null}
|
|
205
|
+
|
|
206
|
+
{/* Header */}
|
|
207
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "16px 22px", borderBottom: "1px solid var(--ws-border)", flexShrink: 0 }}>
|
|
208
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
209
|
+
<div style={{ width: 32, height: 32, borderRadius: 9, background: "var(--ws-accent-soft)", border: "1px solid var(--ws-border-strong)", display: "grid", placeItems: "center", color: "var(--ws-accent)" }}>
|
|
210
|
+
<Network size={16} />
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<div style={{ color: "var(--ws-text)", fontSize: 15, fontWeight: 700, lineHeight: 1 }}>KG Overview</div>
|
|
214
|
+
<div className="ws-body" style={{ fontSize: 11, marginTop: 2 }}>Node/edge counts, type distributions, and top connected nodes</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<button className="ws-btn ws-btn--ghost" onClick={() => void fetchOverview()} disabled={loading} style={{ padding: "6px 12px" }}>
|
|
218
|
+
{loading ? <Loader2 size={13} className="ws-spin" /> : <RefreshCw size={13} />}
|
|
219
|
+
Refresh
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
<div className="ws-scroll" style={{ flex: 1, padding: "18px 22px", display: "flex", flexDirection: "column", gap: 16 }}>
|
|
226
|
+
{/* Stat cards */}
|
|
227
|
+
<div className="ws-stat-grid ws-stat-grid--3">
|
|
228
|
+
{statCards.map(({ label, value, color, sub }) => (
|
|
229
|
+
<div key={label} className="ws-stat-card">
|
|
230
|
+
<div className="ws-eyebrow" style={{ marginBottom: 6 }}>{label}</div>
|
|
231
|
+
<div className="ws-stat-value" style={{ color }}>{loading ? "—" : value}</div>
|
|
232
|
+
<div className="ws-stat-label">{sub}</div>
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Type breakdowns */}
|
|
238
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14 }}>
|
|
239
|
+
<div className="ws-card" style={{ padding: "16px 18px", gap: 8, display: "flex", flexDirection: "column" }}>
|
|
240
|
+
<div className="ws-eyebrow" style={{ marginBottom: 4 }}>Node Type Breakdown</div>
|
|
241
|
+
{loading ? (
|
|
242
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
243
|
+
{[80, 65, 45, 35, 25].map((w, i) => <div key={i} className="ws-skeleton" style={{ height: 10, width: `${w}%` }} />)}
|
|
244
|
+
</div>
|
|
245
|
+
) : nodeTypeEntries.length === 0 ? (
|
|
246
|
+
<div className="ws-body" style={{ fontSize: 12 }}>No data — load the graph first.</div>
|
|
247
|
+
) : (
|
|
248
|
+
nodeTypeEntries.slice(0, 8).map(([type, count], i) => (
|
|
249
|
+
<TypeBar key={type} label={type} count={count} total={totalNodes || 1} color={NODE_COLORS[i % NODE_COLORS.length]} />
|
|
250
|
+
))
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div className="ws-card" style={{ padding: "16px 18px", gap: 8, display: "flex", flexDirection: "column" }}>
|
|
255
|
+
<div className="ws-eyebrow" style={{ marginBottom: 4 }}>Edge Type Breakdown</div>
|
|
256
|
+
{loading ? (
|
|
257
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
258
|
+
{[70, 55, 48, 30, 20].map((w, i) => <div key={i} className="ws-skeleton" style={{ height: 10, width: `${w}%` }} />)}
|
|
259
|
+
</div>
|
|
260
|
+
) : edgeTypeEntries.length === 0 ? (
|
|
261
|
+
<div className="ws-body" style={{ fontSize: 12 }}>Edge type breakdown requires the stats endpoint to return edge_types.</div>
|
|
262
|
+
) : (
|
|
263
|
+
edgeTypeEntries.slice(0, 8).map(([type, count], i) => (
|
|
264
|
+
<TypeBar key={type} label={type} count={count} total={totalEdges || 1} color={EDGE_COLORS[i % EDGE_COLORS.length]} />
|
|
265
|
+
))
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Top connected nodes */}
|
|
271
|
+
{topNodes.length > 0 && (
|
|
272
|
+
<div className="ws-card" style={{ padding: "16px 18px" }}>
|
|
273
|
+
<div className="ws-eyebrow" style={{ marginBottom: 12 }}>Top Connected Nodes (by degree)</div>
|
|
274
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", gap: 8 }}>
|
|
275
|
+
{topNodes.map(({ node, neighborCount }, rank) => (
|
|
276
|
+
<div key={node.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", borderRadius: "var(--ws-radius-sm)", background: "rgba(0,0,0,0.18)", border: "1px solid var(--ws-border)" }}>
|
|
277
|
+
<div style={{ color: "var(--ws-text-dim)", fontSize: 11, fontWeight: 700, minWidth: 22 }}>#{rank + 1}</div>
|
|
278
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
279
|
+
<div style={{ color: "var(--ws-text)", fontSize: 12, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{node.content || node.id}</div>
|
|
280
|
+
<div className="ws-eyebrow" style={{ marginTop: 2, fontSize: 9 }}>{node.type}</div>
|
|
281
|
+
</div>
|
|
282
|
+
<span className="ws-pill ws-pill--accent" style={{ fontSize: 10 }}>{neighborCount}</span>
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/workspaces/ManageWorkspace/OntologySummaryTab.tsx
|
|
3
|
+
*
|
|
4
|
+
* A compact read-only view of all loaded SKOS ConceptSchemes and their
|
|
5
|
+
* top-level concepts. Clicking a concept deep-links to the Vocabulary Browser.
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { BookOpen, ChevronRight, ChevronDown, ExternalLink } from "lucide-react";
|
|
9
|
+
import { useVocabularies, useConceptHierarchy } from "../VocabularyWorkspace/queries";
|
|
10
|
+
import type { ConceptNode, VocabularyScheme } from "../VocabularyWorkspace/types";
|
|
11
|
+
|
|
12
|
+
function countConcepts(nodes: ConceptNode[]): number {
|
|
13
|
+
return nodes.reduce((acc, node) => {
|
|
14
|
+
return acc + 1 + countConcepts(node.children ?? []);
|
|
15
|
+
}, 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ConceptRow({
|
|
19
|
+
concept,
|
|
20
|
+
depth,
|
|
21
|
+
onSelect,
|
|
22
|
+
}: {
|
|
23
|
+
concept: ConceptNode;
|
|
24
|
+
depth: number;
|
|
25
|
+
onSelect: (concept: ConceptNode) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const [expanded, setExpanded] = useState(false);
|
|
28
|
+
const children = concept.children ?? [];
|
|
29
|
+
const hasChildren = children.length > 0;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<div
|
|
34
|
+
style={{
|
|
35
|
+
display: "flex",
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
gap: 6,
|
|
38
|
+
paddingLeft: 12 + depth * 16,
|
|
39
|
+
paddingRight: 12,
|
|
40
|
+
paddingTop: 5,
|
|
41
|
+
paddingBottom: 5,
|
|
42
|
+
borderRadius: 6,
|
|
43
|
+
cursor: "pointer",
|
|
44
|
+
color: depth === 0 ? "#f0cdd3" : "#b08b93",
|
|
45
|
+
fontSize: depth === 0 ? 13 : 12,
|
|
46
|
+
transition: "background 120ms ease",
|
|
47
|
+
}}
|
|
48
|
+
onMouseEnter={(e) => { (e.currentTarget as HTMLDivElement).style.background = "rgba(255,45,75,0.07)"; }}
|
|
49
|
+
onMouseLeave={(e) => { (e.currentTarget as HTMLDivElement).style.background = "transparent"; }}
|
|
50
|
+
>
|
|
51
|
+
{hasChildren ? (
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => setExpanded((v) => !v)}
|
|
54
|
+
style={{ background: "transparent", border: "none", color: "#b08b93", cursor: "pointer", padding: 0, display: "flex", alignItems: "center" }}
|
|
55
|
+
>
|
|
56
|
+
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
57
|
+
</button>
|
|
58
|
+
) : (
|
|
59
|
+
<span style={{ width: 12, display: "inline-block" }} />
|
|
60
|
+
)}
|
|
61
|
+
<span
|
|
62
|
+
onClick={() => onSelect(concept)}
|
|
63
|
+
style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
|
64
|
+
>
|
|
65
|
+
{concept.pref_label || concept.uri}
|
|
66
|
+
</span>
|
|
67
|
+
{children.length > 0 ? (
|
|
68
|
+
<span style={{ color: "#9c7a82", fontSize: 10 }}>{children.length}</span>
|
|
69
|
+
) : null}
|
|
70
|
+
</div>
|
|
71
|
+
{expanded && hasChildren
|
|
72
|
+
? children.map((child) => (
|
|
73
|
+
<ConceptRow key={child.uri} concept={child} depth={depth + 1} onSelect={onSelect} />
|
|
74
|
+
))
|
|
75
|
+
: null}
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function SchemePanel({
|
|
81
|
+
scheme,
|
|
82
|
+
onSelectConcept,
|
|
83
|
+
}: {
|
|
84
|
+
scheme: VocabularyScheme;
|
|
85
|
+
onSelectConcept: (concept: ConceptNode) => void;
|
|
86
|
+
}) {
|
|
87
|
+
const [expanded, setExpanded] = useState(true);
|
|
88
|
+
const { data: hierarchy = [], isLoading } = useConceptHierarchy(scheme.uri);
|
|
89
|
+
const totalConcepts = countConcepts(hierarchy);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div style={schemeCardStyle}>
|
|
93
|
+
{/* Scheme header */}
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => setExpanded((v) => !v)}
|
|
96
|
+
style={schemeHeaderStyle}
|
|
97
|
+
>
|
|
98
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
99
|
+
{expanded ? <ChevronDown size={14} color="#b08b93" /> : <ChevronRight size={14} color="#b08b93" />}
|
|
100
|
+
<span style={{ color: "#ffeaed", fontSize: 14, fontWeight: 700 }}>{scheme.label}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<span style={{ color: "#9c7a82", fontSize: 11 }}>
|
|
103
|
+
{isLoading ? "…" : `${totalConcepts} concept${totalConcepts !== 1 ? "s" : ""}`}
|
|
104
|
+
</span>
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
{/* Concept tree */}
|
|
108
|
+
{expanded ? (
|
|
109
|
+
<div style={{ paddingTop: 4, paddingBottom: 8 }}>
|
|
110
|
+
{isLoading ? (
|
|
111
|
+
<div style={{ padding: "8px 24px", color: "#9c7a82", fontSize: 12 }}>Loading concepts…</div>
|
|
112
|
+
) : hierarchy.length === 0 ? (
|
|
113
|
+
<div style={{ padding: "8px 24px", color: "#9c7a82", fontSize: 12, fontStyle: "italic" }}>
|
|
114
|
+
No concepts found in this scheme.
|
|
115
|
+
</div>
|
|
116
|
+
) : (
|
|
117
|
+
hierarchy.map((concept) => (
|
|
118
|
+
<ConceptRow key={concept.uri} concept={concept} depth={0} onSelect={onSelectConcept} />
|
|
119
|
+
))
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function OntologySummaryTab({
|
|
128
|
+
onOpenVocabularyBrowser,
|
|
129
|
+
}: {
|
|
130
|
+
onOpenVocabularyBrowser?: () => void;
|
|
131
|
+
}) {
|
|
132
|
+
const { data: schemes = [], isLoading } = useVocabularies();
|
|
133
|
+
const [selectedConcept, setSelectedConcept] = useState<ConceptNode | null>(null);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div style={shellStyle}>
|
|
137
|
+
{/* Header */}
|
|
138
|
+
<div style={headerStyle}>
|
|
139
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
140
|
+
<BookOpen size={18} color="#d2a8ff" />
|
|
141
|
+
<div>
|
|
142
|
+
<div style={{ color: "#fff0f2", fontSize: 16, fontWeight: 700 }}>Ontology Summary</div>
|
|
143
|
+
<div style={{ color: "#b08b93", fontSize: 12 }}>
|
|
144
|
+
{isLoading
|
|
145
|
+
? "Loading schemes…"
|
|
146
|
+
: `${schemes.length} vocabulary scheme${schemes.length !== 1 ? "s" : ""} loaded`}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
{onOpenVocabularyBrowser ? (
|
|
151
|
+
<button onClick={onOpenVocabularyBrowser} style={openBrowserBtnStyle}>
|
|
152
|
+
<ExternalLink size={12} />
|
|
153
|
+
<span>Open Full Browser</span>
|
|
154
|
+
</button>
|
|
155
|
+
) : null}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
|
159
|
+
{/* Scheme tree column */}
|
|
160
|
+
<div style={treeColumnStyle}>
|
|
161
|
+
{isLoading ? (
|
|
162
|
+
<div style={{ padding: 24, display: "flex", flexDirection: "column", gap: 10 }}>
|
|
163
|
+
{[90, 75, 60].map((w, i) => (
|
|
164
|
+
<div key={i} style={{ height: 36, borderRadius: 8, background: "rgba(255,255,255,0.04)", width: `${w}%` }} />
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
) : schemes.length === 0 ? (
|
|
168
|
+
<div style={emptyStateStyle}>
|
|
169
|
+
<BookOpen size={32} color="rgba(210,168,255,0.15)" />
|
|
170
|
+
<div style={{ color: "#b08b93", fontSize: 13, marginTop: 12 }}>No vocabulary schemes loaded</div>
|
|
171
|
+
<div style={{ color: "#9c7a82", fontSize: 12, marginTop: 4, textAlign: "center", maxWidth: 240 }}>
|
|
172
|
+
Import a .ttl or .rdf file via the Vocabulary Browser to see your ontology here.
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<div style={{ padding: "12px 8px", display: "flex", flexDirection: "column", gap: 8 }}>
|
|
177
|
+
{schemes.map((scheme) => (
|
|
178
|
+
<SchemePanel key={scheme.uri} scheme={scheme} onSelectConcept={setSelectedConcept} />
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Concept detail panel */}
|
|
185
|
+
{selectedConcept ? (
|
|
186
|
+
<div style={detailPanelStyle}>
|
|
187
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
|
|
188
|
+
<div style={{ color: "#d2a8ff", fontSize: 11, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase" }}>
|
|
189
|
+
Concept Detail
|
|
190
|
+
</div>
|
|
191
|
+
<button onClick={() => setSelectedConcept(null)} style={{ background: "transparent", border: "none", color: "#b08b93", cursor: "pointer", fontSize: 16 }}>×</button>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<h3 style={{ color: "#ffffff", fontSize: 18, fontWeight: 800, letterSpacing: "-0.03em", margin: "0 0 6px 0" }}>
|
|
195
|
+
{selectedConcept.pref_label}
|
|
196
|
+
</h3>
|
|
197
|
+
{selectedConcept.notation ? (
|
|
198
|
+
<div style={{ color: "#b08b93", fontSize: 12, marginBottom: 8 }}>Notation: {selectedConcept.notation}</div>
|
|
199
|
+
) : null}
|
|
200
|
+
<div style={{ color: "#9c7a82", fontSize: 11, fontFamily: "monospace", wordBreak: "break-all", marginBottom: 14 }}>
|
|
201
|
+
{selectedConcept.uri}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{selectedConcept.description ? (
|
|
205
|
+
<div style={detailSectionStyle}>
|
|
206
|
+
<div style={detailLabelStyle}>Description</div>
|
|
207
|
+
<div style={{ color: "#f0cdd3", fontSize: 13, lineHeight: 1.6 }}>{selectedConcept.description}</div>
|
|
208
|
+
</div>
|
|
209
|
+
) : null}
|
|
210
|
+
|
|
211
|
+
{selectedConcept.alt_labels?.length ? (
|
|
212
|
+
<div style={detailSectionStyle}>
|
|
213
|
+
<div style={detailLabelStyle}>Alternative Labels</div>
|
|
214
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
215
|
+
{selectedConcept.alt_labels.map((label) => (
|
|
216
|
+
<span key={label} style={altLabelChipStyle}>{label}</span>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
) : null}
|
|
221
|
+
|
|
222
|
+
{(selectedConcept.children?.length ?? 0) > 0 ? (
|
|
223
|
+
<div style={detailSectionStyle}>
|
|
224
|
+
<div style={detailLabelStyle}>Narrower Concepts ({selectedConcept.children!.length})</div>
|
|
225
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
226
|
+
{selectedConcept.children!.slice(0, 8).map((child) => (
|
|
227
|
+
<div
|
|
228
|
+
key={child.uri}
|
|
229
|
+
onClick={() => setSelectedConcept(child)}
|
|
230
|
+
style={{ color: "#ff8fa0", fontSize: 12, cursor: "pointer", padding: "3px 0" }}
|
|
231
|
+
>
|
|
232
|
+
→ {child.pref_label}
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
{selectedConcept.children!.length > 8 ? (
|
|
236
|
+
<div style={{ color: "#9c7a82", fontSize: 11 }}>+{selectedConcept.children!.length - 8} more</div>
|
|
237
|
+
) : null}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
) : null}
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ─── styles ─────────────────────────────────────────────────────── */
|
|
249
|
+
|
|
250
|
+
const shellStyle: React.CSSProperties = {
|
|
251
|
+
display: "flex",
|
|
252
|
+
flexDirection: "column",
|
|
253
|
+
width: "100%",
|
|
254
|
+
height: "100%",
|
|
255
|
+
background: "#0a0508",
|
|
256
|
+
overflow: "hidden",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const headerStyle: React.CSSProperties = {
|
|
260
|
+
display: "flex",
|
|
261
|
+
alignItems: "center",
|
|
262
|
+
justifyContent: "space-between",
|
|
263
|
+
padding: "20px 24px 16px",
|
|
264
|
+
borderBottom: "1px solid rgba(255,45,75,0.1)",
|
|
265
|
+
flexShrink: 0,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const openBrowserBtnStyle: React.CSSProperties = {
|
|
269
|
+
display: "inline-flex",
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
gap: 6,
|
|
272
|
+
padding: "6px 12px",
|
|
273
|
+
borderRadius: 8,
|
|
274
|
+
border: "1px solid rgba(210,168,255,0.22)",
|
|
275
|
+
background: "rgba(210,168,255,0.08)",
|
|
276
|
+
color: "#d2a8ff",
|
|
277
|
+
fontSize: 12,
|
|
278
|
+
fontWeight: 600,
|
|
279
|
+
cursor: "pointer",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const treeColumnStyle: React.CSSProperties = {
|
|
283
|
+
flex: 1,
|
|
284
|
+
overflowY: "auto",
|
|
285
|
+
borderRight: "1px solid rgba(255,255,255,0.06)",
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const schemeCardStyle: React.CSSProperties = {
|
|
289
|
+
borderRadius: 10,
|
|
290
|
+
border: "1px solid rgba(210,168,255,0.1)",
|
|
291
|
+
background: "rgba(255,255,255,0.02)",
|
|
292
|
+
overflow: "hidden",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const schemeHeaderStyle: React.CSSProperties = {
|
|
296
|
+
width: "100%",
|
|
297
|
+
display: "flex",
|
|
298
|
+
alignItems: "center",
|
|
299
|
+
justifyContent: "space-between",
|
|
300
|
+
padding: "10px 14px",
|
|
301
|
+
background: "transparent",
|
|
302
|
+
border: "none",
|
|
303
|
+
cursor: "pointer",
|
|
304
|
+
borderBottom: "1px solid rgba(255,255,255,0.05)",
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const detailPanelStyle: React.CSSProperties = {
|
|
308
|
+
width: 300,
|
|
309
|
+
padding: "20px",
|
|
310
|
+
overflowY: "auto",
|
|
311
|
+
borderLeft: "1px solid rgba(255,255,255,0.06)",
|
|
312
|
+
flexShrink: 0,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const detailSectionStyle: React.CSSProperties = {
|
|
316
|
+
marginTop: 14,
|
|
317
|
+
paddingTop: 12,
|
|
318
|
+
borderTop: "1px solid rgba(255,255,255,0.06)",
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const detailLabelStyle: React.CSSProperties = {
|
|
322
|
+
color: "#b08b93",
|
|
323
|
+
fontSize: 10,
|
|
324
|
+
fontWeight: 700,
|
|
325
|
+
textTransform: "uppercase",
|
|
326
|
+
letterSpacing: "0.07em",
|
|
327
|
+
marginBottom: 6,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const altLabelChipStyle: React.CSSProperties = {
|
|
331
|
+
padding: "3px 8px",
|
|
332
|
+
borderRadius: 999,
|
|
333
|
+
background: "rgba(255,255,255,0.05)",
|
|
334
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
335
|
+
color: "#c99aa3",
|
|
336
|
+
fontSize: 11,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const emptyStateStyle: React.CSSProperties = {
|
|
340
|
+
display: "flex",
|
|
341
|
+
flexDirection: "column",
|
|
342
|
+
alignItems: "center",
|
|
343
|
+
justifyContent: "center",
|
|
344
|
+
padding: 40,
|
|
345
|
+
height: "100%",
|
|
346
|
+
};
|