@mastra/playground-ui 51.4.0-alpha.1 → 51.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @mastra/playground-ui
2
2
 
3
+ ## 51.4.0-alpha.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed span search results being hidden below the fold: opening a span from the traces search now scrolls the first highlighted match in the span detail panel into view ([#22690](https://github.com/mastra-ai/mastra/pull/22690))
8
+
9
+ - Updated dependencies [[`d94e242`](https://github.com/mastra-ai/mastra/commit/d94e2423909cfc859eaf39827e83c832439e6b6d), [`8571a42`](https://github.com/mastra-ai/mastra/commit/8571a42c8039e938564e5c5fb0a6b75377c4fe67)]:
10
+ - @mastra/memory@1.28.2-alpha.0
11
+
3
12
  ## 51.4.0-alpha.1
4
13
 
5
14
  ### Minor Changes
@@ -19,6 +19,7 @@ const Notice = require('../../../Notice-BoT3Qi4U.cjs');
19
19
  const tabsRoot = require('../../../tabs-root-VtBc6K2q.cjs');
20
20
  const tabsTab = require('../../../tabs-tab-DudcFE6r.cjs');
21
21
  const tabsContent = require('../../../tabs-content-wBU5qmb6.cjs');
22
+ const hooks_useScrollToFirstHighlight = require('../../../hooks/use-scroll-to-first-highlight.cjs.js');
22
23
  const hooks_useTextHighlight = require('../../../hooks/use-text-highlight.cjs.js');
23
24
  const utils_truncateString = require('../../../utils/truncate-string.cjs.js');
24
25
 
@@ -180,7 +181,7 @@ function TraceDataPanelView({
180
181
  /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.CloseButton, { onClick: onClose })
181
182
  ] })
182
183
  ] }) }),
