@parca/profile 0.19.102 → 0.19.103

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
@@ -3,6 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.19.103](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.102...@parca/profile@0.19.103) (2025-12-17)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
6
10
  ## [0.19.102](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.101...@parca/profile@0.19.102) (2025-12-15)
7
11
 
8
12
  **Note:** Version bump only for package @parca/profile
@@ -1 +1 @@
1
- {"version":3,"file":"FlameGraphNodes.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgB,MAAM,OAAO,CAAC;AAErC,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAKnC,OAAO,yCAAyC,CAAC;AAEjD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAelD,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,eAAe;;;CAG3B,CAAC;AACF,eAAO,MAAM,oBAAoB;;;;CAIhC,CAAC;AAEF,eAAO,MAAM,SAAS,4CA6NrB,CAAC"}
1
+ {"version":3,"file":"FlameGraphNodes.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/FlameGraphNodes.tsx"],"names":[],"mappings":"AAaA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAElD,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAKnC,OAAO,yCAAyC,CAAC;AAEjD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAelD,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAGnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CACnD;AAED,eAAO,MAAM,eAAe;;;CAG3B,CAAC;AACF,eAAO,MAAM,oBAAoB;;;;CAIhC,CAAC;AAEF,eAAO,MAAM,SAAS,4CA6PrB,CAAC"}
@@ -11,7 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import React, { useMemo } from 'react';
14
+ import React, { useCallback, useMemo } from 'react';
15
15
  import cx from 'classnames';
16
16
  import { selectBinaries, useAppSelector } from '@parca/store';
17
17
  import 'react-contexify/dist/ReactContexify.css';
@@ -30,28 +30,38 @@ export const fadedFlameRectStyles = {
30
30
  opacity: '0.5',
31
31
  };
