Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d2e6889
feat: add reusable dialog button components
allison-truhlar Dec 9, 2025
84dd4ee
feat: add dialog with code snippets and instructions for data links
allison-truhlar Dec 9, 2025
dde3020
refactor: convert navigation and folder btns to use DialogIconBtn
allison-truhlar Dec 9, 2025
ed5dc87
feat: add data link usage dialog across app
allison-truhlar Dec 9, 2025
5b40748
fix: increase tooltip z-index to prevent overlap with dialogs
allison-truhlar Dec 9, 2025
e657a1a
style: organize example languages/tools in tabs
allison-truhlar Dec 12, 2025
169bb0b
refactor: extract dark mode logic for code blocks
allison-truhlar Dec 12, 2025
2b5eaf6
feat: add SyntaxHighlighter to data link usage dialog
allison-truhlar Dec 12, 2025
207d1d6
style: add padding to FileViewer so scroll bar doesn't cover last lin…
allison-truhlar Dec 12, 2025
93c31cc
chore: change text for data link usage btn in properties panel
allison-truhlar Dec 12, 2025
2507b3a
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Dec 12, 2025
0cfb5ef
refactor: map over data to remove duplicate styling on tabs
allison-truhlar Dec 12, 2025
38da78c
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Jan 5, 2026
033d82e
fix: show 'copied' msg for data link copy tooltip
allison-truhlar Jan 5, 2026
f5ad35d
feat: add fiji instructions to DataLinkUsageDialog
allison-truhlar Jan 5, 2026
cdf17aa
feat: add VVDViewer instructions to DataLinkUsageDialog
allison-truhlar Jan 5, 2026
c0448bd
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Jan 21, 2026
d7442f5
feat: add python sample code
allison-truhlar Jan 21, 2026
f9c26d3
Merge branch 'add-fiji-and-vvd-instructions' into data-link-code-snip…
allison-truhlar Jan 22, 2026
9209142
style: limit height of data link usage dialog
allison-truhlar Jan 22, 2026
1e4b96b
refactor: remove java code block until we have a working example
allison-truhlar Jan 22, 2026
9e0bbb5
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Jan 27, 2026
c2681d6
wip: initial Java example
allison-truhlar Jan 27, 2026
bfcd8ac
fix: put quotes around strings in code snippets
allison-truhlar Jan 27, 2026
2ca6286
fix: move scrollbars to inside tab panel instead of on overall dialog
allison-truhlar Jan 27, 2026
978bb55
feat: add link out to data link docs under "create data link" toggle …
allison-truhlar Feb 12, 2026
12bb6ca
feat: make DataLinkUsageDialog context-aware with dataType prop
allison-truhlar Feb 13, 2026
4076fc3
feat: pass dataType="zarr" to DataLinkUsageDialog in ZarrPreview
allison-truhlar Feb 13, 2026
b355075
feat: pass dataType to DataLinkUsageDialog in PropertiesDrawer
allison-truhlar Feb 13, 2026
985016a
feat: add DataLinkUsageDialog to N5Preview with dataType="n5"
allison-truhlar Feb 13, 2026
268476c
fix: add missing dataType prop to DataLinkUsageDialog in linksColumns
allison-truhlar Feb 13, 2026
fa74db6
refactor: alphabetize tabs, add napari instructions, and fix code blo…
allison-truhlar Feb 13, 2026
a4d81d2
style: widen dialog, increase step spacing, and expand tab max height
allison-truhlar Feb 13, 2026
66f4e8e
style: redesign data tool links and "more way to open" to circular icons
allison-truhlar Feb 17, 2026
17867bf
style: make copy data url more obvious in data link usage dialog
allison-truhlar Feb 17, 2026
bc2335d
fix: improve data mode and code block styles in DataLinkUsageDialog
allison-truhlar Feb 17, 2026
c310fe1
Merge branch 'main' into data-link-code-snippets-dialog
allison-truhlar Feb 17, 2026
49821ff
fix: add button role back to data tool link copy icon
allison-truhlar Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 139 additions & 109 deletions frontend/src/components/ui/BrowsePage/DataToolLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,155 +1,185 @@
import { Button, ButtonGroup, Typography } from '@material-tailwind/react';
import { Typography } from '@material-tailwind/react';
import { Link } from 'react-router';
import { HiOutlineClipboardCopy } from 'react-icons/hi';
import { HiOutlineEllipsisHorizontalCircle } from 'react-icons/hi2';

