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
25 changes: 25 additions & 0 deletions docs/EMBEDDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The component requires its bundled CSS. Import it once at your application root:
import '@ioai/rosview/style.css';
```

Styles are scoped to `#rosview-root` inside the bundle, so Tailwind preflight does not reset global elements (navbar, buttons, etc.) in your host application.

---

## Step 2 — Basic usage
Expand Down Expand Up @@ -133,6 +135,29 @@ const rows = parseRemoteDatasetListJson(await res.json());

---

## Advanced: Navbar branding

The navbar shows **ROS View** on the left by default. When embedding inside a larger app, you can hide it or replace it with your product name:

```tsx
// Hide the brand button (File / Layout menus remain)
<RosViewer url="..." showNavbarBrand={false} />

// Replace with host branding
<RosViewer url="..." navbarBrandLabel="Acme Data Portal" />
```

Related props:

| Prop | Description |
|------|-------------|
| `showNavbarBrand` | Left brand button visibility (`true` by default). |
| `navbarBrandLabel` | Custom brand text; defaults to the localized product name. |
| `navbarSourceName` | Center label for the active dataset (separate from brand). |
| `showNavbar` | Hides the entire navbar when `false` (via `chrome` or explicit prop). |

---

## Advanced: Controlled theme & language

Disable internal localStorage persistence and fully control state from your application:
Expand Down
25 changes: 25 additions & 0 deletions docs/EMBEDDING.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ npm install @ioai/rosview
import '@ioai/rosview/style.css';
```

样式在构建产物中限定在 `#rosview-root` 内,Tailwind preflight 不会重置宿主应用的全局元素(导航栏、按钮等)。

---

## 步骤 2 — 基本用法
Expand Down Expand Up @@ -133,6 +135,29 @@ const rows = parseRemoteDatasetListJson(await res.json());

---

## 进阶:Navbar 品牌文案

Navbar 左侧默认显示 **ROS View**。嵌入到更大页面时,可以隐藏或替换为你的产品名:

```tsx
// 隐藏品牌按钮(File / Layout 菜单仍保留)
<RosViewer url="..." showNavbarBrand={false} />

// 替换为宿主品牌
<RosViewer url="..." navbarBrandLabel="Acme 数据平台" />
```

相关 props:

| Prop | 说明 |
|------|------|
| `showNavbarBrand` | 左侧品牌按钮是否显示(默认 `true`)。 |
| `navbarBrandLabel` | 自定义品牌文案;未设置时使用本地化产品名。 |
| `navbarSourceName` | 中间区域的数据源名称(与品牌文案无关)。 |
| `showNavbar` | 设为 `false` 时隐藏整条 Navbar(通过 `chrome` 或显式 prop)。 |

---

## 进阶:受控主题与语言