32
32
  export const FlameNode = React.memo(function FlameNodeNoMemo({ table, row, colors, colorBy, height, totalWidth, darkMode, compareMode, colorForSimilarNodes, selectedRow, onClick, onContextMenu, hoveringRow, setHoveringRow, isFlameChart, profileSource, isRenderedAsFlamegraph = false, isInSandwichView = false, maxDepth = 0, effectiveDepth, tooltipId = 'default', }) {
33
- // get the columns to read from
34
- const mappingColumn = table.getChild(FIELD_MAPPING_FILE);
35
- const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
36
- const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
37
- const depthColumn = table.getChild(FIELD_DEPTH);
38
- const diffColumn = table.getChild(FIELD_DIFF);
39
- const filenameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
40
- const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
41
- const tsColumn = table.getChild(FIELD_TIMESTAMP);
33
+ // Memoize column references - only changes when table changes
34
+ const columns = useMemo(() => ({
35
+ mapping: table.getChild(FIELD_MAPPING_FILE),
36
+ functionName: table.getChild(FIELD_FUNCTION_NAME),
37
+ cumulative: table.getChild(FIELD_CUMULATIVE),
38
+ depth: table.getChild(FIELD_DEPTH),
39
+ diff: table.getChild(FIELD_DIFF),
40
+ filename: table.getChild(FIELD_FUNCTION_FILE_NAME),
41
+ valueOffset: table.getChild(FIELD_VALUE_OFFSET),
42
+ ts: table.getChild(FIELD_TIMESTAMP),
43
+ }), [table]);
42
44
  // get the actual values from the columns
43
45
  const binaries = useAppSelector(selectBinaries);
44
- const mappingFile = arrowToString(mappingColumn?.get(row));
45
- const functionName = arrowToString(functionNameColumn?.get(row));
46
- const cumulative = cumulativeColumn?.get(row) != null ? BigInt(cumulativeColumn?.get(row)) : 0n;
47
- const diff = diffColumn?.get(row) != null ? BigInt(diffColumn?.get(row)) : null;
48
- const filename = arrowToString(filenameColumn?.get(row));
49
- const depth = depthColumn?.get(row) ?? 0;
50
- const valueOffset = valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
51
- ? BigInt(valueOffsetColumn?.get(row))
52
- : 0n;
46
+ // Memoize row data extraction - only changes when table or row changes
47
+ const rowData = useMemo(() => {
48
+ const mappingFile = arrowToString(columns.mapping?.get(row));
49
+ const functionName = arrowToString(columns.functionName?.get(row));
50
+ const cumulative = columns.cumulative?.get(row) != null ? BigInt(columns.cumulative?.get(row)) : 0n;
51
+ const diff = columns.diff?.get(row) != null ? BigInt(columns.diff?.get(row)) : null;
52
+ const filename = arrowToString(columns.filename?.get(row));
53
+ const depth = columns.depth?.get(row) ?? 0;
54
+ const valueOffset = columns.valueOffset?.get(row) !== null && columns.valueOffset?.get(row) !== undefined
55
+ ? BigInt(columns.valueOffset?.get(row))
56
+ : 0n;
57
+ return { mappingFile, functionName, cumulative, diff, filename, depth, valueOffset };
58
+ }, [columns, row]);
59
+ const { mappingFile, functionName, cumulative, diff, filename, depth, valueOffset } = rowData;
53
60
  const colorAttribute = colorBy === 'filename' ? filename : colorBy === 'binary' ? mappingFile : null;
54
- const hoveringName = hoveringRow !== undefined ? arrowToString(functionNameColumn?.get(hoveringRow)) : '';
61
+ // Memoize hovering name lookup
62
+ const hoveringName = useMemo(() => {
63
+ return hoveringRow !== undefined ? arrowToString(columns.functionName?.get(hoveringRow)) : '';
64
+ }, [columns.functionName, hoveringRow]);
55
65
  const shouldBeHighlighted = functionName != null && hoveringName != null && functionName === hoveringName;
56
66
  const colorResult = useNodeColor({
57
67
  isDarkMode: darkMode,
@@ -64,15 +74,43 @@ export const FlameNode = React.memo(function FlameNodeNoMemo({ table, row, color
64
74
  const name = useMemo(() => {
65
75
  return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
66
76
  }, [table, row, binaries]);
77
+ // Memoize selection data - only changes when selectedRow changes
78
+ const selectionData = useMemo(() => {
79
+ const selectionOffset = columns.valueOffset?.get(selectedRow) !== null &&
80
+ columns.valueOffset?.get(selectedRow) !== undefined
81
+ ? BigInt(columns.valueOffset?.get(selectedRow))
82
+ : 0n;
83
+ const selectionCumulative = columns.cumulative?.get(selectedRow) !== null
84
+ ? BigInt(columns.cumulative?.get(selectedRow))
85
+ : 0n;
86
+ const selectedDepth = columns.depth?.get(selectedRow);
87
+ const total = columns.cumulative?.get(selectedRow);
88
+ return { selectionOffset, selectionCumulative, selectedDepth, total };
89
+ }, [columns, selectedRow]);
90
+ const { selectionOffset, selectionCumulative, selectedDepth, total } = selectionData;
91
+ // Memoize tsBounds - only changes when profileSource changes
92
+ const tsBounds = useMemo(() => boundsFromProfileSource(profileSource), [profileSource]);
93
+ // Memoize event handlers
94
+ const onMouseEnter = useCallback(() => {
95
+ setHoveringRow(row);
96
+ window.dispatchEvent(new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
97
+ detail: { row },
98
+ }));
99
+ }, [setHoveringRow, row, tooltipId]);
100
+ const onMouseLeave = useCallback(() => {
101
+ setHoveringRow(undefined);
102
+ window.dispatchEvent(new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
103
+ detail: { row: null },
104
+ }));
105
+ }, [setHoveringRow, tooltipId]);
106
+ const handleContextMenu = useCallback((e) => {
107
+ onContextMenu(e, row);
108
+ }, [onContextMenu, row]);
109
+ // Early returns - all hooks must be called before this point
67
110
  // Hide frames beyond effective depth limit
68
111
  if (effectiveDepth !== undefined && depth > effectiveDepth) {
69
112
  return _jsx(_Fragment, {});
70
113
  }
71
- const selectionOffset = valueOffsetColumn?.get(selectedRow) !== null &&
72
- valueOffsetColumn?.get(selectedRow) !== undefined
73
- ? BigInt(valueOffsetColumn?.get(selectedRow))
74
- : 0n;
75
- const selectionCumulative = cumulativeColumn?.get(selectedRow) !== null ? BigInt(cumulativeColumn?.get(selectedRow)) : 0n;
76
114
  if (valueOffset + cumulative <= selectionOffset ||
77
115
  valueOffset >= selectionOffset + selectionCumulative) {
78
116
  // If the end of the node is before the selection offset or the start of the node is after the selection offset + totalWidth, we don't render it.
@@ -83,8 +121,6 @@ export const FlameNode = React.memo(function FlameNodeNoMemo({ table, row, color
83
121
  return _jsx(_Fragment, {});
84
122
  }
85
123
  // Cumulative can be larger than total when a selection is made. All parents of the selection are likely larger, but we want to only show them as 100% in the graph.
86
- const tsBounds = boundsFromProfileSource(profileSource);
87
- const total = cumulativeColumn?.get(selectedRow);
88
124
  const totalRatio = cumulative > total ? 1 : Number(cumulative) / Number(total);
89
125
  const width = isFlameChart
90
126
  ? (Number(cumulative) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
@@ -92,25 +128,9 @@ export const FlameNode = React.memo(function FlameNodeNoMemo({ table, row, color
92
128
  if (width <= 1) {
93
129
  return _jsx(_Fragment, {});
94
130
  }
95
- const selectedDepth = depthColumn?.get(selectedRow);
96
131
  const styles = selectedDepth !== undefined && selectedDepth > depth ? fadedFlameRectStyles : flameRectStyles;
97
- const onMouseEnter = () => {
98
- setHoveringRow(row);
99
- window.dispatchEvent(new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
100
- detail: { row },
101
- }));
102
- };
103
- const onMouseLeave = () => {
104
- setHoveringRow(undefined);
105
- window.dispatchEvent(new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
106
- detail: { row: null },
107
- }));
108
- };
109
- const handleContextMenu = (e) => {
110
- onContextMenu(e, row);
111
- };
112
- const ts = tsColumn !== null ? Number(tsColumn.get(row)) : 0;
113
- const x = isFlameChart && tsColumn !== null
132
+ const ts = columns.ts !== null ? Number(columns.ts.get(row)) : 0;
133
+ const x = isFlameChart && columns.ts !== null
114
134
  ? ((ts - Number(tsBounds[0])) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
115
135
  : selectedDepth > depth
116
136
  ? 0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAKtE,OAAO,EACL,gBAAgB,EAMjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDA0P1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAQN,MAAM,OAAO,CAAC;AAKf,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAG9C,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,OAAO,EAAC,KAAK,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIlD,OAAO,EAAuB,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAMtE,OAAO,EACL,gBAAgB,EAMjB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,yBAAyB,uBAAuB,CAAC;AAC9D,eAAO,MAAM,cAAc,aAAa,CAAC;AACzC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,UAAU,oBAAoB;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,UAAU,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IAC/C,YAAY,EAAE,OAAO,CAAC;IACtB,wBAAwB,EAAE,MAAM,EAAE,CAAC;IACnC,yBAAyB,EAAE,MAAM,EAAE,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAC3B,cAAc,MAAM,EAAE,EACtB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,eAAe,MAAM,EAAE,EACvB,YAAY,OAAO,EACnB,qBAAqB,WAAW,KAC/B,aAQF,CAAC;AAIF,eAAO,MAAM,eAAe,kDAkR1B,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
15
15
  import { tableFromIPC } from 'apache-arrow';
16
16
  import { useContextMenu } from 'react-contexify';
17
- import { useParcaContext } from '@parca/components';
17
+ import { FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext } from '@parca/components';
18
18
  import { USER_PREFERENCES, useCurrentColorProfile, useUserPreference } from '@parca/hooks';
19
19
  import { getColorForFeature, selectDarkMode, useAppSelector } from '@parca/store';
20
20
  import { useProfileFilters } from '../../ProfileView/components/ProfileFilters/useProfileFilters';
@@ -23,6 +23,7 @@ import ContextMenuWrapper from './ContextMenuWrapper';
23
23
  import { FlameNode, RowHeight } from './FlameGraphNodes';
24
24
  import { MemoizedTooltip } from './MemoizedTooltip';
25
25
  import { TooltipProvider } from './TooltipContext';
26
+ import { useBatchedRendering } from './useBatchedRendering';
26
27
  import { useScrollViewport } from './useScrollViewport';
27
28
  import { useVisibleNodes } from './useVisibleNodes';
28
29
  import { extractFeature, extractFilenameFeature, getCurrentPathFrameData, getMaxDepth, isCurrentPathFrameMatch, } from './utils';
@@ -64,7 +65,7 @@ export const getFilenameColors = (filenamesList, isDarkMode, currentColorProfile
64
65
  return colors;
65
66
  };
66
67
  const noop = () => { };
67
- export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, profileSource, compareAbsolute, isFlameChart = false, isRenderedAsFlamegraph = false, isInSandwichView = false, tooltipId = 'default', maxFrameCount, isExpanded = false, mappingsListFromMetadata, filenamesListFromMetadata, colorBy, }) {
68
+ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, filtered, width, setCurPath, curPath, profileType, profileSource, compareAbsolute, isFlameChart = false, isRenderedAsFlamegraph = false, isInSandwichView = false, isHalfScreen, tooltipId = 'default', maxFrameCount, isExpanded = false, mappingsListFromMetadata, filenamesListFromMetadata, colorBy, }) {
68
69
  const [highlightSimilarStacksPreference] = useUserPreference(USER_PREFERENCES.HIGHLIGHT_SIMILAR_STACKS.key);
69
70
  const [hoveringRow, setHoveringRow] = useState(undefined);
70
71
  const [dockedMetainfo] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
@@ -80,6 +81,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
80
81
  const svg = useRef(null);
81
82
  const containerRef = useRef(null);
82
83
  const renderStartTime = useRef(0);
84
+ const hasInitialRenderCompleted = useRef(false);
83
85
  const [svgElement, setSvgElement] = useState(null);
84
86
  const { excludeBinary } = useProfileFilters();
85
87
  const { compareMode } = useProfileViewContext();
@@ -179,6 +181,15 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
179
181
  selectedRow,
180
182
  effectiveDepth: deferredEffectiveDepth,
181
183
  });
184
+ // Add nodes in incremental batches to avoid blocking the UI
185
+ const { items: batchedNodes, isComplete: isBatchingComplete } = useBatchedRendering(visibleNodes, {
186
+ batchSize: 500,
187
+ });
188
+ if (isBatchingComplete) {
189
+ hasInitialRenderCompleted.current = true;
190
+ }
191
+ // Show skeleton only during initial load, not during scroll updates
192
+ const showSkeleton = !hasInitialRenderCompleted.current && batchedNodes.length !== visibleNodes.length;
182
193
  useEffect(() => {
183
194
  if (perf?.markInteraction != null) {
184
195
  renderStartTime.current = performance.now();
@@ -187,9 +198,10 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({ arrow, total, fil
187
198
  useEffect(() => {
188
199
  setSvgElement(svg.current);
189
200
  }, [tooltipId]);
190
- return (_jsx(TooltipProvider, { table: table, total: total, totalUnfiltered: total + filtered, profileType: profileType, unit: arrow.unit, compareAbsolute: compareAbsolute, tooltipId: tooltipId, children: _jsxs("div", { className: "relative", children: [_jsx(ContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, table: table, total: total, totalUnfiltered: total + filtered, compareAbsolute: compareAbsolute, resetPath: () => setCurPath([]), hideMenu: hideAll, hideBinary: hideBinary, unit: arrow.unit, profileType: profileType, isInSandwichView: isInSandwichView }), _jsx(MemoizedTooltip, { contextElement: svgElement, dockedMetainfo: dockedMetainfo }), _jsx("div", { ref: containerRef, className: "overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain", style: {
201
+ return (_jsx(TooltipProvider, { table: table, total: total, totalUnfiltered: total + filtered, profileType: profileType, unit: arrow.unit, compareAbsolute: compareAbsolute, tooltipId: tooltipId, children: _jsxs("div", { className: "relative", children: [_jsx(ContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, table: table, total: total, totalUnfiltered: total + filtered, compareAbsolute: compareAbsolute, resetPath: () => setCurPath([]), hideMenu: hideAll, hideBinary: hideBinary, unit: arrow.unit, profileType: profileType, isInSandwichView: isInSandwichView }), _jsx(MemoizedTooltip, { contextElement: svgElement, dockedMetainfo: dockedMetainfo }), showSkeleton && (_jsx("div", { className: "absolute inset-0 z-10", children: isRenderedAsFlamegraph ? (_jsx(SandwichFlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) : (_jsx(FlameGraphSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode })) })), _jsx("div", { ref: containerRef, className: "overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain", style: {
191
202
  width: width ?? '100%',
192
203
  contain: 'layout style paint',
193
- }, children: _jsx("svg", { className: "font-robotoMono", width: width ?? 0, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: visibleNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: width ?? 1, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => handleRowClick(row), onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) }) })] }) }));
204
+ visibility: !showSkeleton ? 'visible' : 'hidden',
205
+ }, children: _jsx("svg", { className: "font-robotoMono", width: width ?? 0, height: totalHeight, preserveAspectRatio: "xMinYMid", ref: svg, children: batchedNodes.map(row => (_jsx(FlameNode, { table: table, row: row, colors: colorByColors, colorBy: colorByValue, totalWidth: width ?? 1, height: RowHeight, darkMode: isDarkMode, compareMode: compareMode, colorForSimilarNodes: colorForSimilarNodes, selectedRow: selectedRow, onClick: () => handleRowClick(row), onContextMenu: displayMenu, hoveringRow: highlightSimilarStacksPreference ? hoveringRow : undefined, setHoveringRow: highlightSimilarStacksPreference ? setHoveringRow : noop, isFlameChart: isFlameChart, profileSource: profileSource, isRenderedAsFlamegraph: isRenderedAsFlamegraph, isInSandwichView: isInSandwichView, maxDepth: maxDepth, effectiveDepth: deferredEffectiveDepth, tooltipId: tooltipId }, row))) }) })] }) }));
194
206
  });
195
207
  export default FlameGraphArrow;
@@ -0,0 +1,11 @@
1
+ interface UseBatchedRenderingOptions {
2
+ batchSize?: number;
3
+ batchDelay?: number;
4
+ }
5
+ interface UseBatchedRenderingResult<T> {
6
+ items: T[];
7
+ isComplete: boolean;
8
+ }
9
+ export declare const useBatchedRendering: <T>(items: T[], options?: UseBatchedRenderingOptions) => UseBatchedRenderingResult<T>;
10
+ export {};
11
+ //# sourceMappingURL=useBatchedRendering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBatchedRendering.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useBatchedRendering.ts"],"names":[],"mappings":"AAeA,UAAU,0BAA0B;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,yBAAyB,CAAC,CAAC;IACnC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,OAAO,CAAC;CACrB;AAGD,eAAO,MAAM,mBAAmB,GAAI,CAAC,EACnC,OAAO,CAAC,EAAE,EACV,UAAS,0BAA+B,KACvC,yBAAyB,CAAC,CAAC,CAqD7B,CAAC"}
@@ -0,0 +1,65 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+ import { useEffect, useRef, useState } from 'react';
14
+ // useBatchedRendering - Helps in incrementally rendering items in batches to avoid UI blocking.
15
+ export const useBatchedRendering = (items, options = {}) => {
16
+ const { batchSize = 500, batchDelay = 0 } = options;
17
+ const [renderedCount, setRenderedCount] = useState(0);
18
+ const itemsRef = useRef(items);
19
+ const rafRef = useRef(null);
20
+ const timeoutRef = useRef(null);
21
+ useEffect(() => {
22
+ if (itemsRef.current !== items) {
23
+ itemsRef.current = items;
24
+ setRenderedCount(prev => {
25
+ if (items.length === 0)
26
+ return 0;
27
+ // If new items were added (scrolling down), keep current progress
28
+ if (items.length > prev)
29
+ return prev;
30
+ // If items reduced, cap to new length
31
+ return Math.min(prev, items.length);
32
+ });
33
+ }
34
+ }, [items]);
35
+ // Progressively render more items
36
+ useEffect(() => {
37
+ if (renderedCount === items.length) {
38
+ return;
39
+ }
40
+ const scheduleNextBatch = () => {
41
+ const incrementState = () => {
42
+ setRenderedCount(prev => Math.min(prev + batchSize, items.length));
43
+ };
44
+ if (batchDelay > 0) {
45
+ timeoutRef.current = setTimeout(incrementState, batchDelay);
46
+ }
47
+ else {
48
+ rafRef.current = requestAnimationFrame(incrementState);
49
+ }
50
+ };
51
+ scheduleNextBatch();
52
+ return () => {
53
+ if (rafRef.current !== null) {
54
+ cancelAnimationFrame(rafRef.current);
55
+ }
56
+ if (timeoutRef.current !== null) {
57
+ clearTimeout(timeoutRef.current);
58
+ }
59
+ };
60
+ }, [renderedCount, items.length, batchSize, batchDelay]);
61
+ return {
62
+ items: items.slice(0, renderedCount),
63
+ isComplete: renderedCount === items.length,
64
+ };
65
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollViewport.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,iBAAiB,GAAI,cAAc,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,KAAG,aAkEjF,CAAC"}
1
+ {"version":3,"file":"useScrollViewport.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useScrollViewport.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAiBD,eAAO,MAAM,iBAAiB,GAAI,cAAc,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,KAAG,aAsFjF,CAAC"}
@@ -11,6 +11,21 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
  import { useCallback, useEffect, useRef, useState } from 'react';
14
+ // Find the scrollable ancestor (the element with overflow: auto/scroll)
15
+ const findScrollableParent = (element) => {
16
+ if (element === null)
17
+ return undefined;
18
+ let current = element.parentElement;
19
+ while (current !== null) {
20
+ const style = window.getComputedStyle(current);
21
+ const overflowY = style.overflowY;
22
+ if (overflowY === 'auto' || overflowY === 'scroll') {
23
+ return current;
24
+ }
25
+ current = current.parentElement;
26
+ }
27
+ return undefined;
28
+ };
14
29
  export const useScrollViewport = (containerRef) => {
15
30
  const [viewport, setViewport] = useState({
16
31
  scrollTop: 0,
@@ -22,10 +37,22 @@ export const useScrollViewport = (containerRef) => {
22
37
  const updateViewport = useCallback(() => {
23
38
  if (containerRef.current !== null) {
24
39
  const container = containerRef.current;
40
+ const rect = container.getBoundingClientRect();
41
+ // Restrict container height to the visible portion on screen
42
+ // This handles cases where the container is partially off-screen
43
+ // We only want to consider the visible part for culling calculations
44
+ const containerTop = rect.top;
45
+ const containerBottom = rect.bottom;
46
+ const viewportTop = 0;
47
+ const viewportBottom = window.innerHeight;
48
+ const visibleTop = Math.max(containerTop, viewportTop);
49
+ const visibleBottom = Math.min(containerBottom, viewportBottom);
50
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
51
+ const scrollOffset = Math.max(0, viewportTop - containerTop);
25
52
  const newViewport = {
26
- scrollTop: container.scrollTop,
53
+ scrollTop: scrollOffset,
27
54
  scrollLeft: container.scrollLeft,
28
- containerHeight: container.clientHeight,
55
+ containerHeight: visibleHeight, // Only the visible portion
29
56
  containerWidth: container.clientWidth,
30
57
  };
31
58
  setViewport(newViewport);
@@ -44,22 +71,25 @@ export const useScrollViewport = (containerRef) => {
44
71
  const container = containerRef.current;
45
72
  if (container === null)
46
73
  return;
74
+ const scrollableParent = findScrollableParent(container);
47
75
  // ResizeObserver Strategy:
48
76
  // Monitor container size changes (window resize, layout shifts)
49
77
  // to update viewport dimensions for accurate culling calculations
50
78
  const resizeObserver = new ResizeObserver(() => {
51
79
  throttledUpdateViewport();
52
80
  });
53
- // Container Scroll Event Strategy:
54
- // Use passive event listeners for better scroll performance
55
- // Throttle with requestAnimationFrame to maintain 60fps target
81
+ // Listen to scroll on the actual scrollable parent
82
+ scrollableParent?.addEventListener('scroll', throttledUpdateViewport, { passive: true });
56
83
  container.addEventListener('scroll', throttledUpdateViewport, { passive: true });
84
+ window.addEventListener('scroll', throttledUpdateViewport, { passive: true });
57
85
  resizeObserver.observe(container);
58
86
  // Initialize viewport state on mount
59
87
  updateViewport();
60
88
  return () => {
61
89
  // Cleanup: Remove event listeners and cancel pending animations
90
+ scrollableParent?.removeEventListener('scroll', throttledUpdateViewport);
62
91
  container.removeEventListener('scroll', throttledUpdateViewport);
92
+ window.removeEventListener('scroll', throttledUpdateViewport);
63
93
  resizeObserver.disconnect();
64
94
  if (throttleRef.current !== null) {
65
95
  cancelAnimationFrame(throttleRef.current);
@@ -1 +1 @@
1
- {"version":3,"file":"useVisibleNodes.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.ts"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAInC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAiClD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAAI,iEAO7B,qBAAqB,KAAG,MAAM,EAwFhC,CAAC"}
1
+ {"version":3,"file":"useVisibleNodes.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/useVisibleNodes.ts"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,EAAC,MAAM,cAAc,CAAC;AAInC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAiClD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAAI,iEAO7B,qBAAqB,KAAG,MAAM,EA4HhC,CAAC"}
@@ -53,22 +53,48 @@ const useDepthBuckets = (table) => {
53
53
  export const useVisibleNodes = ({ table, viewport, total, width, selectedRow, effectiveDepth, }) => {
54
54
  const depthBuckets = useDepthBuckets(table);
55
55
  const lastResultRef = useRef({ key: '', result: [] });
56
+ const renderedRangeRef = useRef({
57
+ minDepth: Infinity,
58
+ maxDepth: -Infinity,
59
+ table: null,
60
+ });
56
61
  return useMemo(() => {
62
+ // This happens when the continer is scrolled off screen, in this case we return all previously rendered nodes
63
+ // to avoid trimming the rendered nodes to zero which would cause jank when scrolling back into view
64
+ if (viewport.containerHeight === 0 && lastResultRef.current.result.length > 0) {
65
+ return lastResultRef.current.result;
66
+ }
57
67
  // Create a stable key for memoization to prevent unnecessary recalculations
58
68
  const memoKey = `${viewport.scrollTop}-${viewport.containerHeight}-${selectedRow}-${effectiveDepth}-${width}-${Number(total)}-${table.numRows}`;
59
69
  // Return cached result if viewport hasn't meaningfully changed
60
70
  if (lastResultRef.current.key === memoKey) {
61
71
  return lastResultRef.current.result;
62
72
  }
63
- if (table === null || viewport.containerHeight === 0)
73
+ if (table === null)
64
74
  return [];
65
75
  const visibleRows = [];
66
76
  const { scrollTop, containerHeight } = viewport;
67
77
  // Viewport Culling Algorithm:
68
78
  // 1. Calculate visible depth range based on scroll position and container height
69
79
  // 2. Add 5-row buffer above/below for smooth scrolling experience
70
- const startDepth = Math.max(0, Math.floor(scrollTop / RowHeight) - 5);
71
- const endDepth = Math.min(effectiveDepth, Math.ceil((scrollTop + containerHeight) / RowHeight) + 5);
80
+ // Note: We never shrink the rendered range to avoid back and forth node removals (and in turn additions when scrolled down again) to the dom.
81
+ const BUFFER = 15; // Buffer for smoother scrolling
82
+ const visibleStartDepth = Math.max(0, Math.floor(scrollTop / RowHeight) - BUFFER);
83
+ const visibleDepths = Math.ceil(containerHeight / RowHeight);
84
+ const visibleEndDepth = Math.min(effectiveDepth, visibleStartDepth + visibleDepths + BUFFER);
85
+ // Reset range if table changed (new data loaded) as this is new data
86
+ if (renderedRangeRef.current.table !== table) {
87
+ renderedRangeRef.current = {
88
+ minDepth: Infinity,
89
+ maxDepth: -Infinity,
90
+ table: table,
91
+ };
92
+ }
93
+ // Expand the rendered range (never shrink when scrolling up/down)
94
+ renderedRangeRef.current.minDepth = Math.min(renderedRangeRef.current.minDepth, visibleStartDepth);
95
+ renderedRangeRef.current.maxDepth = Math.max(renderedRangeRef.current.maxDepth, visibleEndDepth);
96
+ const startDepth = renderedRangeRef.current.minDepth;
97
+ const endDepth = renderedRangeRef.current.maxDepth;
72
98
  const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
73
99
  const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
74
100
  const selectionOffset = valueOffsetColumn?.get(selectedRow) !== null &&
@@ -1 +1 @@
1
- {"version":3,"file":"useLabels.d.ts","sourceRoot":"","sources":["../../src/hooks/useLabels.ts"],"names":[],"mappings":"AAeA,OAAO,EAAgB,cAAc,EAAE,kBAAkB,EAAgB,MAAM,eAAe,CAAC;AAM/F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GACxB,QAAQ,kBAAkB,EAC1B,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,QAAQ,MAAM,EAAE,KACf,aAkCF,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,QAAQ,kBAAkB,EAC1B,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,KACX,cAiCF,CAAC"}
1
+ {"version":3,"file":"useLabels.d.ts","sourceRoot":"","sources":["../../src/hooks/useLabels.ts"],"names":[],"mappings":"AAeA,OAAO,EAAgB,cAAc,EAAE,kBAAkB,EAAgB,MAAM,eAAe,CAAC;AAM/F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,cAAc;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,UAAU,aAAa;IACrB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa,GACxB,QAAQ,kBAAkB,EAC1B,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,QAAQ,MAAM,EAAE,KACf,aAkCF,CAAC;AAEF,eAAO,MAAM,cAAc,GACzB,QAAQ,kBAAkB,EAC1B,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,QAAQ,MAAM,EACd,MAAM,MAAM,KACX,cAmCF,CAAC"}
@@ -67,7 +67,9 @@ export const useLabelValues = (client, labelName, profileType, start, end) => {
67
67
  keepPreviousData: false,
68
68
  },
69
69
  });
70
- console.log('Label values query result:', { data, error, isLoading, labelName });
70
+ useEffect(() => {
71
+ console.log('Label values query result:', { data, error, isLoading, labelName });
72
+ }, [data, error, isLoading, labelName]);
71
73
  return {
72
74
  result: { response: data ?? [], error: error },
73
75
  loading: isLoading,
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-inset-2{bottom:-.5rem;left:-.5rem;right:-.5rem;top:-.5rem}.inset-y-0{bottom:0;top:0}.right-0{right:0}.top-0{top:0}.left-0{left:0}.bottom-3{bottom:.75rem}.top-\[-5px\]{top:-5px}.top-\[-2px\]{top:-2px}.bottom-0{bottom:0}.left-\[-7px\]{left:-7px}.right-\[-7px\]{right:-7px}.right-8{right:2rem}.top-\[-46px\]{top:-46px}.right-full{right:100%}.left-full{left:100%}.top-\[-60px\]{top:-60px}.left-1\/2{left:50%}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.z-30{z-index:30}.z-\[5\]{z-index:5}.z-20{z-index:20}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-0{margin-bottom:0;margin-top:0}.my-20{margin-bottom:5rem;margin-top:5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-2{margin-bottom:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.ml-\[70px\]{margin-left:70px}.ml-2{margin-left:.5rem}.mr-6{margin-right:1.5rem}.ml-auto{margin-left:auto}.mt-auto{margin-top:auto}.mb-0\.5{margin-bottom:.125rem}.mt-1\.5{margin-top:.375rem}.mb-0{margin-bottom:0}.mr-2{margin-right:.5rem}.ml-3{margin-left:.75rem}.mt-2{margin-top:.5rem}.mb-px{margin-bottom:1px}.mb-1{margin-bottom:.25rem}.mr-1{margin-right:.25rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mt-3{margin-top:.75rem}.mt-8{margin-top:2rem}.mt-0{margin-top:0}.ml-1{margin-left:.25rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.\!flex{display:flex!important}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-\[38px\]{height:38px}.h-auto{height:auto}.h-full{height:100%}.h-1{height:.25rem}.h-\[20px\]{height:20px}.h-\[16px\]{height:16px}.h-4{height:1rem}.h-\[30px\]{height:30px}.h-\[45px\]{height:45px}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-\[700px\]{height:700px}.h-\[80vh\]{height:80vh}.h-\[1px\]{height:1px}.h-\[14px\]{height:14px}.h-\[24px\]{height:24px}.h-0{height:0}.max-h-\[400px\]{max-height:400px}.max-h-\[50vh\]{max-height:50vh}.max-h-\[300px\]{max-height:300px}.min-h-10{min-height:2.5rem}.min-h-0{min-height:0}.min-h-5{min-height:1.25rem}.min-h-48{min-height:12rem}.min-h-\[50px\]{min-height:50px}.min-h-\[700px\]{min-height:700px}.min-h-96{min-height:24rem}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-max{width:-moz-max-content;width:max-content}.w-40{width:10rem}.w-\[44px\]{width:44px}.w-\[16px\]{width:16px}.w-4{width:1rem}.w-\[270px\]{width:270px}.w-\[300px\]{width:300px}.w-5{width:1.25rem}.w-3{width:.75rem}.w-7{width:1.75rem}.w-9{width:2.25rem}.w-11{width:2.75rem}.w-\[52px\]{width:52px}.w-\[68px\]{width:68px}.w-\[76px\]{width:76px}.w-\[84px\]{width:84px}.w-\[92px\]{width:92px}.w-\[100px\]{width:100px}.w-\[108px\]{width:108px}.w-\[116px\]{width:116px}.w-56{width:14rem}.w-\[350px\]{width:350px}.w-\[260px\]{width:260px}.w-44{width:11rem}.w-\[460px\]{width:460px}.w-\[19\.25\%\]{width:19.25%}.w-11\/12{width:91.666667%}.w-1\/12{width:8.333333%}.w-8{width:2rem}.w-\[50\%\]{width:50%}.w-32{width:8rem}.w-36{width:9rem}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.w-48{width:12rem}.w-\[14px\]{width:14px}.w-64{width:16rem}.w-80{width:20rem}.w-\[20px\]{width:20px}.w-\[246px\]{width:246px}.w-52{width:13rem}.w-0{width:0}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.min-w-\[350px\]{min-width:350px}.min-w-\[260px\]{min-width:260px}.max-w-\[600px\]{max-width:600px}.max-w-\[500px\]{max-width:500px}.max-w-md{max-width:28rem}.max-w-80{max-width:20rem}.max-w-full{max-width:100%}.max-w-48{max-width:12rem}.max-w-\[400px\]{max-width:400px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow-0{flex-grow:0}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.origin-top-right{transform-origin:top right}.origin-top-left{transform-origin:top left}.translate-x-6{--tw-translate-x:1.5rem}.translate-x-0,.translate-x-6{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.translate-x-5{--tw-translate-x:1.25rem}.-translate-x-1\/2,.translate-x-5{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-1\/2{--tw-translate-x:-50%}.rotate-90{--tw-rotate:90deg}.-rotate-180,.rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-180{--tw-rotate:-180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-ew-resize{cursor:ew-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-\[2px\]{gap:2px}.gap-2{gap:.5rem}.gap-6{gap:1.5rem}.gap-3{gap:.75rem}.gap-0{gap:0}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-clip{overflow:clip}.overflow-scroll{overflow:scroll}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.scroll-smooth{scroll-behavior:smooth}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.rounded-none{border-radius:0}.rounded-\[4px\]{border-radius:4px}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.rounded-r-none{border-bottom-right-radius:0;border-top-right-radius:0}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.rounded-b{border-bottom-right-radius:.25rem}.rounded-b,.rounded-l{border-bottom-left-radius:.25rem}.rounded-l{border-top-left-radius:.25rem}.rounded-r{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-tr-none{border-top-right-radius:0}.rounded-br-none{border-bottom-right-radius:0}.rounded-tl-none{border-top-left-radius:0}.rounded-bl-none{border-bottom-left-radius:0}.border{border-width:1px}.border-2{border-width:2px}.border-x-2{border-left-width:2px;border-right-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-l-4{border-left-width:4px}.border-r-4{border-right-width:4px}.border-t-4{border-top-width:4px}.border-none{border-style:none}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-r-gray-200{--tw-border-opacity:1;border-right-color:rgb(229 231 235/var(--tw-border-opacity))}.border-l-amber-900{--tw-border-opacity:1;border-left-color:rgb(120 53 15/var(--tw-border-opacity))}.border-l-transparent{border-left-color:transparent}.border-r-transparent{border-right-color:transparent}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:hsla(0,0%,100%,.5)}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-500\/50{background-color:hsla(220,9%,46%,.5)}.bg-gray-700\/75{background-color:rgba(55,65,81,.75)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-indigo-400{--tw-bg-opacity:1;background-color:rgb(129 140 248/var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-opacity-90{--tw-bg-opacity:0.9}.fill-transparent{fill:transparent}.fill-current{fill:currentColor}.stroke-gray-300{stroke:#d1d5db}.stroke-white{stroke:#fff}.stroke-\[3\]{stroke-width:3}.p-3{padding:.75rem}.p-2{padding:.5rem}.p-10{padding:2.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.p-0{padding:0}.p-\[6px\]{padding:6px}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-bottom:0;padding-top:0}.\!px-3{padding-left:.75rem!important;padding-right:.75rem!important}.pr-0{padding-right:0}.pt-2{padding-top:.5rem}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pb-4{padding-bottom:1rem}.pr-3{padding-right:.75rem}.pl-2{padding-left:.5rem}.pr-4{padding-right:1rem}.pl-1{padding-left:.25rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pb-2{padding-bottom:.5rem}.pb-\[10px\]{padding-bottom:10px}.pr-1{padding-right:.25rem}.pr-\[1\.7rem\]{padding-right:1.7rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-\[10px\]{font-size:10px}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-5{line-height:1.25rem}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px rgba(0,0,0,.3);--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}.\[stroke-dasharray\:6\2c 4\]{stroke-dasharray:6,4}.\[stroke-linecap\:round\]{stroke-linecap:round}.\[stroke-linejoin\:round\]{stroke-linejoin:round}.\[writing-mode\:vertical-lr\]{writing-mode:vertical-lr}.checked\:border-indigo-600:checked{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity))}.checked\:bg-indigo-600:checked{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:whitespace-normal:hover{white-space:normal}.hover\:bg-amber-700:hover{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:relative:focus{position:relative}.focus\:z-50:focus{z-index:50}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:outline-1:focus{outline-width:1px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-white\/75:focus-visible{--tw-ring-color:hsla(0,0%,100%,.75)}.focus-visible\:ring-white:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.focus-visible\:ring-indigo-500:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus-visible\:ring-opacity-75:focus-visible{--tw-ring-opacity:0.75}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-r-gray-700{--tw-border-opacity:1;border-right-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black\/50{background-color:rgba(0,0,0,.5)}[class~=theme-dark] .dark\:bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-amber-700{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-100\/90{background-color:rgba(243,244,246,.9)}[class~=theme-dark] .dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-200\/75{background-color:rgba(229,231,235,.75)}[class~=theme-dark] .dark\:bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-opacity-80{--tw-bg-opacity:0.8}[class~=theme-dark] .dark\:stroke-gray-500{stroke:#6b7280}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}[class~=theme-dark] .dark\:ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}[class~=theme-dark] .dark\:ring-opacity-20{--tw-ring-opacity:0.2}[class~=theme-dark] .dark\:hover\:bg-amber-600:hover{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity))}[class~=theme-dark] .hover\:dark\:text-gray-100:hover{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:flex-row{flex-direction:row}}
1
+ /*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-inset-2{bottom:-.5rem;left:-.5rem;right:-.5rem;top:-.5rem}.inset-0{left:0;right:0}.inset-0,.inset-y-0{bottom:0;top:0}.right-0{right:0}.top-0{top:0}.left-0{left:0}.bottom-3{bottom:.75rem}.top-\[-5px\]{top:-5px}.top-\[-2px\]{top:-2px}.bottom-0{bottom:0}.left-\[-7px\]{left:-7px}.right-\[-7px\]{right:-7px}.right-8{right:2rem}.top-\[-46px\]{top:-46px}.right-full{right:100%}.left-full{left:100%}.top-\[-60px\]{top:-60px}.left-1\/2{left:50%}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.z-30{z-index:30}.z-\[5\]{z-index:5}.z-20{z-index:20}.m-auto{margin:auto}.m-2{margin:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-0{margin-bottom:0;margin-top:0}.my-20{margin-bottom:5rem;margin-top:5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-2{margin-bottom:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.ml-\[70px\]{margin-left:70px}.ml-2{margin-left:.5rem}.mr-6{margin-right:1.5rem}.ml-auto{margin-left:auto}.mt-auto{margin-top:auto}.mb-0\.5{margin-bottom:.125rem}.mt-1\.5{margin-top:.375rem}.mb-0{margin-bottom:0}.mr-2{margin-right:.5rem}.ml-3{margin-left:.75rem}.mt-2{margin-top:.5rem}.mb-px{margin-bottom:1px}.mb-1{margin-bottom:.25rem}.mr-1{margin-right:.25rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mt-3{margin-top:.75rem}.mt-8{margin-top:2rem}.mt-0{margin-top:0}.ml-1{margin-left:.25rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.\!flex{display:flex!important}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-fit{height:-moz-fit-content;height:fit-content}.h-\[38px\]{height:38px}.h-auto{height:auto}.h-full{height:100%}.h-1{height:.25rem}.h-\[20px\]{height:20px}.h-\[16px\]{height:16px}.h-4{height:1rem}.h-\[30px\]{height:30px}.h-\[45px\]{height:45px}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-\[700px\]{height:700px}.h-\[80vh\]{height:80vh}.h-\[1px\]{height:1px}.h-\[14px\]{height:14px}.h-\[24px\]{height:24px}.h-0{height:0}.max-h-\[400px\]{max-height:400px}.max-h-\[50vh\]{max-height:50vh}.max-h-\[300px\]{max-height:300px}.min-h-10{min-height:2.5rem}.min-h-0{min-height:0}.min-h-5{min-height:1.25rem}.min-h-48{min-height:12rem}.min-h-\[50px\]{min-height:50px}.min-h-\[700px\]{min-height:700px}.min-h-96{min-height:24rem}.w-full{width:100%}.w-auto{width:auto}.w-1\/4{width:25%}.w-3\/4{width:75%}.w-max{width:-moz-max-content;width:max-content}.w-40{width:10rem}.w-\[44px\]{width:44px}.w-\[16px\]{width:16px}.w-4{width:1rem}.w-\[270px\]{width:270px}.w-\[300px\]{width:300px}.w-5{width:1.25rem}.w-3{width:.75rem}.w-7{width:1.75rem}.w-9{width:2.25rem}.w-11{width:2.75rem}.w-\[52px\]{width:52px}.w-\[68px\]{width:68px}.w-\[76px\]{width:76px}.w-\[84px\]{width:84px}.w-\[92px\]{width:92px}.w-\[100px\]{width:100px}.w-\[108px\]{width:108px}.w-\[116px\]{width:116px}.w-56{width:14rem}.w-\[350px\]{width:350px}.w-\[260px\]{width:260px}.w-44{width:11rem}.w-\[460px\]{width:460px}.w-\[19\.25\%\]{width:19.25%}.w-11\/12{width:91.666667%}.w-1\/12{width:8.333333%}.w-8{width:2rem}.w-\[50\%\]{width:50%}.w-32{width:8rem}.w-36{width:9rem}.w-16{width:4rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-\[420px\]{width:420px}.w-48{width:12rem}.w-\[14px\]{width:14px}.w-64{width:16rem}.w-80{width:20rem}.w-\[20px\]{width:20px}.w-\[246px\]{width:246px}.w-52{width:13rem}.w-0{width:0}.min-w-\[300px\]{min-width:300px}.min-w-\[400px\]{min-width:400px}.min-w-\[350px\]{min-width:350px}.min-w-\[260px\]{min-width:260px}.max-w-\[600px\]{max-width:600px}.max-w-\[500px\]{max-width:500px}.max-w-md{max-width:28rem}.max-w-80{max-width:20rem}.max-w-full{max-width:100%}.max-w-48{max-width:12rem}.max-w-\[400px\]{max-width:400px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.flex-grow-0{flex-grow:0}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.origin-top-right{transform-origin:top right}.origin-top-left{transform-origin:top left}.translate-x-6{--tw-translate-x:1.5rem}.translate-x-0,.translate-x-6{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-y-1{--tw-translate-y:0.25rem}.translate-y-0,.translate-y-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.translate-x-5{--tw-translate-x:1.25rem}.-translate-x-1\/2,.translate-x-5{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-1\/2{--tw-translate-x:-50%}.rotate-90{--tw-rotate:90deg}.-rotate-180,.rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-180{--tw-rotate:-180deg}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-ew-resize{cursor:ew-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-\[2px\]{gap:2px}.gap-2{gap:.5rem}.gap-6{gap:1.5rem}.gap-3{gap:.75rem}.gap-0{gap:0}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-clip{overflow:clip}.overflow-scroll{overflow:scroll}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.scroll-smooth{scroll-behavior:smooth}.text-ellipsis{text-overflow:ellipsis}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.rounded-none{border-radius:0}.rounded-\[4px\]{border-radius:4px}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.rounded-r-none{border-bottom-right-radius:0;border-top-right-radius:0}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.rounded-b{border-bottom-right-radius:.25rem}.rounded-b,.rounded-l{border-bottom-left-radius:.25rem}.rounded-l{border-top-left-radius:.25rem}.rounded-r{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-tr-none{border-top-right-radius:0}.rounded-br-none{border-bottom-right-radius:0}.rounded-tl-none{border-top-left-radius:0}.rounded-bl-none{border-bottom-left-radius:0}.border{border-width:1px}.border-2{border-width:2px}.border-x-2{border-left-width:2px;border-right-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.border-l-0{border-left-width:0}.border-r-0{border-right-width:0}.border-l-4{border-left-width:4px}.border-r-4{border-right-width:4px}.border-t-4{border-top-width:4px}.border-none{border-style:none}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-gray-900{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-r-gray-200{--tw-border-opacity:1;border-right-color:rgb(229 231 235/var(--tw-border-opacity))}.border-l-amber-900{--tw-border-opacity:1;border-left-color:rgb(120 53 15/var(--tw-border-opacity))}.border-l-transparent{border-left-color:transparent}.border-r-transparent{border-right-color:transparent}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:hsla(0,0%,100%,.5)}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(254 240 138/var(--tw-bg-opacity))}.bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-500\/50{background-color:hsla(220,9%,46%,.5)}.bg-gray-700\/75{background-color:rgba(55,65,81,.75)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-indigo-400{--tw-bg-opacity:1;background-color:rgb(129 140 248/var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-opacity-90{--tw-bg-opacity:0.9}.fill-transparent{fill:transparent}.fill-current{fill:currentColor}.stroke-gray-300{stroke:#d1d5db}.stroke-white{stroke:#fff}.stroke-\[3\]{stroke-width:3}.p-3{padding:.75rem}.p-2{padding:.5rem}.p-10{padding:2.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.p-0{padding:0}.p-\[6px\]{padding:6px}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-bottom:0;padding-top:0}.\!px-3{padding-left:.75rem!important;padding-right:.75rem!important}.pr-0{padding-right:0}.pt-2{padding-top:.5rem}.pl-3{padding-left:.75rem}.pr-9{padding-right:2.25rem}.pb-4{padding-bottom:1rem}.pr-3{padding-right:.75rem}.pl-2{padding-left:.5rem}.pr-4{padding-right:1rem}.pl-1{padding-left:.25rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pb-2{padding-bottom:.5rem}.pb-\[10px\]{padding-bottom:10px}.pr-1{padding-right:.25rem}.pr-\[1\.7rem\]{padding-right:1.7rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-\[10px\]{font-size:10px}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.leading-6{line-height:1.5rem}.leading-5{line-height:1.25rem}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.\!text-indigo-600{--tw-text-opacity:1!important;color:rgb(79 70 229/var(--tw-text-opacity))!important}.opacity-100{opacity:1}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\],.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_10px_2px_rgba\(0\2c 0\2c 0\2c 0\.3\)\]{--tw-shadow:0 0 10px 2px rgba(0,0,0,.3);--tw-shadow-colored:0 0 10px 2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur{--tw-blur:blur(8px)}.blur,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-150{transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}.\[stroke-dasharray\:6\2c 4\]{stroke-dasharray:6,4}.\[stroke-linecap\:round\]{stroke-linecap:round}.\[stroke-linejoin\:round\]{stroke-linejoin:round}.\[writing-mode\:vertical-lr\]{writing-mode:vertical-lr}.checked\:border-indigo-600:checked{--tw-border-opacity:1;border-color:rgb(79 70 229/var(--tw-border-opacity))}.checked\:bg-indigo-600:checked{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:whitespace-normal:hover{white-space:normal}.hover\:bg-amber-700:hover{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:relative:focus{position:relative}.focus\:z-50:focus{z-index:50}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:outline-1:focus{outline-width:1px}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-indigo-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(79 70 229/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-visible\:ring-white\/75:focus-visible{--tw-ring-color:hsla(0,0%,100%,.75)}.focus-visible\:ring-white:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.focus-visible\:ring-indigo-500:focus-visible{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus-visible\:ring-opacity-75:focus-visible{--tw-ring-opacity:0.75}.group:hover .group-hover\:flex{display:flex}[class~=theme-dark] .dark\:border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}[class~=theme-dark] .dark\:border-r-gray-700{--tw-border-opacity:1;border-right-color:rgb(55 65 81/var(--tw-border-opacity))}[class~=theme-dark] .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-black\/50{background-color:rgba(0,0,0,.5)}[class~=theme-dark] .dark\:bg-amber-900{--tw-bg-opacity:1;background-color:rgb(120 53 15/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-amber-700{--tw-bg-opacity:1;background-color:rgb(180 83 9/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-indigo-700{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-yellow-700{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-100\/90{background-color:rgba(243,244,246,.9)}[class~=theme-dark] .dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-gray-200\/75{background-color:rgba(229,231,235,.75)}[class~=theme-dark] .dark\:bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}[class~=theme-dark] .dark\:bg-opacity-80{--tw-bg-opacity:0.8}[class~=theme-dark] .dark\:stroke-gray-500{stroke:#6b7280}[class~=theme-dark] .dark\:stroke-gray-700{stroke:#374151}[class~=theme-dark] .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-gray-50{--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}[class~=theme-dark] .dark\:text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}[class~=theme-dark] .dark\:\!text-indigo-400{--tw-text-opacity:1!important;color:rgb(129 140 248/var(--tw-text-opacity))!important}[class~=theme-dark] .dark\:ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}[class~=theme-dark] .dark\:ring-opacity-20{--tw-ring-opacity:0.2}[class~=theme-dark] .dark\:hover\:bg-amber-600:hover{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity))}[class~=theme-dark] .hover\:dark\:text-gray-100:hover{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}@media (min-width:640px){.sm\:inline{display:inline}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:block{display:block}.md\:flex-row{flex-direction:row}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.102",
3
+ "version": "0.19.103",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@floating-ui/react": "^0.27.12",
@@ -84,5 +84,5 @@
84
84
  "access": "public",
85
85
  "registry": "https://registry.npmjs.org/"
86
86
  },
87
- "gitHead": "71d719bdbc5b9591b427c668b4ab9685eeb44b4a"
87
+ "gitHead": "14996d0bb0fdf1193c1555e4d171aefd5138303b"
88
88
  }
@@ -11,7 +11,7 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import React, {useMemo} from 'react';
14
+ import React, {useCallback, useMemo} from 'react';
15
15
 
16
16
  import {Table} from 'apache-arrow';
17
17
  import cx from 'classnames';
@@ -101,36 +101,52 @@ export const FlameNode = React.memo(
101
101
  effectiveDepth,
102
102
  tooltipId = 'default',
103
103
  }: FlameNodeProps): React.JSX.Element {
104
- // get the columns to read from
105
- const mappingColumn = table.getChild(FIELD_MAPPING_FILE);
106
- const functionNameColumn = table.getChild(FIELD_FUNCTION_NAME);
107
- const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
108
- const depthColumn = table.getChild(FIELD_DEPTH);
109
- const diffColumn = table.getChild(FIELD_DIFF);
110
- const filenameColumn = table.getChild(FIELD_FUNCTION_FILE_NAME);
111
- const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
112
- const tsColumn = table.getChild(FIELD_TIMESTAMP);
104
+ // Memoize column references - only changes when table changes
105
+ const columns = useMemo(
106
+ () => ({
107
+ mapping: table.getChild(FIELD_MAPPING_FILE),
108
+ functionName: table.getChild(FIELD_FUNCTION_NAME),
109
+ cumulative: table.getChild(FIELD_CUMULATIVE),
110
+ depth: table.getChild(FIELD_DEPTH),
111
+ diff: table.getChild(FIELD_DIFF),
112
+ filename: table.getChild(FIELD_FUNCTION_FILE_NAME),
113
+ valueOffset: table.getChild(FIELD_VALUE_OFFSET),
114
+ ts: table.getChild(FIELD_TIMESTAMP),
115
+ }),
116
+ [table]
117
+ );
113
118
 
114
119
  // get the actual values from the columns
115
120
  const binaries = useAppSelector(selectBinaries);
116
121
 
117
- const mappingFile: string | null = arrowToString(mappingColumn?.get(row));
118
- const functionName: string | null = arrowToString(functionNameColumn?.get(row));
119
- const cumulative = cumulativeColumn?.get(row) != null ? BigInt(cumulativeColumn?.get(row)) : 0n;
120
- const diff: bigint | null = diffColumn?.get(row) != null ? BigInt(diffColumn?.get(row)) : null;
121
- const filename: string | null = arrowToString(filenameColumn?.get(row));
122
- const depth: number = depthColumn?.get(row) ?? 0;
123
-
124
- const valueOffset: bigint =
125
- valueOffsetColumn?.get(row) !== null && valueOffsetColumn?.get(row) !== undefined
126
- ? BigInt(valueOffsetColumn?.get(row))
127
- : 0n;
122
+ // Memoize row data extraction - only changes when table or row changes
123
+ const rowData = useMemo(() => {
124
+ const mappingFile: string | null = arrowToString(columns.mapping?.get(row));
125
+ const functionName: string | null = arrowToString(columns.functionName?.get(row));
126
+ const cumulative =
127
+ columns.cumulative?.get(row) != null ? BigInt(columns.cumulative?.get(row)) : 0n;
128
+ const diff: bigint | null =
129
+ columns.diff?.get(row) != null ? BigInt(columns.diff?.get(row)) : null;
130
+ const filename: string | null = arrowToString(columns.filename?.get(row));
131
+ const depth: number = columns.depth?.get(row) ?? 0;
132
+ const valueOffset: bigint =
133
+ columns.valueOffset?.get(row) !== null && columns.valueOffset?.get(row) !== undefined
134
+ ? BigInt(columns.valueOffset?.get(row))
135
+ : 0n;
136
+
137
+ return {mappingFile, functionName, cumulative, diff, filename, depth, valueOffset};
138
+ }, [columns, row]);
139
+
140
+ const {mappingFile, functionName, cumulative, diff, filename, depth, valueOffset} = rowData;
128
141
 
129
142
  const colorAttribute =
130
143
  colorBy === 'filename' ? filename : colorBy === 'binary' ? mappingFile : null;
131
144
 
132
- const hoveringName =
133
- hoveringRow !== undefined ? arrowToString(functionNameColumn?.get(hoveringRow)) : '';
145
+ // Memoize hovering name lookup
146
+ const hoveringName = useMemo(() => {
147
+ return hoveringRow !== undefined ? arrowToString(columns.functionName?.get(hoveringRow)) : '';
148
+ }, [columns.functionName, hoveringRow]);
149
+
134
150
  const shouldBeHighlighted =
135
151
  functionName != null && hoveringName != null && functionName === hoveringName;
136
152
 
@@ -147,18 +163,59 @@ export const FlameNode = React.memo(
147
163
  return row === 0 ? 'root' : nodeLabel(table, row, binaries.length > 1);
148
164
  }, [table, row, binaries]);
149
165
 
166
+ // Memoize selection data - only changes when selectedRow changes
167
+ const selectionData = useMemo(() => {
168
+ const selectionOffset =
169
+ columns.valueOffset?.get(selectedRow) !== null &&
170
+ columns.valueOffset?.get(selectedRow) !== undefined
171
+ ? BigInt(columns.valueOffset?.get(selectedRow))
172
+ : 0n;
173
+ const selectionCumulative =
174
+ columns.cumulative?.get(selectedRow) !== null
175
+ ? BigInt(columns.cumulative?.get(selectedRow))
176
+ : 0n;
177
+ const selectedDepth = columns.depth?.get(selectedRow);
178
+ const total = columns.cumulative?.get(selectedRow);
179
+ return {selectionOffset, selectionCumulative, selectedDepth, total};
180
+ }, [columns, selectedRow]);
181
+
182
+ const {selectionOffset, selectionCumulative, selectedDepth, total} = selectionData;
183
+
184
+ // Memoize tsBounds - only changes when profileSource changes
185
+ const tsBounds = useMemo(() => boundsFromProfileSource(profileSource), [profileSource]);
186
+
187
+ // Memoize event handlers
188
+ const onMouseEnter = useCallback((): void => {
189
+ setHoveringRow(row);
190
+ window.dispatchEvent(
191
+ new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
192
+ detail: {row},
193
+ })
194
+ );
195
+ }, [setHoveringRow, row, tooltipId]);
196
+
197
+ const onMouseLeave = useCallback((): void => {
198
+ setHoveringRow(undefined);
199
+ window.dispatchEvent(
200
+ new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
201
+ detail: {row: null},
202
+ })
203
+ );
204
+ }, [setHoveringRow, tooltipId]);
205
+
206
+ const handleContextMenu = useCallback(
207
+ (e: React.MouseEvent): void => {
208
+ onContextMenu(e, row);
209
+ },
210
+ [onContextMenu, row]
211
+ );
212
+
213
+ // Early returns - all hooks must be called before this point
150
214
  // Hide frames beyond effective depth limit
151
215
  if (effectiveDepth !== undefined && depth > effectiveDepth) {
152
216
  return <></>;
153
217
  }
154
218
 
155
- const selectionOffset =
156
- valueOffsetColumn?.get(selectedRow) !== null &&
157
- valueOffsetColumn?.get(selectedRow) !== undefined
158
- ? BigInt(valueOffsetColumn?.get(selectedRow))
159
- : 0n;
160
- const selectionCumulative =
161
- cumulativeColumn?.get(selectedRow) !== null ? BigInt(cumulativeColumn?.get(selectedRow)) : 0n;
162
219
  if (
163
220
  valueOffset + cumulative <= selectionOffset ||
164
221
  valueOffset >= selectionOffset + selectionCumulative
@@ -173,8 +230,6 @@ export const FlameNode = React.memo(
173
230
  }
174
231
 
175
232
  // Cumulative can be larger than total when a selection is made. All parents of the selection are likely larger, but we want to only show them as 100% in the graph.
176
- const tsBounds = boundsFromProfileSource(profileSource);
177
- const total = cumulativeColumn?.get(selectedRow);
178
233
  const totalRatio = cumulative > total ? 1 : Number(cumulative) / Number(total);
179
234
  const width: number = isFlameChart
180
235
  ? (Number(cumulative) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
@@ -184,35 +239,12 @@ export const FlameNode = React.memo(
184
239
  return <></>;
185
240
  }
186
241
 
187
- const selectedDepth = depthColumn?.get(selectedRow);
188
242
  const styles =
189
243
  selectedDepth !== undefined && selectedDepth > depth ? fadedFlameRectStyles : flameRectStyles;
190
244
 
191
- const onMouseEnter = (): void => {
192
- setHoveringRow(row);
193
- window.dispatchEvent(
194
- new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
195
- detail: {row},
196
- })
197
- );
198
- };
199
-
200
- const onMouseLeave = (): void => {
201
- setHoveringRow(undefined);
202
- window.dispatchEvent(
203
- new CustomEvent(`flame-tooltip-update-${tooltipId}`, {
204
- detail: {row: null},
205
- })
206
- );
207
- };
208
-
209
- const handleContextMenu = (e: React.MouseEvent): void => {
210
- onContextMenu(e, row);
211
- };
212
-
213
- const ts = tsColumn !== null ? Number(tsColumn.get(row)) : 0;
245
+ const ts = columns.ts !== null ? Number(columns.ts.get(row)) : 0;
214
246
  const x =
215
- isFlameChart && tsColumn !== null
247
+ isFlameChart && columns.ts !== null
216
248
  ? ((ts - Number(tsBounds[0])) / (Number(tsBounds[1]) - Number(tsBounds[0]))) * totalWidth
217
249
  : selectedDepth > depth
218
250
  ? 0
@@ -25,7 +25,7 @@ import {Table, tableFromIPC} from 'apache-arrow';
25
25
  import {useContextMenu} from 'react-contexify';
26
26
 
27
27
  import {FlamegraphArrow} from '@parca/client';
28
- import {useParcaContext} from '@parca/components';
28
+ import {FlameGraphSkeleton, SandwichFlameGraphSkeleton, useParcaContext} from '@parca/components';
29
29
  import {USER_PREFERENCES, useCurrentColorProfile, useUserPreference} from '@parca/hooks';
30
30
  import {ProfileType} from '@parca/parser';
31
31
  import {getColorForFeature, selectDarkMode, useAppSelector} from '@parca/store';
@@ -38,6 +38,7 @@ import ContextMenuWrapper, {ContextMenuWrapperRef} from './ContextMenuWrapper';
38
38
  import {FlameNode, RowHeight, colorByColors} from './FlameGraphNodes';
39
39
  import {MemoizedTooltip} from './MemoizedTooltip';
40
40
  import {TooltipProvider} from './TooltipContext';
41
+ import {useBatchedRendering} from './useBatchedRendering';
41
42
  import {useScrollViewport} from './useScrollViewport';
42
43
  import {useVisibleNodes} from './useVisibleNodes';
43
44
  import {
@@ -136,6 +137,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
136
137
  isFlameChart = false,
137
138
  isRenderedAsFlamegraph = false,
138
139
  isInSandwichView = false,
140
+ isHalfScreen,
139
141
  tooltipId = 'default',
140
142
  maxFrameCount,
141
143
  isExpanded = false,
@@ -163,6 +165,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
163
165
  const svg = useRef(null);
164
166
  const containerRef = useRef<HTMLDivElement>(null);
165
167
  const renderStartTime = useRef<number>(0);
168
+ const hasInitialRenderCompleted = useRef(false);
166
169
 
167
170
  const [svgElement, setSvgElement] = useState<SVGSVGElement | null>(null);
168
171
 
@@ -291,6 +294,18 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
291
294
  effectiveDepth: deferredEffectiveDepth,
292
295
  });
293
296
 
297
+ // Add nodes in incremental batches to avoid blocking the UI
298
+ const {items: batchedNodes, isComplete: isBatchingComplete} = useBatchedRendering(visibleNodes, {
299
+ batchSize: 500,
300
+ });
301
+ if (isBatchingComplete) {
302
+ hasInitialRenderCompleted.current = true;
303
+ }
304
+
305
+ // Show skeleton only during initial load, not during scroll updates
306
+ const showSkeleton =
307
+ !hasInitialRenderCompleted.current && batchedNodes.length !== visibleNodes.length;
308
+
294
309
  useEffect(() => {
295
310
  if (perf?.markInteraction != null) {
296
311
  renderStartTime.current = performance.now();
@@ -327,12 +342,22 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
327
342
  isInSandwichView={isInSandwichView}
328
343
  />
329
344
  <MemoizedTooltip contextElement={svgElement} dockedMetainfo={dockedMetainfo} />
345
+ {showSkeleton && (
346
+ <div className="absolute inset-0 z-10">
347
+ {isRenderedAsFlamegraph ? (
348
+ <SandwichFlameGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
349
+ ) : (
350
+ <FlameGraphSkeleton isHalfScreen={isHalfScreen} isDarkMode={isDarkMode} />
351
+ )}
352
+ </div>
353
+ )}
330
354
  <div
331
355
  ref={containerRef}
332
356
  className="overflow-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100 dark:scrollbar-thumb-gray-600 dark:scrollbar-track-gray-800 will-change-transform scroll-smooth webkit-overflow-scrolling-touch contain"
333
357
  style={{
334
358
  width: width ?? '100%',
335
359
  contain: 'layout style paint',
360
+ visibility: !showSkeleton ? 'visible' : 'hidden',
336
361
  }}
337
362
  >
338
363
  <svg
@@ -342,7 +367,7 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
342
367
  preserveAspectRatio="xMinYMid"
343
368
  ref={svg}
344
369
  >
345
- {visibleNodes.map(row => (
370
+ {batchedNodes.map(row => (
346
371
  <FlameNode
347
372
  key={row}
348
373
  table={table}
@@ -0,0 +1,84 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import {useEffect, useRef, useState} from 'react';
15
+
16
+ interface UseBatchedRenderingOptions {
17
+ batchSize?: number;
18
+ // Delay between batches in ms (0 = next animation frame)
19
+ batchDelay?: number;
20
+ }
21
+
22
+ interface UseBatchedRenderingResult<T> {
23
+ items: T[];
24
+ isComplete: boolean;
25
+ }
26
+
27
+ // useBatchedRendering - Helps in incrementally rendering items in batches to avoid UI blocking.
28
+ export const useBatchedRendering = <T>(
29
+ items: T[],
30
+ options: UseBatchedRenderingOptions = {}
31
+ ): UseBatchedRenderingResult<T> => {
32
+ const {batchSize = 500, batchDelay = 0} = options;
33
+
34
+ const [renderedCount, setRenderedCount] = useState(0);
35
+ const itemsRef = useRef(items);
36
+ const rafRef = useRef<number | null>(null);
37
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
38
+
39
+ useEffect(() => {
40
+ if (itemsRef.current !== items) {
41
+ itemsRef.current = items;
42
+ setRenderedCount(prev => {
43
+ if (items.length === 0) return 0;
44
+ // If new items were added (scrolling down), keep current progress
45
+ if (items.length > prev) return prev;
46
+ // If items reduced, cap to new length
47
+ return Math.min(prev, items.length);
48
+ });
49
+ }
50
+ }, [items]);
51
+
52
+ // Progressively render more items
53
+ useEffect(() => {
54
+ if (renderedCount === items.length) {
55
+ return;
56
+ }
57
+
58
+ const scheduleNextBatch = (): void => {
59
+ const incrementState = (): void => {
60
+ setRenderedCount(prev => Math.min(prev + batchSize, items.length));
61
+ };
62
+ if (batchDelay > 0) {
63
+ timeoutRef.current = setTimeout(incrementState, batchDelay);
64
+ } else {
65
+ rafRef.current = requestAnimationFrame(incrementState);
66
+ }
67
+ };
68
+ scheduleNextBatch();
69
+
70
+ return () => {
71
+ if (rafRef.current !== null) {
72
+ cancelAnimationFrame(rafRef.current);
73
+ }
74
+ if (timeoutRef.current !== null) {
75
+ clearTimeout(timeoutRef.current);
76
+ }
77
+ };
78
+ }, [renderedCount, items.length, batchSize, batchDelay]);
79
+
80
+ return {
81
+ items: items.slice(0, renderedCount),
82
+ isComplete: renderedCount === items.length,
83
+ };
84
+ };
@@ -20,6 +20,21 @@ export interface ViewportState {
20
20
  containerWidth: number;
21
21
  }
22
22
 
23
+ // Find the scrollable ancestor (the element with overflow: auto/scroll)
24
+ const findScrollableParent = (element: HTMLElement | null): HTMLElement | undefined => {
25
+ if (element === null) return undefined;
26
+ let current: HTMLElement | null = element.parentElement;
27
+ while (current !== null) {
28
+ const style = window.getComputedStyle(current);
29
+ const overflowY = style.overflowY;
30
+ if (overflowY === 'auto' || overflowY === 'scroll') {
31
+ return current;
32
+ }
33
+ current = current.parentElement;
34
+ }
35
+ return undefined;
36
+ };
37
+
23
38
  export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>): ViewportState => {
24
39
  const [viewport, setViewport] = useState<ViewportState>({
25
40
  scrollTop: 0,
@@ -33,11 +48,25 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
33
48
  const updateViewport = useCallback(() => {
34
49
  if (containerRef.current !== null) {
35
50
  const container = containerRef.current;
51
+ const rect = container.getBoundingClientRect();
52
+
53
+ // Restrict container height to the visible portion on screen
54
+ // This handles cases where the container is partially off-screen
55
+ // We only want to consider the visible part for culling calculations
56
+
57
+ const containerTop = rect.top;
58
+ const containerBottom = rect.bottom;
59
+ const viewportTop = 0;
60
+ const viewportBottom = window.innerHeight;
61
+ const visibleTop = Math.max(containerTop, viewportTop);
62
+ const visibleBottom = Math.min(containerBottom, viewportBottom);
63
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
64
+ const scrollOffset = Math.max(0, viewportTop - containerTop);
36
65
 
37
66
  const newViewport = {
38
- scrollTop: container.scrollTop,
67
+ scrollTop: scrollOffset,
39
68
  scrollLeft: container.scrollLeft,
40
- containerHeight: container.clientHeight,
69
+ containerHeight: visibleHeight, // Only the visible portion
41
70
  containerWidth: container.clientWidth,
42
71
  };
43
72
 
@@ -59,6 +88,8 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
59
88
  const container = containerRef.current;
60
89
  if (container === null) return;
61
90
 
91
+ const scrollableParent = findScrollableParent(container);
92
+
62
93
  // ResizeObserver Strategy:
63
94
  // Monitor container size changes (window resize, layout shifts)
64
95
  // to update viewport dimensions for accurate culling calculations
@@ -66,10 +97,12 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
66
97
  throttledUpdateViewport();
67
98
  });
68
99
 
69
- // Container Scroll Event Strategy:
70
- // Use passive event listeners for better scroll performance
71
- // Throttle with requestAnimationFrame to maintain 60fps target
100
+ // Listen to scroll on the actual scrollable parent
101
+
102
+ scrollableParent?.addEventListener('scroll', throttledUpdateViewport, {passive: true});
72
103
  container.addEventListener('scroll', throttledUpdateViewport, {passive: true});
104
+ window.addEventListener('scroll', throttledUpdateViewport, {passive: true});
105
+
73
106
  resizeObserver.observe(container);
74
107
 
75
108
  // Initialize viewport state on mount
@@ -77,7 +110,9 @@ export const useScrollViewport = (containerRef: React.RefObject<HTMLDivElement>)
77
110
 
78
111
  return () => {
79
112
  // Cleanup: Remove event listeners and cancel pending animations
113
+ scrollableParent?.removeEventListener('scroll', throttledUpdateViewport);
80
114
  container.removeEventListener('scroll', throttledUpdateViewport);
115
+ window.removeEventListener('scroll', throttledUpdateViewport);
81
116
  resizeObserver.disconnect();
82
117
  if (throttleRef.current !== null) {
83
118
  cancelAnimationFrame(throttleRef.current);
@@ -85,7 +85,19 @@ export const useVisibleNodes = ({
85
85
  result: number[];
86
86
  }>({key: '', result: []});
87
87
 
88
+ const renderedRangeRef = useRef<{minDepth: number; maxDepth: number; table: Table<any> | null}>({
89
+ minDepth: Infinity,
90
+ maxDepth: -Infinity,
91
+ table: null,
92
+ });
93
+
88
94
  return useMemo(() => {
95
+ // This happens when the continer is scrolled off screen, in this case we return all previously rendered nodes
96
+ // to avoid trimming the rendered nodes to zero which would cause jank when scrolling back into view
97
+ if (viewport.containerHeight === 0 && lastResultRef.current.result.length > 0) {
98
+ return lastResultRef.current.result;
99
+ }
100
+
89
101
  // Create a stable key for memoization to prevent unnecessary recalculations
90
102
  const memoKey = `${viewport.scrollTop}-${
91
103
  viewport.containerHeight
@@ -96,7 +108,7 @@ export const useVisibleNodes = ({
96
108
  return lastResultRef.current.result;
97
109
  }
98
110
 
99
- if (table === null || viewport.containerHeight === 0) return [];
111
+ if (table === null) return [];
100
112
 
101
113
  const visibleRows: number[] = [];
102
114
  const {scrollTop, containerHeight} = viewport;
@@ -104,11 +116,35 @@ export const useVisibleNodes = ({
104
116
  // Viewport Culling Algorithm:
105
117
  // 1. Calculate visible depth range based on scroll position and container height
106
118
  // 2. Add 5-row buffer above/below for smooth scrolling experience
107
- const startDepth = Math.max(0, Math.floor(scrollTop / RowHeight) - 5);
108
- const endDepth = Math.min(
109
- effectiveDepth,
110
- Math.ceil((scrollTop + containerHeight) / RowHeight) + 5
119
+ // Note: We never shrink the rendered range to avoid back and forth node removals (and in turn additions when scrolled down again) to the dom.
120
+
121
+ const BUFFER = 15; // Buffer for smoother scrolling
122
+
123
+ const visibleStartDepth = Math.max(0, Math.floor(scrollTop / RowHeight) - BUFFER);
124
+ const visibleDepths = Math.ceil(containerHeight / RowHeight);
125
+ const visibleEndDepth = Math.min(effectiveDepth, visibleStartDepth + visibleDepths + BUFFER);
126
+
127
+ // Reset range if table changed (new data loaded) as this is new data
128
+ if (renderedRangeRef.current.table !== table) {
129
+ renderedRangeRef.current = {
130
+ minDepth: Infinity,
131
+ maxDepth: -Infinity,
132
+ table: table,
133
+ };
134
+ }
135
+
136
+ // Expand the rendered range (never shrink when scrolling up/down)
137
+ renderedRangeRef.current.minDepth = Math.min(
138
+ renderedRangeRef.current.minDepth,
139
+ visibleStartDepth
111
140
  );
141
+ renderedRangeRef.current.maxDepth = Math.max(
142
+ renderedRangeRef.current.maxDepth,
143
+ visibleEndDepth
144
+ );
145
+
146
+ const startDepth = renderedRangeRef.current.minDepth;
147
+ const endDepth = renderedRangeRef.current.maxDepth;
112
148
 
113
149
  const cumulativeColumn = table.getChild(FIELD_CUMULATIVE);
114
150
  const valueOffsetColumn = table.getChild(FIELD_VALUE_OFFSET);
@@ -113,7 +113,9 @@ export const useLabelValues = (
113
113
  },
114
114
  });
115
115
 
116
- console.log('Label values query result:', {data, error, isLoading, labelName});
116
+ useEffect(() => {
117
+ console.log('Label values query result:', {data, error, isLoading, labelName});
118
+ }, [data, error, isLoading, labelName]);
117
119
 
118
120
  return {
119
121
  result: {response: data ?? [], error: error as Error},