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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/viewer/src/chrome/ViewerShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('ViewerShell', () => {
it('shows title, kind badge and counts', () => {
render(<ViewerShell diagram={commerceApiLayout} theme="light" onToggleTheme={() => {}} />);
expect(screen.getByRole('heading', { level: 1, name: 'Commerce API' })).toBeInTheDocument();
expect(screen.getAllByText('Architecture')).toHaveLength(2); // breadcrumb + badge
expect(screen.getByText('Architecture')).toBeInTheDocument();
expect(screen.getByText('6 nodes · 9 connections')).toBeInTheDocument();
});

Expand Down
83 changes: 40 additions & 43 deletions packages/viewer/src/chrome/ViewerShell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ChevronRight } from 'lucide-react';
import { useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react';
import type { LaidOutDiagram } from '@stackmap/core';
import { CanvasPanel } from '../canvas/CanvasPanel';
Expand All @@ -21,7 +20,7 @@ const DIAGRAM_ID = 'sm-diagram';

function ViewTabs() {
const { draft, state, dispatch } = useExplore();
const tabs = [{ id: null, label: 'Overview', caption: undefined }, ...(draft.views ?? []).map((v) => ({ ...v, id: v.id as string | null }))];
const tabs = [{ id: null, label: 'Overview' }, ...(draft.views ?? []).map((v) => ({ ...v, id: v.id as string | null }))];
const current = tabs.find((t) => t.id === state.view) ?? tabs[0]!;
// One indicator that slides between tabs, so a view change reads as a move rather than a blink.
const bar = useRef<HTMLSpanElement>(null);
Expand Down Expand Up @@ -55,31 +54,35 @@ function ViewTabs() {
document.getElementById(tabId(next.id))?.focus();
};
return (
<>
<div role="tablist" aria-label="Views" className="relative mt-5 flex gap-7 border-b border-divider">
{tabs.map((tab, i) => (
<button
key={tab.id ?? 'overview'}
id={tabId(tab.id)}
role="tab"
aria-controls={DIAGRAM_ID}
type="button"
aria-selected={tab === current}
tabIndex={tab === current ? 0 : -1}
onClick={() => dispatch({ type: 'view', id: tab.id })}
onKeyDown={(e) => onKeyDown(e, i)}
className="pb-3 text-[14.5px] text-fg-muted transition-colors duration-150 hover:text-fg aria-selected:text-fg"
>
{tab.label}
</button>
))}
<span ref={bar} aria-hidden="true" className="pointer-events-none absolute -bottom-px left-0 h-0.5 rounded-full bg-fg" />
</div>
{current.caption && <p className="mt-3 text-[13px] text-fg-muted">{current.caption}</p>}
</>
// Scrolls sideways rather than wrapping when views outgrow the row; -mb-px lays the indicator on the header rule.
<div role="tablist" aria-label="Views" className="relative -mb-px flex min-w-0 gap-7 overflow-x-auto [scrollbar-width:none]">
{tabs.map((tab, i) => (
<button
key={tab.id ?? 'overview'}
id={tabId(tab.id)}
role="tab"
aria-controls={DIAGRAM_ID}
type="button"
aria-selected={tab === current}
tabIndex={tab === current ? 0 : -1}
onClick={() => dispatch({ type: 'view', id: tab.id })}
onKeyDown={(e) => onKeyDown(e, i)}
className="shrink-0 text-[14.5px] whitespace-nowrap text-fg-muted transition-colors duration-150 hover:text-fg focus-visible:-outline-offset-2 aria-selected:text-fg"
>
{tab.label}
</button>
))}
<span ref={bar} aria-hidden="true" className="pointer-events-none absolute bottom-0 left-0 h-0.5 rounded-full bg-fg" />
</div>
);
}

function ViewCaption() {
const { draft, state } = useExplore();
const caption = draft.views?.find((v) => v.id === state.view)?.caption;
return caption ? <p className="px-8 pt-3 text-[13px] text-fg-muted">{caption}</p> : null;
}

export function ViewerShell({
diagram,
theme,
Expand All @@ -104,29 +107,23 @@ export function ViewerShell({
return (
<ExploreProvider draft={draft}>
<div ref={shell} className="flex h-full flex-col bg-page font-sans text-fg">
<header className="px-8 pt-6">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-[14px] text-fg-muted">
<span>stackmap</span>
<ChevronRight size={14} strokeWidth={1.75} aria-hidden="true" />
<span className="text-fg">{KIND_LABEL[draft.kind]}</span>
</nav>
<div className="mt-3 flex items-end justify-between gap-6">
<div className="flex min-w-0 items-center gap-3">
<h1 className="truncate text-[28px] leading-9 font-semibold tracking-tight">{draft.title}</h1>
<span
className="shrink-0 rounded-lg bg-panel px-2.5 py-1 text-[13px] text-fg-muted"
style={{ boxShadow: 'inset 0 0 0 1px var(--sm-panel-border)' }}
>
{KIND_LABEL[draft.kind]}
</span>
</div>
<span className="shrink-0 pb-1 text-[14px] text-fg-muted">
{draft.nodes.length} nodes · {draft.edges.length} connections
<header className="mx-8 mt-4 flex gap-8 border-b border-divider">
<div className="flex max-w-[45%] min-w-0 items-center gap-3 py-3">
<h1 className="truncate text-[20px] leading-7 font-semibold tracking-tight">{draft.title}</h1>
<span
className="shrink-0 rounded-lg bg-panel px-2 py-0.5 text-[13px] text-fg-muted"
style={{ boxShadow: 'inset 0 0 0 1px var(--sm-panel-border)' }}
>
{KIND_LABEL[draft.kind]}
</span>
</div>
<ViewTabs />
<span className="ml-auto shrink-0 self-center text-[14px] whitespace-nowrap text-fg-muted max-md:hidden">
{draft.nodes.length} nodes · {draft.edges.length} connections
</span>
</header>
<main className="flex min-h-0 flex-1 gap-4 px-8 pt-5 pb-6">
<ViewCaption />
<main className="flex min-h-0 flex-1 gap-4 px-8 pt-4 pb-6">
<section
id={DIAGRAM_ID}
role="tabpanel"
Expand Down
Loading