${samplePath} and hitting "Go."`
+ });
+
+ // Set up navigation detection to auto-advance when user navigates
+ let navigationCheckInterval: NodeJS.Timeout | null = null;
+ const startPath = window.location.pathname;
+
+ const checkForNavigation = () => {
+ const currentPath = window.location.pathname;
+ // If user navigated away from the starting /browse page
+ if (currentPath !== startPath && currentPath.startsWith('/browse/')) {
+ if (navigationCheckInterval) {
+ clearInterval(navigationCheckInterval);
+ navigationCheckInterval = null;
+ }
+ // Auto-advance to next step
+ if (tour.getCurrentStep()?.id === 'nav-navigation-input') {
+ tour.next();
+ }
+ }
+ };
+
+ // Add event listener when step is shown
+ navInputStep.on('show', () => {
+ // Start checking for navigation every 500ms
+ navigationCheckInterval = setInterval(checkForNavigation, 500);
+ });
+
+ // Clean up interval when step is hidden
+ navInputStep.on('hide', () => {
+ if (navigationCheckInterval) {
+ clearInterval(navigationCheckInterval);
+ navigationCheckInterval = null;
+ }
+ });
+ }
+ };
+
+ // Helper to set up navigation sidebar step with conditional navigation
+ const setupNavigationSidebarStep = (tour: Tour) => {
+ const navSidebarStep = tour.getById('nav-sidebar');
+ if (navSidebarStep) {
+ navSidebarStep.updateStepOptions({
+ buttons: [
+ backButton,
+ {
+ text: 'Next',
+ action: async function (this: any) {
+ // Check if user has navigated from /browse
+ if (window.location.pathname === '/browse') {
+ // User hasn't navigated, force navigation to a file path
+ if (isJaneliaFilesystem) {
+ navigate('/browse/nrs_opendata');
+ } else {
+ const firstZone = Object.values(
+ zonesAndFspQuery.data || {}
+ ).find(item => 'fileSharePaths' in item) as Zone | undefined;
+ if (firstZone && firstZone.fileSharePaths.length > 0) {
+ const firstFsp = firstZone.fileSharePaths[0];
+ navigate(`/browse/${firstFsp.name}`);
+ }
+ }
+ await waitForElement('[data-tour="file-browser"]');
+ }
+ return this.next();
+ }
+ },
+ exitButton
+ ]
+ });
+ }
+ };
+
+ // Helper to set up conversion properties step with convert tab opened
+ const setupConversionStartStep = (tour: Tour) => {
+ const conversionStartStep = tour.getById('conversion-start');
+ if (conversionStartStep) {
+ conversionStartStep.updateStepOptions({
+ buttons: [
+ backButton,
+ {
+ text: 'Next',
+ action: async function (this: any) {
+ // Navigate with openConvertTab state to open properties with Convert tab selected
+ const currentPath = window.location.pathname;
+ navigate(currentPath, {
+ state: { openConvertTab: true },
+ replace: true
+ });
+ await waitForElement('[data-tour="open-conversion-request"]');
+ return this.next();
+ }
+ },
+ exitButton
+ ]
+ });
+ }
+ };
+
+ // Helper to set up conversion properties step with convert tab opened
+ const setupConversionPropertiesStep = (tour: Tour) => {
+ const conversionPropertiesStep = tour.getById('conversion-properties');
+ if (conversionPropertiesStep) {
+ conversionPropertiesStep.updateStepOptions({
+ buttons: [
+ backButton,
+ {
+ text: 'Next',
+ action: async function (this: any) {
+ navigate('/jobs');
+ await waitForElement('[data-tour="tasks-page"]');
+ return this.next();
+ }
+ },
+ exitButton
+ ]
+ });
+ }
+ };
+
+ // Helper to set up completion buttons for tour ending steps
+ const setupCompletionButtons = (tour: Tour) => {
+ const completionStepIds = [
+ 'nav-properties',
+ 'datalinks-janelia-preferences',
+ 'datalinks-general-preferences',
+ 'conversion-jobs'
+ ];
+
+ completionStepIds.forEach(stepId => {
+ const step = tour.getById(stepId);
+ if (step) {
+ const buttons = [
+ backButton,
+ {
+ text: 'Take Another Tour',
+ action: function (this: any) {
+ const currentTour = shepherd.activeTour as Tour;
+ // Re-setup workflow buttons to ensure they work when returning
+ setupWorkflowButtons(currentTour);
+ // Show the workflow selection step
+ currentTour.show('choose-workflow');
+ }
+ },
+ exitButton
+ ];
+
+ step.updateStepOptions({ buttons });
+ }
+ });
+ };
+
+ // Helper to set up workflow selection buttons
+ const setupWorkflowButtons = (tour: Tour) => {
+ const firstStep = tour.getById('choose-workflow');
+ if (!firstStep) {
+ return;
+ }
+
+ const workflowButtons: any[] = [
+ {
+ text: 'Navigation',
+ action: async function (this: any) {
+ const currentTour = shepherd.activeTour as Tour;
+ navigate('/browse');
+ await waitForElement('[data-tour="navigation-input"]');
+ setupNavigationInputStep(currentTour);
+ setupNavigationSidebarStep(currentTour);
+ setupCompletionButtons(currentTour);
+ currentTour.show('nav-navigation-input');
+ }
+ },
+ {
+ text: 'Data Links',
+ action: async function (this: any) {
+ const currentTour = shepherd.activeTour as Tour;
+ if (isJaneliaFilesystem) {
+ navigate(
+ '/browse/nrs_opendata/ome-zarr-examples/fused-timeseries.zarr'
+ );
+ await waitForElement('[data-tour="file-browser"]');
+ setupCompletionButtons(currentTour);
+ currentTour.show('datalinks-janelia-start');
+ } else {
+ // Navigate to first FSP of first zone
+ const firstZone = Object.values(zonesAndFspQuery.data || {}).find(
+ item => 'fileSharePaths' in item
+ ) as Zone | undefined;
+ if (firstZone && firstZone.fileSharePaths.length > 0) {
+ const firstFsp = firstZone.fileSharePaths[0];
+ navigate(`/browse/${firstFsp.name}`);
+ await waitForElement('[data-tour="file-browser"]');
+ setupCompletionButtons(currentTour);
+ currentTour.show('datalinks-general-start');
+ }
+ }
+ }
+ }
+ ];
+
+ // Only add File Conversion option if tasks are enabled
+ if (tasksEnabled) {
+ workflowButtons.push({
+ text: 'File Conversion',
+ action: async function (this: any) {
+ const currentTour = shepherd.activeTour as Tour;
+ if (isJaneliaFilesystem) {
+ navigate(
+ '/browse/nrs_opendata/ome-zarr-examples/fused-timeseries.zarr'
+ );
+ } else {
+ const firstZone = Object.values(zonesAndFspQuery.data || {}).find(
+ item => 'fileSharePaths' in item
+ ) as Zone | undefined;
+ if (firstZone && firstZone.fileSharePaths.length > 0) {
+ const firstFsp = firstZone.fileSharePaths[0];
+ navigate(`/browse/${firstFsp.name}`);
+ }
+ }
+ await waitForElement('[data-tour="file-browser"]');
+ setupConversionStartStep(currentTour);
+ setupConversionPropertiesStep(currentTour);
+ setupCompletionButtons(currentTour);
+ currentTour.show('conversion-start');
+ }
+ });
+ }
+
+ workflowButtons.push({
+ text: 'Exit',
+ action: function (this: any) {
+ const currentTour = shepherd.activeTour as Tour;
+ currentTour.cancel();
+ },
+ classes: 'shepherd-button-secondary'
+ });
+
+ firstStep.updateStepOptions({ buttons: workflowButtons });
+ };
+
+ const handleStartTour = () => {
+ // Get or create the tour instance
+ let tour = shepherd.activeTour as Tour | undefined;
+ if (!tour) {
+ tour = new shepherd.Tour({
+ useModalOverlay: true,
+ defaultStepOptions: {
+ classes: 'shepherd-theme-default',
+ scrollTo: true,
+ cancelIcon: {
+ enabled: true
+ },
+ modalOverlayOpeningPadding: 8,
+ modalOverlayOpeningRadius: 4
+ }
+ });
+ shepherd.activeTour = tour;
+ }
+
+ // Add steps if not already added
+ if (!tour.steps || tour.steps.length === 0) {
+ tour.addSteps(tourSteps);
+ }
+
+ // Set up workflow selection buttons (do this every time to ensure they work when returning)
+ setupWorkflowButtons(tour);
+
+ // Set up completion buttons with proper tour context
+ setupCompletionButtons(tour);
+
+ tour.start();
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/tours/shepherd-overrides.css b/frontend/src/components/tours/shepherd-overrides.css
new file mode 100644
index 00000000..cb724544
--- /dev/null
+++ b/frontend/src/components/tours/shepherd-overrides.css
@@ -0,0 +1,158 @@
+/* Custom Shepherd.js styling overrides to match Material Tailwind dialogs */
+/* This file is imported AFTER shepherd.css to ensure our styles take precedence */
+/* Using CSS custom properties from Tailwind theme instead of @apply */
+/* @apply was not included in the final build CSS when used here */
+
+/* Override default Shepherd theme */
+.shepherd-element {
+ max-width: 600px;
+ width: fit-content;
+}
+
+/* Style the tour content container like FgDialog */
+.shepherd-content {
+ padding: 1.5rem;
+ background-color: rgb(var(--color-surface-light));
+ box-shadow:
+ 0 20px 25px -5px rgb(0 0 0 / 0.1),
+ 0 8px 10px -6px rgb(0 0 0 / 0.1);
+ border: 1px solid rgb(var(--color-surface));
+ min-width: 400px;
+}
+
+/* Style the header */
+.shepherd-has-title .shepherd-content .shepherd-header {
+ background-color: rgb(var(--color-surface-light));
+ padding: 0.75rem 0;
+ border-bottom: 1px solid rgb(var(--color-surface-dark));
+ margin: 0.75rem;
+}
+
+/* Dark mode for content and header */
+.dark .shepherd-content,
+.dark .shepherd-has-title .shepherd-content .shepherd-header {
+ background-color: rgb(var(--color-surface));
+ border-color: rgb(var(--color-surface-light));
+}
+
+/* Style the title */
+.shepherd-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: rgb(var(--color-foreground));
+ margin: 0;
+}
+
+.dark .shepherd-title {
+ color: rgb(var(--color-foreground));
+}
+
+/* Style the text content */
+.shepherd-text {
+ color: rgb(var(--color-foreground));
+ font-size: 0.875rem;
+ line-height: 1.625;
+ margin-bottom: 1.25rem;
+}
+
+.dark .shepherd-text {
+ color: rgb(var(--color-foreground));
+}
+
+/* Style the button container to prevent overflow */
+.shepherd-footer {
+ padding: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+/* Style buttons to match Material Tailwind Button component */
+.shepherd-button {
+ padding: 0.5rem 1rem;
+ border-radius: calc(var(--radius) - 18px);
+ font-weight: 500;
+ font-size: 0.875rem;
+ transition: all 0.2s ease-in-out;
+ border: 0;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+/* Primary button (Next, Submit, Complete) */
+.shepherd-button:not(.shepherd-button-secondary) {
+ background-color: rgb(var(--color-primary));
+ color: rgb(var(--color-primary-foreground));
+}
+
+.shepherd-button:not(.shepherd-button-secondary):hover {
+ background-color: rgb(var(--color-primary-light));
+ color: rgb(var(--color-background));
+}
+
+/* Secondary button (Back, Exit) */
+.shepherd-button-secondary {
+ background-color: rgb(var(--color-surface));
+ color: rgb(var(--color-foreground));
+ border: 1px solid rgb(var(--color-surface-dark));
+}
+
+.shepherd-button-secondary:hover {
+ background-color: rgb(var(--color-surface-dark));
+ color: rgb(var(--color-surface-foreground));
+}
+
+.dark .shepherd-button-secondary {
+ background-color: rgb(var(--color-surface));
+ color: rgb(var(--color-foreground));
+ border-color: rgb(var(--color-surface-light));
+}
+
+.dark .shepherd-button-secondary:hover {
+ background-color: rgb(var(--color-surface-light));
+ color: rgb(var(--color-surface-foreground));
+}
+
+/* Style the cancel icon in header */
+.shepherd-has-title .shepherd-content .shepherd-cancel-icon {
+ color: rgb(var(--color-secondary));
+ font-size: 1.5rem;
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 9999px;
+ border: 1px solid rgb(var(--color-secondary));
+ transition: all 0.3s cubic-bezier(0.4, 0, 1, 1);
+ width: 2rem;
+}
+
+.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover {
+ background-color: rgb(var(--color-secondary));
+ color: rgb(var(--color-surface));
+}
+
+/* Style the arrow pointing to the highlighted element */
+.shepherd-element .shepherd-arrow {
+ content: url(../../assets/arrow-up.svg);
+ display: inline-block;
+ transform: scale(1.5);
+ z-index: 8888;
+}
+
+/* Rotate arrow based on tooltip placement */
+
+/* Left of the target placement: arrow points right (90deg) */
+.shepherd-element[data-popper-placement^='left'] .shepherd-arrow {
+ transform: scale(1.5) rotate(90deg);
+}
+
+/* Top of the target placement: arrow points down (180deg) */
+.shepherd-element[data-popper-placement^='top'] .shepherd-arrow {
+ transform: scale(1.5) rotate(180deg);
+}
+
+/* Right of the target placement: arrow points left (270deg) */
+.shepherd-element[data-popper-placement^='right'] .shepherd-arrow {
+ transform: scale(1.5) rotate(270deg);
+}
diff --git a/frontend/src/components/tours/tourSteps.ts b/frontend/src/components/tours/tourSteps.ts
new file mode 100644
index 00000000..68b5fd53
--- /dev/null
+++ b/frontend/src/components/tours/tourSteps.ts
@@ -0,0 +1,156 @@
+import type { StepOptions } from 'shepherd.js';
+
+// Common button configs
+const nextButton = {
+ text: 'Next',
+ action: function (this: any) {
+ return this.next();
+ }
+};
+
+// Exported for reuse in StartTour.tsx
+// Don't need to export next button as it's dynamically replaced in StartTour
+export const backButton = {
+ text: 'Back',
+ action: function (this: any) {
+ return this.back();
+ },
+ classes: 'shepherd-button-secondary'
+};
+
+export const exitButton = {
+ text: 'Exit Tour',
+ action: function (this: any) {
+ return this.cancel();
+ },
+ classes: 'shepherd-button-secondary'
+};
+
+// Note: This will be replaced dynamically in StartTour.tsx to have proper tour context
+const takeAnotherTourButton = {
+ text: 'Take Another Tour',
+ action: function (this: any) {
+ // This is a placeholder - will be replaced in setupCompletionButtons
+ console.warn('takeAnotherTourButton action not properly initialized');
+ }
+};
+
+export const tourSteps: StepOptions[] = [
+ {
+ id: 'choose-workflow',
+ title: 'Welcome to Fileglancer!',
+ text: 'Which workflow would you like to explore?',
+ buttons: [] // Will be dynamically replaced with branching buttons
+ },
+
+ // Navigation workflow steps
+ {
+ id: 'nav-navigation-input',
+ title: 'Navigation',
+ text: 'Use this navigation bar to quickly jump to any path in the file system. You can type or paste a path here.',
+ attachTo: { element: '[data-tour="navigation-input"]', on: 'bottom' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'nav-sidebar',
+ title: 'Sidebar',
+ text: 'The sidebar shows your zones, file share paths, and favorite folders. Click on any item to navigate to it.',
+ attachTo: { element: '[data-tour="sidebar"]', on: 'right' },
+ buttons: [] // Will be dynamically replaced with navigation logic
+ },
+ {
+ id: 'nav-file-browser',
+ title: 'File Browser',
+ text: 'Browse files and folders here. You can sort by column headers and select items to perform actions.',
+ attachTo: { element: '[data-tour="file-browser"]', on: 'top' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'nav-properties',
+ title: 'Properties Panel',
+ text: 'View file metadata and perform actions like changing file permissions, creating shareable data links, or requesting file conversions.',
+ attachTo: { element: '[data-tour="properties-drawer"]', on: 'left' },
+ buttons: [backButton, takeAnotherTourButton, exitButton]
+ },
+
+ // Data Links workflow - Janelia filesystem
+ {
+ id: 'datalinks-janelia-start',
+ title: 'Data Links',
+ text: "Navigate to a Zarr file to create a data link you can use to open files in external viewers like Neuroglancer. Let's go to an example Zarr dataset.",
+ attachTo: { element: '[data-tour="zarr-metadata"]', on: 'bottom' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-janelia-properties',
+ title: 'Creating Data Links',
+ text: 'You can create a data link from the properties panel using the data link toggle. Turn it on to create a shareable link.',
+ attachTo: { element: '[data-tour="properties-drawer"]', on: 'left' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-janelia-viewer',
+ title: 'Viewer Links',
+ text: 'Click on viewer icons in the metadata section to open the Zarr file in external viewers like Neuroglancer.',
+ attachTo: { element: '[data-tour="data-tool-links"]', on: 'bottom' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-janelia-preferences',
+ title: 'Automatic Data Links',
+ text: 'You can enable automatic data link creation on the Preferences page, accessible from the profile menu.',
+ attachTo: { element: '[data-tour="profile-menu"]', on: 'bottom' },
+ buttons: [backButton, takeAnotherTourButton, exitButton]
+ },
+
+ // Data Links workflow - Non-Janelia filesystem
+ {
+ id: 'datalinks-general-start',
+ title: 'Data Links',
+ text: 'You can create data links for any directory. Navigate to a folder you want to share.',
+ attachTo: { element: '[data-tour="file-browser"]', on: 'top' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-general-properties',
+ title: 'Creating Data Links',
+ text: 'Open the properties panel and toggle the data link option to create a shareable link for this directory.',
+ attachTo: { element: '[data-tour="properties-drawer"]', on: 'left' },
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-general-zarr',
+ title: 'Zarr/N5 Files',
+ text: "If a file is detected as Zarr or N5, you'll see viewer icons in the metadata displayed at the top of the file browser. You can click on a viewer icon to open the data link in external viewers like Neuroglancer.",
+ buttons: [backButton, nextButton, exitButton]
+ },
+ {
+ id: 'datalinks-general-preferences',
+ title: 'Automatic Data Links',
+ text: 'You can enable automatic data link creation on the Preferences page, accessible from the profile menu.',
+ attachTo: { element: '[data-tour="profile-menu"]', on: 'bottom' },
+ buttons: [backButton, takeAnotherTourButton, exitButton]
+ },
+
+ // File Conversion workflow
+ {
+ id: 'conversion-start',
+ title: 'File Conversion',
+ text: "Navigate to a file you want to convert. Let's go to an example file.",
+ buttons: [] // Will be dynamically replaced with navigation logic
+ },
+ {
+ id: 'conversion-properties',
+ title: 'Request Conversion',
+ text: 'Select a file and open the properties panel to the "Convert" tab. Click "Open conversion request" to submit a conversion task.',
+ attachTo: { element: '[data-tour="properties-drawer"]', on: 'left' },
+ buttons: [] // Will be dynamically replaced with navigation logic
+ },
+ {
+ id: 'conversion-jobs',
+ title: 'Monitor Tasks',
+ text: 'View the status of your conversion requests on the Tasks page.',
+ attachTo: { element: '[data-tour="tasks-page"]', on: 'bottom' },
+ buttons: [backButton, takeAnotherTourButton, exitButton]
+ }
+];
diff --git a/frontend/src/components/ui/BrowsePage/Dashboard/FgDashboardCard.tsx b/frontend/src/components/ui/BrowsePage/Dashboard/FgDashboardCard.tsx
index 4aacb9fd..dc0767c7 100644
--- a/frontend/src/components/ui/BrowsePage/Dashboard/FgDashboardCard.tsx
+++ b/frontend/src/components/ui/BrowsePage/Dashboard/FgDashboardCard.tsx
@@ -3,13 +3,17 @@ import { Card, Typography } from '@material-tailwind/react';
export default function DashboardCard({
title,
- children
+ children,
+ className = ''
}: {
readonly title: string;
readonly children: ReactNode;
+ readonly className?: string;
}) {
return (
-