diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 2c6a453..9bc9fe1 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -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 @@ -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) + + +// Replace with host branding + +``` + +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: diff --git a/docs/EMBEDDING.zh.md b/docs/EMBEDDING.zh.md index 2f49bab..84b5295 100644 --- a/docs/EMBEDDING.zh.md +++ b/docs/EMBEDDING.zh.md @@ -31,6 +31,8 @@ npm install @ioai/rosview import '@ioai/rosview/style.css'; ``` +样式在构建产物中限定在 `#rosview-root` 内,Tailwind preflight 不会重置宿主应用的全局元素(导航栏、按钮等)。 + --- ## 步骤 2 — 基本用法 @@ -133,6 +135,29 @@ const rows = parseRemoteDatasetListJson(await res.json()); --- +## 进阶:Navbar 品牌文案 + +Navbar 左侧默认显示 **ROS View**。嵌入到更大页面时,可以隐藏或替换为你的产品名: + +```tsx +// 隐藏品牌按钮(File / Layout 菜单仍保留) + + +// 替换为宿主品牌 + +``` + +相关 props: + +| Prop | 说明 | +|------|------| +| `showNavbarBrand` | 左侧品牌按钮是否显示(默认 `true`)。 | +| `navbarBrandLabel` | 自定义品牌文案;未设置时使用本地化产品名。 | +| `navbarSourceName` | 中间区域的数据源名称(与品牌文案无关)。 | +| `showNavbar` | 设为 `false` 时隐藏整条 Navbar(通过 `chrome` 或显式 prop)。 | + +--- + ## 进阶:受控主题与语言 关闭组件内部的 localStorage 持久化,由宿主应用完全控制状态: diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index 1130b38..3065814 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -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; @@ -77,6 +79,8 @@ export const AppShell: React.FC = ({ onLanguageChange, showLanguageSwitcher = true, showThemeSwitcher = true, + showNavbarBrand = true, + navbarBrandLabel, onBrandClick, preferAutoLayout = false, preferencePersistence, @@ -150,6 +154,8 @@ export const AppShell: React.FC = ({ onLanguageChange={onLanguageChange} showLanguageSwitcher={showLanguageSwitcher} showThemeSwitcher={showThemeSwitcher} + showNavbarBrand={showNavbarBrand} + brandLabel={navbarBrandLabel} onBrandClick={onBrandClick} onOpenFilePick={hideOpenFileMenus ? undefined : onOpenFilePick} onOpenDirectory={hideOpenFileMenus ? undefined : onOpenDirectory} diff --git a/src/features/panels/Plot/usePlotChart.test.ts b/src/features/panels/Plot/usePlotChart.test.ts index 382e21e..b5dcb4d 100644 --- a/src/features/panels/Plot/usePlotChart.test.ts +++ b/src/features/panels/Plot/usePlotChart.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { diffSeriesTopology, + shouldPinPlotXScaleToLogRange, shouldRemountForIncrementalSeriesUpdate, type SeriesSignature, } from './usePlotChart'; @@ -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); + }); +}); diff --git a/src/features/panels/Plot/usePlotChart.ts b/src/features/panels/Plot/usePlotChart.ts index bb475c5..1698c7b 100644 --- a/src/features/panels/Plot/usePlotChart.ts +++ b/src/features/panels/Plot/usePlotChart.ts @@ -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, @@ -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; @@ -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. diff --git a/src/features/viewer/RosViewerImpl.tsx b/src/features/viewer/RosViewerImpl.tsx index febd99f..f075b67 100644 --- a/src/features/viewer/RosViewerImpl.tsx +++ b/src/features/viewer/RosViewerImpl.tsx @@ -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 */ @@ -1145,6 +1149,8 @@ export const RosViewer: React.FC = (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} @@ -1206,6 +1212,8 @@ export const RosViewer: React.FC = (props) => { onLanguageChange={handleLanguageChange} showLanguageSwitcher={props.showLanguageSwitcher ?? true} showThemeSwitcher={props.showThemeSwitcher ?? true} + showNavbarBrand={props.showNavbarBrand ?? true} + brandLabel={props.navbarBrandLabel} onBrandClick={handleGoHome} onOpenFilePick={() => { clearOpenFeedback(); @@ -1309,6 +1317,8 @@ export const RosViewer: React.FC = (props) => { onLanguageChange={handleLanguageChange} showLanguageSwitcher={props.showLanguageSwitcher ?? true} showThemeSwitcher={props.showThemeSwitcher ?? true} + showNavbarBrand={props.showNavbarBrand ?? true} + brandLabel={props.navbarBrandLabel} onBrandClick={handleGoHome} onOpenFilePick={() => { clearOpenFeedback(); diff --git a/src/features/workspace/navbar/Navbar.tsx b/src/features/workspace/navbar/Navbar.tsx index 1ff87c6..d491612 100644 --- a/src/features/workspace/navbar/Navbar.tsx +++ b/src/features/workspace/navbar/Navbar.tsx @@ -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; @@ -103,6 +107,8 @@ export const Navbar: React.FC = ({ onLanguageChange, showLanguageSwitcher = true, showThemeSwitcher = true, + showNavbarBrand = true, + brandLabel, onBrandClick, onOpenFilePick, onOpenDirectory, @@ -164,20 +170,24 @@ export const Navbar: React.FC = ({ 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 ( - - {formatMessage({ id: 'common.productName' })} - + {showNavbarBrand ? ( + + {brandText} + + ) : null}