关闭组件内部的 localStorage 持久化,由宿主应用完全控制状态:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ioai/rosview",
"version": "1.5.1",
"version": "1.5.2",
"description": "High-performance robotics data visualization for MCAP, ROS bag, ROS2 db3, HDF5 and BVH — embeddable React component and standalone SPA",
"keywords": [
"ros",
Expand Down
6 changes: 6 additions & 0 deletions src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface AppShellProps {
onLanguageChange: (lang: 'en' | 'zh' | 'ja') => void;
showLanguageSwitcher?: boolean;
showThemeSwitcher?: boolean;
showNavbarBrand?: boolean;
navbarBrandLabel?: string;
onBrandClick?: () => void;
preferAutoLayout?: boolean;
preferencePersistence: PreferencePersistence;
Expand Down Expand Up @@ -77,6 +79,8 @@ export const AppShell: React.FC<AppShellProps> = ({
onLanguageChange,
showLanguageSwitcher = true,
showThemeSwitcher = true,
showNavbarBrand = true,
navbarBrandLabel,
onBrandClick,
preferAutoLayout = false,
preferencePersistence,
Expand Down Expand Up @@ -150,6 +154,8 @@ export const AppShell: React.FC<AppShellProps> = ({
onLanguageChange={onLanguageChange}
showLanguageSwitcher={showLanguageSwitcher}
showThemeSwitcher={showThemeSwitcher}
showNavbarBrand={showNavbarBrand}
brandLabel={navbarBrandLabel}
onBrandClick={onBrandClick}
onOpenFilePick={hideOpenFileMenus ? undefined : onOpenFilePick}
onOpenDirectory={hideOpenFileMenus ? undefined : onOpenDirectory}
Expand Down
9 changes: 9 additions & 0 deletions src/features/layout/DockviewLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export const DockviewLayout: React.FC<DockviewLayoutProps> = ({
if (!apiRef.current || topics.length === 0) return;
const api = apiRef.current;
if (
preferAutoLayoutRef.current &&
hasAutoInitializedRef.current &&
layoutDatasetSignatureRef.current &&
layoutDatasetSignatureRef.current !== currentDatasetSignature
Expand All @@ -389,6 +390,14 @@ export const DockviewLayout: React.FC<DockviewLayoutProps> = ({
layoutDatasetSignatureRef.current = currentDatasetSignature;
return;
}
if (
hasAutoInitializedRef.current &&
layoutDatasetSignatureRef.current &&
layoutDatasetSignatureRef.current !== currentDatasetSignature
) {
layoutDatasetSignatureRef.current = currentDatasetSignature;
return;
}
if (hasAutoInitializedRef.current) return;
const hasAnyNonWelcomePanel = api.panels.some((panel) => panel.id !== WELCOME_PANEL_ID);
if (hasAnyNonWelcomePanel) return;
Expand Down
17 changes: 17 additions & 0 deletions src/features/panels/Plot/usePlotChart.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
diffSeriesTopology,
shouldPinPlotXScaleToLogRange,
shouldRemountForIncrementalSeriesUpdate,
type SeriesSignature,
} from './usePlotChart';
Expand Down Expand Up @@ -86,3 +87,19 @@ describe('shouldRemountForIncrementalSeriesUpdate', () => {
.toBe(false);
});
});

describe('shouldPinPlotXScaleToLogRange', () => {
const logRange = { min: 0, max: 55 };

it('pins when log range exists and following view is off', () => {
expect(shouldPinPlotXScaleToLogRange(logRange, 0)).toBe(true);
});

it('does not pin without log range', () => {
expect(shouldPinPlotXScaleToLogRange(undefined, 0)).toBe(false);
});

it('does not pin when playhead following owns the X axis', () => {
expect(shouldPinPlotXScaleToLogRange(logRange, 10)).toBe(false);
});
});
49 changes: 47 additions & 2 deletions src/features/panels/Plot/usePlotChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ export function shouldRemountForIncrementalSeriesUpdate(
return false;
}

/**
* Whether incremental updates should pin the X axis to the full log range.
* Skipped when playhead-following mode owns the X scale.
*/
export function shouldPinPlotXScaleToLogRange(
xRange: { min: number; max: number } | undefined,
followingViewWidthSec: number,
): xRange is { min: number; max: number } {
return xRange != null && followingViewWidthSec <= 0;
}

/** Keep the X viewport on the full recording duration during range reads. */
export function pinPlotXScaleToLogRange(
chart: uPlot,
xRange: { min: number; max: number },
): void {
chart.setScale('x', xRange);
}

export function usePlotChart({
containerRef,
player,
Expand Down Expand Up @@ -228,8 +247,15 @@ export function usePlotChart({
isLoading: () => loadingRef.current,
});

uplotRef.current = mountPlotChart(container, dataset, options, xRange);
const chart = mountPlotChart(container, dataset, options, xRange);
uplotRef.current = chart;
seriesSignaturesRef.current = seriesSignatures(dataset, hiddenSeries);
if (
loadingRef.current
&& shouldPinPlotXScaleToLogRange(xRange, followingViewWidthRef.current)
) {
pinPlotXScaleToLogRange(chart, xRange);
}

const observer = new ResizeObserver(() => {
const chart = uplotRef.current;
Expand Down Expand Up @@ -309,13 +335,32 @@ export function usePlotChart({
// setData second arg = false avoids a hard scale reset every batch, which is
// what made the chart flash while data was streaming in.
chart.setData(dataset.data, false);
// setData(false) can shrink X to the loaded points only; pin to the full log
// range so the axis stays 0…duration while curves grow incrementally.
if (
loadingRef.current
&& shouldPinPlotXScaleToLogRange(xRange, followingViewWidthRef.current)
) {
pinPlotXScaleToLogRange(chart, xRange);
}
// Style mutations are read at draw time, so force one rebuild+redraw to
// surface the new width/dash/stroke immediately.
if (diff.kind === 'styleUpdate') {
chart.redraw(true);
} else if (loadingRef.current) {
chart.redraw(false);
}
seriesSignaturesRef.current = nextSignatures;
}, [config.followingViewWidthSec, config.xAxisMode, containerRef, dataset, destroyChart, hiddenSeries, mountChart]);
}, [
config.followingViewWidthSec,
config.xAxisMode,
containerRef,
dataset,
destroyChart,
hiddenSeries,
mountChart,
xRange,
]);

// When loading completes, force one Y-scale recompute so the locked-min/max
// is replaced with the natural auto-fit range.
Expand Down
10 changes: 10 additions & 0 deletions src/features/viewer/RosViewerImpl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ export interface RosViewerProps {
extensions?: RosViewExtension[];
/** Optional center label override shown in navbar source area. */
navbarSourceName?: string;
/** Whether to show the left navbar brand button. @default true */
showNavbarBrand?: boolean;
/** Custom label for the left navbar brand button (defaults to product name). */
navbarBrandLabel?: string;
/** Whether to show navbar language switcher. @default true */
showLanguageSwitcher?: boolean;
/** Whether to show navbar theme switcher. @default true */
Expand Down Expand Up @@ -1145,6 +1149,8 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
showNavbarBrand={props.showNavbarBrand ?? true}
navbarBrandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
preferAutoLayout={props.preferAutoLayout ?? false}
preferencePersistence={persistence}
Expand Down Expand Up @@ -1206,6 +1212,8 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
showNavbarBrand={props.showNavbarBrand ?? true}
brandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
onOpenFilePick={() => {
clearOpenFeedback();
Expand Down Expand Up @@ -1309,6 +1317,8 @@ export const RosViewer: React.FC<RosViewerProps> = (props) => {
onLanguageChange={handleLanguageChange}
showLanguageSwitcher={props.showLanguageSwitcher ?? true}
showThemeSwitcher={props.showThemeSwitcher ?? true}
showNavbarBrand={props.showNavbarBrand ?? true}
brandLabel={props.navbarBrandLabel}
onBrandClick={handleGoHome}
onOpenFilePick={() => {
clearOpenFeedback();
Expand Down
28 changes: 19 additions & 9 deletions src/features/workspace/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ interface NavbarProps {
onLanguageChange?: (lang: 'en' | 'zh' | 'ja') => void;
showLanguageSwitcher?: boolean;
showThemeSwitcher?: boolean;
/** When false, hide the left brand button. @default true */
showNavbarBrand?: boolean;
/** Override default product name in the left brand button. */
brandLabel?: string;
onBrandClick?: () => void;
onOpenFilePick?: () => void;
onOpenDirectory?: () => void;
Expand All @@ -103,6 +107,8 @@ export const Navbar: React.FC<NavbarProps> = ({
onLanguageChange,
showLanguageSwitcher = true,
showThemeSwitcher = true,
showNavbarBrand = true,
brandLabel,
onBrandClick,
onOpenFilePick,
onOpenDirectory,
Expand Down Expand Up @@ -164,20 +170,24 @@ export const Navbar: React.FC<NavbarProps> = ({
onOpenFilePick || onOpenDirectory || onOpenTarPick || onOpenRemotePrompt || onOpenSampleDialog;
const showCenter = Boolean(sourceLoading || (sourceName && sourceName.trim().length > 0));
const centerLabel = sourceLoading ? formatMessage({ id: 'navbar.sourceLoading' }) : (sourceName ?? '');
const brandText = brandLabel ?? formatMessage({ id: 'common.productName' });
const brandAccessibleName = brandLabel ?? formatMessage({ id: 'navbar.goHome' });

return (
<nav className="sticky top-0 z-50 w-full shrink-0 border-b border-border bg-background select-none">
<div className="grid h-12 w-full grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)] items-center gap-x-2 gap-y-0 px-4">
<div className="flex min-w-0 items-center gap-2 justify-self-start sm:gap-3">
<button
type="button"
className="min-w-0 max-w-[min(11rem,28vw)] shrink truncate rounded-sm text-left text-sm font-semibold tracking-tight text-foreground transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background sm:max-w-[11rem]"
onClick={onBrandClick}
title={formatMessage({ id: 'navbar.goHome' })}
aria-label={formatMessage({ id: 'navbar.goHome' })}
>
{formatMessage({ id: 'common.productName' })}
</button>
{showNavbarBrand ? (
<button
type="button"
className="min-w-0 max-w-[min(11rem,28vw)] shrink truncate rounded-sm text-left text-sm font-semibold tracking-tight text-foreground transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background sm:max-w-[11rem]"
onClick={onBrandClick}
title={brandAccessibleName}
aria-label={brandAccessibleName}
>
{brandText}
</button>
) : null}

<Menubar
ref={leftMenubarRef}
Expand Down
10 changes: 6 additions & 4 deletions src/features/workspace/playback/PlaybackBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,24 +473,26 @@ export const PlaybackBar: React.FC<PlaybackBarProps> = ({ player, extensionConte
<div
key={`loaded-${idx}`}
data-testid="playback-loaded-range"
className="absolute top-0 h-full rounded-full bg-muted-foreground/35"
className="pointer-events-none absolute inset-y-0 rounded-full bg-muted-foreground/35"
style={{ left: `${range.left}%`, width: `${range.width}%` }}
/>
))}
<div
ref={fillRef}
className="absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-primary/75 to-primary"
data-testid="playback-progress-fill"
className="pointer-events-none absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-primary/75 to-primary"
style={{ width: '0%' }}
/>
<div
ref={hoverLineRef}
className="pointer-events-none absolute top-1/2 h-6 w-px -translate-y-1/2 bg-primary/55 opacity-0"
data-testid="playback-hover-line"
className="pointer-events-none absolute top-1/2 z-[2] h-6 w-px -translate-x-1/2 -translate-y-1/2 bg-primary/55 opacity-0"
style={{ left: '0%' }}
/>
<div
ref={thumbRef}
data-testid="playback-thumb"
className="pointer-events-none absolute top-1/2 z-[3] h-3 w-3 -translate-y-1/2 -translate-x-1/2 rounded-full border border-primary bg-background"
className="pointer-events-none absolute top-1/2 z-[3] h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-primary bg-background"
style={{ left: '0%' }}
/>
<div
Expand Down
Loading
Loading