@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.
Files changed (43) hide show
  1. package/dist/components/ads/ads-stat.js +2 -4
  2. package/dist/components/ads/ads-stat.js.map +1 -1
  3. package/dist/components/charts/common/color-tag.d.ts.map +1 -1
  4. package/dist/components/charts/common/color-tag.js +1 -0
  5. package/dist/components/charts/common/color-tag.js.map +1 -1
  6. package/dist/components/charts/funnel-chart/components/funnel-chart.js.map +1 -1
  7. package/dist/components/charts/line-chart/components/body.js +8 -8
  8. package/dist/components/charts/line-chart/components/body.js.map +1 -1
  9. package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
  10. package/dist/components/charts/line-chart/components/hover-popover.js +22 -13
  11. package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
  12. package/dist/components/charts/line-chart/components/hover-popover.module.less +5 -0
  13. package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  14. package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
  15. package/dist/components/charts/line-chart/components/svg-bars.js +19 -25
  16. package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
  17. package/dist/components/charts/line-chart/components/svg-lines.js +2 -2
  18. package/dist/components/charts/line-chart/components/svg-lines.js.map +1 -1
  19. package/dist/components/charts/line-chart/stores/line-chart.store.js +2 -2
  20. package/dist/components/charts/line-chart/stores/line-chart.store.js.map +1 -1
  21. package/dist/components/charts/line-chart/utils/labels.js.map +1 -1
  22. package/dist/components/charts/pie-chart/components/pie.js +2 -2
  23. package/dist/components/charts/pie-chart/components/pie.js.map +1 -1
  24. package/dist/components/charts/pie-chart/utils/const.js +1 -1
  25. package/dist/components/charts/pie-chart/utils/const.js.map +1 -1
  26. package/dist/components/stat/stat-card.d.ts.map +1 -1
  27. package/dist/components/stat/stat-card.js +53 -33
  28. package/dist/components/stat/stat-card.js.map +1 -1
  29. package/dist/components/stat/stat-card.module.less +17 -2
  30. package/dist/components/stat/stat-card.module.less.d.ts +3 -1
  31. package/dist/components/ui/line-text/line-text.js.map +1 -1
  32. package/dist/utils/date/date-range-picker-state.d.ts.map +1 -1
  33. package/dist/utils/date/date-range-picker-state.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/components/charts/common/color-tag.tsx +5 -1
  36. package/src/components/charts/line-chart/components/hover-popover.module.less +5 -0
  37. package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  38. package/src/components/charts/line-chart/components/hover-popover.tsx +23 -12
  39. package/src/components/charts/line-chart/components/svg-bars.tsx +20 -37
  40. package/src/components/stat/stat-card.module.less +17 -2
  41. package/src/components/stat/stat-card.module.less.d.ts +3 -1
  42. package/src/components/stat/stat-card.tsx +44 -37
  43. 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.2.0",
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": "^33.1.1",
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": "a44925e37020acb2153c7e10215a31cd22d054c1"
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
- ? { borderColor: outlineColor ?? color, borderRadius: radius }
84
+ ? {
85
+ borderColor: outlineColor ?? color,
86
+ backgroundColor: color,
87
+ borderRadius: radius,
88
+ }
85
89
  : { backgroundColor: color, borderRadius: radius }
86
90
  }
87
91
  />
@@ -44,6 +44,11 @@
44
44
  transform: rotate(135deg);
45
45
  }
46
46
 
47
+ .arrow-bottom {
48
+ top: auto;
49
+ bottom: 12px;
50
+ }
51
+
47
52
  .color-tag {
48
53
  width: 20px;
49
54
  height: 6px;
@@ -1,5 +1,6 @@
1
1
  export const __esModule: true;
2
2
  export const arrow: string;
3
+ export const arrowBottom: string;
3
4
  export const arrowLeft: string;
4
5
  export const arrowRight: string;
5
6
  export const colorTag: string;
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC, CSSProperties } from 'react';
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<CSSProperties>(() => {
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 barTopPositionPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;
71
- const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);
72
- const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);
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
- top: `${barHeightPercent}%`,
76
- transform: `translateY(${popoverOffsetPx}px)`,
77
- position: 'absolute',
78
- ...(isChartLeftSide
79
- ? { left: `${svgStore.fpx(posX + 2)}%` }
80
- : { right: `${svgStore.fpx(102 - posX)}%` }),
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 totalSpacing = (values.length - 1) * spacingBetweenSegments;
62
- let stackedBarHeight =
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
- const zeroSegments = values.slice(j + 1).filter(v => v.value <= 0).length;
132
-
133
- // Adjust yTop: move down by the space 0-value segments would occupy
134
- const yTop =
135
- +fpy(stackedBarHeight) +
136
- (values.length - 2) +
137
- zeroSegments * spacingBetweenSegments;
138
- const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);
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
- border-bottom: 1px dashed @color-neutral-90;
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
  }
@@ -1,5 +1,7 @@
1
1
  export const __esModule: true;
2
+ export const card: string;
2
3
  export const statDiff: string;
3
4
  export const statExtendedDiff: string;
4
- export const title: string;
5
+ export const titleRow: string;
6
+ export const valueRow: string;
5
7
 
@@ -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
- <Eyebrow
174
- className={classNames(Styles.title, 'ta-center')}
175
- data-cy={`marketing-stat-${title}-title`}
176
- onMouseEnter={() => {
177
- setPopoverShown(true);
178
- }}
179
- >
180
- {title}
181
- </Eyebrow>
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-y-3',
194
+ 'p-2',
190
195
  {
191
- 'bg-white border-radius-2 border': !clean,
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
- {popoverContent ? (
199
- <Popover open={popoverShown} trigger={eyebrow}>
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
- </Tooltip>
206
- ) : (
207
- eyebrow
208
- )}
209
- <BodyText className="fs-6-i ff-display" data-cy={`marketing-stat-${title}-value`}>
210
- {val}
211
- </BodyText>
212
- {!valueOnly && (
213
- <StatDiff
214
- value={value}
215
- prev={prev}
216
- format={format}
217
- inverted={inverted}
218
- neutral={neutral}
219
- diffPercentOnly={diffPercentOnly}
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<OptionKeys extends string>
16
- extends DateRangePickerStateType {
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
  }