From d2e6889a390e4bf000bb1e6f0ec72a3978b4bb48 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:29:00 -0500 Subject: [PATCH 01/12] feat: add reusable dialog button components --- .../components/ui/buttons/DialogIconBtn.tsx | 46 +++++++++++++++++++ .../components/ui/buttons/DialogTextBtn.tsx | 45 ++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 frontend/src/components/ui/buttons/DialogIconBtn.tsx create mode 100644 frontend/src/components/ui/buttons/DialogTextBtn.tsx diff --git a/frontend/src/components/ui/buttons/DialogIconBtn.tsx b/frontend/src/components/ui/buttons/DialogIconBtn.tsx new file mode 100644 index 00000000..2f81af6a --- /dev/null +++ b/frontend/src/components/ui/buttons/DialogIconBtn.tsx @@ -0,0 +1,46 @@ +import { useState, type ReactNode, type MouseEvent } from 'react'; +import type { IconType } from 'react-icons'; + +import FgTooltip from '@/components/ui/widgets/FgTooltip'; +import FgDialog from '@/components/ui/Dialogs/FgDialog'; + +type DialogIconBtnProps = { + readonly icon: IconType; + readonly label: string; + readonly triggerClasses: string; + readonly disabled?: boolean; + readonly children: ReactNode | ((closeDialog: () => void) => ReactNode); +}; + +export default function DialogIconBtn({ + icon, + label, + triggerClasses, + disabled = false, + children +}: DialogIconBtnProps) { + const [showDialog, setShowDialog] = useState(false); + + const closeDialog = () => setShowDialog(false); + + return ( + <> + ) => { + setShowDialog(true); + e.currentTarget.blur(); + }} + triggerClasses={triggerClasses} + /> + {showDialog ? ( + + {typeof children === 'function' ? children(closeDialog) : children} + + ) : null} + + ); +} diff --git a/frontend/src/components/ui/buttons/DialogTextBtn.tsx b/frontend/src/components/ui/buttons/DialogTextBtn.tsx new file mode 100644 index 00000000..29071a35 --- /dev/null +++ b/frontend/src/components/ui/buttons/DialogTextBtn.tsx @@ -0,0 +1,45 @@ +import { useState, type ReactNode, type MouseEvent } from 'react'; +import { Button } from '@material-tailwind/react'; + +import FgDialog from '@/components/ui/Dialogs/FgDialog'; + +type TextDialogBtnProps = { + readonly label: string; + readonly variant?: 'solid' | 'outline' | 'ghost' | 'gradient' | undefined; + readonly className?: string; + readonly disabled?: boolean; + readonly children: ReactNode | ((closeDialog: () => void) => ReactNode); +}; + +export default function TextDialogBtn({ + label, + variant = 'outline', + className = '!rounded-md w-fit', + disabled = false, + children +}: TextDialogBtnProps) { + const [showDialog, setShowDialog] = useState(false); + + const closeDialog = () => setShowDialog(false); + + return ( + <> + + {showDialog ? ( + + {typeof children === 'function' ? children(closeDialog) : children} + + ) : null} + + ); +} From 84dd4ee9818e227352bccced75899e074ae9fcf9 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:29:46 -0500 Subject: [PATCH 02/12] feat: add dialog with code snippets and instructions for data links --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx new file mode 100644 index 00000000..e01b3dd0 --- /dev/null +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -0,0 +1,194 @@ +import { Typography } from '@material-tailwind/react'; +import { HiOutlineClipboardCopy } from 'react-icons/hi'; + +import FgDialog from './FgDialog'; +import FgTooltip from '../widgets/FgTooltip'; +import useCopyTooltip from '@/hooks/useCopyTooltip'; +import { copy } from '@testing-library/user-event/dist/cjs/clipboard/copy.js'; + +type CodeSnippetItem = { + type: 'code'; + label: string; + code: string; + copyText: string; + copyLabel?: string; +}; + +type InstructionItem = { + type: 'instructions'; + label: string; + steps: string[]; + copyText: string; + copyLabel?: string; +}; + +type DialogItem = CodeSnippetItem | InstructionItem; + +type CodeSnippetBlockProps = { + readonly label: string; + readonly code: string; + readonly copyText: string; + readonly copyLabel?: string; +}; + +function CopyIconAndTooltip({ + copyLabel, + copyText +}: { + readonly copyLabel: string; + readonly copyText: string; +}) { + const { showCopiedTooltip, handleCopy } = useCopyTooltip(); + + return ( + await handleCopy(copyText)} + triggerClasses="text-foreground/50 hover:text-foreground" + variant="ghost" + > + {showCopiedTooltip ? ( +
+ {copyLabel === 'Copy data link' + ? 'Data link copied!' + : copyLabel === 'Copy code' + ? 'Code copied!' + : 'Copied!'} +
+ ) : null} +
+ ); +} + +function CodeSnippetBlock({ + label, + code, + copyText, + copyLabel = 'Copy' +}: CodeSnippetBlockProps) { + return ( +
+ {label} +
+
+          {code}
+        
+
+ +
+
+
+ ); +} + +type InstructionBlockProps = { + readonly label: string; + readonly steps: string[]; + readonly copyText: string; + readonly copyLabel?: string; +}; + +function InstructionBlock({ + label, + steps, + copyText, + copyLabel = 'Copy' +}: InstructionBlockProps) { + return ( +
+
+ + {label} + + +
+
+
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + {step} +
  2. + ))} +