import neuroglancer_logo from '@/assets/neuroglancer.png';
import validator_logo from '@/assets/ome-ngff-validator.png';
import volE_logo from '@/assets/aics_website-3d-cell-viewer.png';
import avivator_logo from '@/assets/vizarr_logo.png';
import copy_logo from '@/assets/copy-link-64.png';
import type { OpenWithToolUrls, PendingToolKey } from '@/hooks/useZarrMetadata';
import FgTooltip from '@/components/ui/widgets/FgTooltip';
import DialogIconBtn from '@/components/ui/buttons/DialogIconBtn';
import DataLinkUsageDialog from '@/components/ui/Dialogs/DataLinkUsageDialog';
import type { DataLinkType } from '@/components/ui/Dialogs/DataLinkUsageDialog';

const CIRCLE_CLASSES =
'rounded-full bg-surface-light dark:bg-surface-light hover:bg-surface dark:hover:bg-surface w-12 h-12 flex items-center justify-center cursor-pointer transform active:scale-90 transition-all duration-75';

const LABEL_CLASSES = 'text-xs text-center text-foreground mt-1';

export default function DataToolLinks({
compact = false,
dataLinkUrl,
dataType,
onToolClick,
showCopiedTooltip,
title,
urls
}: {
readonly compact?: boolean;
readonly dataLinkUrl?: string;
readonly dataType?: DataLinkType;
readonly onToolClick: (toolKey: PendingToolKey) => Promise<void>;
readonly showCopiedTooltip: boolean;
readonly title: string;
readonly urls: OpenWithToolUrls | null;
}) {
const tooltipTriggerClasses =
'rounded-sm m-0 p-0 transform active:scale-90 transition-transform duration-75';

if (!urls) {
return null;
}

return (
<div className="my-1" data-tour="data-tool-links">
<Typography className="font-semibold text-sm text-surface-foreground">
<Typography className="font-semibold text-sm text-surface-foreground mb-1">
{title}
</Typography>
<ButtonGroup className="relative">
<div
className={`flex flex-wrap gap-2 items-start ${compact ? 'max-w-[13.5rem]' : ''}`}
>
{urls.neuroglancer !== null ? (
<FgTooltip
as={Button}
label="View in Neuroglancer"
triggerClasses={tooltipTriggerClasses}
variant="ghost"
>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('neuroglancer');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.neuroglancer}
<div className="flex flex-col items-center w-16">
<FgTooltip
label="View in Neuroglancer"
triggerClasses={CIRCLE_CLASSES}
>
<img
alt="Neuroglancer logo"
className="max-h-8 max-w-8 m-1 rounded-sm"
src={neuroglancer_logo}
/>
</Link>
</FgTooltip>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('neuroglancer');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.neuroglancer}
>
<img
alt="Neuroglancer logo"
className="max-h-7 max-w-7 rounded-sm"
src={neuroglancer_logo}
/>
</Link>
</FgTooltip>
<span className={LABEL_CLASSES}>Neuroglancer</span>
</div>
) : null}

{urls.vole !== null ? (
<FgTooltip
as={Button}
label="View in Vol-E"
triggerClasses={tooltipTriggerClasses}
variant="ghost"
>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('vole');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.vole}
>
<img
alt="Vol-E logo"
className="max-h-8 max-w-8 m-1 rounded-sm"
src={volE_logo}
/>
</Link>
</FgTooltip>
<div className="flex flex-col items-center w-16">
<FgTooltip label="View in Vol-E" triggerClasses={CIRCLE_CLASSES}>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('vole');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.vole}
>
<img
alt="Vol-E logo"
className="max-h-7 max-w-7 rounded-sm"
src={volE_logo}
/>
</Link>
</FgTooltip>
<span className={LABEL_CLASSES}>Vol-E</span>
</div>
) : null}

{urls.avivator !== null ? (
<FgTooltip
as={Button}
label="View in Avivator"
triggerClasses={tooltipTriggerClasses}
variant="ghost"
>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('avivator');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.avivator}
>
<img
alt="Avivator logo"
className="max-h-8 max-w-8 m-1 rounded-sm"
src={avivator_logo}
/>
</Link>
</FgTooltip>
<div className="flex flex-col items-center w-16">
<FgTooltip label="View in Avivator" triggerClasses={CIRCLE_CLASSES}>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('avivator');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.avivator}
>
<img
alt="Avivator logo"
className="max-h-7 max-w-7 rounded-sm"
src={avivator_logo}
/>
</Link>
</FgTooltip>
<span className={LABEL_CLASSES}>Avivator</span>
</div>
) : null}

