From 25a7982b60b20941fa2845b6f6fa0ee09f5a07e4 Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 21 Jun 2026 15:17:06 +0200 Subject: [PATCH 1/7] mv catalog to zarr fetch store in modal --- src/components/ui/MainPanel/DataSetsModal.tsx | 58 +++++++++++-------- src/components/ui/MainPanel/RemoteZarr.tsx | 39 +++++++------ 2 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/components/ui/MainPanel/DataSetsModal.tsx b/src/components/ui/MainPanel/DataSetsModal.tsx index 4a70513b..b25d3897 100644 --- a/src/components/ui/MainPanel/DataSetsModal.tsx +++ b/src/components/ui/MainPanel/DataSetsModal.tsx @@ -7,9 +7,7 @@ import { DialogContent, DialogTitle, } from "@/components/ui/dialog"; -import { - ButtonGroup, -} from "@/components/ui/button-group"; +import { ButtonGroup } from "@/components/ui/button-group"; import { Button } from "@/components/ui/button-enhanced"; import { DescriptionContent } from './DescriptionContent'; import CuratedDatasets from './CuratedDatasets'; @@ -17,13 +15,12 @@ import RemoteZarr from './RemoteZarr'; import LocalContent from './LocalContent'; import RemoteIcechunk from './RemoteIcechunk'; -type Tab = 'curated' | 'remote' | 'local' | 'icechunk'; +type Tab = 'remote' | 'local' | 'icechunk'; const TABS: { value: Tab; label: string }[] = [ - { value: 'curated', label: 'Curated' }, - { value: 'remote', label: 'Remote Zarr' }, - { value: 'icechunk', label: 'Icechunk' }, - { value: 'local', label: 'Local' }, + { value: 'remote', label: 'Remote Zarr' }, + { value: 'icechunk', label: 'Icechunk' }, + { value: 'local', label: 'Local' }, ]; type Props = { @@ -34,8 +31,9 @@ type Props = { const DatasetsModal = ({ open, onOpenChange, isSafari }: Props) => { const [activeOption, setActiveOption] = useState(''); + const [selectedUrl, setSelectedUrl] = useState(''); const [hasFetched, setHasFetched] = useState(false); - const [activeTab, setActiveTab] = useState('curated'); + const [activeTab, setActiveTab] = useState('remote'); const { initStore, setInitStore, setOpenVariables, status } = useGlobalStore( useShallow(state => ({ @@ -47,16 +45,26 @@ const DatasetsModal = ({ open, onOpenChange, isSafari }: Props) => { ); const showDescription = hasFetched && status === null; - const openDescription = () => setHasFetched(true); const handleTabChange = (tab: Tab) => { setActiveTab(tab); setHasFetched(false); + setSelectedUrl(''); + setActiveOption(''); + }; + + const handleOpenChange = (v: boolean) => { + if (!v) { + setSelectedUrl(''); + setActiveOption(''); + setHasFetched(false); + } + onOpenChange(v); }; return ( - + Open Dataset @@ -82,20 +90,22 @@ const DatasetsModal = ({ open, onOpenChange, isSafari }: Props) => {
- {activeTab === 'curated' && ( - - )} {activeTab === 'remote' && ( - + <> + + {}} + /> + )} {activeTab === 'local' && ( , string> = { @@ -33,14 +32,16 @@ type Props = { initStore: string; setInitStore: (v: string) => void; onOpenDescription: () => void; + selectedUrl?: string; }; -const RemoteZarr = ({ initStore, setInitStore, onOpenDescription }: Props) => { +const RemoteZarr = ({ initStore, setInitStore, onOpenDescription, selectedUrl = '' }: Props) => { const [showFetchOptions, setShowFetchOptions] = useState(false); const [preset, setPreset] = useState('none'); const [presetValue, setPresetValue] = useState(''); const [headers, setHeaders] = useState([{ key: '', value: '' }]); const [showCustom, setShowCustom] = useState(false); + const [urlValue, setUrlValue] = useState(selectedUrl); const addHeaderRow = () => setHeaders(h => [...h, { key: '', value: '' }]); const removeHeaderRow = (i: number) => setHeaders(h => h.filter((_, idx) => idx !== i)); @@ -73,17 +74,16 @@ const RemoteZarr = ({ initStore, setInitStore, onOpenDescription }: Props) => { className="flex flex-col gap-3" onSubmit={(e: React.FormEvent) => { e.preventDefault(); - const input = e.currentTarget.elements[0] as HTMLInputElement; - const url = input.value; - if (!url) return; + if (!urlValue) return; const fetchHandler = buildFetchHandler(); - if (fetchHandler && url.startsWith('http://')) { + if (fetchHandler && urlValue.startsWith('http://')) { useGlobalStore.getState().setStatus('Error: Cannot send auth headers over plain HTTP — use HTTPS.'); return; } + const fetchOptions = { - ...(fetchHandler && { fetch: fetchHandler}), + ...(fetchHandler && { fetch: fetchHandler }), }; useZarrStore.getState().setIcechunkOptions(null); @@ -93,19 +93,24 @@ const RemoteZarr = ({ initStore, setInitStore, onOpenDescription }: Props) => { useGlobalStore.getState().setStatus('Fetching...'); useZarrStore.getState().bumpFetchKey(); - setInitStore(url); + setInitStore(urlValue); onOpenDescription(); }} > {/* URL + Fetch */}
- + setUrlValue(e.target.value)} + />
- {/* FetchOptions */} + {/* fetchOptions toggle */}
-
+
{activeTab === 'remote' && ( - <> + <> { setInitStore={setSelectedUrl} onOpenDescription={() => {}} /> - Date: Sun, 21 Jun 2026 19:49:03 +0200 Subject: [PATCH 6/7] states, redundancy --- src/components/ui/MainPanel/DataSetsModal.tsx | 2 -- src/components/ui/MainPanel/RemoteZarr.tsx | 2 +- src/components/ui/MainPanel/StoreCatalog.tsx | 11 ++++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/ui/MainPanel/DataSetsModal.tsx b/src/components/ui/MainPanel/DataSetsModal.tsx index 1291ff1a..4dab59bf 100644 --- a/src/components/ui/MainPanel/DataSetsModal.tsx +++ b/src/components/ui/MainPanel/DataSetsModal.tsx @@ -120,7 +120,6 @@ const DatasetsModal = ({ open, onOpenChange, isSafari }: Props) => { activeOption={activeOption} setActiveOption={setActiveOption} setInitStore={setSelectedUrl} - onOpenDescription={() => {}} /> { activeOption={activeOption} setActiveOption={setActiveOption} setInitStore={setSelectedIcechunkUrl} - onOpenDescription={() => {}} /> void; setInitStore: (v: string) => void; - onOpenDescription: () => void; + onOpenDescription?: () => void; placeholder?: string; gradient?: GradientPreset; }; @@ -138,7 +138,7 @@ const StoreCatalog = ({ observer.observe(sentinel); return () => observer.disconnect(); - }, [filtered.length]); + }, [filtered.length, visibleCount]); return ( @@ -162,7 +162,7 @@ const StoreCatalog = ({ onSelect={() => { setActiveOption(ds.key); setInitStore(ds.store); - onOpenDescription(); + onOpenDescription?.(); }} className={`flex flex-col items-start gap-0.5 mb-2 cursor-pointer ${ activeOption === ds.key ? 'bg-accent' : '' @@ -187,12 +187,9 @@ const StoreCatalog = ({ {hasMore && (
From a3f24519397aa482214667663e3416eec0e33a1c Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Sun, 21 Jun 2026 20:03:44 +0200 Subject: [PATCH 7/7] minimal keyboard arrows functionality --- src/components/ui/MainPanel/StoreCatalog.tsx | 39 ++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/components/ui/MainPanel/StoreCatalog.tsx b/src/components/ui/MainPanel/StoreCatalog.tsx index 3627e7c7..82eec6b0 100644 --- a/src/components/ui/MainPanel/StoreCatalog.tsx +++ b/src/components/ui/MainPanel/StoreCatalog.tsx @@ -89,15 +89,14 @@ const StoreCatalog = ({ placeholder = 'Search datasets...', gradient, }: Props) => { - // Keep search, pagination, and scroll-hint in one place so they reset atomically. const [search, setSearch] = useState(''); const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [isScrolled, setIsScrolled] = useState(false); const sentinelRef = useRef(null); const listRef = useRef(null); + const rootRef = useRef(null); - // Resetting visibleCount and isScrolled here (in the event handler) const handleSearchChange = (value: string) => { setSearch(value); setVisibleCount(PAGE_SIZE); @@ -113,6 +112,28 @@ const StoreCatalog = ({ const hasMore = visibleCount < filtered.length; const gradientValue = gradient ? GRADIENTS[gradient] : undefined; + // Intercept ArrowDown at the Command root. cmdk handles navigation itself + // and gives no callback when it reaches the last item — so we check whether + // the currently selected item is the last rendered one, and if there's more + // to load, we load it. cmdk will then naturally move focus to the new item + // on the next keydown. + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key !== 'ArrowDown' || !hasMore) return; + + const root = rootRef.current; + if (!root) return; + + const items = Array.from(root.querySelectorAll('[cmdk-item]:not([aria-disabled="true"])')); + const selected = root.querySelector('[cmdk-item][aria-selected="true"]'); + + if (selected && items[items.length - 1] === selected) { + // We're on the last item — load the next page. This is still has issues but kinda works :/ + // Don't preventDefault: let cmdk process the key after state updates + // so it can move selection to the newly rendered item. + setVisibleCount(c => Math.min(c + PAGE_SIZE, filtered.length)); + } + }; + // Track scroll position to fade out the hint once the user starts scrolling. useEffect(() => { const el = listRef.current; @@ -123,6 +144,7 @@ const StoreCatalog = ({ }, []); // Load the next page when the sentinel scrolls into view. + // biome-ignore lint/correctness/useExhaustiveDependencies: visibleCount controls sentinel mount/unmount; needed to re-bind observer after resets even when filtered.length is unchanged useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) return; @@ -141,7 +163,12 @@ const StoreCatalog = ({ }, [filtered.length, visibleCount]); return ( - + No datasets found. - {visible.map(ds => ( + {visible.map((ds) => (