+
+
+ ); +} + +type DataLinkUsageDialogProps = { + readonly dataLinkUrl: string; + readonly open: boolean; + readonly onClose: () => void; +}; + +export default function DataLinkUsageDialog({ + dataLinkUrl, + open, + onClose +}: DataLinkUsageDialogProps) { + const items: DialogItem[] = [ + { + type: 'instructions', + label: 'Napari', + steps: [ + 'Install napari-ome-zarr plugin', + 'Launch napari', + 'Open the data URL' + ], + copyText: dataLinkUrl, + copyLabel: 'Copy data link' + }, + { + type: 'code', + label: 'Python', + code: `import zarr +store = zarr.open("${dataLinkUrl}")`, + copyText: `import zarr +store = zarr.open("${dataLinkUrl}")`, + copyLabel: 'Copy code' + }, + { + type: 'code', + label: 'Java', + code: `String url = "${dataLinkUrl}";`, + copyText: `String url = "${dataLinkUrl}";`, + copyLabel: 'Copy code' + } + ]; + + return ( + +
+ + How to use your data link + + {items.map(item => { + if (item.type === 'code') { + return ( + + ); + } + return ( + + ); + })} +
+
+ ); +} From dde30209c540dde1bf97e7b8d52c0b6a1ee82057 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:31:34 -0500 Subject: [PATCH 03/12] refactor: convert navigation and folder btns to use DialogIconBtn --- .../ui/BrowsePage/NavigationButton.tsx | 41 ++---- .../ui/BrowsePage/NewFolderButton.tsx | 129 ++++++++---------- 2 files changed, 72 insertions(+), 98 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/NavigationButton.tsx b/frontend/src/components/ui/BrowsePage/NavigationButton.tsx index f2a56d58..85d9ad9d 100644 --- a/frontend/src/components/ui/BrowsePage/NavigationButton.tsx +++ b/frontend/src/components/ui/BrowsePage/NavigationButton.tsx @@ -1,10 +1,7 @@ -import { useState } from 'react'; -import type { MouseEvent } from 'react'; import { IoNavigateCircleSharp } from 'react-icons/io5'; -import FgTooltip from '@/components/ui/widgets/FgTooltip'; +import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; import NavigationInput from '@/components/ui/BrowsePage/NavigateInput'; -import FgDialog from '@/components/ui/Dialogs/FgDialog'; type NavigationButtonProps = { readonly triggerClasses: string; @@ -13,30 +10,18 @@ type NavigationButtonProps = { export default function NavigationButton({ triggerClasses }: NavigationButtonProps) { - const [showNavigationDialog, setShowNavigationDialog] = useState(false); - return ( - <> - ) => { - setShowNavigationDialog(true); - e.currentTarget.blur(); - }} - triggerClasses={triggerClasses} - /> - {showNavigationDialog ? ( - setShowNavigationDialog(false)} - open={showNavigationDialog} - > - - - ) : null} - + + {closeDialog => ( + + )} + ); } diff --git a/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx b/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx index e9984c86..0130fff8 100644 --- a/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx +++ b/frontend/src/components/ui/BrowsePage/NewFolderButton.tsx @@ -1,11 +1,9 @@ -import { useState } from 'react'; -import type { ChangeEvent, MouseEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { Button, Typography } from '@material-tailwind/react'; import { HiFolderAdd } from 'react-icons/hi'; import toast from 'react-hot-toast'; -import FgTooltip from '@/components/ui/widgets/FgTooltip'; -import FgDialog from '@/components/ui/Dialogs/FgDialog'; +import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn'; import { Spinner } from '@/components/ui/widgets/Loaders'; import useNewFolderDialog from '@/hooks/useNewFolderDialog'; import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; @@ -17,7 +15,6 @@ type NewFolderButtonProps = { export default function NewFolderButton({ triggerClasses }: NewFolderButtonProps) { - const [showNewFolderDialog, setShowNewFolderDialog] = useState(false); const { fspName, mutations } = useFileBrowserContext(); const { handleNewFolderSubmit, newName, setNewName, isDuplicateName } = useNewFolderDialog(); @@ -25,7 +22,10 @@ export default function NewFolderButton({ const isSubmitDisabled = !newName.trim() || isDuplicateName || mutations.createFolder.isPending; - const formSubmit = async (event: React.FormEvent) => { + const formSubmit = async ( + event: React.FormEvent, + closeDialog: () => void + ) => { event.preventDefault(); const result = await handleNewFolderSubmit(); if (result.success) { @@ -34,73 +34,62 @@ export default function NewFolderButton({ } else { toast.error(`Error creating folder: ${result.error}`); } - setShowNewFolderDialog(false); + closeDialog(); }; return ( - <> - ) => { - setShowNewFolderDialog(true); - e.currentTarget.blur(); - }} - triggerClasses={triggerClasses} - /> - {showNewFolderDialog ? ( - setShowNewFolderDialog(false)} - open={showNewFolderDialog} - > -
-
- - Create a New Folder + + {closeDialog => ( + formSubmit(e, closeDialog)}> +
+ + Create a New Folder + + ) => { + setNewName(event.target.value); + }} + placeholder="Folder name ..." + type="text" + value={newName} + /> +
+
+ + {!newName.trim() ? ( + + Please enter a folder name - ) => { - setNewName(event.target.value); - }} - placeholder="Folder name ..." - type="text" - value={newName} - /> -
-
- - {!newName.trim() ? ( - - Please enter a folder name - - ) : newName.trim() && isDuplicateName ? ( - - A file or folder with this name already exists - - ) : null} -
- - - ) : null} - + ) : newName.trim() && isDuplicateName ? ( + + A file or folder with this name already exists + + ) : null} +
+ + )} + ); } From ed5dc87f36035f079085c5e4a27d27269a7328d6 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:32:48 -0500 Subject: [PATCH 04/12] feat: add data link usage dialog across app --- .../components/ui/BrowsePage/ZarrPreview.tsx | 25 ++++++++-- .../ui/PropertiesDrawer/PropertiesDrawer.tsx | 48 +++++++++++++++---- .../src/components/ui/Table/linksColumns.tsx | 22 +++++++++ 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx index 403d31fc..4b68b978 100644 --- a/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx +++ b/frontend/src/components/ui/BrowsePage/ZarrPreview.tsx @@ -5,6 +5,8 @@ import type { UseQueryResult } from '@tanstack/react-query'; import zarrLogo from '@/assets/zarr.jpg'; import ZarrMetadataTable from '@/components/ui/BrowsePage/ZarrMetadataTable'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import DataToolLinks from './DataToolLinks'; import type { OpenWithToolUrls, @@ -87,11 +89,24 @@ export default function ZarrPreview({ {openWithToolUrls ? ( - + <> + + {openWithToolUrls.copy ? ( + + {closeDialog => ( + + )} + + ) : null} + ) : null} {showDataLinkDialog ? ( diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index 570d32db..bcd40e1d 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -17,6 +17,8 @@ import OverviewTable from '@/components/ui/PropertiesDrawer/OverviewTable'; import TicketDetails from '@/components/ui/PropertiesDrawer/TicketDetails'; import FgTooltip from '@/components/ui/widgets/FgTooltip'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; +import TextDialogBtn from '@/components/ui/buttons/DialogTextBtn'; import { getPreferredPathForDisplay } from '@/utils'; import { copyToClipboard } from '@/utils/copyText'; import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; @@ -276,15 +278,45 @@ export default function PropertiesDrawer({ {externalDataUrlQuery.data ? ( - + <> + + + {closeDialog => ( + + )} + + ) : proxiedPathByFspAndPathQuery.data?.url ? ( - + <> + + + {closeDialog => ( + + )} + + ) : null} ) : null} diff --git a/frontend/src/components/ui/Table/linksColumns.tsx b/frontend/src/components/ui/Table/linksColumns.tsx index c0ca1ab9..d9b7c7f9 100644 --- a/frontend/src/components/ui/Table/linksColumns.tsx +++ b/frontend/src/components/ui/Table/linksColumns.tsx @@ -4,6 +4,7 @@ import { Typography } from '@material-tailwind/react'; import type { ColumnDef } from '@tanstack/react-table'; import DataLinkDialog from '@/components/ui/Dialogs/DataLink'; +import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog'; import DataLinksActionsMenu from '@/components/ui/Menus/DataLinksActions'; import { usePreferencesContext } from '@/contexts/PreferencesContext'; import { useZoneAndFspMapContext } from '@/contexts/ZonesAndFspMapContext'; @@ -37,6 +38,7 @@ type ProxiedPathRowActionProps = { handleCopyPath: (path: string) => Promise; handleCopyUrl: (item: ProxiedPath) => Promise; handleUnshare: () => void; + handleViewDataLinkUsage: () => void; item: ProxiedPath; displayPath: string; pathFsp: FileSharePath | undefined; @@ -94,6 +96,8 @@ function PathCell({ function ActionsCell({ item }: { readonly item: ProxiedPath }) { const [showDataLinkDialog, setShowDataLinkDialog] = useState(false); + const [showDataLinkUsageDialog, setShowDataLinkUsageDialog] = + useState(false); const { handleDeleteDataLink } = useDataToolLinks(setShowDataLinkDialog); const { pathPreference } = usePreferencesContext(); const { zonesAndFspQuery } = useZoneAndFspMapContext(); @@ -114,6 +118,10 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { item.path ); + const handleViewDataLinkUsage = () => { + setShowDataLinkUsageDialog(true); + }; + const menuItems: MenuItem[] = [ { name: 'Copy path', @@ -127,6 +135,11 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { await props.handleCopyUrl(props.item); } }, + { + name: 'Example code snippets', + action: (props: ProxiedPathRowActionProps) => + props.handleViewDataLinkUsage() + }, { name: 'Unshare', action: (props: ProxiedPathRowActionProps) => props.handleUnshare(), @@ -138,6 +151,7 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { handleCopyPath, handleCopyUrl, handleUnshare, + handleViewDataLinkUsage, item, displayPath, pathFsp @@ -169,6 +183,14 @@ function ActionsCell({ item }: { readonly item: ProxiedPath }) { showDataLinkDialog={showDataLinkDialog} /> ) : null} + {/* Code snippets dialog */} + {showDataLinkUsageDialog ? ( + setShowDataLinkUsageDialog(false)} + open={showDataLinkUsageDialog} + /> + ) : null} ); } From 5b40748ab6b7e251de68d7f8c13375efbe372b67 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Tue, 9 Dec 2025 14:33:07 -0500 Subject: [PATCH 05/12] fix: increase tooltip z-index to prevent overlap with dialogs --- frontend/src/components/ui/widgets/FgTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ui/widgets/FgTooltip.tsx b/frontend/src/components/ui/widgets/FgTooltip.tsx index 55d73896..35e5bc26 100644 --- a/frontend/src/components/ui/widgets/FgTooltip.tsx +++ b/frontend/src/components/ui/widgets/FgTooltip.tsx @@ -42,7 +42,7 @@ export default function FgTooltip({ > {Icon ? : null} {children} - + {label} From e657a1a52832bd40b634b269e57eba4d496e5558 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 15:05:11 -0500 Subject: [PATCH 06/12] style: organize example languages/tools in tabs --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 204 ++++++++---------- 1 file changed, 91 insertions(+), 113 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index e01b3dd0..730c0ba4 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -1,28 +1,10 @@ -import { Typography } from '@material-tailwind/react'; +import { useState } from 'react'; +import { Typography, Tabs } from '@material-tailwind/react'; import { HiOutlineClipboardCopy } from 'react-icons/hi'; import FgDialog from './FgDialog'; import FgTooltip from '../widgets/FgTooltip'; import useCopyTooltip from '@/hooks/useCopyTooltip'; -import { copy } from '@testing-library/user-event/dist/cjs/clipboard/copy.js'; - -type CodeSnippetItem = { - type: 'code'; - label: string; - code: string; - copyText: string; - copyLabel?: string; -}; - -type InstructionItem = { - type: 'instructions'; - label: string; - steps: string[]; - copyText: string; - copyLabel?: string; -}; - -type DialogItem = CodeSnippetItem | InstructionItem; type CodeSnippetBlockProps = { readonly label: string; @@ -62,60 +44,38 @@ function CopyIconAndTooltip({ } function CodeSnippetBlock({ - label, code, copyText, copyLabel = 'Copy' -}: CodeSnippetBlockProps) { +}: Omit) { return ( -
- {label} -
-
-          {code}
-        
-
- -
+
+
+        {code}
+      
+
+
); } type InstructionBlockProps = { - readonly label: string; readonly steps: string[]; - readonly copyText: string; - readonly copyLabel?: string; }; -function InstructionBlock({ - label, - steps, - copyText, - copyLabel = 'Copy' -}: InstructionBlockProps) { +function InstructionBlock({ steps }: InstructionBlockProps) { return ( -
-
- - {label} - - -
-
-
    - {steps.map((step, index) => ( -
  1. - - {index + 1} - - {step} -
  2. - ))} -
-
-
+
    + {steps.map((step, index) => ( +
  1. + + {index + 1} + + {step} +
  2. + ))} +
); } @@ -130,64 +90,82 @@ export default function DataLinkUsageDialog({ open, onClose }: DataLinkUsageDialogProps) { - const items: DialogItem[] = [ - { - type: 'instructions', - label: 'Napari', - steps: [ - 'Install napari-ome-zarr plugin', - 'Launch napari', - 'Open the data URL' - ], - copyText: dataLinkUrl, - copyLabel: 'Copy data link' - }, - { - type: 'code', - label: 'Python', - code: `import zarr -store = zarr.open("${dataLinkUrl}")`, - copyText: `import zarr -store = zarr.open("${dataLinkUrl}")`, - copyLabel: 'Copy code' - }, - { - type: 'code', - label: 'Java', - code: `String url = "${dataLinkUrl}";`, - copyText: `String url = "${dataLinkUrl}";`, - copyLabel: 'Copy code' - } - ]; + const [activeTab, setActiveTab] = useState('napari'); return ( -
- - How to use your data link - - {items.map(item => { - if (item.type === 'code') { - return ( - - ); - } - return ( +
+
+ + How to use your data link + + +
+ + + + Napari + + + + Python + + + + Java + + + + + {/* Napari panel */} + + + + {/* Python panel */} + + + + + + {/* Java panel */} + + - ); - })} + +
); From 169bb0b8a28ec3a18537716d4472e5d94c28350d Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 15:59:00 -0500 Subject: [PATCH 07/12] refactor: extract dark mode logic for code blocks --- .../components/ui/BrowsePage/FileViewer.tsx | 22 ++------------- frontend/src/hooks/useDarkMode.ts | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 frontend/src/hooks/useDarkMode.ts diff --git a/frontend/src/components/ui/BrowsePage/FileViewer.tsx b/frontend/src/components/ui/BrowsePage/FileViewer.tsx index a4c16ebf..8feaec50 100644 --- a/frontend/src/components/ui/BrowsePage/FileViewer.tsx +++ b/frontend/src/components/ui/BrowsePage/FileViewer.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import { Typography } from '@material-tailwind/react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { @@ -10,6 +9,7 @@ import { useFileBrowserContext } from '@/contexts/FileBrowserContext'; import { formatFileSize, formatUnixTimestamp } from '@/utils'; import type { FileOrFolder } from '@/shared.types'; import { useFileContentQuery } from '@/queries/fileContentQueries'; +import useDarkMode from '@/hooks/useDarkMode'; type FileViewerProps = { readonly file: FileOrFolder; @@ -76,27 +76,9 @@ const getLanguageFromExtension = (filename: string): string => { export default function FileViewer({ file }: FileViewerProps) { const { fspName } = useFileBrowserContext(); - - const [isDarkMode, setIsDarkMode] = useState(false); - + const isDarkMode = useDarkMode(); const contentQuery = useFileContentQuery(fspName, file.path); - // Detect dark mode from document - useEffect(() => { - const checkDarkMode = () => { - setIsDarkMode(document.documentElement.classList.contains('dark')); - }; - - checkDarkMode(); - const observer = new MutationObserver(checkDarkMode); - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['class'] - }); - - return () => observer.disconnect(); - }, []); - const renderViewer = () => { if (contentQuery.isLoading) { return ( diff --git a/frontend/src/hooks/useDarkMode.ts b/frontend/src/hooks/useDarkMode.ts new file mode 100644 index 00000000..83187a9e --- /dev/null +++ b/frontend/src/hooks/useDarkMode.ts @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react'; + +/** + * Hook to detect dark mode from the document element's class list. + * Observes changes to the document element's class attribute. + * @returns boolean indicating if dark mode is active + */ +export default function useDarkMode(): boolean { + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + const checkDarkMode = () => { + setIsDarkMode(document.documentElement.classList.contains('dark')); + }; + + checkDarkMode(); + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, []); + + return isDarkMode; +} From 2b5eaf619796b882d3c3937a2df614c793e60c02 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 16:00:05 -0500 Subject: [PATCH 08/12] feat: add SyntaxHighlighter to data link usage dialog --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 113 ++++++++++++++---- 1 file changed, 90 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 730c0ba4..bf15205e 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -1,17 +1,16 @@ import { useState } from 'react'; import { Typography, Tabs } from '@material-tailwind/react'; import { HiOutlineClipboardCopy } from 'react-icons/hi'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { + materialDark, + coy +} from 'react-syntax-highlighter/dist/esm/styles/prism'; import FgDialog from './FgDialog'; import FgTooltip from '../widgets/FgTooltip'; import useCopyTooltip from '@/hooks/useCopyTooltip'; - -type CodeSnippetBlockProps = { - readonly label: string; - readonly code: string; - readonly copyText: string; - readonly copyLabel?: string; -}; +import useDarkMode from '@/hooks/useDarkMode'; function CopyIconAndTooltip({ copyLabel, @@ -43,20 +42,87 @@ function CopyIconAndTooltip({ ); } -function CodeSnippetBlock({ +type CodeBlockProps = { + readonly code: string; + readonly language?: string; + readonly showLineNumbers?: boolean; + readonly wrapLines?: boolean; + readonly wrapLongLines?: boolean; + readonly copyable?: boolean; + readonly copyLabel?: string; + readonly customStyle?: React.CSSProperties; +}; + +function CodeBlock({ code, - copyText, - copyLabel = 'Copy' -}: Omit) { + language = 'text', + showLineNumbers = false, + wrapLines = true, + wrapLongLines = true, + copyable = false, + copyLabel = 'Copy code', + customStyle = { + margin: 0, + marginTop: 0, + marginRight: 0, + marginBottom: 0, + marginLeft: 0, + paddingTop: '1em', + paddingRight: '1em', + paddingBottom: '0', + paddingLeft: '1em', + fontSize: '14px', + lineHeight: '1.5' + } +}: CodeBlockProps) { + const isDarkMode = useDarkMode(); + const { showCopiedTooltip, handleCopy } = useCopyTooltip(); + + // Get the theme's code styles and merge with custom codeTagProps + const theme = isDarkMode ? materialDark : coy; + const themeCodeStyles = theme['code[class*="language-"]'] || {}; + const mergedCodeTagProps = { + style: { + ...themeCodeStyles, + paddingBottom: '1em' + } + }; + return ( -
-
+    <>
+      
         {code}
-      
-
- -
-
+ + {copyable ? ( +
+ await handleCopy(code)} + triggerClasses="text-foreground/50 hover:text-foreground" + variant="ghost" + > + {showCopiedTooltip ? ( +
+ {copyLabel === 'Copy data link' + ? 'Data link copied!' + : copyLabel === 'Copy code' + ? 'Code copied!' + : 'Copied!'} +
+ ) : null} +
+
+ ) : null} + ); } @@ -145,12 +211,12 @@ export default function DataLinkUsageDialog({ value="python" > - @@ -159,10 +225,11 @@ store = zarr.open("${dataLinkUrl}")`} className="flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface" value="java" > - From 207d1d636cbaed0e8e005c201376fe4f1b5a81be Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 16:00:23 -0500 Subject: [PATCH 09/12] style: add padding to FileViewer so scroll bar doesn't cover last line of code --- .../src/components/ui/BrowsePage/FileViewer.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ui/BrowsePage/FileViewer.tsx b/frontend/src/components/ui/BrowsePage/FileViewer.tsx index 8feaec50..8c94a4e9 100644 --- a/frontend/src/components/ui/BrowsePage/FileViewer.tsx +++ b/frontend/src/components/ui/BrowsePage/FileViewer.tsx @@ -103,11 +103,25 @@ export default function FileViewer({ file }: FileViewerProps) { const language = getLanguageFromExtension(file.name); const content = contentQuery.data ?? ''; + // Get the theme's code styles and merge with padding bottom for scrollbar + const theme = isDarkMode ? materialDark : coy; + const themeCodeStyles = theme['code[class*="language-"]'] || {}; + const mergedCodeTagProps = { + style: { + ...themeCodeStyles, + paddingBottom: '1em' + } + }; + return ( Date: Fri, 12 Dec 2025 16:06:06 -0500 Subject: [PATCH 10/12] chore: change text for data link usage btn in properties panel --- .../src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx index bcd40e1d..c139ac9d 100644 --- a/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx +++ b/frontend/src/components/ui/PropertiesDrawer/PropertiesDrawer.tsx @@ -303,7 +303,7 @@ export default function PropertiesDrawer({ path={proxiedPathByFspAndPathQuery.data.url} /> {closeDialog => ( From 0cfb5ef7fe08e0a1968ac2417aa6ba295ca5e105 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Fri, 12 Dec 2025 17:10:25 -0500 Subject: [PATCH 11/12] refactor: map over data to remove duplicate styling on tabs - also fixes location of the copy icon for the code blocks --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 119 ++++++++++-------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index bf15205e..3a49fb0e 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -67,7 +67,7 @@ function CodeBlock({ marginRight: 0, marginBottom: 0, marginLeft: 0, - paddingTop: '1em', + paddingTop: '2em', paddingRight: '1em', paddingBottom: '0', paddingLeft: '1em', @@ -89,7 +89,7 @@ function CodeBlock({ }; return ( - <> +
) : null} - +
); } @@ -158,6 +158,53 @@ export default function DataLinkUsageDialog({ }: DataLinkUsageDialogProps) { const [activeTab, setActiveTab] = useState('napari'); + const tabs = [ + { + id: 'napari', + label: 'Napari', + content: ( + + ) + }, + { + id: 'python', + label: 'Python', + content: ( + <> + + + + ) + }, + { + id: 'java', + label: 'Java', + content: ( + + ) + } + ]; + + const TRIGGER_CLASSES = '!text-foreground h-full'; + const PANEL_CLASSES = + 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light'; + return (
@@ -177,61 +224,23 @@ export default function DataLinkUsageDialog({ value={activeTab} > - - Napari - - - - Python - - - - Java - + {tabs.map(tab => ( + + {tab.label} + + ))} - {/* Napari panel */} - - - - - {/* Python panel */} - - - - - - {/* Java panel */} - - - + {tabs.map(tab => ( + + {tab.content} + + ))}
From 033d82e04cb4b7f5da8b30a44b316e6729b12546 Mon Sep 17 00:00:00 2001 From: allison-truhlar Date: Mon, 5 Jan 2026 16:01:19 -0500 Subject: [PATCH 12/12] fix: show 'copied' msg for data link copy tooltip - in the DataLinkUsageDialog, the copied message was not showing due to a CSS issue. However, in addressing this, I realized that this component was recreating logic from the CopyTooltip component and reworked it to use that component --- .../ui/Dialogs/DataLinkUsageDialog.tsx | 76 +++++-------------- 1 file changed, 20 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx index 3a49fb0e..5ef23393 100644 --- a/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx +++ b/frontend/src/components/ui/Dialogs/DataLinkUsageDialog.tsx @@ -8,39 +8,8 @@ import { } from 'react-syntax-highlighter/dist/esm/styles/prism'; import FgDialog from './FgDialog'; -import FgTooltip from '../widgets/FgTooltip'; -import useCopyTooltip from '@/hooks/useCopyTooltip'; import useDarkMode from '@/hooks/useDarkMode'; - -function CopyIconAndTooltip({ - copyLabel, - copyText -}: { - readonly copyLabel: string; - readonly copyText: string; -}) { - const { showCopiedTooltip, handleCopy } = useCopyTooltip(); - - return ( - await handleCopy(copyText)} - triggerClasses="text-foreground/50 hover:text-foreground" - variant="ghost" - > - {showCopiedTooltip ? ( -
- {copyLabel === 'Copy data link' - ? 'Data link copied!' - : copyLabel === 'Copy code' - ? 'Code copied!' - : 'Copied!'} -
- ) : null} -
- ); -} +import CopyTooltip from '@/components/ui/widgets/CopyTooltip'; type CodeBlockProps = { readonly code: string; @@ -53,6 +22,9 @@ type CodeBlockProps = { readonly customStyle?: React.CSSProperties; }; +const TOOLTIP_TRIGGER_CLASSES = + 'text-foreground/50 hover:text-foreground py-1 px-2'; + function CodeBlock({ code, language = 'text', @@ -67,7 +39,7 @@ function CodeBlock({ marginRight: 0, marginBottom: 0, marginLeft: 0, - paddingTop: '2em', + paddingTop: '3em', paddingRight: '1em', paddingBottom: '0', paddingLeft: '1em', @@ -76,7 +48,6 @@ function CodeBlock({ } }: CodeBlockProps) { const isDarkMode = useDarkMode(); - const { showCopiedTooltip, handleCopy } = useCopyTooltip(); // Get the theme's code styles and merge with custom codeTagProps const theme = isDarkMode ? materialDark : coy; @@ -103,23 +74,13 @@ function CodeBlock({ {copyable ? (
- await handleCopy(code)} - triggerClasses="text-foreground/50 hover:text-foreground" - variant="ghost" + - {showCopiedTooltip ? ( -
- {copyLabel === 'Copy data link' - ? 'Data link copied!' - : copyLabel === 'Copy code' - ? 'Code copied!' - : 'Copied!'} -
- ) : null} -
+ +
) : null}
@@ -201,7 +162,7 @@ export default function DataLinkUsageDialog({ } ]; - const TRIGGER_CLASSES = '!text-foreground h-full'; + const TAB_TRIGGER_CLASSES = '!text-foreground h-full'; const PANEL_CLASSES = 'flex-1 flex flex-col gap-4 max-w-full p-4 rounded-b-lg border border-t-0 border-surface bg-surface-light'; @@ -212,10 +173,13 @@ export default function DataLinkUsageDialog({ How to use your data link - + + +
{tabs.map(tab => (