@servicetitan/marketing-ui 7.2.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ads/ads-stat.js +2 -4
- package/dist/components/ads/ads-stat.js.map +1 -1
- package/dist/components/charts/common/color-tag.d.ts.map +1 -1
- package/dist/components/charts/common/color-tag.js +1 -0
- package/dist/components/charts/common/color-tag.js.map +1 -1
- package/dist/components/charts/funnel-chart/components/funnel-chart.js.map +1 -1
- package/dist/components/charts/line-chart/components/body.js +8 -8
- package/dist/components/charts/line-chart/components/body.js.map +1 -1
- package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
- package/dist/components/charts/line-chart/components/hover-popover.js +22 -13
- package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
- package/dist/components/charts/line-chart/components/hover-popover.module.less +5 -0
- package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
- package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
- package/dist/components/charts/line-chart/components/svg-bars.js +19 -25
- package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
- package/dist/components/charts/line-chart/components/svg-lines.js +2 -2
- package/dist/components/charts/line-chart/components/svg-lines.js.map +1 -1
- package/dist/components/charts/line-chart/stores/line-chart.store.js +2 -2
- package/dist/components/charts/line-chart/stores/line-chart.store.js.map +1 -1
- package/dist/components/charts/line-chart/utils/labels.js.map +1 -1
- package/dist/components/charts/pie-chart/components/pie.js +2 -2
- package/dist/components/charts/pie-chart/components/pie.js.map +1 -1
- package/dist/components/charts/pie-chart/utils/const.js +1 -1
- package/dist/components/charts/pie-chart/utils/const.js.map +1 -1
- package/dist/components/stat/stat-card.d.ts.map +1 -1
- package/dist/components/stat/stat-card.js +53 -33
- package/dist/components/stat/stat-card.js.map +1 -1
- package/dist/components/stat/stat-card.module.less +17 -2
- package/dist/components/stat/stat-card.module.less.d.ts +3 -1
- package/dist/components/ui/line-text/line-text.js.map +1 -1
- package/dist/utils/date/date-range-picker-state.d.ts.map +1 -1
- package/dist/utils/date/date-range-picker-state.js.map +1 -1
- package/package.json +3 -3
- package/src/components/charts/common/color-tag.tsx +5 -1
- package/src/components/charts/line-chart/components/hover-popover.module.less +5 -0
- package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
- package/src/components/charts/line-chart/components/hover-popover.tsx +23 -12
- package/src/components/charts/line-chart/components/svg-bars.tsx +20 -37
- package/src/components/stat/stat-card.module.less +17 -2
- package/src/components/stat/stat-card.module.less.d.ts +3 -1
- package/src/components/stat/stat-card.tsx +44 -37
- package/src/utils/date/date-range-picker-state.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/marketing-ui",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Marketing UI component and utils",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@servicetitan/anvil2": "^2.0.1",
|
|
34
34
|
"@servicetitan/design-system": "~14.5.1",
|
|
35
|
-
"@servicetitan/react-ioc": "^
|
|
35
|
+
"@servicetitan/react-ioc": "^34.0.1",
|
|
36
36
|
"@servicetitan/tokens": ">=12.2.1",
|
|
37
37
|
"@testing-library/react": "^14.2.1",
|
|
38
38
|
"@types/accounting": "~0.4.2",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"less": true,
|
|
54
54
|
"webpack": false
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "7e2b147613dc2f96854d2e2c4458a4891dc6aadc"
|
|
57
57
|
}
|
|
@@ -81,7 +81,11 @@ export const ColorTag: FC<ColorTagProps> = ({
|
|
|
81
81
|
dashed
|
|
82
82
|
? { borderColor: strokeColor ?? color }
|
|
83
83
|
: pattern === 'outline'
|
|
84
|
-
? {
|
|
84
|
+
? {
|
|
85
|
+
borderColor: outlineColor ?? color,
|
|
86
|
+
backgroundColor: color,
|
|
87
|
+
borderRadius: radius,
|
|
88
|
+
}
|
|
85
89
|
: { backgroundColor: color, borderRadius: radius }
|
|
86
90
|
}
|
|
87
91
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC
|
|
1
|
+
import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import { useDependencies } from '@servicetitan/react-ioc';
|
|
@@ -52,7 +52,7 @@ export const HoverPopover: FC = observer(() => {
|
|
|
52
52
|
}
|
|
53
53
|
}, [hoveredIndex, metrics, display, popH]);
|
|
54
54
|
|
|
55
|
-
const popoverStyle = useMemo
|
|
55
|
+
const { popoverStyle, arrowPosition } = useMemo(() => {
|
|
56
56
|
const posX = svgStore.periodX(hoveredIndex);
|
|
57
57
|
|
|
58
58
|
const yHeights = metrics
|
|
@@ -67,17 +67,23 @@ export const HoverPopover: FC = observer(() => {
|
|
|
67
67
|
const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));
|
|
68
68
|
const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
70
|
+
const barTopPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;
|
|
71
|
+
const idealTopPx = barTopPx + OFFSET_PX;
|
|
72
|
+
const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);
|
|
73
|
+
const popoverRepositioned = idealTopPx > maxTopPx;
|
|
74
|
+
const clampedTopPx = Math.min(idealTopPx, maxTopPx);
|
|
75
|
+
const topPercent = (clampedTopPx / CHART_HEIGHT_PX) * 100;
|
|
73
76
|
|
|
74
77
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
popoverStyle: {
|
|
79
|
+
top: `${topPercent.toFixed(1)}%`,
|
|
80
|
+
transform: 'translateY(0)',
|
|
81
|
+
position: 'absolute' as const,
|
|
82
|
+
...(isChartLeftSide
|
|
83
|
+
? { left: `${svgStore.fpx(posX + 2)}%` }
|
|
84
|
+
: { right: `${svgStore.fpx(102 - posX)}%` }),
|
|
85
|
+
},
|
|
86
|
+
arrowPosition: popoverRepositioned ? 'bottom' : 'top',
|
|
81
87
|
};
|
|
82
88
|
}, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);
|
|
83
89
|
|
|
@@ -85,6 +91,10 @@ export const HoverPopover: FC = observer(() => {
|
|
|
85
91
|
return null;
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
if (stackedTotals?.[hoveredIndex] === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
const period = periods[hoveredIndex]!;
|
|
89
99
|
const partialWeek = !!period.partial;
|
|
90
100
|
|
|
@@ -97,7 +107,8 @@ export const HoverPopover: FC = observer(() => {
|
|
|
97
107
|
<div
|
|
98
108
|
className={classNames(
|
|
99
109
|
Styles.arrow,
|
|
100
|
-
isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight
|
|
110
|
+
isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight,
|
|
111
|
+
arrowPosition === 'bottom' && Styles.arrowBottom
|
|
101
112
|
)}
|
|
102
113
|
/>
|
|
103
114
|
<Text size="small" variant="headline" el="h6">
|
|
@@ -56,38 +56,18 @@ export const SvgBars: FC<SvgBarsProps> = observer(
|
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
58
|
if (isStackedBarChart) {
|
|
59
|
-
// Use ORIGINAL calculations - keep all spacing/positioning unchanged
|
|
60
59
|
const spacingBetweenSegments = 1;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
values.reduce((sum, curr) => sum + curr.val, 0) + totalSpacing;
|
|
60
|
+
const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);
|
|
61
|
+
const totalYValue = values.reduce((sum, curr) => sum + curr.val, 0);
|
|
64
62
|
|
|
65
|
-
// Find first/last non-zero indices for visual styling
|
|
66
63
|
const firstNonZeroIdx = values.findIndex(v => v.value > 0);
|
|
67
64
|
const lastNonZeroIdx = values.reduce(
|
|
68
65
|
(last, v, idx) => (v.value > 0 ? idx : last),
|
|
69
66
|
-1
|
|
70
67
|
);
|
|
71
68
|
|
|
72
|
-
// Count 0-value segments below first non-zero (for text position adjustment)
|
|
73
|
-
const zeroSegmentsBelowFirst =
|
|
74
|
-
firstNonZeroIdx >= 0
|
|
75
|
-
? values.slice(firstNonZeroIdx + 1).filter(v => v.value <= 0).length
|
|
76
|
-
: 0;
|
|
77
|
-
|
|
78
|
-
const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);
|
|
79
69
|
if (totalValue > 0) {
|
|
80
|
-
|
|
81
|
-
* Adjust text position to maintain consistent gap with first rendered segment:
|
|
82
|
-
* 1. Subtract spacing for skipped segments from fpy argument
|
|
83
|
-
* 2. Add pixel offset for zeros below first non-zero
|
|
84
|
-
*/
|
|
85
|
-
const textStackedBarHeight =
|
|
86
|
-
stackedBarHeight -
|
|
87
|
-
(firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;
|
|
88
|
-
const yTop =
|
|
89
|
-
+fpy(textStackedBarHeight) +
|
|
90
|
-
zeroSegmentsBelowFirst * spacingBetweenSegments;
|
|
70
|
+
const yTop = +fpy(totalYValue) - 2;
|
|
91
71
|
const scaleX = 0.3;
|
|
92
72
|
const scaleY = 1;
|
|
93
73
|
|
|
@@ -116,26 +96,29 @@ export const SvgBars: FC<SvgBarsProps> = observer(
|
|
|
116
96
|
);
|
|
117
97
|
}
|
|
118
98
|
|
|
99
|
+
let stackedBarHeight = totalYValue;
|
|
100
|
+
let isFirstRendered = true;
|
|
101
|
+
const nonZeroCount = values.filter(v => v.value > 0).length;
|
|
102
|
+
const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;
|
|
103
|
+
|
|
119
104
|
for (let j = 0; j < values.length; j++) {
|
|
120
105
|
const value = values[j];
|
|
121
|
-
stackedBarHeight -= spacingBetweenSegments;
|
|
122
|
-
|
|
123
|
-
const TOP_RADIUS = 1;
|
|
124
|
-
const xLeft = +fpx(x - barWidth / 2);
|
|
125
|
-
const width = +fpx(barWidth);
|
|
126
106
|
|
|
127
107
|
if (value.value <= 0) {
|
|
128
|
-
stackedBarHeight -= value.val;
|
|
129
108
|
continue;
|
|
130
109
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
110
|
+
|
|
111
|
+
if (!isFirstRendered) {
|
|
112
|
+
stackedBarHeight -= spacingBetweenSegments;
|
|
113
|
+
}
|
|
114
|
+
isFirstRendered = false;
|
|
115
|
+
|
|
116
|
+
const TOP_RADIUS = 1;
|
|
117
|
+
const xLeft = +fpx(x - barWidth / 2);
|
|
118
|
+
const width = +fpx(barWidth);
|
|
119
|
+
const yTop = +fpy(stackedBarHeight);
|
|
120
|
+
const height =
|
|
121
|
+
j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);
|
|
139
122
|
const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;
|
|
140
123
|
|
|
141
124
|
const d = [
|
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
margin-bottom: 2px;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
.title {
|
|
15
|
-
|
|
14
|
+
.title-row {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: flex-start;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.value-row {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
align-items: flex-end;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
margin-top: @spacing-1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card {
|
|
29
|
+
border-radius: 12px;
|
|
30
|
+
min-width: min-content;
|
|
16
31
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
BodyText,
|
|
5
5
|
BodyTextPropsStrict,
|
|
6
6
|
Eyebrow,
|
|
7
|
+
Headline,
|
|
7
8
|
Popover,
|
|
8
9
|
Stack,
|
|
9
10
|
Tooltip,
|
|
@@ -13,6 +14,8 @@ import { formatValue, NumberFormatter } from '../../utils/formatters';
|
|
|
13
14
|
import { Icon } from '@servicetitan/anvil2';
|
|
14
15
|
import TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';
|
|
15
16
|
import TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';
|
|
17
|
+
import InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';
|
|
18
|
+
import { tokens } from '@servicetitan/tokens/core/index';
|
|
16
19
|
|
|
17
20
|
const calculateDiff = (
|
|
18
21
|
value: number,
|
|
@@ -168,57 +171,61 @@ export const StatCard: FC<StatCardProps> = ({
|
|
|
168
171
|
const [popoverShown, setPopoverShown] = useState(false);
|
|
169
172
|
const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';
|
|
170
173
|
const val = value === undefined ? '\u00A0' : formatValue(value, format);
|
|
174
|
+
const hasInfo = !!description || !!popoverContent;
|
|
171
175
|
|
|
172
|
-
const eyebrow =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
176
|
+
const eyebrow = <Eyebrow data-cy={`marketing-stat-${title}-title`}>{title}</Eyebrow>;
|
|
177
|
+
|
|
178
|
+
const infoIcon = <Icon svg={InfoSVG} color={tokens.colorNeutral100} />;
|
|
179
|
+
|
|
180
|
+
const infoContent = popoverContent ? (
|
|
181
|
+
<Popover open={popoverShown} trigger={infoIcon} onMouseEnter={() => setPopoverShown(true)}>
|
|
182
|
+
{popoverContent}
|
|
183
|
+
</Popover>
|
|
184
|
+
) : description ? (
|
|
185
|
+
<Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
|
|
186
|
+
{infoIcon}
|
|
187
|
+
</Tooltip>
|
|
188
|
+
) : null;
|
|
183
189
|
|
|
184
190
|
return (
|
|
185
191
|
<Stack
|
|
186
192
|
direction="column"
|
|
187
|
-
alignItems="center"
|
|
188
193
|
className={classNames(
|
|
189
|
-
'p-
|
|
194
|
+
'p-2',
|
|
190
195
|
{
|
|
191
|
-
'bg-white border
|
|
196
|
+
'bg-white border': !clean,
|
|
197
|
+
[Styles.card]: !clean,
|
|
192
198
|
'flex-grow-1 flex-basis-0': fill,
|
|
193
199
|
},
|
|
194
200
|
className
|
|
195
201
|
)}
|
|
196
202
|
onMouseLeave={() => setPopoverShown(false)}
|
|
197
203
|
>
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
{popoverContent}
|
|
201
|
-
</Popover>
|
|
202
|
-
) : description ? (
|
|
203
|
-
<Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
|
|
204
|
+
<div className="p-3">
|
|
205
|
+
<div className={Styles.titleRow}>
|
|
204
206
|
{eyebrow}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
207
|
+
{hasInfo && infoContent}
|
|
208
|
+
</div>
|
|
209
|
+
<div className={Styles.valueRow}>
|
|
210
|
+
<Headline
|
|
211
|
+
size="xlarge"
|
|
212
|
+
className="m-b-0-i"
|
|
213
|
+
data-cy={`marketing-stat-${title}-value`}
|
|
214
|
+
>
|
|
215
|
+
{val}
|
|
216
|
+
</Headline>
|
|
217
|
+
{!valueOnly && (
|
|
218
|
+
<StatDiff
|
|
219
|
+
value={value}
|
|
220
|
+
prev={prev}
|
|
221
|
+
format={format}
|
|
222
|
+
inverted={inverted}
|
|
223
|
+
neutral={neutral}
|
|
224
|
+
diffPercentOnly={diffPercentOnly}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
222
229
|
</Stack>
|
|
223
230
|
);
|
|
224
231
|
};
|
|
@@ -12,8 +12,9 @@ export interface DateRangePickerStateType {
|
|
|
12
12
|
onChange(val?: DateRange): void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface DateRangePickerOptionsStateType<
|
|
16
|
-
extends
|
|
15
|
+
export interface DateRangePickerOptionsStateType<
|
|
16
|
+
OptionKeys extends string,
|
|
17
|
+
> extends DateRangePickerStateType {
|
|
17
18
|
readonly options: DateRangePickerOptions<OptionKeys>;
|
|
18
19
|
readonly selectedOption?: DateRangePickerOption<OptionKeys>;
|
|
19
20
|
}
|