{urls.validator !== null ? (
<div className="flex flex-col items-center w-16">
<FgTooltip
label="View in OME-Zarr Validator"
triggerClasses={CIRCLE_CLASSES}
>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('validator');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.validator}
>
<img
alt="OME-Zarr Validator logo"
className="max-h-7 max-w-7 rounded-sm"
src={validator_logo}
/>
</Link>
</FgTooltip>
<span className={LABEL_CLASSES}>Validator</span>
</div>
) : null}

<div className="flex flex-col items-center w-16">
<FgTooltip
as={Button}
label="View in OME-Zarr Validator"
triggerClasses={tooltipTriggerClasses}
variant="ghost"
as="button"
label={showCopiedTooltip ? 'Copied!' : 'Copy data URL'}
onClick={async () => {
await onToolClick('copy');
}}
openCondition={showCopiedTooltip ? true : undefined}
triggerClasses={CIRCLE_CLASSES}
>
<Link
onClick={async e => {
e.preventDefault();
await onToolClick('validator');
}}
rel="noopener noreferrer"
target="_blank"
to={urls.validator}
>
<img
alt="OME-Zarr Validator logo"
className="max-h-8 max-w-8 m-1 rounded-sm"
src={validator_logo}
/>
</Link>
<HiOutlineClipboardCopy className="icon-default text-foreground" />
</FgTooltip>
) : null}
<span className={LABEL_CLASSES}>Copy</span>
</div>

<FgTooltip
as={Button}
label={showCopiedTooltip ? 'Copied!' : 'Copy data URL'}
onClick={async () => {
await onToolClick('copy');
}}
openCondition={showCopiedTooltip ? true : undefined}
triggerClasses={tooltipTriggerClasses}
variant="ghost"
>
<img
alt="Copy URL icon"
className="max-h-8 max-w-8 m-1 rounded-sm"
src={copy_logo}
/>
</FgTooltip>
</ButtonGroup>
{dataLinkUrl && dataType ? (
<div className="flex flex-col items-center w-16">
<DialogIconBtn
icon={HiOutlineEllipsisHorizontalCircle}
label="More ways to open"
triggerClasses={CIRCLE_CLASSES}
>
{closeDialog => (
<DataLinkUsageDialog
dataLinkUrl={dataLinkUrl}
dataType={dataType}
onClose={closeDialog}
open={true}
/>
)}
</DialogIconBtn>
<span className={LABEL_CLASSES}>More...</span>
</div>
) : null}
</div>
</div>
);
}
27 changes: 7 additions & 20 deletions frontend/src/components/ui/BrowsePage/FileViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { Switch, Typography } from '@material-tailwind/react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
Expand All @@ -13,6 +13,7 @@ import {
useFileContentQuery,
useFileMetadataQuery
} from '@/queries/fileContentQueries';
import useDarkMode from '@/hooks/useDarkMode';

type FileViewerProps = {
readonly file: FileOrFolder;
Expand Down Expand Up @@ -79,8 +80,7 @@ const getLanguageFromExtension = (filename: string): string => {

export default function FileViewer({ file }: FileViewerProps) {
const { fspName } = useFileBrowserContext();

const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
const isDarkMode = useDarkMode();
const [formatJson, setFormatJson] = useState<boolean>(true);

// First, fetch metadata to check if file is binary
Expand All @@ -97,22 +97,6 @@ export default function FileViewer({ file }: FileViewerProps) {
const language = getLanguageFromExtension(file.name);
const isJsonFile = language === 'json';

// 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 (metadataQuery.isLoading) {
return (
Expand Down Expand Up @@ -184,7 +168,10 @@ export default function FileViewer({ file }: FileViewerProps) {
codeTagProps={mergedCodeTagProps}
customStyle={{
margin: 0,
padding: '1rem',
paddingTop: '1em',
paddingRight: '1em',
paddingBottom: '0',
paddingLeft: '1em',
fontSize: '14px',
lineHeight: '1.5',
overflow: 'visible',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/ui/BrowsePage/N5Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export default function N5Preview({

{openWithToolUrls ? (
<DataToolLinks
compact={mainPanelWidth <= 1000}
dataLinkUrl={openWithToolUrls.copy || undefined}
dataType="n5"
onToolClick={handleToolClick}
showCopiedTooltip={showCopiedTooltip}
title="Open with:"
Expand Down
Loading