@vectara/vectara-ui 16.3.1 → 16.4.0

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.
@@ -79,6 +79,8 @@ import { VuiStepsVertical, StepsVertical, StepVerticalStatus } from "./stepsVert
79
79
  import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
80
80
  import { VuiSummary, VuiSummaryCitation } from "./summary";
81
81
  import { VuiTable } from "./table/Table";
82
+ import { VuiSpans } from "./spans/Spans";
83
+ import { SpansRow } from "./spans/types";
82
84
  import { VuiTab } from "./tabs/Tab";
83
85
  import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
84
86
  import { VuiTabs } from "./tabs/Tabs";
@@ -95,5 +97,5 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
95
97
  import { VuiTopicButton } from "./topicButton/TopicButton";
96
98
  import { copyToClipboard } from "../utils/copyToClipboard";
97
99
  import { toRgb, toRgba } from "./context/Theme";
98
- export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
99
- export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
100
+ export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CheckboxConfig, CodeEditorColorConfig, CodeEditorError, CodeLanguage, InfoListItemType, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, SpansRow, Stat, StepStatus, StepSize, Steps, StepsVertical, StepVerticalStatus, TabSize, Tree, TreeItem };
101
+ export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
@@ -72,6 +72,7 @@ import { VuiStepsVertical } from "./stepsVertical/StepsVertical";
72
72
  import { SKELETON_COLOR, VuiSkeleton } from "./skeleton/Skeleton";
73
73
  import { VuiSummary, VuiSummaryCitation } from "./summary";
74
74
  import { VuiTable } from "./table/Table";
75
+ import { VuiSpans } from "./spans/Spans";
75
76
  import { VuiTab } from "./tabs/Tab";
76
77
  import { VuiTabbedRoutes } from "./tabs/TabbedRoutes";
77
78
  import { VuiTabs } from "./tabs/Tabs";
@@ -88,4 +89,4 @@ import { VuiInfoTooltip } from "./tooltip/InfoTooltip";
88
89
  import { VuiTopicButton } from "./topicButton/TopicButton";
89
90
  import { copyToClipboard } from "../utils/copyToClipboard";
90
91
  import { toRgb, toRgba } from "./context/Theme";
