import { Fragment, useEffect, useRef, useState } from "react";
import {
Bot,
Boxes,
Check,
CircleCheck,
Code,
Copy,
Cuboid,
DraftingCompass,
FileBox,
Folder,
Layers3,
LoaderCircle,
Moon,
Package,
Route,
SlidersHorizontal,
Sun
} from "lucide-react";
import {
DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND,
isViewerReleaseMajorMinorNewer,
isViewerReleaseNewer,
viewerGithubLatestReleaseApiUrl,
viewerGithubLatestReleaseUrl,
normalizeViewerDiscordUrl,
normalizeViewerGithubUrl,
viewerGithubReleaseUrl,
viewerSkillsInstallCommandFromText
} from "../../../shared/viewerConfig.mjs";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbSeparator
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import { SidebarTrigger } from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip";
import { cn } from "@/ui/utils";
import { copyTextToClipboard } from "@/ui/clipboard";
import {
themeSettingsSupportsSystemColorMode
} from "cadjs/lib/themeSettings";
import {
DARK_COLOR_SCHEME_ID,
LIGHT_COLOR_SCHEME_ID
} from "@/ui/colorScheme";
import {
ENTRY_ICON_KIND,
entryIconKind
} from "@/workbench/entryIconKind";
import { entryIconStatus } from "@/workbench/entryIconStatus";
import { ThemePresetDropdown } from "./ThemeSettingsPopover";
import FileAccessContextMenu from "./FileAccessContextMenu";
import {
fileKey,
listSidebarItems,
} from "@/workbench/sidebar";
import {
buildBreadcrumbNodes,
collapsedBreadcrumbNodes,
directoryTitle,
ellipsisBreadcrumbMenuDirectory
} from "@/workbench/breadcrumbs";
function fileSheetLabel(fileSheetKind) {
if (fileSheetKind === "dxf") {
return "DXF sheet";
}
if (fileSheetKind === "urdf") {
return "URDF sheet";
}
if (fileSheetKind === "srdf") {
return "SRDF sheet";
}
if (fileSheetKind === "sdf") {
return "SDF sheet";
}
if (fileSheetKind === "step") {
return "STEP sheet";
}
if (fileSheetKind === "implicit") {
return "Implicit CAD sheet";
}
return "file sheet";
}
function sourceFormatForEntry(entry, entrySourceFormat) {
const sourceFormat = typeof entrySourceFormat === "function"
? entrySourceFormat(entry)
: (entry?.kind || "");
return String(sourceFormat || "").trim().toLowerCase();
}
function entryStatusForMenu(entry, {
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true
}) {
const sourceFormat = sourceFormatForEntry(entry, entrySourceFormat);
const hasDxf = typeof entryHasDxf === "function" ? entryHasDxf(entry) : true;
const hasGcode = typeof entryHasGcode === "function" ? entryHasGcode(entry) : true;
const hasUrdf = typeof entryHasUrdf === "function" ? entryHasUrdf(entry) : true;
const hasMesh = typeof entryHasMesh === "function" ? entryHasMesh(entry) : true;
return entryIconStatus(entry, {
sourceFormat,
entryKey: fileKey(entry),
hasMesh,
hasDxf,
hasGcode,
hasUrdf,
activeGenerationFiles,
activeStepArtifactGenerationFile,
stepArtifactGenerationAvailable
});
}
const ENTRY_ICON_COMPONENTS = {
[ENTRY_ICON_KIND.LOADING]: LoaderCircle,
[ENTRY_ICON_KIND.ASSEMBLY]: Boxes,
[ENTRY_ICON_KIND.DXF]: DraftingCompass,
[ENTRY_ICON_KIND.GCODE]: Route,
[ENTRY_ICON_KIND.IMPLICIT]: Code,
[ENTRY_ICON_KIND.ROBOT]: Bot,
[ENTRY_ICON_KIND.STEP_PART]: Package,
[ENTRY_ICON_KIND.STL_MESH]: Cuboid,
[ENTRY_ICON_KIND.THREE_MF_MESH]: Layers3,
[ENTRY_ICON_KIND.GLB_MESH]: FileBox
};
function iconForEntry(entry, sourceFormat, status) {
return ENTRY_ICON_COMPONENTS[entryIconKind(entry, { sourceFormat, status })] || Package;
}
function BreadcrumbEntryMenuItem({
entry,
selectedKey,
onSelectEntry,
sidebarLabelForEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true
}) {
const key = fileKey(entry);
const active = key === selectedKey;
const label = typeof sidebarLabelForEntry === "function"
? sidebarLabelForEntry(entry)
: key;
const status = entryStatusForMenu(entry, {
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles,
activeStepArtifactGenerationFile,
stepArtifactGenerationAvailable
});
const { sourceFormat } = status;
const EntryIcon = iconForEntry(entry, sourceFormat, status);
const title = [
label,
status.statusLabel,
entry?.kind,
String(entry?.file || key)
].filter(Boolean).join(" | ");
return (
{
if (key && typeof onSelectEntry === "function") {
onSelectEntry(key);
}
}}
>
{label}
);
}
function BreadcrumbDirectoryMenuItems({
directory,
selectedKey,
onSelectEntry,
sidebarLabelForEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true,
canRevealFileAssets = false,
canCopyFileAssetLinks = false,
canCopyFileAssetPaths = false,
fileAccessBusyKey = "",
onDownloadFileAsset,
onExportImplicitFile,
onRevealFileAsset,
onCopyFileAssetReference
}) {
const items = listSidebarItems(directory);
if (!items.length) {
return (
{String(directory?.name || "Empty")}
);
}
return items.map((item) => {
if (item.type === "directory") {
return (
);
}
return (
);
});
}
function DropdownMenuScrollArea({ children }) {
return (
{children}
);
}
function BreadcrumbDirectorySubMenu({
directory,
label = "",
title: titleProp = "",
selectedKey,
onSelectEntry,
sidebarLabelForEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true,
canRevealFileAssets = false,
canCopyFileAssetLinks = false,
canCopyFileAssetPaths = false,
fileAccessBusyKey = "",
onDownloadFileAsset,
onExportImplicitFile,
onRevealFileAsset,
onCopyFileAssetReference
}) {
const labelText = String(label || directory?.name || "Folder");
const title = String(titleProp || directoryTitle(directory));
return (
{labelText}
);
}
function BreadcrumbNodeDropdown({
node,
current,
selectedKey,
onSelectEntry,
sidebarLabelForEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true,
selectedStepSourceStatus = null,
canRevealFileAssets = false,
canCopyFileAssetLinks = false,
canCopyFileAssetPaths = false,
fileAccessBusyKey = "",
onDownloadFileAsset,
onExportImplicitFile,
onRevealFileAsset,
onRevealInExplorerView,
onCopyFileAssetReference,
filenameLoadActivity
}) {
const label = String(node?.label || "");
const title = String(node?.title || label);
const menuDirectory = node?.type === "directory" || node?.type === "placeholder" || node?.type === "entry"
? node?.menuDirectory || null
: null;
const canBrowse = !!menuDirectory && listSidebarItems(menuDirectory).length > 0;
if (!canBrowse) {
const labelNode = (
{label}
{current && node?.type === "entry" ? (
) : null}
);
if (node?.type !== "entry" || !node?.entry) {
return labelNode;
}
return (
{labelNode}
);
}
const triggerButton = (
);
const dropdownTrigger = (
{triggerButton}
);
const trigger = node?.type === "entry" && node?.entry ? (
{dropdownTrigger}
) : dropdownTrigger;
return (
{trigger}
);
}
function BreadcrumbEllipsisDropdown({
nodes,
selectedKey,
onSelectEntry,
sidebarLabelForEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true,
canRevealFileAssets = false,
canCopyFileAssetLinks = false,
canCopyFileAssetPaths = false,
fileAccessBusyKey = "",
onDownloadFileAsset,
onExportImplicitFile,
onRevealFileAsset,
onCopyFileAssetReference,
title
}) {
const hiddenNodes = Array.isArray(nodes) ? nodes.filter(Boolean) : [];
const menuTitle = hiddenNodes.map((node) => node.label).join(" / ") || title;
return (
{hiddenNodes.map((node, index) => {
const directory = ellipsisBreadcrumbMenuDirectory(node);
if (node.type === "directory" && directory) {
return (
);
}
if (node.type === "entry" && node.entry) {
return (
);
}
return (
{node.label}
);
})}
);
}
function FilenameLoadStatus({ activity }) {
const label = String(activity?.label || "").trim();
if (!activity?.loading || !label) {
return null;
}
const title = String(activity?.title || label).trim();
return (
{label}
);
}
function DiscordMark(props) {
return (
);
}
const topBarIconButtonClasses = "size-7";
const topBarIconClasses = "size-4";
const latestReleaseCacheKeyPrefix = "cad-viewer:latest-release:v1:";
const latestReleaseCacheTtlMs = 6 * 60 * 60 * 1000;
const updateVersionTooltipDelayMs = 250;
const passiveVersionTooltipDelayMs = 700;
const emptyLatestReleaseCheck = Object.freeze({
updateAvailable: false,
latestVersion: "",
releaseUrl: "",
installCommand: DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND,
latestReleaseNewer: false
});
function nextColorMode(currentColorMode) {
return currentColorMode === DARK_COLOR_SCHEME_ID
? LIGHT_COLOR_SCHEME_ID
: DARK_COLOR_SCHEME_ID;
}
function latestReleaseCacheKey(apiUrl) {
return `${latestReleaseCacheKeyPrefix}${apiUrl}`;
}
function readLatestReleaseCache(apiUrl, now = Date.now()) {
if (!apiUrl || typeof window === "undefined" || !window.localStorage) {
return null;
}
try {
const rawValue = window.localStorage.getItem(latestReleaseCacheKey(apiUrl));
const value = rawValue ? JSON.parse(rawValue) : null;
const expiresAt = Number(value?.expiresAt || 0);
const latestVersion = String(value?.latestVersion || "").trim();
const releaseUrl = String(value?.releaseUrl || "").trim();
const installCommand = String(value?.installCommand || "").trim();
if (!latestVersion || expiresAt <= now) {
return null;
}
return { latestVersion, releaseUrl, installCommand };
} catch {
return null;
}
}
function writeLatestReleaseCache(apiUrl, release, now = Date.now()) {
if (!apiUrl || typeof window === "undefined" || !window.localStorage) {
return;
}
const latestVersion = String(release?.latestVersion || "").trim();
if (!latestVersion) {
return;
}
try {
window.localStorage.setItem(latestReleaseCacheKey(apiUrl), JSON.stringify({
latestVersion,
releaseUrl: String(release?.releaseUrl || "").trim(),
installCommand: String(release?.installCommand || "").trim(),
expiresAt: now + latestReleaseCacheTtlMs
}));
} catch {
// Local storage availability is browser-policy dependent; the release check is optional.
}
}
function latestReleaseFromPayload(payload, fallbackReleaseUrl = "") {
const latestVersion = String(payload?.tag_name || "").trim();
if (!latestVersion) {
return null;
}
return {
latestVersion,
releaseUrl: String(payload?.html_url || fallbackReleaseUrl || "").trim(),
installCommand: viewerSkillsInstallCommandFromText(
payload?.body,
DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND
)
};
}
function latestReleaseCheckState(currentVersion, release) {
const latestVersion = String(release?.latestVersion || "").trim();
const releaseUrl = String(release?.releaseUrl || "").trim();
const installCommand = String(release?.installCommand || DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND).trim();
if (!latestVersion) {
return emptyLatestReleaseCheck;
}
const latestReleaseNewer = isViewerReleaseNewer(currentVersion, latestVersion);
return {
updateAvailable: isViewerReleaseMajorMinorNewer(currentVersion, latestVersion),
latestVersion,
releaseUrl,
installCommand,
latestReleaseNewer
};
}
function useViewerLatestReleaseCheck({
currentVersion,
latestReleaseApiUrl,
latestReleaseUrl,
mockLatestVersion = "",
mockLatestReleaseUrl = ""
}) {
const [releaseCheck, setReleaseCheck] = useState(emptyLatestReleaseCheck);
useEffect(() => {
const version = String(currentVersion || "").trim();
const apiUrl = String(latestReleaseApiUrl || "").trim();
const mockedVersion = String(mockLatestVersion || "").trim();
if (!version) {
setReleaseCheck(emptyLatestReleaseCheck);
return undefined;
}
if (mockedVersion) {
setReleaseCheck(latestReleaseCheckState(version, {
latestVersion: mockedVersion,
releaseUrl: String(mockLatestReleaseUrl || latestReleaseUrl || "").trim(),
installCommand: DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND
}));
return undefined;
}
if (!apiUrl || typeof fetch !== "function") {
setReleaseCheck(emptyLatestReleaseCheck);
return undefined;
}
const cachedRelease = readLatestReleaseCache(apiUrl);
if (cachedRelease) {
setReleaseCheck(latestReleaseCheckState(version, cachedRelease));
return undefined;
}
setReleaseCheck(emptyLatestReleaseCheck);
const controller = new AbortController();
fetch(apiUrl, {
signal: controller.signal,
headers: {
Accept: "application/vnd.github+json"
}
})
.then((response) => {
if (!response.ok) {
throw new Error(`GitHub latest release check failed with ${response.status}`);
}
return response.json();
})
.then((payload) => {
if (controller.signal.aborted) {
return;
}
const release = latestReleaseFromPayload(payload, latestReleaseUrl);
if (!release) {
if (!cachedRelease) {
setReleaseCheck(emptyLatestReleaseCheck);
}
return;
}
writeLatestReleaseCache(apiUrl, release);
setReleaseCheck(latestReleaseCheckState(version, release));
})
.catch((error) => {
if (error?.name === "AbortError") {
return;
}
if (!cachedRelease) {
setReleaseCheck(emptyLatestReleaseCheck);
}
});
return () => {
controller.abort();
};
}, [currentVersion, latestReleaseApiUrl, latestReleaseUrl, mockLatestReleaseUrl, mockLatestVersion]);
return releaseCheck;
}
function VersionTooltipRow({ label, version, action = null }) {
const normalizedVersion = String(version || "").trim();
if (!normalizedVersion) {
return null;
}
return (
{label}
{normalizedVersion}
{action}
);
}
function VersionReleaseLink({ version, releaseUrl, releaseCheck = emptyLatestReleaseCheck }) {
const normalizedVersion = String(version || "").trim();
const [installCopyStatus, setInstallCopyStatus] = useState("");
const copyGestureHandledRef = useRef(false);
if (!normalizedVersion) {
return null;
}
const updateAvailable = Boolean(releaseCheck?.updateAvailable);
const targetReleaseUrl = updateAvailable
? String(releaseCheck?.releaseUrl || releaseUrl || "").trim()
: String(releaseUrl || "").trim();
const latestVersion = String(releaseCheck?.latestVersion || "").trim();
const latestReleaseNewer = Boolean(releaseCheck?.latestReleaseNewer);
const latestVersionVisible = latestVersion && latestReleaseNewer;
const latestReleaseUrl = latestVersionVisible
? String(releaseCheck?.releaseUrl || "").trim()
: "";
const installCommand = String(
releaseCheck?.installCommand || DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND
).trim() || DEFAULT_VIEWER_SKILLS_INSTALL_COMMAND;
const upToDate = Boolean(latestVersion) && !latestReleaseNewer;
const label = updateAvailable
? "Update CAD Viewer"
: (targetReleaseUrl ? `Open release ${normalizedVersion}` : `Version ${normalizedVersion}`);
const handleCopyInstallCommand = async (event) => {
event.preventDefault();
event.stopPropagation();
if (copyGestureHandledRef.current) {
return;
}
copyGestureHandledRef.current = true;
globalThis.setTimeout(() => {
copyGestureHandledRef.current = false;
}, 250);
try {
await copyTextToClipboard(installCommand);
setInstallCopyStatus("copied");
globalThis.setTimeout(() => {
setInstallCopyStatus("");
}, 1600);
} catch {
setInstallCopyStatus("failed");
}
};
const releaseButton = (
);
return (
{releaseButton}
{latestVersionVisible ? (
) : (
)}
Update Command
{installCommand}
{installCopyStatus === "failed" ? (
Copy failed
) : null}
{upToDate ? (
You are up to date
) : null}
);
}
export default function CadWorkspaceTopBar({
previewMode,
sidebarLabelForEntry,
directoryTree = null,
selectedKey = "",
selectedEntry,
onSelectEntry,
entrySourceFormat,
entryHasMesh,
entryHasDxf,
entryHasGcode,
entryHasUrdf,
activeGenerationFiles = [],
activeStepArtifactGenerationFile = "",
stepArtifactGenerationAvailable = true,
themePresets = [],
themeSettings,
themePresetId = "",
resolvedColorSchemeMode = LIGHT_COLOR_SCHEME_ID,
onColorSchemePreferenceChange,
updateThemeSettings,
handleResetThemeSettings,
handleSaveCustomThemePreset,
handleUpdateThemePresetSettings,
handleDeleteCustomThemePreset,
handleEditThemePreset,
handleResetThemePresetToDefault,
handleRestoreDefaultThemePresets,
filenameLoadActivity = null,
selectedStepSourceStatus = null,
canRevealFileAssets = false,
canCopyFileAssetLinks = false,
canCopyFileAssetPaths = false,
fileAccessBusyKey = "",
onDownloadFileAsset,
onExportImplicitFile,
onRevealFileAsset,
onRevealInExplorerView,
onCopyFileAssetReference,
fileSheetKind = "",
fileSheetOpen = false,
onToggleFileSheet,
navigationAvailable = true
}) {
const discordUrl = normalizeViewerDiscordUrl(import.meta.env?.VIEWER_DISCORD_URL);
if (previewMode) {
return null;
}
const selectedFileLabel = selectedEntry && typeof sidebarLabelForEntry === "function"
? sidebarLabelForEntry(selectedEntry)
: "Select a file";
const selectedFileTitle = selectedEntry
? String(selectedEntry.file || selectedEntry.id || selectedFileLabel)
: selectedFileLabel;
const breadcrumbAvailable = navigationAvailable || Boolean(selectedEntry);
const breadcrumbNodes = buildBreadcrumbNodes({
directoryTree: navigationAvailable ? directoryTree : null,
selectedEntry,
selectedFileLabel,
selectedFileTitle
});
const breadcrumbItems = collapsedBreadcrumbNodes(breadcrumbNodes);
const mobileBreadcrumbNode = breadcrumbNodes[breadcrumbNodes.length - 1] || null;
const activeIconButtonClasses = "bg-accent text-accent-foreground";
const showFileSheetToggle = !!fileSheetKind && typeof onToggleFileSheet === "function";
const fileSheetToggleLabel = fileSheetOpen
? `Collapse ${fileSheetLabel(fileSheetKind)}`
: `Expand ${fileSheetLabel(fileSheetKind)}`;
const showThemeColorModeToggle = themeSettingsSupportsSystemColorMode(themeSettings);
const activeColorSchemeMode = resolvedColorSchemeMode === DARK_COLOR_SCHEME_ID
? DARK_COLOR_SCHEME_ID
: LIGHT_COLOR_SCHEME_ID;
const nextColorSchemeMode = nextColorMode(activeColorSchemeMode);
const activeColorSchemeModeLabel = activeColorSchemeMode === DARK_COLOR_SCHEME_ID ? "dark" : "light";
const nextColorSchemeModeLabel = nextColorSchemeMode === DARK_COLOR_SCHEME_ID ? "dark" : "light";
const colorModeToggleLabel = `Browser color mode: ${activeColorSchemeModeLabel}. Switch to ${nextColorSchemeModeLabel} mode.`;
const ColorModeIcon = activeColorSchemeMode === DARK_COLOR_SCHEME_ID ? Moon : Sun;
const handleThemeColorModeToggle = () => {
if (typeof onColorSchemePreferenceChange !== "function") {
return;
}
onColorSchemePreferenceChange(nextColorSchemeMode);
};
return (
{navigationAvailable ? (
) : null}
{breadcrumbAvailable ? (
{mobileBreadcrumbNode ? (
) : null}
{breadcrumbItems.map((item, index) => (
{item.type === "ellipsis" ? (
) : (
)}
{index < breadcrumbItems.length - 1 ? (
) : null}
))}
) : (
)}
{showThemeColorModeToggle ? (
) : null}
{showFileSheetToggle ? (
) : null}
);
}