183
- !collapsed && /* @__PURE__ */ jsxRuntime.jsx(SplitWithSpanPanel, { spanPanelSlot, highlightQuery: query, children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.LoadingData, { children: "Loading trace..." }) : !spans?.length ? /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.NoData, { children: "No spans found for this trace." }) : /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.Content, { children: (() => {
184
+ !collapsed && /* @__PURE__ */ jsxRuntime.jsx(SplitWithSpanPanel, { spanPanelSlot, highlightQuery: query, spanPanelKey: selectedSpanId, children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.LoadingData, { children: "Loading trace..." }) : !spans?.length ? /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.NoData, { children: "No spans found for this trace." }) : /* @__PURE__ */ jsxRuntime.jsx(dataPanel.DataPanel.Content, { children: (() => {
184
185
  const detailsBody = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
185
186
  !isOnTracePage && rootSpan && /* @__PURE__ */ jsxRuntime.jsx(domains_traces_components_traceKeysAndValues.TraceKeysAndValues, { rootSpan, usage: isSubtrace ? void 0 : usage, className: "mb-6" }),
186
187
  !isOnTracePage && !onEvaluateTrace && !onSaveAsDatasetItem && !onAddTraceMocksToItem && showUnavailableFeaturesMsg && /* @__PURE__ */ jsxRuntime.jsx(Notice.Notice, { variant: "info", className: "mb-6", children: /* @__PURE__ */ jsxRuntime.jsx(Notice.Notice.Message, { children: "Evaluating traces and saving them as dataset items is available in Mastra Studio (local or deployed)." }) }),
@@ -252,9 +253,11 @@ function TraceDataPanelView({
252
253
  function SplitWithSpanPanel({
253
254
  spanPanelSlot,
254
255
  highlightQuery,
256
+ spanPanelKey,
255
257
  children
256
258
  }) {
257
259
  const { ref: highlightRef } = hooks_useTextHighlight.useTextHighlight(highlightQuery);
260
+ const { ref: scrollToMatchRef } = hooks_useScrollToFirstHighlight.useScrollToFirstHighlight(highlightQuery, spanPanelKey);
258
261
  if (!spanPanelSlot) {
259
262
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: highlightRef, className: "flex min-h-0 flex-1 flex-col overflow-hidden", children });
260
263
  }
@@ -263,6 +266,7 @@ function SplitWithSpanPanel({
263
266
  /* @__PURE__ */ jsxRuntime.jsx(
264
267
  "div",
265
268
  {
269
+ ref: scrollToMatchRef,
266
270
  "data-highlight": true,
267
271
  className: "animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300",
268
272
  children: spanPanelSlot
@@ -1 +1 @@
1
- {"version":3,"file":"trace-data-panel-view.cjs.js","sources":["../../../../src/domains/traces/components/trace-data-panel-view.tsx"],"sourcesContent":["import {\n CircleGaugeIcon,\n ChevronsDownUpIcon,\n ChevronsUpDownIcon,\n DownloadIcon,\n Link2Icon,\n Loader2Icon,\n SaveIcon,\n WrenchIcon,\n} from 'lucide-react';\nimport { useEffect, useId, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { getAllSpanIds } from '../hooks/get-all-span-ids';\nimport { useDownloadTraceJson } from '../hooks/use-download-trace-json';\nimport { useTraceSearch } from '../hooks/use-trace-search';\nimport type { TraceUsageSummary } from '../trace-list-columns';\nimport type { SearchableSpan } from '../types';\nimport { formatHierarchicalSpans } from './format-hierarchical-spans';\nimport { TraceKeysAndValues } from './trace-keys-and-values';\nimport { TraceTimeline } from './trace-timeline';\nimport { Button } from '@/ds/components/Button';\nimport { ButtonsGroup } from '@/ds/components/ButtonsGroup';\nimport { DataPanel } from '@/ds/components/DataPanel';\nimport { SearchFieldBlock } from '@/ds/components/FormFieldBlocks';\nimport { Notice } from '@/ds/components/Notice';\nimport { Tab, TabContent, TabList, Tabs } from '@/ds/components/Tabs';\nimport type { LinkComponent } from '@/ds/types/link-component';\nimport { useTextHighlight } from '@/hooks/use-text-highlight';\nimport { truncateString } from '@/lib/truncate-string';\n\nexport type TraceDataPanelPlacement = 'traces-list' | 'trace-page';\n\nexport type TraceDataPanelTab = 'details' | 'scores' | 'feedback';\n\nexport interface TraceDataPanelViewProps {\n traceId: string;\n /** Lightweight spans for the trace. Caller fetches via useTraceLightSpans. */\n spans: SearchableSpan[] | undefined;\n /**\n * Token and estimated-cost totals for the trace (from `useTraceUsage`).\n * Rendered in the trace summary when the panel is in the list side-panel\n * placement; the trace page renders its own `TraceKeysAndValues` instead.\n */\n usage?: TraceUsageSummary;\n isLoading?: boolean;\n onClose: () => void;\n onSpanSelect?: (spanId: string | undefined) => void;\n onEvaluateTrace?: () => void;\n /** When set, an \"Add full trace to dataset\" button appears; the consumer owns the dialog. */\n onSaveAsDatasetItem?: (args: { traceId: string; rootSpanId: string | undefined }) => void;\n /** When set, an \"Add tool mocks to item\" button appears; the consumer owns the dialog. */\n onAddTraceMocksToItem?: (args: { traceId: string }) => void;\n initialSpanId?: string | null;\n onPrevious?: () => void;\n onNext?: () => void;\n collapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n placement: TraceDataPanelPlacement;\n timelineChartWidth?: 'wide' | 'default';\n /** When both are provided, renders an \"Open trace page\" button. */\n LinkComponent?: LinkComponent;\n traceHref?: string;\n /**\n * Span treated as the displayed root of the timeline. Required for branch\n * subtrees from `getBranch` where the anchor has a real parent that's outside\n * `spans`. When omitted, the span with no parent is used (trace case).\n */\n anchorSpanId?: string;\n /**\n * Whether to render the \"Evaluating traces and saving them as dataset items is\n * available in Mastra Studio\" info notice when neither `onEvaluateTrace` nor\n * `onSaveAsDatasetItem` is provided. Defaults to `true`. Pass `false` when this\n * panel is rendered inside Studio in a context that intentionally omits those\n * handlers (e.g. inline below an experiment result).\n */\n showUnavailableFeaturesMsg?: boolean;\n /**\n * When provided, the panel content becomes tabbed (\"Details\" / \"Scores\"); the slot\n * renders whatever trace-level scoring UI the consumer wants.\n */\n scoresTabSlot?: (args: { traceId: string; rootSpanId: string | undefined }) => ReactNode;\n /** Optional count shown in the \"Scores\" tab label. */\n scoresTabBadge?: ReactNode;\n /**\n * When provided, a \"Feedback\" tab appears; the slot renders the trace-level\n * feedback UI. Trace feedback is not scoped to a span — the span panel owns that.\n */\n feedbackTabSlot?: (args: { traceId: string }) => ReactNode;\n /** Optional count shown in the \"Feedback\" tab label. */\n feedbackTabBadge?: ReactNode;\n activeTab?: TraceDataPanelTab;\n onTabChange?: (tab: TraceDataPanelTab) => void;\n /**\n * When provided, the panel splits into two columns inside the same card: the\n * trace content on the left, this slot (typically the span detail) on the right.\n */\n spanPanelSlot?: ReactNode;\n /** Extra classes applied to the panel root (e.g. `h-full` on the trace page). */\n className?: string;\n}\n\nexport function TraceDataPanelView({\n traceId,\n spans,\n usage,\n isLoading,\n onClose,\n onSpanSelect,\n onEvaluateTrace,\n onSaveAsDatasetItem,\n onAddTraceMocksToItem,\n initialSpanId,\n onPrevious,\n onNext,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n placement,\n timelineChartWidth = 'default',\n LinkComponent,\n traceHref,\n anchorSpanId,\n showUnavailableFeaturesMsg = true,\n scoresTabSlot,\n scoresTabBadge,\n feedbackTabSlot,\n feedbackTabBadge,\n activeTab,\n onTabChange,\n spanPanelSlot,\n className,\n}: TraceDataPanelViewProps) {\n const isOnTracePage = placement === 'trace-page';\n const [internalCollapsed, setInternalCollapsed] = useState(false);\n const collapsed = controlledCollapsed ?? internalCollapsed;\n const setCollapsed = onCollapsedChange ?? setInternalCollapsed;\n\n const { download: downloadTraceJson, isPending: isDownloadingTrace } = useDownloadTraceJson();\n\n const [selectedSpanId, setSelectedSpanId] = useState<string | undefined>(initialSpanId ?? undefined);\n\n // Sync selected span when initialSpanId or trace data changes\n useEffect(() => {\n // No span requested: clear immediately.\n if (!initialSpanId) {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n return;\n }\n // Span requested: wait for trace data before deciding so an in-flight\n // fetch doesn't wipe a URL-provided selection. Callers that default their\n // spans to `[]` while loading only say so through `isLoading`.\n if (isLoading || !spans) return;\n\n const found = spans.find(s => s.spanId === initialSpanId);\n if (found) {\n setSelectedSpanId(initialSpanId);\n onSpanSelect?.(initialSpanId);\n } else {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialSpanId, spans, isLoading]);\n\n const searchFieldName = useId();\n const { query, setQuery, results, payloadOnlyMatchIds } = useTraceSearch(spans ?? []);\n\n const hierarchicalSpans = useMemo(\n () =>\n formatHierarchicalSpans(\n // Carried on the span rather than drilled as a prop: the tree is rebuilt here and\n // rendered several components deeper.\n results.map(span => ({ ...span, matchedInPayloadOnly: payloadOnlyMatchIds.has(span.spanId) })),\n anchorSpanId,\n ),\n [results, payloadOnlyMatchIds, anchorSpanId],\n );\n\n const [expandedSpanIds, setExpandedSpanIds] = useState<string[]>([]);\n\n useEffect(() => {\n if (hierarchicalSpans.length > 0) {\n setExpandedSpanIds(getAllSpanIds(hierarchicalSpans));\n }\n }, [hierarchicalSpans]);\n\n const rootSpan = useMemo(\n () => (anchorSpanId ? spans?.find(s => s.spanId === anchorSpanId) : spans?.find(s => s.parentSpanId == null)),\n [spans, anchorSpanId],\n );\n const isSubtrace = anchorSpanId !== undefined && rootSpan?.parentSpanId != null;\n\n const handleSpanClick = (id: string) => {\n const newId = selectedSpanId === id ? undefined : id;\n setSelectedSpanId(newId);\n onSpanSelect?.(newId);\n };\n\n const showOpenTracePageLink = !isOnTracePage && LinkComponent && traceHref;\n\n // Shared across both header layouts (list side panel and full trace page) so a trace can be\n // downloaded from wherever it's being inspected.\n const downloadTraceButton = (\n <Button\n size=\"md\"\n tooltip=\"Download trace JSON\"\n aria-label=\"Download trace JSON\"\n disabled={isDownloadingTrace}\n onClick={() => downloadTraceJson(traceId)}\n >\n {isDownloadingTrace ? <Loader2Icon className=\"animate-spin\" /> : <DownloadIcon />}\n </Button>\n );\n\n return (\n <DataPanel collapsed={collapsed} className={className}>\n <DataPanel.Header>\n {isOnTracePage ? (\n <>\n <DataPanel.Heading>Trace Timeline</DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">{downloadTraceButton}</ButtonsGroup>\n </>\n ) : (\n <>\n <DataPanel.Heading>\n Trace <b># {truncateString(traceId, 12)}</b>\n </DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">\n {onCollapsedChange && (\n <Button\n size=\"md\"\n tooltip={collapsed ? 'Expand panel' : 'Collapse panel'}\n onClick={() => setCollapsed(!collapsed)}\n >\n {collapsed ? <ChevronsUpDownIcon /> : <ChevronsDownUpIcon />}\n </Button>\n )}\n {onEvaluateTrace && (\n <Button size=\"md\" tooltip=\"Evaluate trace\" aria-label=\"Evaluate trace\" onClick={onEvaluateTrace}>\n <CircleGaugeIcon />\n </Button>\n )}\n {onSaveAsDatasetItem && (\n <Button\n size=\"md\"\n tooltip=\"Add full trace to dataset\"\n aria-label=\"Add full trace to dataset\"\n onClick={() => onSaveAsDatasetItem({ traceId, rootSpanId: rootSpan?.spanId })}\n >\n <SaveIcon />\n </Button>\n )}\n {onAddTraceMocksToItem && (\n <Button\n size=\"md\"\n tooltip=\"Add tool mocks to item\"\n aria-label=\"Add tool mocks to item\"\n onClick={() => onAddTraceMocksToItem({ traceId })}\n >\n <WrenchIcon />\n </Button>\n )}\n {(onPrevious || onNext) && (\n <DataPanel.NextPrevNav\n onPrevious={onPrevious}\n onNext={onNext}\n previousLabel=\"Previous trace\"\n nextLabel=\"Next trace\"\n />\n )}\n {showOpenTracePageLink && (\n <Button\n as={LinkComponent}\n href={traceHref}\n size=\"md\"\n tooltip=\"Open trace page\"\n aria-label=\"Open trace page\"\n >\n <Link2Icon />\n </Button>\n )}\n {downloadTraceButton}\n <DataPanel.CloseButton onClick={onClose} />\n </ButtonsGroup>\n </>\n )}\n </DataPanel.Header>\n\n {!collapsed && (\n <SplitWithSpanPanel spanPanelSlot={spanPanelSlot} highlightQuery={query}>\n {isLoading ? (\n <DataPanel.LoadingData>Loading trace...</DataPanel.LoadingData>\n ) : !spans?.length ? (\n <DataPanel.NoData>No spans found for this trace.</DataPanel.NoData>\n ) : (\n <DataPanel.Content>\n {(() => {\n const detailsBody = (\n <>\n {!isOnTracePage && rootSpan && (\n <TraceKeysAndValues rootSpan={rootSpan} usage={isSubtrace ? undefined : usage} className=\"mb-6\" />\n )}\n\n {!isOnTracePage &&\n !onEvaluateTrace &&\n !onSaveAsDatasetItem &&\n !onAddTraceMocksToItem &&\n showUnavailableFeaturesMsg && (\n <Notice variant=\"info\" className=\"mb-6\">\n <Notice.Message>\n Evaluating traces and saving them as dataset items is available in Mastra Studio (local or\n deployed).\n </Notice.Message>\n </Notice>\n )}\n\n {/* The timeline stays mounted even with no results, because it\n hosts the search field: unmounting it would strand the user\n with a query they can no longer clear. */}\n <TraceTimeline\n hierarchicalSpans={hierarchicalSpans}\n onSpanClick={handleSpanClick}\n selectedSpanId={selectedSpanId}\n expandedSpanIds={expandedSpanIds}\n setExpandedSpanIds={setExpandedSpanIds}\n chartWidth={timelineChartWidth}\n leadingSlot={\n <SearchFieldBlock\n name={searchFieldName}\n label=\"Search spans\"\n labelIsHidden\n placeholder=\"Search spans...\"\n value={query}\n onChange={e => setQuery(e.target.value)}\n onReset={() => setQuery('')}\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n />\n }\n />\n\n {hierarchicalSpans.length === 0 && <DataPanel.NoData>No spans match your search.</DataPanel.NoData>}\n </>\n );\n\n // No extra tab slots → render details directly without the Tabs wrapper.\n if (!scoresTabSlot && !feedbackTabSlot) return detailsBody;\n\n return (\n <Tabs<TraceDataPanelTab>\n defaultTab=\"details\"\n value={activeTab}\n onValueChange={onTabChange}\n className={\n activeTab === 'scores' || activeTab === 'feedback'\n ? 'grid h-full min-h-0 grid-rows-[auto_1fr]'\n : undefined\n }\n >\n <TabList variant=\"pill-ghost\" className=\"px-0\">\n <Tab value=\"details\">Details</Tab>\n {scoresTabSlot && (\n <Tab value=\"scores\">Evaluations {scoresTabBadge != null && <>({scoresTabBadge})</>}</Tab>\n )}\n {feedbackTabSlot && (\n <Tab value=\"feedback\">Feedback {feedbackTabBadge != null && <>({feedbackTabBadge})</>}</Tab>\n )}\n </TabList>\n\n <TabContent value=\"details\">{detailsBody}</TabContent>\n {scoresTabSlot && (\n <TabContent value=\"scores\" className=\"h-full min-h-0\">\n {scoresTabSlot({ traceId, rootSpanId: rootSpan?.spanId })}\n </TabContent>\n )}\n {feedbackTabSlot && (\n <TabContent value=\"feedback\" className=\"h-full min-h-0\">\n {feedbackTabSlot({ traceId })}\n </TabContent>\n )}\n </Tabs>\n );\n })()}\n </DataPanel.Content>\n )}\n </SplitWithSpanPanel>\n )}\n </DataPanel>\n );\n}\n\n/**\n * Renders the trace content as-is, or — when a span panel is provided — as a\n * two-column split inside the same card, with the span detail on the right.\n * Search matches — span names in the timeline tree as well as values in the span\n * detail — are highlighted while a query is active.\n */\nfunction SplitWithSpanPanel({\n spanPanelSlot,\n highlightQuery,\n children,\n}: {\n spanPanelSlot?: ReactNode;\n highlightQuery: string;\n children: ReactNode;\n}) {\n // A single hook call on the common ancestor covers both the timeline tree and\n // the span detail, so span names and payload values highlight together.\n const { ref: highlightRef } = useTextHighlight<HTMLDivElement>(highlightQuery);\n\n if (!spanPanelSlot) {\n return (\n <div ref={highlightRef} className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">\n {children}\n </div>\n );\n }\n\n return (\n <div ref={highlightRef} className=\"grid min-h-0 flex-1 grid-cols-[1fr_1fr]\">\n <div className=\"flex min-h-0 flex-col overflow-hidden\">{children}</div>\n {/* Searchable: the span detail is where a match hides inside a large payload. */}\n <div\n data-highlight\n className=\"animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300\"\n >\n {spanPanelSlot}\n </div>\n </div>\n );\n}\n"],"names":["useState","useDownloadTraceJson","useEffect","useId","useTraceSearch","useMemo","formatHierarchicalSpans","getAllSpanIds","jsx","Button","Loader2Icon","DownloadIcon","jsxs","DataPanel","Fragment","ButtonsGroup","truncateString","ChevronsUpDownIcon","ChevronsDownUpIcon","CircleGaugeIcon","SaveIcon","WrenchIcon","Link2Icon","TraceKeysAndValues","Notice","TraceTimeline","SearchFieldBlock","Tabs","TabList","Tab","TabContent","useTextHighlight"],"mappings":"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"}
1
+ {"version":3,"file":"trace-data-panel-view.cjs.js","sources":["../../../../src/domains/traces/components/trace-data-panel-view.tsx"],"sourcesContent":["import {\n CircleGaugeIcon,\n ChevronsDownUpIcon,\n ChevronsUpDownIcon,\n DownloadIcon,\n Link2Icon,\n Loader2Icon,\n SaveIcon,\n WrenchIcon,\n} from 'lucide-react';\nimport { useEffect, useId, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { getAllSpanIds } from '../hooks/get-all-span-ids';\nimport { useDownloadTraceJson } from '../hooks/use-download-trace-json';\nimport { useTraceSearch } from '../hooks/use-trace-search';\nimport type { TraceUsageSummary } from '../trace-list-columns';\nimport type { SearchableSpan } from '../types';\nimport { formatHierarchicalSpans } from './format-hierarchical-spans';\nimport { TraceKeysAndValues } from './trace-keys-and-values';\nimport { TraceTimeline } from './trace-timeline';\nimport { Button } from '@/ds/components/Button';\nimport { ButtonsGroup } from '@/ds/components/ButtonsGroup';\nimport { DataPanel } from '@/ds/components/DataPanel';\nimport { SearchFieldBlock } from '@/ds/components/FormFieldBlocks';\nimport { Notice } from '@/ds/components/Notice';\nimport { Tab, TabContent, TabList, Tabs } from '@/ds/components/Tabs';\nimport type { LinkComponent } from '@/ds/types/link-component';\nimport { useScrollToFirstHighlight } from '@/hooks/use-scroll-to-first-highlight';\nimport { useTextHighlight } from '@/hooks/use-text-highlight';\nimport { truncateString } from '@/lib/truncate-string';\n\nexport type TraceDataPanelPlacement = 'traces-list' | 'trace-page';\n\nexport type TraceDataPanelTab = 'details' | 'scores' | 'feedback';\n\nexport interface TraceDataPanelViewProps {\n traceId: string;\n /** Lightweight spans for the trace. Caller fetches via useTraceLightSpans. */\n spans: SearchableSpan[] | undefined;\n /**\n * Token and estimated-cost totals for the trace (from `useTraceUsage`).\n * Rendered in the trace summary when the panel is in the list side-panel\n * placement; the trace page renders its own `TraceKeysAndValues` instead.\n */\n usage?: TraceUsageSummary;\n isLoading?: boolean;\n onClose: () => void;\n onSpanSelect?: (spanId: string | undefined) => void;\n onEvaluateTrace?: () => void;\n /** When set, an \"Add full trace to dataset\" button appears; the consumer owns the dialog. */\n onSaveAsDatasetItem?: (args: { traceId: string; rootSpanId: string | undefined }) => void;\n /** When set, an \"Add tool mocks to item\" button appears; the consumer owns the dialog. */\n onAddTraceMocksToItem?: (args: { traceId: string }) => void;\n initialSpanId?: string | null;\n onPrevious?: () => void;\n onNext?: () => void;\n collapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n placement: TraceDataPanelPlacement;\n timelineChartWidth?: 'wide' | 'default';\n /** When both are provided, renders an \"Open trace page\" button. */\n LinkComponent?: LinkComponent;\n traceHref?: string;\n /**\n * Span treated as the displayed root of the timeline. Required for branch\n * subtrees from `getBranch` where the anchor has a real parent that's outside\n * `spans`. When omitted, the span with no parent is used (trace case).\n */\n anchorSpanId?: string;\n /**\n * Whether to render the \"Evaluating traces and saving them as dataset items is\n * available in Mastra Studio\" info notice when neither `onEvaluateTrace` nor\n * `onSaveAsDatasetItem` is provided. Defaults to `true`. Pass `false` when this\n * panel is rendered inside Studio in a context that intentionally omits those\n * handlers (e.g. inline below an experiment result).\n */\n showUnavailableFeaturesMsg?: boolean;\n /**\n * When provided, the panel content becomes tabbed (\"Details\" / \"Scores\"); the slot\n * renders whatever trace-level scoring UI the consumer wants.\n */\n scoresTabSlot?: (args: { traceId: string; rootSpanId: string | undefined }) => ReactNode;\n /** Optional count shown in the \"Scores\" tab label. */\n scoresTabBadge?: ReactNode;\n /**\n * When provided, a \"Feedback\" tab appears; the slot renders the trace-level\n * feedback UI. Trace feedback is not scoped to a span — the span panel owns that.\n */\n feedbackTabSlot?: (args: { traceId: string }) => ReactNode;\n /** Optional count shown in the \"Feedback\" tab label. */\n feedbackTabBadge?: ReactNode;\n activeTab?: TraceDataPanelTab;\n onTabChange?: (tab: TraceDataPanelTab) => void;\n /**\n * When provided, the panel splits into two columns inside the same card: the\n * trace content on the left, this slot (typically the span detail) on the right.\n */\n spanPanelSlot?: ReactNode;\n /** Extra classes applied to the panel root (e.g. `h-full` on the trace page). */\n className?: string;\n}\n\nexport function TraceDataPanelView({\n traceId,\n spans,\n usage,\n isLoading,\n onClose,\n onSpanSelect,\n onEvaluateTrace,\n onSaveAsDatasetItem,\n onAddTraceMocksToItem,\n initialSpanId,\n onPrevious,\n onNext,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n placement,\n timelineChartWidth = 'default',\n LinkComponent,\n traceHref,\n anchorSpanId,\n showUnavailableFeaturesMsg = true,\n scoresTabSlot,\n scoresTabBadge,\n feedbackTabSlot,\n feedbackTabBadge,\n activeTab,\n onTabChange,\n spanPanelSlot,\n className,\n}: TraceDataPanelViewProps) {\n const isOnTracePage = placement === 'trace-page';\n const [internalCollapsed, setInternalCollapsed] = useState(false);\n const collapsed = controlledCollapsed ?? internalCollapsed;\n const setCollapsed = onCollapsedChange ?? setInternalCollapsed;\n\n const { download: downloadTraceJson, isPending: isDownloadingTrace } = useDownloadTraceJson();\n\n const [selectedSpanId, setSelectedSpanId] = useState<string | undefined>(initialSpanId ?? undefined);\n\n // Sync selected span when initialSpanId or trace data changes\n useEffect(() => {\n // No span requested: clear immediately.\n if (!initialSpanId) {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n return;\n }\n // Span requested: wait for trace data before deciding so an in-flight\n // fetch doesn't wipe a URL-provided selection. Callers that default their\n // spans to `[]` while loading only say so through `isLoading`.\n if (isLoading || !spans) return;\n\n const found = spans.find(s => s.spanId === initialSpanId);\n if (found) {\n setSelectedSpanId(initialSpanId);\n onSpanSelect?.(initialSpanId);\n } else {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialSpanId, spans, isLoading]);\n\n const searchFieldName = useId();\n const { query, setQuery, results, payloadOnlyMatchIds } = useTraceSearch(spans ?? []);\n\n const hierarchicalSpans = useMemo(\n () =>\n formatHierarchicalSpans(\n // Carried on the span rather than drilled as a prop: the tree is rebuilt here and\n // rendered several components deeper.\n results.map(span => ({ ...span, matchedInPayloadOnly: payloadOnlyMatchIds.has(span.spanId) })),\n anchorSpanId,\n ),\n [results, payloadOnlyMatchIds, anchorSpanId],\n );\n\n const [expandedSpanIds, setExpandedSpanIds] = useState<string[]>([]);\n\n useEffect(() => {\n if (hierarchicalSpans.length > 0) {\n setExpandedSpanIds(getAllSpanIds(hierarchicalSpans));\n }\n }, [hierarchicalSpans]);\n\n const rootSpan = useMemo(\n () => (anchorSpanId ? spans?.find(s => s.spanId === anchorSpanId) : spans?.find(s => s.parentSpanId == null)),\n [spans, anchorSpanId],\n );\n const isSubtrace = anchorSpanId !== undefined && rootSpan?.parentSpanId != null;\n\n const handleSpanClick = (id: string) => {\n const newId = selectedSpanId === id ? undefined : id;\n setSelectedSpanId(newId);\n onSpanSelect?.(newId);\n };\n\n const showOpenTracePageLink = !isOnTracePage && LinkComponent && traceHref;\n\n // Shared across both header layouts (list side panel and full trace page) so a trace can be\n // downloaded from wherever it's being inspected.\n const downloadTraceButton = (\n <Button\n size=\"md\"\n tooltip=\"Download trace JSON\"\n aria-label=\"Download trace JSON\"\n disabled={isDownloadingTrace}\n onClick={() => downloadTraceJson(traceId)}\n >\n {isDownloadingTrace ? <Loader2Icon className=\"animate-spin\" /> : <DownloadIcon />}\n </Button>\n );\n\n return (\n <DataPanel collapsed={collapsed} className={className}>\n <DataPanel.Header>\n {isOnTracePage ? (\n <>\n <DataPanel.Heading>Trace Timeline</DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">{downloadTraceButton}</ButtonsGroup>\n </>\n ) : (\n <>\n <DataPanel.Heading>\n Trace <b># {truncateString(traceId, 12)}</b>\n </DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">\n {onCollapsedChange && (\n <Button\n size=\"md\"\n tooltip={collapsed ? 'Expand panel' : 'Collapse panel'}\n onClick={() => setCollapsed(!collapsed)}\n >\n {collapsed ? <ChevronsUpDownIcon /> : <ChevronsDownUpIcon />}\n </Button>\n )}\n {onEvaluateTrace && (\n <Button size=\"md\" tooltip=\"Evaluate trace\" aria-label=\"Evaluate trace\" onClick={onEvaluateTrace}>\n <CircleGaugeIcon />\n </Button>\n )}\n {onSaveAsDatasetItem && (\n <Button\n size=\"md\"\n tooltip=\"Add full trace to dataset\"\n aria-label=\"Add full trace to dataset\"\n onClick={() => onSaveAsDatasetItem({ traceId, rootSpanId: rootSpan?.spanId })}\n >\n <SaveIcon />\n </Button>\n )}\n {onAddTraceMocksToItem && (\n <Button\n size=\"md\"\n tooltip=\"Add tool mocks to item\"\n aria-label=\"Add tool mocks to item\"\n onClick={() => onAddTraceMocksToItem({ traceId })}\n >\n <WrenchIcon />\n </Button>\n )}\n {(onPrevious || onNext) && (\n <DataPanel.NextPrevNav\n onPrevious={onPrevious}\n onNext={onNext}\n previousLabel=\"Previous trace\"\n nextLabel=\"Next trace\"\n />\n )}\n {showOpenTracePageLink && (\n <Button\n as={LinkComponent}\n href={traceHref}\n size=\"md\"\n tooltip=\"Open trace page\"\n aria-label=\"Open trace page\"\n >\n <Link2Icon />\n </Button>\n )}\n {downloadTraceButton}\n <DataPanel.CloseButton onClick={onClose} />\n </ButtonsGroup>\n </>\n )}\n </DataPanel.Header>\n\n {!collapsed && (\n <SplitWithSpanPanel spanPanelSlot={spanPanelSlot} highlightQuery={query} spanPanelKey={selectedSpanId}>\n {isLoading ? (\n <DataPanel.LoadingData>Loading trace...</DataPanel.LoadingData>\n ) : !spans?.length ? (\n <DataPanel.NoData>No spans found for this trace.</DataPanel.NoData>\n ) : (\n <DataPanel.Content>\n {(() => {\n const detailsBody = (\n <>\n {!isOnTracePage && rootSpan && (\n <TraceKeysAndValues rootSpan={rootSpan} usage={isSubtrace ? undefined : usage} className=\"mb-6\" />\n )}\n\n {!isOnTracePage &&\n !onEvaluateTrace &&\n !onSaveAsDatasetItem &&\n !onAddTraceMocksToItem &&\n showUnavailableFeaturesMsg && (\n <Notice variant=\"info\" className=\"mb-6\">\n <Notice.Message>\n Evaluating traces and saving them as dataset items is available in Mastra Studio (local or\n deployed).\n </Notice.Message>\n </Notice>\n )}\n\n {/* The timeline stays mounted even with no results, because it\n hosts the search field: unmounting it would strand the user\n with a query they can no longer clear. */}\n <TraceTimeline\n hierarchicalSpans={hierarchicalSpans}\n onSpanClick={handleSpanClick}\n selectedSpanId={selectedSpanId}\n expandedSpanIds={expandedSpanIds}\n setExpandedSpanIds={setExpandedSpanIds}\n chartWidth={timelineChartWidth}\n leadingSlot={\n <SearchFieldBlock\n name={searchFieldName}\n label=\"Search spans\"\n labelIsHidden\n placeholder=\"Search spans...\"\n value={query}\n onChange={e => setQuery(e.target.value)}\n onReset={() => setQuery('')}\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n />\n }\n />\n\n {hierarchicalSpans.length === 0 && <DataPanel.NoData>No spans match your search.</DataPanel.NoData>}\n </>\n );\n\n // No extra tab slots → render details directly without the Tabs wrapper.\n if (!scoresTabSlot && !feedbackTabSlot) return detailsBody;\n\n return (\n <Tabs<TraceDataPanelTab>\n defaultTab=\"details\"\n value={activeTab}\n onValueChange={onTabChange}\n className={\n activeTab === 'scores' || activeTab === 'feedback'\n ? 'grid h-full min-h-0 grid-rows-[auto_1fr]'\n : undefined\n }\n >\n <TabList variant=\"pill-ghost\" className=\"px-0\">\n <Tab value=\"details\">Details</Tab>\n {scoresTabSlot && (\n <Tab value=\"scores\">Evaluations {scoresTabBadge != null && <>({scoresTabBadge})</>}</Tab>\n )}\n {feedbackTabSlot && (\n <Tab value=\"feedback\">Feedback {feedbackTabBadge != null && <>({feedbackTabBadge})</>}</Tab>\n )}\n </TabList>\n\n <TabContent value=\"details\">{detailsBody}</TabContent>\n {scoresTabSlot && (\n <TabContent value=\"scores\" className=\"h-full min-h-0\">\n {scoresTabSlot({ traceId, rootSpanId: rootSpan?.spanId })}\n </TabContent>\n )}\n {feedbackTabSlot && (\n <TabContent value=\"feedback\" className=\"h-full min-h-0\">\n {feedbackTabSlot({ traceId })}\n </TabContent>\n )}\n </Tabs>\n );\n })()}\n </DataPanel.Content>\n )}\n </SplitWithSpanPanel>\n )}\n </DataPanel>\n );\n}\n\n/**\n * Renders the trace content as-is, or — when a span panel is provided — as a\n * two-column split inside the same card, with the span detail on the right.\n * Search matches — span names in the timeline tree as well as values in the span\n * detail — are highlighted while a query is active.\n */\nfunction SplitWithSpanPanel({\n spanPanelSlot,\n highlightQuery,\n spanPanelKey,\n children,\n}: {\n spanPanelSlot?: ReactNode;\n highlightQuery: string;\n /** Identity of the span shown in the panel; changing it re-triggers the match scroll. */\n spanPanelKey?: string;\n children: ReactNode;\n}) {\n // A single hook call on the common ancestor covers both the timeline tree and\n // the span detail, so span names and payload values highlight together.\n const { ref: highlightRef } = useTextHighlight<HTMLDivElement>(highlightQuery);\n\n // Scoped to the span-panel column only: a match deep in a large payload sits below\n // the fold, so the first painted match is brought into view when the panel opens.\n // The timeline column must never be scrolled by this.\n const { ref: scrollToMatchRef } = useScrollToFirstHighlight<HTMLDivElement>(highlightQuery, spanPanelKey);\n\n if (!spanPanelSlot) {\n return (\n <div ref={highlightRef} className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">\n {children}\n </div>\n );\n }\n\n return (\n <div ref={highlightRef} className=\"grid min-h-0 flex-1 grid-cols-[1fr_1fr]\">\n <div className=\"flex min-h-0 flex-col overflow-hidden\">{children}</div>\n {/* Searchable: the span detail is where a match hides inside a large payload. */}\n <div\n ref={scrollToMatchRef}\n data-highlight\n className=\"animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300\"\n >\n {spanPanelSlot}\n </div>\n </div>\n );\n}\n"],"names":["useState","useDownloadTraceJson","useEffect","useId","useTraceSearch","useMemo","formatHierarchicalSpans","getAllSpanIds","jsx","Button","Loader2Icon","DownloadIcon","jsxs","DataPanel","Fragment","ButtonsGroup","truncateString","ChevronsUpDownIcon","ChevronsDownUpIcon","CircleGaugeIcon","SaveIcon","WrenchIcon","Link2Icon","TraceKeysAndValues","Notice","TraceTimeline","SearchFieldBlock","Tabs","TabList","Tab","TabContent","useTextHighlight","useScrollToFirstHighlight"],"mappings":"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"}
@@ -15,6 +15,7 @@ import { N as Notice } from '../../../Notice-XR_HdGMF.js';
15
15
  import { a as Tabs, T as TabList } from '../../../tabs-root-BBPou4QC.js';
16
16
  import { T as Tab } from '../../../tabs-tab-mZ-Tl7-d.js';
17
17
  import { T as TabContent } from '../../../tabs-content-XUh680fO.js';
18
+ import { useScrollToFirstHighlight } from '../../../hooks/use-scroll-to-first-highlight.es.js';
18
19
  import { useTextHighlight } from '../../../hooks/use-text-highlight.es.js';
19
20
  import { truncateString } from '../../../utils/truncate-string.es.js';
20
21
 
@@ -176,7 +177,7 @@ function TraceDataPanelView({
176
177
  /* @__PURE__ */ jsx(DataPanel.CloseButton, { onClick: onClose })
177
178
  ] })
178
179
  ] }) }),
179
- !collapsed && /* @__PURE__ */ jsx(SplitWithSpanPanel, { spanPanelSlot, highlightQuery: query, children: isLoading ? /* @__PURE__ */ jsx(DataPanel.LoadingData, { children: "Loading trace..." }) : !spans?.length ? /* @__PURE__ */ jsx(DataPanel.NoData, { children: "No spans found for this trace." }) : /* @__PURE__ */ jsx(DataPanel.Content, { children: (() => {
180
+ !collapsed && /* @__PURE__ */ jsx(SplitWithSpanPanel, { spanPanelSlot, highlightQuery: query, spanPanelKey: selectedSpanId, children: isLoading ? /* @__PURE__ */ jsx(DataPanel.LoadingData, { children: "Loading trace..." }) : !spans?.length ? /* @__PURE__ */ jsx(DataPanel.NoData, { children: "No spans found for this trace." }) : /* @__PURE__ */ jsx(DataPanel.Content, { children: (() => {
180
181
  const detailsBody = /* @__PURE__ */ jsxs(Fragment, { children: [
181
182
  !isOnTracePage && rootSpan && /* @__PURE__ */ jsx(TraceKeysAndValues, { rootSpan, usage: isSubtrace ? void 0 : usage, className: "mb-6" }),
182
183
  !isOnTracePage && !onEvaluateTrace && !onSaveAsDatasetItem && !onAddTraceMocksToItem && showUnavailableFeaturesMsg && /* @__PURE__ */ jsx(Notice, { variant: "info", className: "mb-6", children: /* @__PURE__ */ jsx(Notice.Message, { children: "Evaluating traces and saving them as dataset items is available in Mastra Studio (local or deployed)." }) }),
@@ -248,9 +249,11 @@ function TraceDataPanelView({
248
249
  function SplitWithSpanPanel({
249
250
  spanPanelSlot,
250
251
  highlightQuery,
252
+ spanPanelKey,
251
253
  children
252
254
  }) {
253
255
  const { ref: highlightRef } = useTextHighlight(highlightQuery);
256
+ const { ref: scrollToMatchRef } = useScrollToFirstHighlight(highlightQuery, spanPanelKey);
254
257
  if (!spanPanelSlot) {
255
258
  return /* @__PURE__ */ jsx("div", { ref: highlightRef, className: "flex min-h-0 flex-1 flex-col overflow-hidden", children });
256
259
  }
@@ -259,6 +262,7 @@ function SplitWithSpanPanel({
259
262
  /* @__PURE__ */ jsx(
260
263
  "div",
261
264
  {
265
+ ref: scrollToMatchRef,
262
266
  "data-highlight": true,
263
267
  className: "animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300",
264
268
  children: spanPanelSlot
@@ -1 +1 @@
1
- {"version":3,"file":"trace-data-panel-view.es.js","sources":["../../../../src/domains/traces/components/trace-data-panel-view.tsx"],"sourcesContent":["import {\n CircleGaugeIcon,\n ChevronsDownUpIcon,\n ChevronsUpDownIcon,\n DownloadIcon,\n Link2Icon,\n Loader2Icon,\n SaveIcon,\n WrenchIcon,\n} from 'lucide-react';\nimport { useEffect, useId, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { getAllSpanIds } from '../hooks/get-all-span-ids';\nimport { useDownloadTraceJson } from '../hooks/use-download-trace-json';\nimport { useTraceSearch } from '../hooks/use-trace-search';\nimport type { TraceUsageSummary } from '../trace-list-columns';\nimport type { SearchableSpan } from '../types';\nimport { formatHierarchicalSpans } from './format-hierarchical-spans';\nimport { TraceKeysAndValues } from './trace-keys-and-values';\nimport { TraceTimeline } from './trace-timeline';\nimport { Button } from '@/ds/components/Button';\nimport { ButtonsGroup } from '@/ds/components/ButtonsGroup';\nimport { DataPanel } from '@/ds/components/DataPanel';\nimport { SearchFieldBlock } from '@/ds/components/FormFieldBlocks';\nimport { Notice } from '@/ds/components/Notice';\nimport { Tab, TabContent, TabList, Tabs } from '@/ds/components/Tabs';\nimport type { LinkComponent } from '@/ds/types/link-component';\nimport { useTextHighlight } from '@/hooks/use-text-highlight';\nimport { truncateString } from '@/lib/truncate-string';\n\nexport type TraceDataPanelPlacement = 'traces-list' | 'trace-page';\n\nexport type TraceDataPanelTab = 'details' | 'scores' | 'feedback';\n\nexport interface TraceDataPanelViewProps {\n traceId: string;\n /** Lightweight spans for the trace. Caller fetches via useTraceLightSpans. */\n spans: SearchableSpan[] | undefined;\n /**\n * Token and estimated-cost totals for the trace (from `useTraceUsage`).\n * Rendered in the trace summary when the panel is in the list side-panel\n * placement; the trace page renders its own `TraceKeysAndValues` instead.\n */\n usage?: TraceUsageSummary;\n isLoading?: boolean;\n onClose: () => void;\n onSpanSelect?: (spanId: string | undefined) => void;\n onEvaluateTrace?: () => void;\n /** When set, an \"Add full trace to dataset\" button appears; the consumer owns the dialog. */\n onSaveAsDatasetItem?: (args: { traceId: string; rootSpanId: string | undefined }) => void;\n /** When set, an \"Add tool mocks to item\" button appears; the consumer owns the dialog. */\n onAddTraceMocksToItem?: (args: { traceId: string }) => void;\n initialSpanId?: string | null;\n onPrevious?: () => void;\n onNext?: () => void;\n collapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n placement: TraceDataPanelPlacement;\n timelineChartWidth?: 'wide' | 'default';\n /** When both are provided, renders an \"Open trace page\" button. */\n LinkComponent?: LinkComponent;\n traceHref?: string;\n /**\n * Span treated as the displayed root of the timeline. Required for branch\n * subtrees from `getBranch` where the anchor has a real parent that's outside\n * `spans`. When omitted, the span with no parent is used (trace case).\n */\n anchorSpanId?: string;\n /**\n * Whether to render the \"Evaluating traces and saving them as dataset items is\n * available in Mastra Studio\" info notice when neither `onEvaluateTrace` nor\n * `onSaveAsDatasetItem` is provided. Defaults to `true`. Pass `false` when this\n * panel is rendered inside Studio in a context that intentionally omits those\n * handlers (e.g. inline below an experiment result).\n */\n showUnavailableFeaturesMsg?: boolean;\n /**\n * When provided, the panel content becomes tabbed (\"Details\" / \"Scores\"); the slot\n * renders whatever trace-level scoring UI the consumer wants.\n */\n scoresTabSlot?: (args: { traceId: string; rootSpanId: string | undefined }) => ReactNode;\n /** Optional count shown in the \"Scores\" tab label. */\n scoresTabBadge?: ReactNode;\n /**\n * When provided, a \"Feedback\" tab appears; the slot renders the trace-level\n * feedback UI. Trace feedback is not scoped to a span — the span panel owns that.\n */\n feedbackTabSlot?: (args: { traceId: string }) => ReactNode;\n /** Optional count shown in the \"Feedback\" tab label. */\n feedbackTabBadge?: ReactNode;\n activeTab?: TraceDataPanelTab;\n onTabChange?: (tab: TraceDataPanelTab) => void;\n /**\n * When provided, the panel splits into two columns inside the same card: the\n * trace content on the left, this slot (typically the span detail) on the right.\n */\n spanPanelSlot?: ReactNode;\n /** Extra classes applied to the panel root (e.g. `h-full` on the trace page). */\n className?: string;\n}\n\nexport function TraceDataPanelView({\n traceId,\n spans,\n usage,\n isLoading,\n onClose,\n onSpanSelect,\n onEvaluateTrace,\n onSaveAsDatasetItem,\n onAddTraceMocksToItem,\n initialSpanId,\n onPrevious,\n onNext,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n placement,\n timelineChartWidth = 'default',\n LinkComponent,\n traceHref,\n anchorSpanId,\n showUnavailableFeaturesMsg = true,\n scoresTabSlot,\n scoresTabBadge,\n feedbackTabSlot,\n feedbackTabBadge,\n activeTab,\n onTabChange,\n spanPanelSlot,\n className,\n}: TraceDataPanelViewProps) {\n const isOnTracePage = placement === 'trace-page';\n const [internalCollapsed, setInternalCollapsed] = useState(false);\n const collapsed = controlledCollapsed ?? internalCollapsed;\n const setCollapsed = onCollapsedChange ?? setInternalCollapsed;\n\n const { download: downloadTraceJson, isPending: isDownloadingTrace } = useDownloadTraceJson();\n\n const [selectedSpanId, setSelectedSpanId] = useState<string | undefined>(initialSpanId ?? undefined);\n\n // Sync selected span when initialSpanId or trace data changes\n useEffect(() => {\n // No span requested: clear immediately.\n if (!initialSpanId) {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n return;\n }\n // Span requested: wait for trace data before deciding so an in-flight\n // fetch doesn't wipe a URL-provided selection. Callers that default their\n // spans to `[]` while loading only say so through `isLoading`.\n if (isLoading || !spans) return;\n\n const found = spans.find(s => s.spanId === initialSpanId);\n if (found) {\n setSelectedSpanId(initialSpanId);\n onSpanSelect?.(initialSpanId);\n } else {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialSpanId, spans, isLoading]);\n\n const searchFieldName = useId();\n const { query, setQuery, results, payloadOnlyMatchIds } = useTraceSearch(spans ?? []);\n\n const hierarchicalSpans = useMemo(\n () =>\n formatHierarchicalSpans(\n // Carried on the span rather than drilled as a prop: the tree is rebuilt here and\n // rendered several components deeper.\n results.map(span => ({ ...span, matchedInPayloadOnly: payloadOnlyMatchIds.has(span.spanId) })),\n anchorSpanId,\n ),\n [results, payloadOnlyMatchIds, anchorSpanId],\n );\n\n const [expandedSpanIds, setExpandedSpanIds] = useState<string[]>([]);\n\n useEffect(() => {\n if (hierarchicalSpans.length > 0) {\n setExpandedSpanIds(getAllSpanIds(hierarchicalSpans));\n }\n }, [hierarchicalSpans]);\n\n const rootSpan = useMemo(\n () => (anchorSpanId ? spans?.find(s => s.spanId === anchorSpanId) : spans?.find(s => s.parentSpanId == null)),\n [spans, anchorSpanId],\n );\n const isSubtrace = anchorSpanId !== undefined && rootSpan?.parentSpanId != null;\n\n const handleSpanClick = (id: string) => {\n const newId = selectedSpanId === id ? undefined : id;\n setSelectedSpanId(newId);\n onSpanSelect?.(newId);\n };\n\n const showOpenTracePageLink = !isOnTracePage && LinkComponent && traceHref;\n\n // Shared across both header layouts (list side panel and full trace page) so a trace can be\n // downloaded from wherever it's being inspected.\n const downloadTraceButton = (\n <Button\n size=\"md\"\n tooltip=\"Download trace JSON\"\n aria-label=\"Download trace JSON\"\n disabled={isDownloadingTrace}\n onClick={() => downloadTraceJson(traceId)}\n >\n {isDownloadingTrace ? <Loader2Icon className=\"animate-spin\" /> : <DownloadIcon />}\n </Button>\n );\n\n return (\n <DataPanel collapsed={collapsed} className={className}>\n <DataPanel.Header>\n {isOnTracePage ? (\n <>\n <DataPanel.Heading>Trace Timeline</DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">{downloadTraceButton}</ButtonsGroup>\n </>\n ) : (\n <>\n <DataPanel.Heading>\n Trace <b># {truncateString(traceId, 12)}</b>\n </DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">\n {onCollapsedChange && (\n <Button\n size=\"md\"\n tooltip={collapsed ? 'Expand panel' : 'Collapse panel'}\n onClick={() => setCollapsed(!collapsed)}\n >\n {collapsed ? <ChevronsUpDownIcon /> : <ChevronsDownUpIcon />}\n </Button>\n )}\n {onEvaluateTrace && (\n <Button size=\"md\" tooltip=\"Evaluate trace\" aria-label=\"Evaluate trace\" onClick={onEvaluateTrace}>\n <CircleGaugeIcon />\n </Button>\n )}\n {onSaveAsDatasetItem && (\n <Button\n size=\"md\"\n tooltip=\"Add full trace to dataset\"\n aria-label=\"Add full trace to dataset\"\n onClick={() => onSaveAsDatasetItem({ traceId, rootSpanId: rootSpan?.spanId })}\n >\n <SaveIcon />\n </Button>\n )}\n {onAddTraceMocksToItem && (\n <Button\n size=\"md\"\n tooltip=\"Add tool mocks to item\"\n aria-label=\"Add tool mocks to item\"\n onClick={() => onAddTraceMocksToItem({ traceId })}\n >\n <WrenchIcon />\n </Button>\n )}\n {(onPrevious || onNext) && (\n <DataPanel.NextPrevNav\n onPrevious={onPrevious}\n onNext={onNext}\n previousLabel=\"Previous trace\"\n nextLabel=\"Next trace\"\n />\n )}\n {showOpenTracePageLink && (\n <Button\n as={LinkComponent}\n href={traceHref}\n size=\"md\"\n tooltip=\"Open trace page\"\n aria-label=\"Open trace page\"\n >\n <Link2Icon />\n </Button>\n )}\n {downloadTraceButton}\n <DataPanel.CloseButton onClick={onClose} />\n </ButtonsGroup>\n </>\n )}\n </DataPanel.Header>\n\n {!collapsed && (\n <SplitWithSpanPanel spanPanelSlot={spanPanelSlot} highlightQuery={query}>\n {isLoading ? (\n <DataPanel.LoadingData>Loading trace...</DataPanel.LoadingData>\n ) : !spans?.length ? (\n <DataPanel.NoData>No spans found for this trace.</DataPanel.NoData>\n ) : (\n <DataPanel.Content>\n {(() => {\n const detailsBody = (\n <>\n {!isOnTracePage && rootSpan && (\n <TraceKeysAndValues rootSpan={rootSpan} usage={isSubtrace ? undefined : usage} className=\"mb-6\" />\n )}\n\n {!isOnTracePage &&\n !onEvaluateTrace &&\n !onSaveAsDatasetItem &&\n !onAddTraceMocksToItem &&\n showUnavailableFeaturesMsg && (\n <Notice variant=\"info\" className=\"mb-6\">\n <Notice.Message>\n Evaluating traces and saving them as dataset items is available in Mastra Studio (local or\n deployed).\n </Notice.Message>\n </Notice>\n )}\n\n {/* The timeline stays mounted even with no results, because it\n hosts the search field: unmounting it would strand the user\n with a query they can no longer clear. */}\n <TraceTimeline\n hierarchicalSpans={hierarchicalSpans}\n onSpanClick={handleSpanClick}\n selectedSpanId={selectedSpanId}\n expandedSpanIds={expandedSpanIds}\n setExpandedSpanIds={setExpandedSpanIds}\n chartWidth={timelineChartWidth}\n leadingSlot={\n <SearchFieldBlock\n name={searchFieldName}\n label=\"Search spans\"\n labelIsHidden\n placeholder=\"Search spans...\"\n value={query}\n onChange={e => setQuery(e.target.value)}\n onReset={() => setQuery('')}\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n />\n }\n />\n\n {hierarchicalSpans.length === 0 && <DataPanel.NoData>No spans match your search.</DataPanel.NoData>}\n </>\n );\n\n // No extra tab slots → render details directly without the Tabs wrapper.\n if (!scoresTabSlot && !feedbackTabSlot) return detailsBody;\n\n return (\n <Tabs<TraceDataPanelTab>\n defaultTab=\"details\"\n value={activeTab}\n onValueChange={onTabChange}\n className={\n activeTab === 'scores' || activeTab === 'feedback'\n ? 'grid h-full min-h-0 grid-rows-[auto_1fr]'\n : undefined\n }\n >\n <TabList variant=\"pill-ghost\" className=\"px-0\">\n <Tab value=\"details\">Details</Tab>\n {scoresTabSlot && (\n <Tab value=\"scores\">Evaluations {scoresTabBadge != null && <>({scoresTabBadge})</>}</Tab>\n )}\n {feedbackTabSlot && (\n <Tab value=\"feedback\">Feedback {feedbackTabBadge != null && <>({feedbackTabBadge})</>}</Tab>\n )}\n </TabList>\n\n <TabContent value=\"details\">{detailsBody}</TabContent>\n {scoresTabSlot && (\n <TabContent value=\"scores\" className=\"h-full min-h-0\">\n {scoresTabSlot({ traceId, rootSpanId: rootSpan?.spanId })}\n </TabContent>\n )}\n {feedbackTabSlot && (\n <TabContent value=\"feedback\" className=\"h-full min-h-0\">\n {feedbackTabSlot({ traceId })}\n </TabContent>\n )}\n </Tabs>\n );\n })()}\n </DataPanel.Content>\n )}\n </SplitWithSpanPanel>\n )}\n </DataPanel>\n );\n}\n\n/**\n * Renders the trace content as-is, or — when a span panel is provided — as a\n * two-column split inside the same card, with the span detail on the right.\n * Search matches — span names in the timeline tree as well as values in the span\n * detail — are highlighted while a query is active.\n */\nfunction SplitWithSpanPanel({\n spanPanelSlot,\n highlightQuery,\n children,\n}: {\n spanPanelSlot?: ReactNode;\n highlightQuery: string;\n children: ReactNode;\n}) {\n // A single hook call on the common ancestor covers both the timeline tree and\n // the span detail, so span names and payload values highlight together.\n const { ref: highlightRef } = useTextHighlight<HTMLDivElement>(highlightQuery);\n\n if (!spanPanelSlot) {\n return (\n <div ref={highlightRef} className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">\n {children}\n </div>\n );\n }\n\n return (\n <div ref={highlightRef} className=\"grid min-h-0 flex-1 grid-cols-[1fr_1fr]\">\n <div className=\"flex min-h-0 flex-col overflow-hidden\">{children}</div>\n {/* Searchable: the span detail is where a match hides inside a large payload. */}\n <div\n data-highlight\n className=\"animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300\"\n >\n {spanPanelSlot}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace-data-panel-view.es.js","sources":["../../../../src/domains/traces/components/trace-data-panel-view.tsx"],"sourcesContent":["import {\n CircleGaugeIcon,\n ChevronsDownUpIcon,\n ChevronsUpDownIcon,\n DownloadIcon,\n Link2Icon,\n Loader2Icon,\n SaveIcon,\n WrenchIcon,\n} from 'lucide-react';\nimport { useEffect, useId, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { getAllSpanIds } from '../hooks/get-all-span-ids';\nimport { useDownloadTraceJson } from '../hooks/use-download-trace-json';\nimport { useTraceSearch } from '../hooks/use-trace-search';\nimport type { TraceUsageSummary } from '../trace-list-columns';\nimport type { SearchableSpan } from '../types';\nimport { formatHierarchicalSpans } from './format-hierarchical-spans';\nimport { TraceKeysAndValues } from './trace-keys-and-values';\nimport { TraceTimeline } from './trace-timeline';\nimport { Button } from '@/ds/components/Button';\nimport { ButtonsGroup } from '@/ds/components/ButtonsGroup';\nimport { DataPanel } from '@/ds/components/DataPanel';\nimport { SearchFieldBlock } from '@/ds/components/FormFieldBlocks';\nimport { Notice } from '@/ds/components/Notice';\nimport { Tab, TabContent, TabList, Tabs } from '@/ds/components/Tabs';\nimport type { LinkComponent } from '@/ds/types/link-component';\nimport { useScrollToFirstHighlight } from '@/hooks/use-scroll-to-first-highlight';\nimport { useTextHighlight } from '@/hooks/use-text-highlight';\nimport { truncateString } from '@/lib/truncate-string';\n\nexport type TraceDataPanelPlacement = 'traces-list' | 'trace-page';\n\nexport type TraceDataPanelTab = 'details' | 'scores' | 'feedback';\n\nexport interface TraceDataPanelViewProps {\n traceId: string;\n /** Lightweight spans for the trace. Caller fetches via useTraceLightSpans. */\n spans: SearchableSpan[] | undefined;\n /**\n * Token and estimated-cost totals for the trace (from `useTraceUsage`).\n * Rendered in the trace summary when the panel is in the list side-panel\n * placement; the trace page renders its own `TraceKeysAndValues` instead.\n */\n usage?: TraceUsageSummary;\n isLoading?: boolean;\n onClose: () => void;\n onSpanSelect?: (spanId: string | undefined) => void;\n onEvaluateTrace?: () => void;\n /** When set, an \"Add full trace to dataset\" button appears; the consumer owns the dialog. */\n onSaveAsDatasetItem?: (args: { traceId: string; rootSpanId: string | undefined }) => void;\n /** When set, an \"Add tool mocks to item\" button appears; the consumer owns the dialog. */\n onAddTraceMocksToItem?: (args: { traceId: string }) => void;\n initialSpanId?: string | null;\n onPrevious?: () => void;\n onNext?: () => void;\n collapsed?: boolean;\n onCollapsedChange?: (collapsed: boolean) => void;\n placement: TraceDataPanelPlacement;\n timelineChartWidth?: 'wide' | 'default';\n /** When both are provided, renders an \"Open trace page\" button. */\n LinkComponent?: LinkComponent;\n traceHref?: string;\n /**\n * Span treated as the displayed root of the timeline. Required for branch\n * subtrees from `getBranch` where the anchor has a real parent that's outside\n * `spans`. When omitted, the span with no parent is used (trace case).\n */\n anchorSpanId?: string;\n /**\n * Whether to render the \"Evaluating traces and saving them as dataset items is\n * available in Mastra Studio\" info notice when neither `onEvaluateTrace` nor\n * `onSaveAsDatasetItem` is provided. Defaults to `true`. Pass `false` when this\n * panel is rendered inside Studio in a context that intentionally omits those\n * handlers (e.g. inline below an experiment result).\n */\n showUnavailableFeaturesMsg?: boolean;\n /**\n * When provided, the panel content becomes tabbed (\"Details\" / \"Scores\"); the slot\n * renders whatever trace-level scoring UI the consumer wants.\n */\n scoresTabSlot?: (args: { traceId: string; rootSpanId: string | undefined }) => ReactNode;\n /** Optional count shown in the \"Scores\" tab label. */\n scoresTabBadge?: ReactNode;\n /**\n * When provided, a \"Feedback\" tab appears; the slot renders the trace-level\n * feedback UI. Trace feedback is not scoped to a span — the span panel owns that.\n */\n feedbackTabSlot?: (args: { traceId: string }) => ReactNode;\n /** Optional count shown in the \"Feedback\" tab label. */\n feedbackTabBadge?: ReactNode;\n activeTab?: TraceDataPanelTab;\n onTabChange?: (tab: TraceDataPanelTab) => void;\n /**\n * When provided, the panel splits into two columns inside the same card: the\n * trace content on the left, this slot (typically the span detail) on the right.\n */\n spanPanelSlot?: ReactNode;\n /** Extra classes applied to the panel root (e.g. `h-full` on the trace page). */\n className?: string;\n}\n\nexport function TraceDataPanelView({\n traceId,\n spans,\n usage,\n isLoading,\n onClose,\n onSpanSelect,\n onEvaluateTrace,\n onSaveAsDatasetItem,\n onAddTraceMocksToItem,\n initialSpanId,\n onPrevious,\n onNext,\n collapsed: controlledCollapsed,\n onCollapsedChange,\n placement,\n timelineChartWidth = 'default',\n LinkComponent,\n traceHref,\n anchorSpanId,\n showUnavailableFeaturesMsg = true,\n scoresTabSlot,\n scoresTabBadge,\n feedbackTabSlot,\n feedbackTabBadge,\n activeTab,\n onTabChange,\n spanPanelSlot,\n className,\n}: TraceDataPanelViewProps) {\n const isOnTracePage = placement === 'trace-page';\n const [internalCollapsed, setInternalCollapsed] = useState(false);\n const collapsed = controlledCollapsed ?? internalCollapsed;\n const setCollapsed = onCollapsedChange ?? setInternalCollapsed;\n\n const { download: downloadTraceJson, isPending: isDownloadingTrace } = useDownloadTraceJson();\n\n const [selectedSpanId, setSelectedSpanId] = useState<string | undefined>(initialSpanId ?? undefined);\n\n // Sync selected span when initialSpanId or trace data changes\n useEffect(() => {\n // No span requested: clear immediately.\n if (!initialSpanId) {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n return;\n }\n // Span requested: wait for trace data before deciding so an in-flight\n // fetch doesn't wipe a URL-provided selection. Callers that default their\n // spans to `[]` while loading only say so through `isLoading`.\n if (isLoading || !spans) return;\n\n const found = spans.find(s => s.spanId === initialSpanId);\n if (found) {\n setSelectedSpanId(initialSpanId);\n onSpanSelect?.(initialSpanId);\n } else {\n setSelectedSpanId(undefined);\n onSpanSelect?.(undefined);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialSpanId, spans, isLoading]);\n\n const searchFieldName = useId();\n const { query, setQuery, results, payloadOnlyMatchIds } = useTraceSearch(spans ?? []);\n\n const hierarchicalSpans = useMemo(\n () =>\n formatHierarchicalSpans(\n // Carried on the span rather than drilled as a prop: the tree is rebuilt here and\n // rendered several components deeper.\n results.map(span => ({ ...span, matchedInPayloadOnly: payloadOnlyMatchIds.has(span.spanId) })),\n anchorSpanId,\n ),\n [results, payloadOnlyMatchIds, anchorSpanId],\n );\n\n const [expandedSpanIds, setExpandedSpanIds] = useState<string[]>([]);\n\n useEffect(() => {\n if (hierarchicalSpans.length > 0) {\n setExpandedSpanIds(getAllSpanIds(hierarchicalSpans));\n }\n }, [hierarchicalSpans]);\n\n const rootSpan = useMemo(\n () => (anchorSpanId ? spans?.find(s => s.spanId === anchorSpanId) : spans?.find(s => s.parentSpanId == null)),\n [spans, anchorSpanId],\n );\n const isSubtrace = anchorSpanId !== undefined && rootSpan?.parentSpanId != null;\n\n const handleSpanClick = (id: string) => {\n const newId = selectedSpanId === id ? undefined : id;\n setSelectedSpanId(newId);\n onSpanSelect?.(newId);\n };\n\n const showOpenTracePageLink = !isOnTracePage && LinkComponent && traceHref;\n\n // Shared across both header layouts (list side panel and full trace page) so a trace can be\n // downloaded from wherever it's being inspected.\n const downloadTraceButton = (\n <Button\n size=\"md\"\n tooltip=\"Download trace JSON\"\n aria-label=\"Download trace JSON\"\n disabled={isDownloadingTrace}\n onClick={() => downloadTraceJson(traceId)}\n >\n {isDownloadingTrace ? <Loader2Icon className=\"animate-spin\" /> : <DownloadIcon />}\n </Button>\n );\n\n return (\n <DataPanel collapsed={collapsed} className={className}>\n <DataPanel.Header>\n {isOnTracePage ? (\n <>\n <DataPanel.Heading>Trace Timeline</DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">{downloadTraceButton}</ButtonsGroup>\n </>\n ) : (\n <>\n <DataPanel.Heading>\n Trace <b># {truncateString(traceId, 12)}</b>\n </DataPanel.Heading>\n <ButtonsGroup className=\"ml-auto shrink-0\">\n {onCollapsedChange && (\n <Button\n size=\"md\"\n tooltip={collapsed ? 'Expand panel' : 'Collapse panel'}\n onClick={() => setCollapsed(!collapsed)}\n >\n {collapsed ? <ChevronsUpDownIcon /> : <ChevronsDownUpIcon />}\n </Button>\n )}\n {onEvaluateTrace && (\n <Button size=\"md\" tooltip=\"Evaluate trace\" aria-label=\"Evaluate trace\" onClick={onEvaluateTrace}>\n <CircleGaugeIcon />\n </Button>\n )}\n {onSaveAsDatasetItem && (\n <Button\n size=\"md\"\n tooltip=\"Add full trace to dataset\"\n aria-label=\"Add full trace to dataset\"\n onClick={() => onSaveAsDatasetItem({ traceId, rootSpanId: rootSpan?.spanId })}\n >\n <SaveIcon />\n </Button>\n )}\n {onAddTraceMocksToItem && (\n <Button\n size=\"md\"\n tooltip=\"Add tool mocks to item\"\n aria-label=\"Add tool mocks to item\"\n onClick={() => onAddTraceMocksToItem({ traceId })}\n >\n <WrenchIcon />\n </Button>\n )}\n {(onPrevious || onNext) && (\n <DataPanel.NextPrevNav\n onPrevious={onPrevious}\n onNext={onNext}\n previousLabel=\"Previous trace\"\n nextLabel=\"Next trace\"\n />\n )}\n {showOpenTracePageLink && (\n <Button\n as={LinkComponent}\n href={traceHref}\n size=\"md\"\n tooltip=\"Open trace page\"\n aria-label=\"Open trace page\"\n >\n <Link2Icon />\n </Button>\n )}\n {downloadTraceButton}\n <DataPanel.CloseButton onClick={onClose} />\n </ButtonsGroup>\n </>\n )}\n </DataPanel.Header>\n\n {!collapsed && (\n <SplitWithSpanPanel spanPanelSlot={spanPanelSlot} highlightQuery={query} spanPanelKey={selectedSpanId}>\n {isLoading ? (\n <DataPanel.LoadingData>Loading trace...</DataPanel.LoadingData>\n ) : !spans?.length ? (\n <DataPanel.NoData>No spans found for this trace.</DataPanel.NoData>\n ) : (\n <DataPanel.Content>\n {(() => {\n const detailsBody = (\n <>\n {!isOnTracePage && rootSpan && (\n <TraceKeysAndValues rootSpan={rootSpan} usage={isSubtrace ? undefined : usage} className=\"mb-6\" />\n )}\n\n {!isOnTracePage &&\n !onEvaluateTrace &&\n !onSaveAsDatasetItem &&\n !onAddTraceMocksToItem &&\n showUnavailableFeaturesMsg && (\n <Notice variant=\"info\" className=\"mb-6\">\n <Notice.Message>\n Evaluating traces and saving them as dataset items is available in Mastra Studio (local or\n deployed).\n </Notice.Message>\n </Notice>\n )}\n\n {/* The timeline stays mounted even with no results, because it\n hosts the search field: unmounting it would strand the user\n with a query they can no longer clear. */}\n <TraceTimeline\n hierarchicalSpans={hierarchicalSpans}\n onSpanClick={handleSpanClick}\n selectedSpanId={selectedSpanId}\n expandedSpanIds={expandedSpanIds}\n setExpandedSpanIds={setExpandedSpanIds}\n chartWidth={timelineChartWidth}\n leadingSlot={\n <SearchFieldBlock\n name={searchFieldName}\n label=\"Search spans\"\n labelIsHidden\n placeholder=\"Search spans...\"\n value={query}\n onChange={e => setQuery(e.target.value)}\n onReset={() => setQuery('')}\n size=\"sm\"\n variant=\"outline\"\n className=\"w-full\"\n />\n }\n />\n\n {hierarchicalSpans.length === 0 && <DataPanel.NoData>No spans match your search.</DataPanel.NoData>}\n </>\n );\n\n // No extra tab slots → render details directly without the Tabs wrapper.\n if (!scoresTabSlot && !feedbackTabSlot) return detailsBody;\n\n return (\n <Tabs<TraceDataPanelTab>\n defaultTab=\"details\"\n value={activeTab}\n onValueChange={onTabChange}\n className={\n activeTab === 'scores' || activeTab === 'feedback'\n ? 'grid h-full min-h-0 grid-rows-[auto_1fr]'\n : undefined\n }\n >\n <TabList variant=\"pill-ghost\" className=\"px-0\">\n <Tab value=\"details\">Details</Tab>\n {scoresTabSlot && (\n <Tab value=\"scores\">Evaluations {scoresTabBadge != null && <>({scoresTabBadge})</>}</Tab>\n )}\n {feedbackTabSlot && (\n <Tab value=\"feedback\">Feedback {feedbackTabBadge != null && <>({feedbackTabBadge})</>}</Tab>\n )}\n </TabList>\n\n <TabContent value=\"details\">{detailsBody}</TabContent>\n {scoresTabSlot && (\n <TabContent value=\"scores\" className=\"h-full min-h-0\">\n {scoresTabSlot({ traceId, rootSpanId: rootSpan?.spanId })}\n </TabContent>\n )}\n {feedbackTabSlot && (\n <TabContent value=\"feedback\" className=\"h-full min-h-0\">\n {feedbackTabSlot({ traceId })}\n </TabContent>\n )}\n </Tabs>\n );\n })()}\n </DataPanel.Content>\n )}\n </SplitWithSpanPanel>\n )}\n </DataPanel>\n );\n}\n\n/**\n * Renders the trace content as-is, or — when a span panel is provided — as a\n * two-column split inside the same card, with the span detail on the right.\n * Search matches — span names in the timeline tree as well as values in the span\n * detail — are highlighted while a query is active.\n */\nfunction SplitWithSpanPanel({\n spanPanelSlot,\n highlightQuery,\n spanPanelKey,\n children,\n}: {\n spanPanelSlot?: ReactNode;\n highlightQuery: string;\n /** Identity of the span shown in the panel; changing it re-triggers the match scroll. */\n spanPanelKey?: string;\n children: ReactNode;\n}) {\n // A single hook call on the common ancestor covers both the timeline tree and\n // the span detail, so span names and payload values highlight together.\n const { ref: highlightRef } = useTextHighlight<HTMLDivElement>(highlightQuery);\n\n // Scoped to the span-panel column only: a match deep in a large payload sits below\n // the fold, so the first painted match is brought into view when the panel opens.\n // The timeline column must never be scrolled by this.\n const { ref: scrollToMatchRef } = useScrollToFirstHighlight<HTMLDivElement>(highlightQuery, spanPanelKey);\n\n if (!spanPanelSlot) {\n return (\n <div ref={highlightRef} className=\"flex min-h-0 flex-1 flex-col overflow-hidden\">\n {children}\n </div>\n );\n }\n\n return (\n <div ref={highlightRef} className=\"grid min-h-0 flex-1 grid-cols-[1fr_1fr]\">\n <div className=\"flex min-h-0 flex-col overflow-hidden\">{children}</div>\n {/* Searchable: the span detail is where a match hides inside a large payload. */}\n <div\n ref={scrollToMatchRef}\n data-highlight\n className=\"animate-in border-border1 fade-in-0 flex min-h-0 flex-col overflow-hidden border-l duration-300\"\n >\n {spanPanelSlot}\n </div>\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
+
5
+ const React = require('react');
6
+ const hooks_useTextHighlight = require('./use-text-highlight.cjs.js');
7
+
8
+ function useScrollToFirstHighlight(search, resetKey) {
9
+ const [root, setRoot] = React.useState(null);
10
+ React.useEffect(() => {
11
+ if (!root || search.trim().length < hooks_useTextHighlight.MIN_SEARCH_LENGTH) return;
12
+ let animationFrame = null;
13
+ let scrolledTo = null;
14
+ let scrolledToData = "";
15
+ const scan = () => {
16
+ const match = hooks_useTextHighlight.findFirstMatchTextNode(root, search);
17
+ if (!match) return;
18
+ scrolledTo = match;
19
+ scrolledToData = match.data;
20
+ const target = match.parentElement;
21
+ if (target && typeof target.scrollIntoView === "function") {
22
+ target.scrollIntoView({ block: "center" });
23
+ }
24
+ };
25
+ const scrolledContentIntact = () => scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;
26
+ scan();
27
+ const observer = new MutationObserver(() => {
28
+ if (animationFrame !== null) return;
29
+ animationFrame = requestAnimationFrame(() => {
30
+ animationFrame = null;
31
+ if (!scrolledContentIntact()) scan();
32
+ });
33
+ });
34
+ observer.observe(root, { subtree: true, childList: true, characterData: true });
35
+ return () => {
36
+ observer.disconnect();
37
+ if (animationFrame !== null) cancelAnimationFrame(animationFrame);
38
+ };
39
+ }, [root, search, resetKey]);
40
+ return { ref: setRoot };
41
+ }
42
+
43
+ exports.useScrollToFirstHighlight = useScrollToFirstHighlight;
44
+ //# sourceMappingURL=use-scroll-to-first-highlight.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-scroll-to-first-highlight.cjs.js","sources":["../../src/hooks/use-scroll-to-first-highlight.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight';\n\nexport interface UseScrollToFirstHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Scrolls the first text-highlight match inside the referenced subtree into view. The\n * CSS Custom Highlight API paints ranges without wrapping DOM nodes, so there is no\n * `<mark>` to scroll to — instead the first text node `useTextHighlight` would paint is\n * located (same matching rules) and its parent element is brought into view.\n *\n * Content often arrives after the effect runs: the span payload loads async, and when a\n * different span is selected the panel briefly still shows the previous span's DOM. So a\n * MutationObserver stays attached for the effect's lifetime (rescans coalesced to one per\n * frame) and re-scrolls whenever the previously scrolled-to node is gone or no longer\n * contains the term — i.e. the content it scrolled to was replaced. While the matched\n * node stays put, mutations never re-scroll, so the user can scroll freely afterwards.\n * Pass `resetKey` to re-trigger the scroll for the same query, e.g. when a different\n * span is selected.\n */\nexport function useScrollToFirstHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n resetKey?: unknown,\n): UseScrollToFirstHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useEffect(() => {\n if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;\n\n let animationFrame: number | null = null;\n let scrolledTo: Text | null = null;\n let scrolledToData = '';\n\n const scan = () => {\n const match = findFirstMatchTextNode(root, search);\n if (!match) return;\n\n scrolledTo = match;\n scrolledToData = match.data;\n const target = match.parentElement;\n // jsdom guard: scrollIntoView is not implemented there.\n if (target && typeof target.scrollIntoView === 'function') {\n target.scrollIntoView({ block: 'center' });\n }\n };\n\n // The scrolled-to content survived the mutation iff the node is still attached with\n // its text unchanged. React reuses text nodes and edits characterData in place, so\n // swapped-in content (e.g. another span's payload matching the same term) shows up\n // as a data change on the same node, not as a removal.\n const scrolledContentIntact = () =>\n scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;\n\n scan();\n\n const observer = new MutationObserver(() => {\n if (animationFrame !== null) return;\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n if (!scrolledContentIntact()) scan();\n });\n });\n observer.observe(root, { subtree: true, childList: true, characterData: true });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n };\n }, [root, search, resetKey]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useEffect","MIN_SEARCH_LENGTH","findFirstMatchTextNode"],"mappings":";;;;;;;AAwBO,SAAS,yBAAA,CACd,QACA,QAAA,EAC2C;AAC3C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIA,eAA0B,IAAI,CAAA;AAEtD,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,IAAQ,MAAA,CAAO,IAAA,EAAK,CAAE,SAASC,wCAAA,EAAmB;AAEvD,IAAA,IAAI,cAAA,GAAgC,IAAA;AACpC,IAAA,IAAI,UAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,cAAA,GAAiB,EAAA;AAErB,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,KAAA,GAAQC,6CAAA,CAAuB,IAAA,EAAM,MAAM,CAAA;AACjD,MAAA,IAAI,CAAC,KAAA,EAAO;AAEZ,MAAA,UAAA,GAAa,KAAA;AACb,MAAA,cAAA,GAAiB,KAAA,CAAM,IAAA;AACvB,MAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AAErB,MAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,cAAA,KAAmB,UAAA,EAAY;AACzD,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,KAAA,EAAO,QAAA,EAAU,CAAA;AAAA,MAC3C;AAAA,IACF,CAAA;AAMA,IAAA,MAAM,wBAAwB,MAC5B,UAAA,KAAe,QAAQ,UAAA,CAAW,WAAA,IAAe,WAAW,IAAA,KAAS,cAAA;AAEvE,IAAA,IAAA,EAAK;AAEL,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,MAAA,IAAI,mBAAmB,IAAA,EAAM;AAE7B,MAAA,cAAA,GAAiB,sBAAsB,MAAM;AAC3C,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA,IAAI,CAAC,qBAAA,EAAsB,EAAG,IAAA,EAAK;AAAA,MACrC,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,OAAA,CAAQ,MAAM,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,CAAA;AAE9E,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,IAAI,cAAA,KAAmB,IAAA,EAAM,oBAAA,CAAqB,cAAc,CAAA;AAAA,IAClE,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAE3B,EAAA,OAAO,EAAE,KAAK,OAAA,EAAQ;AACxB;;;;"}
@@ -0,0 +1,2 @@
1
+ export * from '../src/hooks/use-scroll-to-first-highlight'
2
+ export {}
@@ -0,0 +1,40 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight.es.js';
3
+
4
+ function useScrollToFirstHighlight(search, resetKey) {
5
+ const [root, setRoot] = useState(null);
6
+ useEffect(() => {
7
+ if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;
8
+ let animationFrame = null;
9
+ let scrolledTo = null;
10
+ let scrolledToData = "";
11
+ const scan = () => {
12
+ const match = findFirstMatchTextNode(root, search);
13
+ if (!match) return;
14
+ scrolledTo = match;
15
+ scrolledToData = match.data;
16
+ const target = match.parentElement;
17
+ if (target && typeof target.scrollIntoView === "function") {
18
+ target.scrollIntoView({ block: "center" });
19
+ }
20
+ };
21
+ const scrolledContentIntact = () => scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;
22
+ scan();
23
+ const observer = new MutationObserver(() => {
24
+ if (animationFrame !== null) return;
25
+ animationFrame = requestAnimationFrame(() => {
26
+ animationFrame = null;
27
+ if (!scrolledContentIntact()) scan();
28
+ });
29
+ });
30
+ observer.observe(root, { subtree: true, childList: true, characterData: true });
31
+ return () => {
32
+ observer.disconnect();
33
+ if (animationFrame !== null) cancelAnimationFrame(animationFrame);
34
+ };
35
+ }, [root, search, resetKey]);
36
+ return { ref: setRoot };
37
+ }
38
+
39
+ export { useScrollToFirstHighlight };
40
+ //# sourceMappingURL=use-scroll-to-first-highlight.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-scroll-to-first-highlight.es.js","sources":["../../src/hooks/use-scroll-to-first-highlight.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight';\n\nexport interface UseScrollToFirstHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Scrolls the first text-highlight match inside the referenced subtree into view. The\n * CSS Custom Highlight API paints ranges without wrapping DOM nodes, so there is no\n * `<mark>` to scroll to — instead the first text node `useTextHighlight` would paint is\n * located (same matching rules) and its parent element is brought into view.\n *\n * Content often arrives after the effect runs: the span payload loads async, and when a\n * different span is selected the panel briefly still shows the previous span's DOM. So a\n * MutationObserver stays attached for the effect's lifetime (rescans coalesced to one per\n * frame) and re-scrolls whenever the previously scrolled-to node is gone or no longer\n * contains the term — i.e. the content it scrolled to was replaced. While the matched\n * node stays put, mutations never re-scroll, so the user can scroll freely afterwards.\n * Pass `resetKey` to re-trigger the scroll for the same query, e.g. when a different\n * span is selected.\n */\nexport function useScrollToFirstHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n resetKey?: unknown,\n): UseScrollToFirstHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useEffect(() => {\n if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;\n\n let animationFrame: number | null = null;\n let scrolledTo: Text | null = null;\n let scrolledToData = '';\n\n const scan = () => {\n const match = findFirstMatchTextNode(root, search);\n if (!match) return;\n\n scrolledTo = match;\n scrolledToData = match.data;\n const target = match.parentElement;\n // jsdom guard: scrollIntoView is not implemented there.\n if (target && typeof target.scrollIntoView === 'function') {\n target.scrollIntoView({ block: 'center' });\n }\n };\n\n // The scrolled-to content survived the mutation iff the node is still attached with\n // its text unchanged. React reuses text nodes and edits characterData in place, so\n // swapped-in content (e.g. another span's payload matching the same term) shows up\n // as a data change on the same node, not as a removal.\n const scrolledContentIntact = () =>\n scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;\n\n scan();\n\n const observer = new MutationObserver(() => {\n if (animationFrame !== null) return;\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n if (!scrolledContentIntact()) scan();\n });\n });\n observer.observe(root, { subtree: true, childList: true, characterData: true });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n };\n }, [root, search, resetKey]);\n\n return { ref: setRoot };\n}\n"],"names":[],"mappings":";;;AAwBO,SAAS,yBAAA,CACd,QACA,QAAA,EAC2C;AAC3C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAA0B,IAAI,CAAA;AAEtD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,IAAQ,MAAA,CAAO,IAAA,EAAK,CAAE,SAAS,iBAAA,EAAmB;AAEvD,IAAA,IAAI,cAAA,GAAgC,IAAA;AACpC,IAAA,IAAI,UAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,cAAA,GAAiB,EAAA;AAErB,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,IAAA,EAAM,MAAM,CAAA;AACjD,MAAA,IAAI,CAAC,KAAA,EAAO;AAEZ,MAAA,UAAA,GAAa,KAAA;AACb,MAAA,cAAA,GAAiB,KAAA,CAAM,IAAA;AACvB,MAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AAErB,MAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,cAAA,KAAmB,UAAA,EAAY;AACzD,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,KAAA,EAAO,QAAA,EAAU,CAAA;AAAA,MAC3C;AAAA,IACF,CAAA;AAMA,IAAA,MAAM,wBAAwB,MAC5B,UAAA,KAAe,QAAQ,UAAA,CAAW,WAAA,IAAe,WAAW,IAAA,KAAS,cAAA;AAEvE,IAAA,IAAA,EAAK;AAEL,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,MAAA,IAAI,mBAAmB,IAAA,EAAM;AAE7B,MAAA,cAAA,GAAiB,sBAAsB,MAAM;AAC3C,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA,IAAI,CAAC,qBAAA,EAAsB,EAAG,IAAA,EAAK;AAAA,MACrC,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,OAAA,CAAQ,MAAM,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,CAAA;AAE9E,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,IAAI,cAAA,KAAmB,IAAA,EAAM,oBAAA,CAAqB,cAAc,CAAA;AAAA,IAClE,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAE3B,EAAA,OAAO,EAAE,KAAK,OAAA,EAAQ;AACxB;;;;"}
@@ -13,6 +13,25 @@ const INDIRECT_SELECTOR = "[data-highlight-indirect]";
13
13
  function escapeRegExp(value) {
14
14
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15
15
  }
16
+ function findFirstMatchTextNode(root, search) {
17
+ const regex = new RegExp(escapeRegExp(search), "iu");
18
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
19
+ acceptNode(node) {
20
+ const parent = node.parentElement;
21
+ return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
22
+ }
23
+ });
24
+ while (walker.nextNode()) {
25
+ const textNode = walker.currentNode;
26
+ const parent = textNode.parentElement;
27
+ if (parent.closest(INDIRECT_SELECTOR)) {
28
+ if (textNode.data.length > 0) return textNode;
29
+ continue;
30
+ }
31
+ if (regex.test(textNode.data)) return textNode;
32
+ }
33
+ return null;
34
+ }
16
35
  function useTextHighlight(search) {
17
36
  const [root, setRoot] = React.useState(null);
18
37
  hooks_useIsomorphicLayoutEffect.useIsomorphicLayoutEffect(() => {
@@ -90,5 +109,7 @@ function useTextHighlight(search) {
90
109
  return { ref: setRoot };
91
110
  }
92
111
 
112
+ exports.MIN_SEARCH_LENGTH = MIN_SEARCH_LENGTH;
113
+ exports.findFirstMatchTextNode = findFirstMatchTextNode;
93
114
  exports.useTextHighlight = useTextHighlight;
94
115
  //# sourceMappingURL=use-text-highlight.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-text-highlight.cjs.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * Second registry entry for text that carries no occurrence of the term itself, but stands\n * for something elsewhere that does — a span name whose match hides in its metadata. Painted\n * in its own color so the two claims stay distinguishable.\n */\nconst INDIRECT_HIGHLIGHT_NAME = 'search-result-indirect';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nconst MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\n/**\n * Opt-in marker for the indirect highlight: the whole text of such a subtree is painted\n * while a query is active, regardless of what the term is. Marking is the caller's decision\n * — it knows why the row survived the filter, this hook only knows the term.\n */\nconst INDIRECT_SELECTOR = '[data-highlight-indirect]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Text under `data-highlight-indirect` is painted in full, in a second color\n * (`::highlight(search-result-indirect)`), whatever the term is. That is for labels standing\n * in for a match that lives somewhere not on screen — a span name whose hit is in its\n * metadata. It wins over `data-highlight` when both apply, so a text is never painted twice.\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const indirectRanges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n // The indirect claim is about the whole text, so there is nothing to search for\n // inside it: the term lives in the payload this text stands for, not in the text.\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) {\n indirectRanges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: 0,\n endContainer: textNode,\n endOffset: textNode.data.length,\n }),\n );\n }\n continue;\n }\n\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n // The opt-in attributes are watched too: a region can change which highlight it claims\n // while its text stays put, and that alone must repaint it.\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n attributeFilter: ['data-highlight', 'data-highlight-indirect'],\n });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useIsomorphicLayoutEffect"],"mappings":"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"}
1
+ {"version":3,"file":"use-text-highlight.cjs.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * Second registry entry for text that carries no occurrence of the term itself, but stands\n * for something elsewhere that does — a span name whose match hides in its metadata. Painted\n * in its own color so the two claims stay distinguishable.\n */\nconst INDIRECT_HIGHLIGHT_NAME = 'search-result-indirect';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nexport const MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\n/**\n * Opt-in marker for the indirect highlight: the whole text of such a subtree is painted\n * while a query is active, regardless of what the term is. Marking is the caller's decision\n * — it knows why the row survived the filter, this hook only knows the term.\n */\nconst INDIRECT_SELECTOR = '[data-highlight-indirect]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * The first text node the highlighter would paint under `root` for `search`, or `null`.\n * Mirrors the walker below exactly: only `data-highlight` / `data-highlight-indirect`\n * subtrees count; indirect text is painted whole, so any non-empty text node in one is a\n * hit; direct text needs a literal, case-insensitive occurrence of the term. Colocated\n * here so the scroll target (`useScrollToFirstHighlight`) can't drift from the paint.\n */\nexport function findFirstMatchTextNode(root: HTMLElement, search: string): Text | null {\n const regex = new RegExp(escapeRegExp(search), 'iu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) return textNode;\n continue;\n }\n\n if (regex.test(textNode.data)) return textNode;\n }\n\n return null;\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Text under `data-highlight-indirect` is painted in full, in a second color\n * (`::highlight(search-result-indirect)`), whatever the term is. That is for labels standing\n * in for a match that lives somewhere not on screen — a span name whose hit is in its\n * metadata. It wins over `data-highlight` when both apply, so a text is never painted twice.\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const indirectRanges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n // The indirect claim is about the whole text, so there is nothing to search for\n // inside it: the term lives in the payload this text stands for, not in the text.\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) {\n indirectRanges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: 0,\n endContainer: textNode,\n endOffset: textNode.data.length,\n }),\n );\n }\n continue;\n }\n\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n // The opt-in attributes are watched too: a region can change which highlight it claims\n // while its text stays put, and that alone must repaint it.\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n attributeFilter: ['data-highlight', 'data-highlight-indirect'],\n });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useIsomorphicLayoutEffect"],"mappings":"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"}
@@ -9,6 +9,25 @@ const INDIRECT_SELECTOR = "[data-highlight-indirect]";
9
9
  function escapeRegExp(value) {
10
10
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
11
  }
12
+ function findFirstMatchTextNode(root, search) {
13
+ const regex = new RegExp(escapeRegExp(search), "iu");
14
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
15
+ acceptNode(node) {
16
+ const parent = node.parentElement;
17
+ return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
18
+ }
19
+ });
20
+ while (walker.nextNode()) {
21
+ const textNode = walker.currentNode;
22
+ const parent = textNode.parentElement;
23
+ if (parent.closest(INDIRECT_SELECTOR)) {
24
+ if (textNode.data.length > 0) return textNode;
25
+ continue;
26
+ }
27
+ if (regex.test(textNode.data)) return textNode;
28
+ }
29
+ return null;
30
+ }
12
31
  function useTextHighlight(search) {
13
32
  const [root, setRoot] = useState(null);
14
33
  useIsomorphicLayoutEffect(() => {
@@ -86,5 +105,5 @@ function useTextHighlight(search) {
86
105
  return { ref: setRoot };
87
106
  }
88
107
 
89
- export { useTextHighlight };
108
+ export { MIN_SEARCH_LENGTH, findFirstMatchTextNode, useTextHighlight };
90
109
  //# sourceMappingURL=use-text-highlight.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-text-highlight.es.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * Second registry entry for text that carries no occurrence of the term itself, but stands\n * for something elsewhere that does — a span name whose match hides in its metadata. Painted\n * in its own color so the two claims stay distinguishable.\n */\nconst INDIRECT_HIGHLIGHT_NAME = 'search-result-indirect';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nconst MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\n/**\n * Opt-in marker for the indirect highlight: the whole text of such a subtree is painted\n * while a query is active, regardless of what the term is. Marking is the caller's decision\n * — it knows why the row survived the filter, this hook only knows the term.\n */\nconst INDIRECT_SELECTOR = '[data-highlight-indirect]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Text under `data-highlight-indirect` is painted in full, in a second color\n * (`::highlight(search-result-indirect)`), whatever the term is. That is for labels standing\n * in for a match that lives somewhere not on screen — a span name whose hit is in its\n * metadata. It wins over `data-highlight` when both apply, so a text is never painted twice.\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const indirectRanges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n // The indirect claim is about the whole text, so there is nothing to search for\n // inside it: the term lives in the payload this text stands for, not in the text.\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) {\n indirectRanges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: 0,\n endContainer: textNode,\n endOffset: textNode.data.length,\n }),\n );\n }\n continue;\n }\n\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n // The opt-in attributes are watched too: a region can change which highlight it claims\n // while its text stays put, and that alone must repaint it.\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n attributeFilter: ['data-highlight', 'data-highlight-indirect'],\n });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-text-highlight.es.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * Second registry entry for text that carries no occurrence of the term itself, but stands\n * for something elsewhere that does — a span name whose match hides in its metadata. Painted\n * in its own color so the two claims stay distinguishable.\n */\nconst INDIRECT_HIGHLIGHT_NAME = 'search-result-indirect';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nexport const MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\n/**\n * Opt-in marker for the indirect highlight: the whole text of such a subtree is painted\n * while a query is active, regardless of what the term is. Marking is the caller's decision\n * — it knows why the row survived the filter, this hook only knows the term.\n */\nconst INDIRECT_SELECTOR = '[data-highlight-indirect]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * The first text node the highlighter would paint under `root` for `search`, or `null`.\n * Mirrors the walker below exactly: only `data-highlight` / `data-highlight-indirect`\n * subtrees count; indirect text is painted whole, so any non-empty text node in one is a\n * hit; direct text needs a literal, case-insensitive occurrence of the term. Colocated\n * here so the scroll target (`useScrollToFirstHighlight`) can't drift from the paint.\n */\nexport function findFirstMatchTextNode(root: HTMLElement, search: string): Text | null {\n const regex = new RegExp(escapeRegExp(search), 'iu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) return textNode;\n continue;\n }\n\n if (regex.test(textNode.data)) return textNode;\n }\n\n return null;\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Text under `data-highlight-indirect` is painted in full, in a second color\n * (`::highlight(search-result-indirect)`), whatever the term is. That is for labels standing\n * in for a match that lives somewhere not on screen — a span name whose hit is in its\n * metadata. It wins over `data-highlight` when both apply, so a text is never painted twice.\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const indirectRanges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n // The indirect claim is about the whole text, so there is nothing to search for\n // inside it: the term lives in the payload this text stands for, not in the text.\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) {\n indirectRanges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: 0,\n endContainer: textNode,\n endOffset: textNode.data.length,\n }),\n );\n }\n continue;\n }\n\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n // The opt-in attributes are watched too: a region can change which highlight it claims\n // while its text stays put, and that alone must repaint it.\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n attributeFilter: ['data-highlight', 'data-highlight-indirect'],\n });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { RefCallback } from 'react';
2
+ export interface UseScrollToFirstHighlightResult<TElement extends HTMLElement> {
3
+ ref: RefCallback<TElement>;
4
+ }
5
+ /**
6
+ * Scrolls the first text-highlight match inside the referenced subtree into view. The
7
+ * CSS Custom Highlight API paints ranges without wrapping DOM nodes, so there is no
8
+ * `<mark>` to scroll to — instead the first text node `useTextHighlight` would paint is
9
+ * located (same matching rules) and its parent element is brought into view.
10
+ *
11
+ * Content often arrives after the effect runs: the span payload loads async, and when a
12
+ * different span is selected the panel briefly still shows the previous span's DOM. So a
13
+ * MutationObserver stays attached for the effect's lifetime (rescans coalesced to one per
14
+ * frame) and re-scrolls whenever the previously scrolled-to node is gone or no longer
15
+ * contains the term — i.e. the content it scrolled to was replaced. While the matched
16
+ * node stays put, mutations never re-scroll, so the user can scroll freely afterwards.
17
+ * Pass `resetKey` to re-trigger the scroll for the same query, e.g. when a different
18
+ * span is selected.
19
+ */
20
+ export declare function useScrollToFirstHighlight<TElement extends HTMLElement = HTMLElement>(search: string, resetKey?: unknown): UseScrollToFirstHighlightResult<TElement>;
@@ -1,4 +1,17 @@
1
1
  import { RefCallback } from 'react';
2
+ /**
3
+ * A single character matches almost everywhere, which paints noise instead of results.
4
+ * Highlighting only starts once the term is discriminating enough.
5
+ */
6
+ export declare const MIN_SEARCH_LENGTH = 2;
7
+ /**
8
+ * The first text node the highlighter would paint under `root` for `search`, or `null`.
9
+ * Mirrors the walker below exactly: only `data-highlight` / `data-highlight-indirect`
10
+ * subtrees count; indirect text is painted whole, so any non-empty text node in one is a
11
+ * hit; direct text needs a literal, case-insensitive occurrence of the term. Colocated
12
+ * here so the scroll target (`useScrollToFirstHighlight`) can't drift from the paint.
13
+ */
14
+ export declare function findFirstMatchTextNode(root: HTMLElement, search: string): Text | null;
2
15
  export interface UseTextHighlightResult<TElement extends HTMLElement> {
3
16
  ref: RefCallback<TElement>;
4
17
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mastra/playground-ui",
3
3
  "type": "module",
4
- "version": "51.4.0-alpha.1",
4
+ "version": "51.4.0-alpha.2",
5
5
  "description": "Mastra Playground components",
6
6
  "sideEffects": [
7
7
  "**/*.css"
@@ -161,8 +161,8 @@
161
161
  "react": ">=19.0.0",
162
162
  "react-dom": ">=19.0.0",
163
163
  "tailwindcss": "^4.0.0",
164
- "@mastra/client-js": "^1.42.5-alpha.1",
165
- "@mastra/react": "1.4.10-alpha.1"
164
+ "@mastra/react": "1.4.10-alpha.1",
165
+ "@mastra/client-js": "^1.42.5-alpha.1"
166
166
  },
167
167
  "devDependencies": {
168
168
  "@storybook/addon-a11y": "^10.4.1",
@@ -203,9 +203,9 @@
203
203
  "vite-plugin-lib-inject-css": "^2.2.2",
204
204
  "vitest": "4.1.10",
205
205
  "@internal/lint": "0.0.129",
206
- "@mastra/core": "1.63.3-alpha.1",
207
- "@mastra/memory": "1.28.1",
208
206
  "@mastra/client-js": "^1.42.5-alpha.1",
207
+ "@mastra/core": "1.63.3-alpha.1",
208
+ "@mastra/memory": "1.28.2-alpha.0",
209
209
  "@mastra/react": "1.4.10-alpha.1"
210
210
  },
211
211
  "homepage": "https://mastra.ai",