91
- export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
92
+ export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SKELETON_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, generateTokensProvider, toRgb, toRgba, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiAppSideNavGroup, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiCodeEditor, VuiComplexConfigurationButton, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiGridItem, VuiHorizontalRule, VuiIcon, VuiImage, VuiImagePreview, VuiInfoList, VuiInfoListItem, VuiInfoMenu, VuiInfoTable, VuiInfoTooltip, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSimpleCard, VuiSimpleGrid, VuiSpacer, VuiSpans, VuiSpinner, VuiStat, VuiStatList, VuiStatus, VuiSteps, VuiStepsVertical, VuiSummary, VuiSkeleton, VuiSummaryCitation, VuiSuperCheckboxGroup, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiTabsNavigator, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Column } from "../table/types";
3
+ import { SpansRow } from "./types";
4
+ type Props<T extends SpansRow> = {
5
+ idField: keyof T | ((row: T) => string);
6
+ parentField: keyof T | ((row: T) => string | null);
7
+ columns: Column<T>[];
8
+ rows: T[];
9
+ expandedIds: Set<string>;
10
+ onExpandedIdsChange: (ids: Set<string>) => void;
11
+ onExpand?: (row: T) => void | Promise<void>;
12
+ isLoadingChildren?: (row: T) => boolean;
13
+ isLoading?: boolean;
14
+ content?: React.ReactNode;
15
+ className?: string;
16
+ rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
17
+ indentSize?: number;
18
+ isHeaderSticky?: boolean;
19
+ fluid?: boolean;
20
+ "data-testid"?: string;
21
+ };
22
+ export declare const VuiSpans: <T extends SpansRow>({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize, isHeaderSticky, fluid, "data-testid": dataTestId }: Props<T>) => import("react/jsx-runtime").JSX.Element;
23
+ export {};
@@ -0,0 +1,94 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useMemo, useRef, useState } from "react";
3
+ import classNames from "classnames";
4
+ import { VuiFlexItem } from "../flex/FlexItem";
5
+ import { VuiSpinner } from "../spinner/Spinner";
6
+ import { VuiText } from "../typography/Text";
7
+ import { VuiTableContent } from "../table/TableContent";
8
+ import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
9
+ import { VuiSpansRow } from "./SpansRow";
10
+ import { VuiSpansHeaderCell } from "./SpansHeaderCell";
11
+ import { VuiSpansLoadingRow } from "./SpansLoadingRow";
12
+ const DEFAULT_INDENT_SIZE = 16;
13
+ const extractId = (row, idField) => {
14
+ return typeof idField === "function" ? idField(row) : row[idField];
15
+ };
16
+ const extractParentId = (row, parentField) => {
17
+ if (typeof parentField === "function")
18
+ return parentField(row);
19
+ const value = row[parentField];
20
+ return value === undefined || value === null ? null : value;
21
+ };
22
+ export const VuiSpans = ({ idField, parentField, columns, rows, expandedIds, onExpandedIdsChange, onExpand, isLoadingChildren, isLoading, content, className, rowDecorator, indentSize = DEFAULT_INDENT_SIZE, isHeaderSticky, fluid, "data-testid": dataTestId }) => {
23
+ const [internalLoadingIds, setInternalLoadingIds] = useState(new Set());
24
+ // Tracks pending fetches so we ignore stale resolutions (e.g. user collapses
25
+ // mid-fetch, then re-expands — the original promise should not clear loading
26
+ // state for the new attempt).
27
+ const fetchTokensRef = useRef(new Map());
28
+ const getId = useCallback((row) => extractId(row, idField), [idField]);
29
+ const getParentId = useCallback((row) => extractParentId(row, parentField), [parentField]);
30
+ const flatSpans = useMemo(() => buildAndFlattenSpans(rows, expandedIds, getId, getParentId), [rows, expandedIds, getId, getParentId]);
31
+ const columnCount = columns.length;
32
+ const handleToggle = useCallback((row, id, hasChildren, hasLoadedChildren) => {
33
+ var _a;
34
+ const isCurrentlyExpanded = expandedIds.has(id);
35
+ const nextExpandedIds = new Set(expandedIds);
36
+ if (isCurrentlyExpanded) {
37
+ nextExpandedIds.delete(id);
38
+ onExpandedIdsChange(nextExpandedIds);
39
+ return;
40
+ }
41
+ nextExpandedIds.add(id);
42
+ onExpandedIdsChange(nextExpandedIds);
43
+ // Only fire the consumer fetch when the row claims to have children
44
+ // (`hasChildren`) but none are present in the rows list yet.
45
+ const needsFetch = hasChildren && !hasLoadedChildren && Boolean(onExpand);
46
+ if (!needsFetch)
47
+ return;
48
+ const pendingFetchTokens = fetchTokensRef.current;
49
+ const currentFetchToken = ((_a = pendingFetchTokens.get(id)) !== null && _a !== void 0 ? _a : 0) + 1;
50
+ pendingFetchTokens.set(id, currentFetchToken);
51
+ setInternalLoadingIds((prev) => {
52
+ const nextLoadingIds = new Set(prev);
53
+ nextLoadingIds.add(id);
54
+ return nextLoadingIds;
55
+ });
56
+ const clearLoading = () => {
57
+ // Only clear if our token is still the latest — otherwise a newer
58
+ // fetch is in flight and owns the loading state.
59
+ if (pendingFetchTokens.get(id) !== currentFetchToken)
60
+ return;
61
+ setInternalLoadingIds((prev) => {
62
+ const nextLoadingIds = new Set(prev);
63
+ nextLoadingIds.delete(id);
64
+ return nextLoadingIds;
65
+ });
66
+ };
67
+ Promise.resolve(onExpand(row)).finally(clearLoading);
68
+ }, [expandedIds, onExpandedIdsChange, onExpand]);
69
+ const classes = classNames("vuiSpans", className, {
70
+ "vuiSpans--fluid": fluid
71
+ });
72
+ let tbodyContent;
73
+ if (content) {
74
+ tbodyContent = _jsx(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: content }));
75
+ }
76
+ else if (isLoading) {
77
+ tbodyContent = (_jsxs(VuiTableContent, Object.assign({ colSpan: columnCount }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, { children: _jsx("p", { children: "Loading" }) }) }))] })));
78
+ }
79
+ else {
80
+ tbodyContent = flatSpans.map((flat, rowIndex) => {
81
+ const { row, id, depth, hasChildren, hasLoadedChildren, posInSet, setSize } = flat;
82
+ const isExpanded = expandedIds.has(id);
83
+ const externalLoading = isLoadingChildren ? isLoadingChildren(row) : false;
84
+ const isLoadingRow = internalLoadingIds.has(id) || externalLoading;
85
+ const showLoadingRow = isExpanded && hasChildren && !hasLoadedChildren && isLoadingRow;
86
+ return (_jsxs(React.Fragment, { children: [_jsx(VuiSpansRow, { row: row, rowIndex: rowIndex, columns: columns, id: id, depth: depth, indentSize: indentSize, posInSet: posInSet, setSize: setSize, hasChildren: hasChildren, isExpanded: isExpanded, onToggle: () => handleToggle(row, id, hasChildren, hasLoadedChildren), rowDecorator: rowDecorator }), showLoadingRow && (_jsx(VuiSpansLoadingRow, { colSpan: columnCount, depth: depth + 1, indentSize: indentSize }, `${id}__loading`))] }, id));
87
+ });
88
+ }
89
+ return (_jsx("div", Object.assign({ className: "vuiSpansWrapper", "data-testid": dataTestId }, { children: _jsxs("table", Object.assign({ className: classes, role: "treegrid" }, { children: [_jsx("thead", Object.assign({ className: isHeaderSticky ? "vuiSpansStickyHeader" : undefined }, { children: _jsx("tr", Object.assign({ role: "row" }, { children: columns.map((column) => {
90
+ const { name, width } = column;
91
+ const styles = width ? { width } : undefined;
92
+ return (_jsx("th", Object.assign({ role: "columnheader", style: styles }, { children: _jsx(VuiSpansHeaderCell, { column: column }) }), name));
93
+ }) })) })), _jsx("tbody", { children: tbodyContent })] })) })));
94
+ };
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ children: React.ReactNode;
3
+ };
4
+ export declare const VuiSpansCell: ({ children }: Props) => import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export const VuiSpansCell = ({ children }) => {
3
+ return _jsx("div", Object.assign({ className: "vuiSpansCell" }, { children: children }));
4
+ };
@@ -0,0 +1,6 @@
1
+ import { Column } from "../table/types";
2
+ type Props<T> = {
3
+ column: Column<T>;
4
+ };
5
+ export declare const VuiSpansHeaderCell: <T>({ column }: Props<T>) => import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export const VuiSpansHeaderCell = ({ column }) => {
3
+ const { name, header } = column;
4
+ return _jsx("div", Object.assign({ className: "vuiSpansHeaderCell" }, { children: header.render ? header.render() : name }));
5
+ };
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ colSpan: number;
3
+ depth: number;
4
+ indentSize: number;
5
+ message?: React.ReactNode;
6
+ };
7
+ export declare const VuiSpansLoadingRow: ({ colSpan, depth, indentSize, message }: Props) => import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { VuiFlexContainer } from "../flex/FlexContainer";
3
+ import { VuiFlexItem } from "../flex/FlexItem";
4
+ import { VuiSpinner } from "../spinner/Spinner";
5
+ import { VuiText } from "../typography/Text";
6
+ import { VuiTextColor } from "../typography/TextColor";
7
+ export const VuiSpansLoadingRow = ({ colSpan, depth, indentSize, message = "Loading…" }) => {
8
+ return (_jsx("tr", Object.assign({ className: "vuiSpansLoadingRow vuiSpansRow--inert" }, { children: _jsx("td", Object.assign({ colSpan: colSpan, className: "vuiSpansLoadingRow__cell" }, { children: _jsx("div", Object.assign({ className: "vuiSpansLoadingRow__inner", style: { paddingLeft: depth * indentSize } }, { children: _jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, Object.assign({ size: "xs" }, { children: _jsx("p", { children: _jsx(VuiTextColor, Object.assign({ color: "subdued" }, { children: message })) }) })) }))] })) })) })) })));
9
+ };
@@ -0,0 +1,17 @@
1
+ import { Column, Row } from "../table/types";
2
+ type Props<T> = {
3
+ row: T;
4
+ rowIndex: number;
5
+ columns: Column<T>[];
6
+ id: string;
7
+ depth: number;
8
+ indentSize: number;
9
+ posInSet: number;
10
+ setSize: number;
11
+ hasChildren: boolean;
12
+ isExpanded: boolean;
13
+ onToggle: () => void;
14
+ rowDecorator?: (row: T, depth: number) => React.HTMLAttributes<HTMLTableRowElement>;
15
+ };
16
+ export declare const VuiSpansRow: <T extends Row>({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }: Props<T>) => import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,43 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import classNames from "classnames";
14
+ import { BiChevronDown, BiChevronRight } from "react-icons/bi";
15
+ import { VuiIcon } from "../icon/Icon";
16
+ import { VuiIconButton } from "../button/IconButton";
17
+ import { VuiSpansCell } from "./SpansCell";
18
+ // Cap visual indentation at this depth so the first column doesn't overflow
19
+ // for very deep traces. `aria-level` continues to reflect the true depth.
20
+ const MAX_VISUAL_DEPTH = 12;
21
+ export const VuiSpansRow = ({ row, rowIndex, columns, id, depth, indentSize, posInSet, setSize, hasChildren, isExpanded, onToggle, rowDecorator }) => {
22
+ var _a;
23
+ const decoratorAttrs = (_a = rowDecorator === null || rowDecorator === void 0 ? void 0 : rowDecorator(row, depth)) !== null && _a !== void 0 ? _a : {};
24
+ const { className: decoratorClassName } = decoratorAttrs, restDecoratorAttrs = __rest(decoratorAttrs, ["className"]);
25
+ const rowClassName = classNames("vuiSpansRow", decoratorClassName, {
26
+ "vuiSpansRow-isExpanded": hasChildren && isExpanded,
27
+ "vuiSpansRow--leaf": !hasChildren
28
+ });
29
+ const visualDepth = Math.min(depth, MAX_VISUAL_DEPTH);
30
+ const indentStyle = { paddingLeft: visualDepth * indentSize };
31
+ return (_jsx("tr", Object.assign({ role: "row", "aria-level": depth + 1, "aria-posinset": posInSet, "aria-setsize": setSize, "aria-expanded": hasChildren ? isExpanded : undefined, "data-row-id": id, className: rowClassName }, restDecoratorAttrs, { children: columns.map((column, columnIndex) => {
32
+ const { name, render, className: columnClassName, testId } = column;
33
+ const cellClasses = classNames("vuiSpansCellWrapper", columnClassName, {
34
+ "vuiSpansCellWrapper--first": columnIndex === 0,
35
+ "vuiSpansCellWrapper--truncate": column.truncate
36
+ });
37
+ const cellContent = render ? render(row, rowIndex) : row[name];
38
+ if (columnIndex === 0) {
39
+ return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsxs("div", Object.assign({ className: "vuiSpansCell__indent", style: indentStyle }, { children: [_jsx("div", Object.assign({ className: "vuiSpansCell__chevron" }, { children: hasChildren ? (_jsx(VuiIconButton, { icon: _jsx(VuiIcon, { children: isExpanded ? _jsx(BiChevronDown, {}) : _jsx(BiChevronRight, {}) }), size: "xs", color: "neutral", "aria-label": isExpanded ? "Collapse span" : "Expand span", onClick: onToggle, "data-testid": `spanExpandToggle-${id}` })) : (_jsx("span", { className: "vuiSpansCell__chevronPlaceholder", "aria-hidden": "true" })) })), _jsx(VuiSpansCell, { children: cellContent })] })) }), name));
40
+ }
41
+ return (_jsx("td", Object.assign({ role: "gridcell", className: cellClasses, "data-testid": typeof testId === "function" ? testId(row) : testId }, { children: _jsx(VuiSpansCell, { children: cellContent }) }), name));
42
+ }) })));
43
+ };
@@ -0,0 +1,110 @@
1
+ $spansChevronWidth: 24px;
2
+
3
+ .vuiSpansWrapper {
4
+ width: 100%;
5
+ position: relative;
6
+ }
7
+
8
+ .vuiSpans {
9
+ width: 100%;
10
+ table-layout: fixed;
11
+
12
+ thead {
13
+ border-bottom: 1px solid var(--vui-color-border-medium);
14
+ }
15
+
16
+ tbody tr {
17
+ border-bottom: 1px solid var(--vui-color-border-light);
18
+
19
+ &:not(.vuiSpansRow--inert):hover {
20
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.25);
21
+ }
22
+
23
+ &:last-child {
24
+ border-bottom: 1px solid var(--vui-color-border-medium);
25
+ }
26
+ }
27
+
28
+ th {
29
+ font-size: $fontSizeStandard;
30
+ font-weight: $fontWeightBold;
31
+ padding: $sizeXxs;
32
+ text-align: left;
33
+ }
34
+
35
+ td {
36
+ font-size: $fontSizeStandard;
37
+ padding: $sizeXxs;
38
+ vertical-align: middle;
39
+ word-break: break-word;
40
+ }
41
+ }
42
+
43
+ .vuiSpans--fluid {
44
+ table-layout: auto;
45
+ }
46
+
47
+ // First column: depth-indent + chevron + content laid out in a row.
48
+ .vuiSpansCell__indent {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: $sizeXxs;
52
+ // padding-left set inline based on depth.
53
+ }
54
+
55
+ .vuiSpansCell__chevron {
56
+ flex: 0 0 auto;
57
+ width: $spansChevronWidth;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ }
62
+
63
+ .vuiSpansCell__chevronPlaceholder {
64
+ display: inline-block;
65
+ width: $spansChevronWidth;
66
+ height: 1px; // Reserves horizontal space; height irrelevant.
67
+ }
68
+
69
+ .vuiSpansCell {
70
+ display: flex;
71
+ align-items: center;
72
+ min-width: 0;
73
+ flex: 1 1 auto;
74
+ }
75
+
76
+ .vuiSpansCellWrapper--truncate {
77
+ min-width: 0;
78
+ overflow: hidden;
79
+ white-space: nowrap;
80
+ text-overflow: ellipsis;
81
+ }
82
+
83
+ .vuiSpansHeaderCell {
84
+ display: flex;
85
+ align-items: center;
86
+ padding: $sizeXxs;
87
+ }
88
+
89
+ // Sticky header — same treatment as VuiTable.
90
+ .vuiSpansStickyHeader {
91
+ position: sticky;
92
+ top: 0;
93
+ background-color: var(--vui-color-empty-shade);
94
+ z-index: 1;
95
+ }
96
+
97
+ // Loading row injected under a parent during lazy fetch.
98
+ .vuiSpansLoadingRow {
99
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.15);
100
+ }
101
+
102
+ .vuiSpansLoadingRow__cell {
103
+ padding: $sizeXxs !important;
104
+ }
105
+
106
+ .vuiSpansLoadingRow__inner {
107
+ display: flex;
108
+ align-items: center;
109
+ // padding-left set inline based on depth.
110
+ }
@@ -0,0 +1,14 @@
1
+ import { SpansRow } from "./types";
2
+ export type FlatSpan<T> = {
3
+ row: T;
4
+ id: string;
5
+ parentId: string | null;
6
+ depth: number;
7
+ hasChildren: boolean;
8
+ hasLoadedChildren: boolean;
9
+ posInSet: number;
10
+ setSize: number;
11
+ };
12
+ export type IdAccessor<T> = (row: T) => string;
13
+ export type ParentAccessor<T> = (row: T) => string | null;
14
+ export declare const buildAndFlattenSpans: <T extends SpansRow>(rows: T[], expandedIds: Set<string>, getId: IdAccessor<T>, getParentId: ParentAccessor<T>) => FlatSpan<T>[];
@@ -0,0 +1,69 @@
1
+ // Flattens a list of rows (linked by `parentId`) into a render-ordered, depth-tagged array,
2
+ // showing only visible nodes based on `expandedIds`. The render layer maps this 1:1 to <tr>s.
3
+ // Orphans (rows with missing parents) are dropped. Cycles are avoided using an ancestor set.
4
+ export const buildAndFlattenSpans = (rows, expandedIds, getId, getParentId) => {
5
+ // First pass: collect every known id so we can detect orphans below.
6
+ const allIds = new Set();
7
+ for (const row of rows) {
8
+ allIds.add(getId(row));
9
+ }
10
+ // Group rows by their parent id, with top-level rows under a sentinel key.
11
+ // The sentinel can't collide with a real id because it contains spaces.
12
+ const ROOT_KEY = " __VuiSpansRoot__ ";
13
+ const childrenByParent = new Map();
14
+ for (const row of rows) {
15
+ const parentId = getParentId(row);
16
+ // Drop orphans: parent id set but not present in the rows list.
17
+ if (parentId !== null && !allIds.has(parentId))
18
+ continue;
19
+ const key = parentId === null ? ROOT_KEY : parentId;
20
+ const list = childrenByParent.get(key);
21
+ if (list) {
22
+ list.push(row);
23
+ }
24
+ else {
25
+ childrenByParent.set(key, [row]);
26
+ }
27
+ }
28
+ const result = [];
29
+ // Recursive depth-first walk. `ancestors` carries the chain of ids from the
30
+ // root down to (but not including) the current row, used to detect cycles.
31
+ const walk = (parentKey, parentId, depth, ancestors) => {
32
+ const siblings = childrenByParent.get(parentKey);
33
+ if (!siblings)
34
+ return;
35
+ siblings.forEach((row, index) => {
36
+ const id = getId(row);
37
+ // Break cycle: this id appears earlier in its own ancestor chain.
38
+ if (ancestors.has(id))
39
+ return;
40
+ const ownChildren = childrenByParent.get(id);
41
+ const ownChildCount = ownChildren ? ownChildren.length : 0;
42
+ const hasLoadedChildren = ownChildCount > 0;
43
+ // The chevron should appear if either we already have children to show
44
+ // or the row is hinting that lazily-loadable children exist.
45
+ const hasChildren = row.hasChildren || hasLoadedChildren;
46
+ result.push({
47
+ row,
48
+ id,
49
+ parentId,
50
+ depth,
51
+ hasChildren,
52
+ hasLoadedChildren,
53
+ posInSet: index + 1,
54
+ setSize: siblings.length
55
+ });
56
+ // Only descend if the user has expanded this row AND there's something
57
+ // loaded to descend into. The lazy-only case (`hasChildren` but
58
+ // `!hasLoadedChildren`) is handled by the loading row in the render
59
+ // layer, not here.
60
+ if (hasChildren && expandedIds.has(id) && ownChildCount > 0) {
61
+ const nextAncestors = new Set(ancestors);
62
+ nextAncestors.add(id);
63
+ walk(id, id, depth + 1, nextAncestors);
64
+ }
65
+ });
66
+ };
67
+ walk(ROOT_KEY, null, 0, new Set());
68
+ return result;
69
+ };
@@ -0,0 +1,388 @@
1
+ import { buildAndFlattenSpans } from "./buildAndFlattenSpans";
2
+ const getId = (s) => s.id;
3
+ const getParentId = (s) => s.parentId;
4
+ const flatten = (rows, expandedIds = new Set()) => buildAndFlattenSpans(rows, expandedIds, getId, getParentId);
5
+ describe("buildAndFlattenSpans", () => {
6
+ test("emits only top-level rows when nothing is expanded", () => {
7
+ const rows = [
8
+ { id: "a", parentId: null },
9
+ { id: "b", parentId: null },
10
+ { id: "a-1", parentId: "a" },
11
+ { id: "a-2", parentId: "a" }
12
+ ];
13
+ expect(flatten(rows)).toEqual([
14
+ {
15
+ row: rows[0],
16
+ id: "a",
17
+ parentId: null,
18
+ depth: 0,
19
+ hasChildren: true,
20
+ hasLoadedChildren: true,
21
+ posInSet: 1,
22
+ setSize: 2
23
+ },
24
+ {
25
+ row: rows[1],
26
+ id: "b",
27
+ parentId: null,
28
+ depth: 0,
29
+ hasChildren: false,
30
+ hasLoadedChildren: false,
31
+ posInSet: 2,
32
+ setSize: 2
33
+ }
34
+ ]);
35
+ });
36
+ test("emits children of expanded parents in render order", () => {
37
+ const rows = [
38
+ { id: "a", parentId: null },
39
+ { id: "b", parentId: null },
40
+ { id: "a-1", parentId: "a" },
41
+ { id: "a-2", parentId: "a" }
42
+ ];
43
+ expect(flatten(rows, new Set(["a"]))).toEqual([
44
+ {
45
+ row: rows[0],
46
+ id: "a",
47
+ parentId: null,
48
+ depth: 0,
49
+ hasChildren: true,
50
+ hasLoadedChildren: true,
51
+ posInSet: 1,
52
+ setSize: 2
53
+ },
54
+ {
55
+ row: rows[2],
56
+ id: "a-1",
57
+ parentId: "a",
58
+ depth: 1,
59
+ hasChildren: false,
60
+ hasLoadedChildren: false,
61
+ posInSet: 1,
62
+ setSize: 2
63
+ },
64
+ {
65
+ row: rows[3],
66
+ id: "a-2",
67
+ parentId: "a",
68
+ depth: 1,
69
+ hasChildren: false,
70
+ hasLoadedChildren: false,
71
+ posInSet: 2,
72
+ setSize: 2
73
+ },
74
+ {
75
+ row: rows[1],
76
+ id: "b",
77
+ parentId: null,
78
+ depth: 0,
79
+ hasChildren: false,
80
+ hasLoadedChildren: false,
81
+ posInSet: 2,
82
+ setSize: 2
83
+ }
84
+ ]);
85
+ });
86
+ test("walks deep trees", () => {
87
+ const rows = [
88
+ { id: "a", parentId: null },
89
+ { id: "a-1", parentId: "a" },
90
+ { id: "a-1-1", parentId: "a-1" },
91
+ { id: "a-1-1-1", parentId: "a-1-1" }
92
+ ];
93
+ expect(flatten(rows, new Set(["a", "a-1", "a-1-1"]))).toEqual([
94
+ {
95
+ row: rows[0],
96
+ id: "a",
97
+ parentId: null,
98
+ depth: 0,
99
+ hasChildren: true,
100
+ hasLoadedChildren: true,
101
+ posInSet: 1,
102
+ setSize: 1
103
+ },
104
+ {
105
+ row: rows[1],
106
+ id: "a-1",
107
+ parentId: "a",
108
+ depth: 1,
109
+ hasChildren: true,
110
+ hasLoadedChildren: true,
111
+ posInSet: 1,
112
+ setSize: 1
113
+ },
114
+ {
115
+ row: rows[2],
116
+ id: "a-1-1",
117
+ parentId: "a-1",
118
+ depth: 2,
119
+ hasChildren: true,
120
+ hasLoadedChildren: true,
121
+ posInSet: 1,
122
+ setSize: 1
123
+ },
124
+ {
125
+ row: rows[3],
126
+ id: "a-1-1-1",
127
+ parentId: "a-1-1",
128
+ depth: 3,
129
+ hasChildren: false,
130
+ hasLoadedChildren: false,
131
+ posInSet: 1,
132
+ setSize: 1
133
+ }
134
+ ]);
135
+ });
136
+ test("flags hasChildren when actual children exist", () => {
137
+ const rows = [
138
+ { id: "a", parentId: null },
139
+ { id: "b", parentId: null },
140
+ { id: "a-1", parentId: "a" }
141
+ ];
142
+ expect(flatten(rows)).toEqual([
143
+ {
144
+ row: rows[0],
145
+ id: "a",
146
+ parentId: null,
147
+ depth: 0,
148
+ hasChildren: true,
149
+ hasLoadedChildren: true,
150
+ posInSet: 1,
151
+ setSize: 2
152
+ },
153
+ {
154
+ row: rows[1],
155
+ id: "b",
156
+ parentId: null,
157
+ depth: 0,
158
+ hasChildren: false,
159
+ hasLoadedChildren: false,
160
+ posInSet: 2,
161
+ setSize: 2
162
+ }
163
+ ]);
164
+ });
165
+ test("flags hasChildren when row says so even with no actual children", () => {
166
+ const rows = [{ id: "a", parentId: null, hasChildren: true }];
167
+ expect(flatten(rows)).toEqual([
168
+ {
169
+ row: rows[0],
170
+ id: "a",
171
+ parentId: null,
172
+ depth: 0,
173
+ hasChildren: true,
174
+ hasLoadedChildren: false,
175
+ posInSet: 1,
176
+ setSize: 1
177
+ }
178
+ ]);
179
+ });
180
+ test("does not recurse when expanded but no children loaded", () => {
181
+ // Lazy-load case: row says it has children but they aren't in the list.
182
+ const rows = [{ id: "a", parentId: null, hasChildren: true }];
183
+ expect(flatten(rows, new Set(["a"]))).toEqual([
184
+ {
185
+ row: rows[0],
186
+ id: "a",
187
+ parentId: null,
188
+ depth: 0,
189
+ hasChildren: true,
190
+ hasLoadedChildren: false,
191
+ posInSet: 1,
192
+ setSize: 1
193
+ }
194
+ ]);
195
+ });
196
+ test("drops orphans (parent id that doesn't appear in rows)", () => {
197
+ const rows = [
198
+ { id: "a", parentId: null },
199
+ { id: "ghost", parentId: "missing" }
200
+ ];
201
+ expect(flatten(rows)).toEqual([
202
+ {
203
+ row: rows[0],
204
+ id: "a",
205
+ parentId: null,
206
+ depth: 0,
207
+ hasChildren: false,
208
+ hasLoadedChildren: false,
209
+ posInSet: 1,
210
+ setSize: 1
211
+ }
212
+ ]);
213
+ });
214
+ test("breaks cycles", () => {
215
+ // Duplicate id forms a cycle: the root "a" claims "a" as a child of itself.
216
+ // The recursive walk should bail at the duplicate, leaving just the root.
217
+ const rows = [
218
+ { id: "a", parentId: null },
219
+ { id: "a", parentId: "a" }
220
+ ];
221
+ expect(flatten(rows, new Set(["a"]))).toEqual([
222
+ {
223
+ row: rows[0],
224
+ id: "a",
225
+ parentId: null,
226
+ depth: 0,
227
+ hasChildren: true,
228
+ hasLoadedChildren: true,
229
+ posInSet: 1,
230
+ setSize: 1
231
+ }
232
+ ]);
233
+ });
234
+ test("sets posInSet and setSize per sibling group", () => {
235
+ const rows = [
236
+ { id: "a", parentId: null },
237
+ { id: "b", parentId: null },
238
+ { id: "c", parentId: null },
239
+ { id: "b-1", parentId: "b" },
240
+ { id: "b-2", parentId: "b" }
241
+ ];
242
+ expect(flatten(rows, new Set(["b"]))).toEqual([
243
+ {
244
+ row: rows[0],
245
+ id: "a",
246
+ parentId: null,
247
+ depth: 0,
248
+ hasChildren: false,
249
+ hasLoadedChildren: false,
250
+ posInSet: 1,
251
+ setSize: 3
252
+ },
253
+ {
254
+ row: rows[1],
255
+ id: "b",
256
+ parentId: null,
257
+ depth: 0,
258
+ hasChildren: true,
259
+ hasLoadedChildren: true,
260
+ posInSet: 2,
261
+ setSize: 3
262
+ },
263
+ {
264
+ row: rows[3],
265
+ id: "b-1",
266
+ parentId: "b",
267
+ depth: 1,
268
+ hasChildren: false,
269
+ hasLoadedChildren: false,
270
+ posInSet: 1,
271
+ setSize: 2
272
+ },
273
+ {
274
+ row: rows[4],
275
+ id: "b-2",
276
+ parentId: "b",
277
+ depth: 1,
278
+ hasChildren: false,
279
+ hasLoadedChildren: false,
280
+ posInSet: 2,
281
+ setSize: 2
282
+ },
283
+ {
284
+ row: rows[2],
285
+ id: "c",
286
+ parentId: null,
287
+ depth: 0,
288
+ hasChildren: false,
289
+ hasLoadedChildren: false,
290
+ posInSet: 3,
291
+ setSize: 3
292
+ }
293
+ ]);
294
+ });
295
+ test("preserves input order among siblings", () => {
296
+ const rows = [
297
+ { id: "z", parentId: null },
298
+ { id: "a", parentId: null },
299
+ { id: "m", parentId: null }
300
+ ];
301
+ expect(flatten(rows)).toEqual([
302
+ {
303
+ row: rows[0],
304
+ id: "z",
305
+ parentId: null,
306
+ depth: 0,
307
+ hasChildren: false,
308
+ hasLoadedChildren: false,
309
+ posInSet: 1,
310
+ setSize: 3
311
+ },
312
+ {
313
+ row: rows[1],
314
+ id: "a",
315
+ parentId: null,
316
+ depth: 0,
317
+ hasChildren: false,
318
+ hasLoadedChildren: false,
319
+ posInSet: 2,
320
+ setSize: 3
321
+ },
322
+ {
323
+ row: rows[2],
324
+ id: "m",
325
+ parentId: null,
326
+ depth: 0,
327
+ hasChildren: false,
328
+ hasLoadedChildren: false,
329
+ posInSet: 3,
330
+ setSize: 3
331
+ }
332
+ ]);
333
+ });
334
+ test("collapsed parent hides its descendants but state persists for re-expand", () => {
335
+ const rows = [
336
+ { id: "a", parentId: null },
337
+ { id: "a-1", parentId: "a" },
338
+ { id: "a-1-1", parentId: "a-1" }
339
+ ];
340
+ // Both a and a-1 expanded — full subtree visible.
341
+ expect(flatten(rows, new Set(["a", "a-1"]))).toEqual([
342
+ {
343
+ row: rows[0],
344
+ id: "a",
345
+ parentId: null,
346
+ depth: 0,
347
+ hasChildren: true,
348
+ hasLoadedChildren: true,
349
+ posInSet: 1,
350
+ setSize: 1
351
+ },
352
+ {
353
+ row: rows[1],
354
+ id: "a-1",
355
+ parentId: "a",
356
+ depth: 1,
357
+ hasChildren: true,
358
+ hasLoadedChildren: true,
359
+ posInSet: 1,
360
+ setSize: 1
361
+ },
362
+ {
363
+ row: rows[2],
364
+ id: "a-1-1",
365
+ parentId: "a-1",
366
+ depth: 2,
367
+ hasChildren: false,
368
+ hasLoadedChildren: false,
369
+ posInSet: 1,
370
+ setSize: 1
371
+ }
372
+ ]);
373
+ // a-1 still in expandedIds but a is collapsed — only a should render. The
374
+ // a-1 expand state is preserved for when the user re-expands a.
375
+ expect(flatten(rows, new Set(["a-1"]))).toEqual([
376
+ {
377
+ row: rows[0],
378
+ id: "a",
379
+ parentId: null,
380
+ depth: 0,
381
+ hasChildren: true,
382
+ hasLoadedChildren: true,
383
+ posInSet: 1,
384
+ setSize: 1
385
+ }
386
+ ]);
387
+ });
388
+ });
@@ -0,0 +1,5 @@
1
+ import { Column, Row } from "../table/types";
2
+ export type { Column };
3
+ export type SpansRow = Row & {
4
+ hasChildren?: boolean;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -4786,6 +4786,104 @@ h2.react-datepicker__current-month {
4786
4786
  height: 40px;
4787
4787
  }
4788
4788
 
4789
+ .vuiSpansWrapper {
4790
+ width: 100%;
4791
+ position: relative;
4792
+ }
4793
+
4794
+ .vuiSpans {
4795
+ width: 100%;
4796
+ table-layout: fixed;
4797
+ }
4798
+ .vuiSpans thead {
4799
+ border-bottom: 1px solid var(--vui-color-border-medium);
4800
+ }
4801
+ .vuiSpans tbody tr {
4802
+ border-bottom: 1px solid var(--vui-color-border-light);
4803
+ }
4804
+ .vuiSpans tbody tr:not(.vuiSpansRow--inert):hover {
4805
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.25);
4806
+ }
4807
+ .vuiSpans tbody tr:last-child {
4808
+ border-bottom: 1px solid var(--vui-color-border-medium);
4809
+ }
4810
+ .vuiSpans th {
4811
+ font-size: 14px;
4812
+ font-weight: 600;
4813
+ padding: 4px;
4814
+ text-align: left;
4815
+ }
4816
+ .vuiSpans td {
4817
+ font-size: 14px;
4818
+ padding: 4px;
4819
+ vertical-align: middle;
4820
+ word-break: break-word;
4821
+ }
4822
+
4823
+ .vuiSpans--fluid {
4824
+ table-layout: auto;
4825
+ }
4826
+
4827
+ .vuiSpansCell__indent {
4828
+ display: flex;
4829
+ align-items: center;
4830
+ gap: 4px;
4831
+ }
4832
+
4833
+ .vuiSpansCell__chevron {
4834
+ flex: 0 0 auto;
4835
+ width: 24px;
4836
+ display: flex;
4837
+ align-items: center;
4838
+ justify-content: center;
4839
+ }
4840
+
4841
+ .vuiSpansCell__chevronPlaceholder {
4842
+ display: inline-block;
4843
+ width: 24px;
4844
+ height: 1px;
4845
+ }
4846
+
4847
+ .vuiSpansCell {
4848
+ display: flex;
4849
+ align-items: center;
4850
+ min-width: 0;
4851
+ flex: 1 1 auto;
4852
+ }
4853
+
4854
+ .vuiSpansCellWrapper--truncate {
4855
+ min-width: 0;
4856
+ overflow: hidden;
4857
+ white-space: nowrap;
4858
+ text-overflow: ellipsis;
4859
+ }
4860
+
4861
+ .vuiSpansHeaderCell {
4862
+ display: flex;
4863
+ align-items: center;
4864
+ padding: 4px;
4865
+ }
4866
+
4867
+ .vuiSpansStickyHeader {
4868
+ position: sticky;
4869
+ top: 0;
4870
+ background-color: var(--vui-color-empty-shade);
4871
+ z-index: 1;
4872
+ }
4873
+
4874
+ .vuiSpansLoadingRow {
4875
+ background-color: rgba(var(--vui-color-light-shade-rgb), 0.15);
4876
+ }
4877
+
4878
+ .vuiSpansLoadingRow__cell {
4879
+ padding: 4px !important;
4880
+ }
4881
+
4882
+ .vuiSpansLoadingRow__inner {
4883
+ display: flex;
4884
+ align-items: center;
4885
+ }
4886
+
4789
4887
  .vuiSpinner {
4790
4888
  display: inline-block;
4791
4889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectara/vectara-ui",
3
- "version": "16.3.1",
3
+ "version": "16.4.0",
4
4
  "homepage": "./",
5
5
  "description": "Vectara's design system, codified as a React and Sass component library",
6
6
  "author": "Vectara",
@@ -0,0 +1,170 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ VuiBadge,
4
+ VuiFlexContainer,
5
+ VuiFlexItem,
6
+ VuiIcon,
7
+ VuiSpacer,
8
+ VuiSpans,
9
+ VuiText,
10
+ VuiTextColor,
11
+ VuiToggle
12
+ } from "../../../lib";
13
+ import { BiCog, BiError, BiNetworkChart, BiSearch, BiSitemap, BiSpreadsheet } from "react-icons/bi";
14
+ import { FakeSpan, LAZY_CHILDREN, ROOT_SPANS } from "./createFakeSpans";
15
+
16
+ const KIND_ICON = {
17
+ workflow: BiSitemap,
18
+ tool: BiCog,
19
+ llm: BiNetworkChart,
20
+ search: BiSearch,
21
+ embedding: BiSpreadsheet
22
+ } as const;
23
+
24
+ const STATUS_COLOR = {
25
+ ok: "success",
26
+ error: "danger",
27
+ running: "primary"
28
+ } as const;
29
+
30
+ export const Spans = () => {
31
+ const [hasData, setHasData] = useState(true);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [hasError, setHasError] = useState(false);
34
+ const [isHeaderSticky, setIsHeaderSticky] = useState(false);
35
+ const [fluid, setFluid] = useState(true);
36
+
37
+ const [rows, setRows] = useState<FakeSpan[]>(ROOT_SPANS);
38
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set(["wf-1"]));
39
+
40
+ const visibleRows = hasData ? rows : [];
41
+
42
+ const onExpand = async (row: FakeSpan) => {
43
+ // Only the wf-1-c2 span lazy-loads in this demo.
44
+ if (row.id !== "wf-1-c2") return;
45
+
46
+ // Simulate a network round-trip.
47
+ await new Promise((resolve) => setTimeout(resolve, 1200));
48
+
49
+ setRows((prev) => {
50
+ // De-dupe in case the user toggles repeatedly.
51
+ const alreadyLoaded = prev.some((r) => r.parentId === row.id);
52
+ if (alreadyLoaded) return prev;
53
+ return prev.concat(LAZY_CHILDREN);
54
+ });
55
+ };
56
+
57
+ const columns = useMemo(
58
+ () => [
59
+ {
60
+ name: "name",
61
+ width: "55%",
62
+ header: { render: () => "Name" },
63
+ render: (row: FakeSpan) => {
64
+ const KindIcon = KIND_ICON[row.kind];
65
+ return (
66
+ <VuiFlexContainer alignItems="center" spacing="xs">
67
+ <VuiFlexItem grow={false} shrink={false}>
68
+ <VuiIcon size="s" color="subdued">
69
+ <KindIcon />
70
+ </VuiIcon>
71
+ </VuiFlexItem>
72
+ <VuiFlexItem grow={false}>{row.name}</VuiFlexItem>
73
+ </VuiFlexContainer>
74
+ );
75
+ }
76
+ },
77
+ {
78
+ name: "status",
79
+ width: "120px",
80
+ header: { render: () => "Status" },
81
+ render: (row: FakeSpan) => <VuiBadge color={STATUS_COLOR[row.status]}>{row.status}</VuiBadge>
82
+ },
83
+ {
84
+ name: "startAt",
85
+ width: "140px",
86
+ header: { render: () => "Start at" },
87
+ render: (row: FakeSpan) => (
88
+ <VuiText size="s">
89
+ <p>
90
+ <VuiTextColor color="subdued">{row.startAt}</VuiTextColor>
91
+ </p>
92
+ </VuiText>
93
+ )
94
+ },
95
+ {
96
+ name: "duration",
97
+ width: "120px",
98
+ header: { render: () => "Duration" },
99
+ render: (row: FakeSpan) => (
100
+ <VuiText size="s">
101
+ <p>
102
+ <VuiTextColor color="subdued">{row.durationMs}ms</VuiTextColor>
103
+ </p>
104
+ </VuiText>
105
+ )
106
+ }
107
+ ],
108
+ []
109
+ );
110
+
111
+ const errorContent = (
112
+ <>
113
+ <VuiFlexItem grow={false}>
114
+ <VuiIcon color="danger">
115
+ <BiError />
116
+ </VuiIcon>
117
+ </VuiFlexItem>
118
+ <VuiFlexItem grow={false}>
119
+ <VuiText>
120
+ <p>
121
+ <VuiTextColor color="danger">Couldn't retrieve trace</VuiTextColor>
122
+ </p>
123
+ </VuiText>
124
+ </VuiFlexItem>
125
+ </>
126
+ );
127
+
128
+ return (
129
+ <>
130
+ <VuiFlexContainer wrap spacing="l">
131
+ <VuiFlexItem shrink={false}>
132
+ <VuiToggle label="Has data" checked={hasData} onChange={(e) => setHasData(e.target.checked)} />
133
+ </VuiFlexItem>
134
+ <VuiFlexItem shrink={false}>
135
+ <VuiToggle label="Is loading" checked={isLoading} onChange={(e) => setIsLoading(e.target.checked)} />
136
+ </VuiFlexItem>
137
+ <VuiFlexItem shrink={false}>
138
+ <VuiToggle label="Has error" checked={hasError} onChange={(e) => setHasError(e.target.checked)} />
139
+ </VuiFlexItem>
140
+ <VuiFlexItem shrink={false}>
141
+ <VuiToggle
142
+ label="Sticky header"
143
+ checked={isHeaderSticky}
144
+ onChange={(e) => setIsHeaderSticky(e.target.checked)}
145
+ />
146
+ </VuiFlexItem>
147
+ <VuiFlexItem shrink={false}>
148
+ <VuiToggle label="Fluid layout" checked={fluid} onChange={(e) => setFluid(e.target.checked)} />
149
+ </VuiFlexItem>
150
+ </VuiFlexContainer>
151
+
152
+ <VuiSpacer size="xl" />
153
+
154
+ <VuiSpans
155
+ data-testid="spansTable"
156
+ idField="id"
157
+ parentField="parentId"
158
+ rows={visibleRows}
159
+ columns={columns}
160
+ expandedIds={expandedIds}
161
+ onExpandedIdsChange={setExpandedIds}
162
+ onExpand={onExpand}
163
+ isLoading={isLoading}
164
+ content={hasError ? errorContent : undefined}
165
+ isHeaderSticky={isHeaderSticky}
166
+ fluid={fluid}
167
+ />
168
+ </>
169
+ );
170
+ };
@@ -0,0 +1,118 @@
1
+ export type FakeSpan = {
2
+ id: string;
3
+ parentId: string | null;
4
+ name: string;
5
+ kind: "workflow" | "tool" | "llm" | "search" | "embedding";
6
+ status: "ok" | "error" | "running";
7
+ startAt: string;
8
+ durationMs: number;
9
+ hasChildren?: boolean;
10
+ };
11
+
12
+ export const ROOT_SPANS: FakeSpan[] = [
13
+ {
14
+ id: "wf-1",
15
+ parentId: null,
16
+ name: "Run search workflow",
17
+ kind: "workflow",
18
+ status: "ok",
19
+ startAt: "10:42:17.103",
20
+ durationMs: 3420
21
+ },
22
+ {
23
+ id: "wf-1-c1",
24
+ parentId: "wf-1",
25
+ name: "Generate query embeddings",
26
+ kind: "embedding",
27
+ status: "ok",
28
+ startAt: "10:42:17.110",
29
+ durationMs: 142
30
+ },
31
+ {
32
+ id: "wf-1-c2",
33
+ parentId: "wf-1",
34
+ name: "Vector search (corpus: support_docs)",
35
+ kind: "search",
36
+ status: "ok",
37
+ startAt: "10:42:17.260",
38
+ durationMs: 980,
39
+ // Lazy: chevron shown but no child rows present in initial fetch.
40
+ hasChildren: true
41
+ },
42
+ {
43
+ id: "wf-1-c3",
44
+ parentId: "wf-1",
45
+ name: "Re-rank top-25 candidates",
46
+ kind: "tool",
47
+ status: "ok",
48
+ startAt: "10:42:18.250",
49
+ durationMs: 412
50
+ },
51
+ {
52
+ id: "wf-1-c4",
53
+ parentId: "wf-1",
54
+ name: "Generate summary",
55
+ kind: "llm",
56
+ status: "running",
57
+ startAt: "10:42:18.670",
58
+ durationMs: 1850
59
+ },
60
+ {
61
+ id: "wf-1-c4-c1",
62
+ parentId: "wf-1-c4",
63
+ name: "Build prompt context",
64
+ kind: "tool",
65
+ status: "ok",
66
+ startAt: "10:42:18.671",
67
+ durationMs: 12
68
+ },
69
+ {
70
+ id: "wf-1-c4-c2",
71
+ parentId: "wf-1-c4",
72
+ name: "claude-haiku-4-5 streaming call",
73
+ kind: "llm",
74
+ status: "running",
75
+ startAt: "10:42:18.700",
76
+ durationMs: 1820
77
+ },
78
+ {
79
+ id: "wf-2",
80
+ parentId: null,
81
+ name: "Persist trace",
82
+ kind: "tool",
83
+ status: "error",
84
+ startAt: "10:42:20.523",
85
+ durationMs: 38
86
+ }
87
+ ];
88
+
89
+ // Children of "wf-1-c2"
90
+ export const LAZY_CHILDREN: FakeSpan[] = [
91
+ {
92
+ id: "wf-1-c2-c1",
93
+ parentId: "wf-1-c2",
94
+ name: "Search Vectara API",
95
+ kind: "search",
96
+ status: "ok",
97
+ startAt: "10:42:17.262",
98
+ durationMs: 940
99
+ },
100
+ {
101
+ id: "wf-1-c2-c2",
102
+ parentId: "wf-1-c2",
103
+ name: "Parse filter expression",
104
+ kind: "tool",
105
+ status: "ok",
106
+ startAt: "10:42:17.265",
107
+ durationMs: 4
108
+ },
109
+ {
110
+ id: "wf-1-c2-c3",
111
+ parentId: "wf-1-c2",
112
+ name: "Apply MMR diversification",
113
+ kind: "tool",
114
+ status: "ok",
115
+ startAt: "10:42:18.205",
116
+ durationMs: 32
117
+ }
118
+ ];
@@ -0,0 +1,11 @@
1
+ import { Spans } from "./Spans";
2
+ const SpansSource = require("!!raw-loader!./Spans");
3
+
4
+ export const spans = {
5
+ name: "Spans",
6
+ path: "/spans",
7
+ example: {
8
+ component: <Spans />,
9
+ source: SpansSource.default.toString()
10
+ }
11
+ };
@@ -57,6 +57,7 @@ import { summary } from "./pages/summary";
57
57
  import { skeleton } from "./pages/skeleton";
58
58
  import { superCheckboxGroup } from "./pages/superCheckboxGroup";
59
59
  import { superRadioGroup } from "./pages/superRadioGroup";
60
+ import { spans } from "./pages/spans";
60
61
  import { table } from "./pages/table";
61
62
  import { tabs } from "./pages/tabs";
62
63
  import { text } from "./pages/text";
@@ -87,7 +88,7 @@ export const categories: Category[] = [
87
88
  },
88
89
  {
89
90
  name: "Info",
90
- pages: [table, infoTable, infoList, statList, list, pagination]
91
+ pages: [table, spans, infoTable, infoList, statList, list, pagination]
91
92
  },
92
93
  {
93
94
  name: "Layout",