Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 8 additions & 11 deletions vortex-web/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { createElement } from 'react';
import type { Preview } from '@storybook/react-vite';
import { ThemeContext } from '../src/contexts/ThemeContextCore';
import { ThemeProvider } from '../src/contexts/ThemeContext';
import '../src/index.css';

const preview: Preview = {
Expand All @@ -26,16 +26,13 @@ const preview: Preview = {
},
decorators: [
(Story, context) => {
const theme = context.globals.theme || 'light';
document.documentElement.classList.toggle('dark', theme === 'dark');
document.documentElement.classList.toggle('light', theme === 'light');
// Supply the theme context so components using useTheme render in stories,
// following the Storybook theme toolbar rather than persisted preferences.
return createElement(
ThemeContext.Provider,
{ value: { theme, setTheme: () => {} } },
Story(),
);
// Wrap every story in the real ThemeProvider so theme-aware components
// (e.g. FileHeader/ThemePicker, which call useTheme) have their context.
// Seed it from the toolbar's theme global; `key` remounts the provider
// when the toolbar theme changes so the switch takes effect.
const theme = (context.globals.theme as string) || 'light';
localStorage.setItem('vortex-theme', theme);
return createElement(ThemeProvider, { key: theme }, Story());
},
],
parameters: {
Expand Down
7 changes: 4 additions & 3 deletions vortex-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { LayoutTreeNode } from './components/swimlane/types';
import { arrayTreeToLayoutChildren, findNodeById } from './components/swimlane/utils';
import { FileDropScreen } from './components/explorer/FileDropScreen';
import { FileHeader } from './components/explorer/FileHeader';
import { MainArea } from './components/explorer/MainArea';
import { MainArea, type MainView } from './components/explorer/MainArea';
import { StatusBar } from './components/explorer/StatusBar';
import { VortexWorker } from './workers/VortexWorker';

Expand All @@ -18,6 +18,7 @@ function App() {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [view, setView] = useState<MainView>('details');
const dragCounter = useRef(0);
const workerRef = useRef<VortexWorker | null>(null);

Expand Down Expand Up @@ -177,8 +178,8 @@ function App() {
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<FileHeader onClose={closeFile} />
<MainArea />
<FileHeader onClose={closeFile} view={view} onViewChange={setView} />
<MainArea view={view} />
<StatusBar />
{isDragging && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-vortex-black/50 dark:bg-black/50 backdrop-blur-sm pointer-events-none">
Expand Down
12 changes: 11 additions & 1 deletion vortex-web/src/components/detail/DetailPanel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ const mockFileState: VortexFileState = {

const meta: Meta<typeof DetailPanel> = {
component: DetailPanel,
decorators: [withMockFileContext(mockFileState), withMockSelection(layout)],
decorators: [
withMockFileContext(mockFileState),
withMockSelection(layout),
// DetailPanel fills its parent's height; give it a bounded flex container.
(Story) => (
<div className="flex" style={{ height: 520 }}>
<Story />
</div>
),
],
parameters: {
layout: 'padded',
},
Expand All @@ -35,4 +44,5 @@ export default meta;

type Story = StoryObj<typeof DetailPanel>;

/** No node selected — the panel shows its empty state. */
export const NoSelection: Story = {};
17 changes: 5 additions & 12 deletions vortex-web/src/components/detail/DetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {
shortEncoding,
DTYPE_COLORS,
} from '../swimlane/utils';
import { SummaryPane } from './SummaryPane';
import { ArraySummaryPane } from './ArraySummaryPane';
import { SummarySidebar } from './SummarySidebar';
import { EncodingPane } from './EncodingPane';
import { SegmentsPane } from './SegmentsPane';
import { BlockTreemap } from '../explorer/BlockTreemap';
Expand Down Expand Up @@ -66,7 +65,8 @@ export function DetailPanel() {
const selectedIdSet = useMemo(() => new Set(selectedPath.map((n) => n.id)), [selectedPath]);
const isHoverBreadcrumb = hoveredPath.length > 0;

const currentTab = tabs.find((t) => t.id === activeTab) ? activeTab : tabs[0]?.id;
// Prefer the active tab; otherwise fall back to the first available tab.
const currentTab = tabs.find((t) => t.id === activeTab)?.id ?? tabs[0]?.id;

return (
<div className="flex flex-col flex-1 min-h-0 h-full bg-vortex-white dark:bg-vortex-black">
Expand Down Expand Up @@ -144,7 +144,6 @@ export function DetailPanel() {

{/* Main content: tab content (left) + summary sidebar (right) */}
<div className="flex flex-1 min-h-0 overflow-hidden">
{/* Tab content */}
<div className="flex-1 overflow-auto min-w-0">
{currentTab === 'encoding' && selection.selectedNode && (
<div className="p-2.5">
Expand All @@ -169,21 +168,15 @@ export function DetailPanel() {
{currentTab === 'buffers' && selection.selectedNode && (
<BuffersPane node={selection.selectedNode} />
)}
{!currentTab && !selection.selectedNode && (
{!selection.selectedNode && (
<div className="p-2.5 text-xs text-vortex-grey-dark">
Select a node to view details.
</div>
)}
</div>

{/* Summary sidebar — always visible */}
<div className="w-[180px] flex-shrink-0 overflow-y-auto border-l border-vortex-grey-light/40 dark:border-white/[0.06] p-2.5">
{isArrayNode && selection.selectedNode ? (
<ArraySummaryPane node={selection.selectedNode} />
) : (
<SummaryPane node={selection.selectedNode} file={file} />
)}
</div>
<SummarySidebar />
</div>
</div>
);
Expand Down
28 changes: 28 additions & 0 deletions vortex-web/src/components/detail/SummarySidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

import { useVortexFile } from '../../contexts/VortexFileContextCore';
import { useSelection } from '../../contexts/SelectionContextCore';
import { SummaryPane } from './SummaryPane';
import { ArraySummaryPane } from './ArraySummaryPane';

/**
* Right-hand summary sidebar shared by the details and swimlane views. Shows the
* selected node's summary (array-aware), falling back to the whole-file summary
* when nothing is selected.
*/
export function SummarySidebar() {
const file = useVortexFile();
const { state: selection } = useSelection();
const isArrayNode = selection.selectedNode?.isArrayNode ?? false;

return (
<div className="w-[180px] flex-shrink-0 overflow-y-auto border-l border-vortex-grey-light/40 dark:border-white/[0.06] p-2.5">
{isArrayNode && selection.selectedNode ? (
<ArraySummaryPane node={selection.selectedNode} />
) : (
<SummaryPane node={selection.selectedNode} file={file} />
)}
</div>
);
}
8 changes: 5 additions & 3 deletions vortex-web/src/components/explorer/ExplorerShell.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { FileHeader } from './FileHeader';
import { MainArea } from './MainArea';
import { MainArea, type MainView } from './MainArea';
import { StatusBar } from './StatusBar';
import { withMockFileContext, withMockSelection } from '../../storybook/decorators';
import { ordersMock } from '../../mocks/layouts';
Expand All @@ -29,10 +30,11 @@ const mockFileState: VortexFileState = {

/** Full page layout — mirrors App.tsx when a file is loaded */
function ExplorerPage() {
const [view, setView] = useState<MainView>('details');
return (
<div className="flex flex-col h-screen bg-vortex-white dark:bg-vortex-black">
<FileHeader onClose={() => {}} />
<MainArea />
<FileHeader onClose={() => {}} view={view} onViewChange={setView} />
<MainArea view={view} />
<StatusBar />
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion vortex-web/src/components/explorer/FileHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export default meta;

type Story = StoryObj<typeof FileHeader>;

export const Default: Story = {};
export const Default: Story = {
args: { view: 'details', onViewChange: () => {} },
};
66 changes: 44 additions & 22 deletions vortex-web/src/components/explorer/FileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@

import { useVortexFile } from '../../contexts/VortexFileContextCore';
import { ThemePicker } from '../ThemePicker';
import type { MainView } from './MainArea';

interface FileHeaderProps {
onClose: () => void;
view: MainView;
onViewChange: (view: MainView) => void;
}

export function FileHeader({ onClose }: FileHeaderProps) {
export function FileHeader({ onClose, view, onViewChange }: FileHeaderProps) {
const file = useVortexFile();

return (
Expand All @@ -22,28 +25,47 @@ export function FileHeader({ onClose }: FileHeaderProps) {
>
v{file.version}
</span>
<div className="ml-auto flex items-center gap-1">
<ThemePicker />
<button
onClick={onClose}
className="p-1.5 rounded-md text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg hover:bg-vortex-grey-lightest dark:hover:bg-white/[0.06] transition-colors cursor-pointer"
title="Close file"
aria-label="Close file"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
<div className="ml-auto flex items-center gap-2">
{/* Primary view switch — sits with the global controls in the header. */}
<div className="flex rounded-md bg-vortex-grey-lightest dark:bg-white/[0.06] p-0.5">
{(['details', 'swimlane'] as const).map((v) => (
<button
key={v}
className={`px-3 py-0.5 text-[11px] rounded-[3px] transition-colors ${
view === v
? 'bg-white dark:bg-white/[0.1] text-vortex-fg-light dark:text-vortex-fg shadow-sm font-medium'
: 'text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg'
}`}
onClick={() => onViewChange(v)}
>
{v === 'details' ? 'Details' : 'Swimlane'}
</button>
))}
</div>

<div className="flex items-center gap-1">
<ThemePicker />
<button
onClick={onClose}
className="p-1.5 rounded-md text-vortex-grey-dark hover:text-vortex-fg-light dark:hover:text-vortex-fg hover:bg-vortex-grey-lightest dark:hover:bg-white/[0.06] transition-colors cursor-pointer"
title="Close file"
aria-label="Close file"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
</div>
);
Expand Down
43 changes: 29 additions & 14 deletions vortex-web/src/components/explorer/MainArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@

import { useCallback, useRef, useState } from 'react';
import { TreePanel } from './TreePanel';
import { FileMap } from './FileMap';
import { SwimlaneOverview } from '../swimlane/SwimlaneOverview';
import { DataPreview } from './DataPreview';
import { DetailPanel } from '../detail/DetailPanel';
import { SummarySidebar } from '../detail/SummarySidebar';

const MIN_PANEL_HEIGHT = 120;
const DEFAULT_PREVIEW_HEIGHT = 200;

export type MainView = 'details' | 'swimlane';

/**
* Main explorer area: tree panel (left) | detail + filemap + preview (right).
* Main explorer area: tree panel (left) | the active main view (details or
* swimlane, chosen in the top header) over a resizable data preview (right). The
* tree stays put as column navigation while the header tabs switch the view.
*
* The preview panel at the bottom is vertically resizable via a drag handle.
*/
export function MainArea() {
export function MainArea({ view }: { view: MainView }) {
const [previewHeight, setPreviewHeight] = useState(DEFAULT_PREVIEW_HEIGHT);
const dragging = useRef(false);
const startY = useRef(0);
Expand Down Expand Up @@ -48,19 +53,29 @@ export function MainArea() {

return (
<div className="flex flex-1 min-h-0 overflow-hidden">
{/* Left: tree panel — full height, fixed width */}
<div className="w-[260px] flex-shrink-0 h-full overflow-hidden">
<TreePanel />
</div>
{/* Left: tree panel — column navigation paired with the details view. The
swimlane view hides it so the byte-bars get the full width. */}
{view === 'details' && (
<div className="w-[260px] flex-shrink-0 h-full overflow-hidden">
<TreePanel />
</div>
)}

{/* Right: detail pane, file map, data preview — stacked vertically */}
{/* Right: active main view over the resizable data preview */}
<div ref={containerRef} className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
{/* Detail pane — fills available vertical space, scrolls internally */}
<DetailPanel />

{/* File map strip */}
<div className="flex-shrink-0">
<FileMap />
{/* Active view — fills available vertical space, scrolls internally. The
swimlane keeps the summary sidebar alongside it, like the details view. */}
<div className="flex-1 min-h-0 flex overflow-hidden">
{view === 'details' ? (
<DetailPanel />
) : (
<>
<div className="flex-1 min-w-0 flex flex-col">
<SwimlaneOverview />
</div>
<SummarySidebar />
</>
)}
</div>

{/* Resize handle */}
Expand Down
Loading
Loading