@servicetitan/marketing-ui 7.3.0 → 7.5.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 (26) hide show
  1. package/dist/components/charts/common/color-tag.d.ts.map +1 -1
  2. package/dist/components/charts/common/color-tag.js +1 -0
  3. package/dist/components/charts/common/color-tag.js.map +1 -1
  4. package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
  5. package/dist/components/charts/line-chart/components/hover-popover.js +22 -13
  6. package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
  7. package/dist/components/charts/line-chart/components/hover-popover.module.less +5 -0
  8. package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  9. package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
  10. package/dist/components/charts/line-chart/components/svg-bars.js +16 -22
  11. package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
  12. package/dist/components/stat/stat-card.d.ts +2 -0
  13. package/dist/components/stat/stat-card.d.ts.map +1 -1
  14. package/dist/components/stat/stat-card.js +71 -36
  15. package/dist/components/stat/stat-card.js.map +1 -1
  16. package/dist/components/stat/stat-card.module.less +33 -2
  17. package/dist/components/stat/stat-card.module.less.d.ts +6 -1
  18. package/package.json +2 -2
  19. package/src/components/charts/common/color-tag.tsx +5 -1
  20. package/src/components/charts/line-chart/components/hover-popover.module.less +5 -0
  21. package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  22. package/src/components/charts/line-chart/components/hover-popover.tsx +23 -12
  23. package/src/components/charts/line-chart/components/svg-bars.tsx +20 -37
  24. package/src/components/stat/stat-card.module.less +33 -2
  25. package/src/components/stat/stat-card.module.less.d.ts +6 -1
  26. package/src/components/stat/stat-card.tsx +65 -40
@@ -1 +1 @@
1
- {"version":3,"file":"color-tag.d.ts","sourceRoot":"","sources":["../../../../src/components/charts/common/color-tag.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAS,MAAM,OAAO,CAAC;AAKlC,UAAU,aAAa;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;CAC7C;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA0EtC,CAAC"}
1
+ {"version":3,"file":"color-tag.d.ts","sourceRoot":"","sources":["../../../../src/components/charts/common/color-tag.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAS,MAAM,OAAO,CAAC;AAKlC,UAAU,aAAa;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;CAC7C;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA8EtC,CAAC"}
@@ -62,6 +62,7 @@ export const ColorTag = ({ label, color, className, small, dashed, outlineColor,
62
62
  borderColor: strokeColor !== null && strokeColor !== void 0 ? strokeColor : color
63
63
  } : pattern === 'outline' ? {
64
64
  borderColor: outlineColor !== null && outlineColor !== void 0 ? outlineColor : color,
65
+ backgroundColor: color,
65
66
  borderRadius: radius
66
67
  } : {
67
68
  backgroundColor: color,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/charts/common/color-tag.tsx"],"sourcesContent":["import { FC, useId } from 'react';\nimport classNames from 'classnames';\nimport { BodyText, Stack } from '@servicetitan/design-system';\nimport * as Styles from './color-tag.module.less';\n\ninterface ColorTagProps {\n label: string;\n color: string;\n className?: string;\n colorTagClassName?: string;\n small?: boolean;\n dashed?: boolean;\n strokeColor?: string;\n outlineColor?: string;\n pattern?: 'solid' | 'striped' | 'outline';\n}\n\nexport const ColorTag: FC<ColorTagProps> = ({\n label,\n color,\n className,\n small,\n dashed,\n outlineColor,\n pattern,\n strokeColor,\n colorTagClassName,\n}) => {\n const uid = useId();\n const patternId = `pattern-${uid}`;\n\n const width = small ? 50 : 22;\n const height = small ? 10 : 25;\n const radius = small ? 0 : 3;\n\n return (\n <Stack alignItems=\"center\" className={className} gap={1}>\n {pattern === 'striped' ? (\n <svg\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={classNames(Styles.colorTag, colorTagClassName)}\n aria-hidden\n >\n <defs>\n <pattern\n id={patternId}\n patternUnits=\"userSpaceOnUse\"\n width=\"8\"\n height=\"8\"\n patternTransform=\"rotate(45)\"\n >\n <rect width=\"4\" height=\"8\" fill={color} opacity=\"0.08\" />\n <rect width=\"2\" height=\"8\" fill={color} />\n </pattern>\n </defs>\n <rect\n x=\"0\"\n y=\"0\"\n width={width}\n height={height}\n rx={radius}\n ry={radius}\n fill={`url(#${patternId})`}\n stroke={strokeColor ?? color}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n />\n </svg>\n ) : (\n <div\n className={classNames(\n Styles.colorTag,\n small && Styles.colorTagSmall,\n dashed && Styles.colorTagDashed,\n pattern === 'outline' && Styles.colorTagOutline,\n colorTagClassName\n )}\n style={\n dashed\n ? { borderColor: strokeColor ?? color }\n : pattern === 'outline'\n ? { borderColor: outlineColor ?? color, borderRadius: radius }\n : { backgroundColor: color, borderRadius: radius }\n }\n />\n )}\n <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>\n </Stack>\n );\n};\n"],"names":["useId","classNames","BodyText","Stack","Styles","ColorTag","label","color","className","small","dashed","outlineColor","pattern","strokeColor","colorTagClassName","uid","patternId","width","height","radius","alignItems","gap","svg","viewBox","colorTag","aria-hidden","defs","id","patternUnits","patternTransform","rect","fill","opacity","x","y","rx","ry","stroke","strokeWidth","vectorEffect","div","colorTagSmall","colorTagDashed","colorTagOutline","style","borderColor","borderRadius","backgroundColor","size"],"mappings":";AAAA,SAAaA,KAAK,QAAQ,QAAQ;AAClC,OAAOC,gBAAgB,aAAa;AACpC,SAASC,QAAQ,EAAEC,KAAK,QAAQ,8BAA8B;AAC9D,YAAYC,YAAY,0BAA0B;AAclD,OAAO,MAAMC,WAA8B,CAAC,EACxCC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,KAAK,EACLC,MAAM,EACNC,YAAY,EACZC,OAAO,EACPC,WAAW,EACXC,iBAAiB,EACpB;IACG,MAAMC,MAAMf;IACZ,MAAMgB,YAAY,CAAC,QAAQ,EAAED,KAAK;IAElC,MAAME,QAAQR,QAAQ,KAAK;IAC3B,MAAMS,SAAST,QAAQ,KAAK;IAC5B,MAAMU,SAASV,QAAQ,IAAI;IAE3B,qBACI,MAACN;QAAMiB,YAAW;QAASZ,WAAWA;QAAWa,KAAK;;YACjDT,YAAY,0BACT,MAACU;gBACGL,OAAOA;gBACPC,QAAQA;gBACRK,SAAS,CAAC,IAAI,EAAEN,MAAM,CAAC,EAAEC,QAAQ;gBACjCV,WAAWP,WAAWG,OAAOoB,QAAQ,EAAEV;gBACvCW,aAAW;;kCAEX,KAACC;kCACG,cAAA,MAACd;4BACGe,IAAIX;4BACJY,cAAa;4BACbX,OAAM;4BACNC,QAAO;4BACPW,kBAAiB;;8CAEjB,KAACC;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;oCAAOyB,SAAQ;;8CAChD,KAACF;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;;;;;kCAGzC,KAACuB;wBACGG,GAAE;wBACFC,GAAE;wBACFjB,OAAOA;wBACPC,QAAQA;wBACRiB,IAAIhB;wBACJiB,IAAIjB;wBACJY,MAAM,CAAC,KAAK,EAAEf,UAAU,CAAC,CAAC;wBAC1BqB,MAAM,EAAExB,wBAAAA,yBAAAA,cAAeN;wBACvB+B,aAAa;wBACbC,cAAa;;;+BAIrB,KAACC;gBACGhC,WAAWP,WACPG,OAAOoB,QAAQ,EACff,SAASL,OAAOqC,aAAa,EAC7B/B,UAAUN,OAAOsC,cAAc,EAC/B9B,YAAY,aAAaR,OAAOuC,eAAe,EAC/C7B;gBAEJ8B,OACIlC,SACM;oBAAEmC,WAAW,EAAEhC,wBAAAA,yBAAAA,cAAeN;gBAAM,IACpCK,YAAY,YACV;oBAAEiC,WAAW,EAAElC,yBAAAA,0BAAAA,eAAgBJ;oBAAOuC,cAAc3B;gBAAO,IAC3D;oBAAE4B,iBAAiBxC;oBAAOuC,cAAc3B;gBAAO;;0BAInE,KAACjB;gBAAS8C,MAAMvC,QAAQ,WAAW;0BAAUH;;;;AAGzD,EAAE"}
1
+ {"version":3,"sources":["../../../../src/components/charts/common/color-tag.tsx"],"sourcesContent":["import { FC, useId } from 'react';\nimport classNames from 'classnames';\nimport { BodyText, Stack } from '@servicetitan/design-system';\nimport * as Styles from './color-tag.module.less';\n\ninterface ColorTagProps {\n label: string;\n color: string;\n className?: string;\n colorTagClassName?: string;\n small?: boolean;\n dashed?: boolean;\n strokeColor?: string;\n outlineColor?: string;\n pattern?: 'solid' | 'striped' | 'outline';\n}\n\nexport const ColorTag: FC<ColorTagProps> = ({\n label,\n color,\n className,\n small,\n dashed,\n outlineColor,\n pattern,\n strokeColor,\n colorTagClassName,\n}) => {\n const uid = useId();\n const patternId = `pattern-${uid}`;\n\n const width = small ? 50 : 22;\n const height = small ? 10 : 25;\n const radius = small ? 0 : 3;\n\n return (\n <Stack alignItems=\"center\" className={className} gap={1}>\n {pattern === 'striped' ? (\n <svg\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={classNames(Styles.colorTag, colorTagClassName)}\n aria-hidden\n >\n <defs>\n <pattern\n id={patternId}\n patternUnits=\"userSpaceOnUse\"\n width=\"8\"\n height=\"8\"\n patternTransform=\"rotate(45)\"\n >\n <rect width=\"4\" height=\"8\" fill={color} opacity=\"0.08\" />\n <rect width=\"2\" height=\"8\" fill={color} />\n </pattern>\n </defs>\n <rect\n x=\"0\"\n y=\"0\"\n width={width}\n height={height}\n rx={radius}\n ry={radius}\n fill={`url(#${patternId})`}\n stroke={strokeColor ?? color}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n />\n </svg>\n ) : (\n <div\n className={classNames(\n Styles.colorTag,\n small && Styles.colorTagSmall,\n dashed && Styles.colorTagDashed,\n pattern === 'outline' && Styles.colorTagOutline,\n colorTagClassName\n )}\n style={\n dashed\n ? { borderColor: strokeColor ?? color }\n : pattern === 'outline'\n ? {\n borderColor: outlineColor ?? color,\n backgroundColor: color,\n borderRadius: radius,\n }\n : { backgroundColor: color, borderRadius: radius }\n }\n />\n )}\n <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>\n </Stack>\n );\n};\n"],"names":["useId","classNames","BodyText","Stack","Styles","ColorTag","label","color","className","small","dashed","outlineColor","pattern","strokeColor","colorTagClassName","uid","patternId","width","height","radius","alignItems","gap","svg","viewBox","colorTag","aria-hidden","defs","id","patternUnits","patternTransform","rect","fill","opacity","x","y","rx","ry","stroke","strokeWidth","vectorEffect","div","colorTagSmall","colorTagDashed","colorTagOutline","style","borderColor","backgroundColor","borderRadius","size"],"mappings":";AAAA,SAAaA,KAAK,QAAQ,QAAQ;AAClC,OAAOC,gBAAgB,aAAa;AACpC,SAASC,QAAQ,EAAEC,KAAK,QAAQ,8BAA8B;AAC9D,YAAYC,YAAY,0BAA0B;AAclD,OAAO,MAAMC,WAA8B,CAAC,EACxCC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,KAAK,EACLC,MAAM,EACNC,YAAY,EACZC,OAAO,EACPC,WAAW,EACXC,iBAAiB,EACpB;IACG,MAAMC,MAAMf;IACZ,MAAMgB,YAAY,CAAC,QAAQ,EAAED,KAAK;IAElC,MAAME,QAAQR,QAAQ,KAAK;IAC3B,MAAMS,SAAST,QAAQ,KAAK;IAC5B,MAAMU,SAASV,QAAQ,IAAI;IAE3B,qBACI,MAACN;QAAMiB,YAAW;QAASZ,WAAWA;QAAWa,KAAK;;YACjDT,YAAY,0BACT,MAACU;gBACGL,OAAOA;gBACPC,QAAQA;gBACRK,SAAS,CAAC,IAAI,EAAEN,MAAM,CAAC,EAAEC,QAAQ;gBACjCV,WAAWP,WAAWG,OAAOoB,QAAQ,EAAEV;gBACvCW,aAAW;;kCAEX,KAACC;kCACG,cAAA,MAACd;4BACGe,IAAIX;4BACJY,cAAa;4BACbX,OAAM;4BACNC,QAAO;4BACPW,kBAAiB;;8CAEjB,KAACC;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;oCAAOyB,SAAQ;;8CAChD,KAACF;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;;;;;kCAGzC,KAACuB;wBACGG,GAAE;wBACFC,GAAE;wBACFjB,OAAOA;wBACPC,QAAQA;wBACRiB,IAAIhB;wBACJiB,IAAIjB;wBACJY,MAAM,CAAC,KAAK,EAAEf,UAAU,CAAC,CAAC;wBAC1BqB,MAAM,EAAExB,wBAAAA,yBAAAA,cAAeN;wBACvB+B,aAAa;wBACbC,cAAa;;;+BAIrB,KAACC;gBACGhC,WAAWP,WACPG,OAAOoB,QAAQ,EACff,SAASL,OAAOqC,aAAa,EAC7B/B,UAAUN,OAAOsC,cAAc,EAC/B9B,YAAY,aAAaR,OAAOuC,eAAe,EAC/C7B;gBAEJ8B,OACIlC,SACM;oBAAEmC,WAAW,EAAEhC,wBAAAA,yBAAAA,cAAeN;gBAAM,IACpCK,YAAY,YACV;oBACIiC,WAAW,EAAElC,yBAAAA,0BAAAA,eAAgBJ;oBAC7BuC,iBAAiBvC;oBACjBwC,cAAc5B;gBAClB,IACA;oBAAE2B,iBAAiBvC;oBAAOwC,cAAc5B;gBAAO;;0BAInE,KAACjB;gBAAS8C,MAAMvC,QAAQ,WAAW;0BAAUH;;;;AAGzD,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"hover-popover.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2D,EAAE,EAAiB,MAAM,OAAO,CAAC;AAgBnG,eAAO,MAAM,YAAY,EAAE,EAuHzB,CAAC"}
1
+ {"version":3,"file":"hover-popover.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2D,EAAE,EAAE,MAAM,OAAO,CAAC;AAgBpF,eAAO,MAAM,YAAY,EAAE,EAkIzB,CAAC"}
@@ -38,24 +38,30 @@ export const HoverPopover = observer(()=>{
38
38
  display,
39
39
  popH
40
40
  ]);
41
- const popoverStyle = useMemo(()=>{
41
+ const { popoverStyle, arrowPosition } = useMemo(()=>{
42
42
  const posX = svgStore.periodX(hoveredIndex);
43
43
  const yHeights = metrics.filter((m)=>m.values[hoveredIndex] !== undefined).map((m)=>svgStore.periodY(m, hoveredIndex));
44
44
  const barHeight = yHeights.length ? stackedTotals ? yHeights.reduce((a, b)=>a + b, 0) : Math.max(...yHeights) : 0;
45
45
  const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));
46
46
  const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));
47
- const barTopPositionPx = barHeightPercent / 100 * CHART_HEIGHT_PX;
48
- const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);
49
- const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);
47
+ const barTopPx = barHeightPercent / 100 * CHART_HEIGHT_PX;
48
+ const idealTopPx = barTopPx + OFFSET_PX;
49
+ const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);
50
+ const popoverRepositioned = idealTopPx > maxTopPx;
51
+ const clampedTopPx = Math.min(idealTopPx, maxTopPx);
52
+ const topPercent = clampedTopPx / CHART_HEIGHT_PX * 100;
50
53
  return {
51
- top: `${barHeightPercent}%`,
52
- transform: `translateY(${popoverOffsetPx}px)`,
53
- position: 'absolute',
54
- ...isChartLeftSide ? {
55
- left: `${svgStore.fpx(posX + 2)}%`
56
- } : {
57
- right: `${svgStore.fpx(102 - posX)}%`
58
- }
54
+ popoverStyle: {
55
+ top: `${topPercent.toFixed(1)}%`,
56
+ transform: 'translateY(0)',
57
+ position: 'absolute',
58
+ ...isChartLeftSide ? {
59
+ left: `${svgStore.fpx(posX + 2)}%`
60
+ } : {
61
+ right: `${svgStore.fpx(102 - posX)}%`
62
+ }
63
+ },
64
+ arrowPosition: popoverRepositioned ? 'bottom' : 'top'
59
65
  };
60
66
  }, [
61
67
  svgStore,
@@ -68,6 +74,9 @@ export const HoverPopover = observer(()=>{
68
74
  if (hoveredIndex < 0 || hoveredIndex >= periods.length) {
69
75
  return null;
70
76
  }
77
+ if ((stackedTotals === null || stackedTotals === void 0 ? void 0 : stackedTotals[hoveredIndex]) === 0) {
78
+ return null;
79
+ }
71
80
  const period = periods[hoveredIndex];
72
81
  const partialWeek = !!period.partial;
73
82
  return /*#__PURE__*/ _jsxs("div", {
@@ -76,7 +85,7 @@ export const HoverPopover = observer(()=>{
76
85
  style: popoverStyle,
77
86
  children: [
78
87
  /*#__PURE__*/ _jsx("div", {
79
- className: classNames(Styles.arrow, isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight)
88
+ className: classNames(Styles.arrow, isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight, arrowPosition === 'bottom' && Styles.arrowBottom)
80
89
  }),
81
90
  /*#__PURE__*/ _jsx(Text, {
82
91
  size: "small",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC, CSSProperties } from 'react';\nimport classNames from 'classnames';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { BodyText } from '@servicetitan/design-system';\nimport { LineChartStore } from '../stores/line-chart.store';\nimport { SvgStore } from '../stores/svg.store';\nimport { getFormatter } from '../utils/formatters';\nimport { periodDateTitleFormatter } from '../utils/labels';\nimport * as Styles from './hover-popover.module.less';\nimport { ColorTag } from '../../common';\nimport { Text } from '@servicetitan/anvil2';\n\nconst CHART_HEIGHT_PX = 400;\nconst OFFSET_PX = 16;\n\nexport const HoverPopover: FC = observer(() => {\n const [\n {\n periods,\n resolution,\n hoveredIndex,\n metrics,\n display,\n formattedTotalAt,\n totalLabel,\n stackedTotals,\n },\n svgStore,\n ] = useDependencies(LineChartStore, SvgStore);\n const isChartLeftSide = hoveredIndex < periods.length / 2;\n\n const formatDateTitle = useMemo(() => periodDateTitleFormatter[resolution], [resolution]);\n const formatValue = useCallback(\n (title: string, value: number, isRight: boolean) =>\n getFormatter(isRight ? display.metricsRightFormat : display.metricsLeftFormat)(value) +\n ' ' +\n title,\n [display]\n );\n\n const popRef = useRef<HTMLDivElement | null>(null);\n const [popH, setPopH] = useState(0);\n\n useLayoutEffect(() => {\n if (!popRef.current) {\n return;\n }\n const rect = popRef.current.getBoundingClientRect();\n if (rect.height && Math.abs(rect.height - popH) > 0.5) {\n setPopH(rect.height);\n }\n }, [hoveredIndex, metrics, display, popH]);\n\n const popoverStyle = useMemo<CSSProperties>(() => {\n const posX = svgStore.periodX(hoveredIndex);\n\n const yHeights = metrics\n .filter(m => m.values[hoveredIndex] !== undefined)\n .map(m => svgStore.periodY(m, hoveredIndex));\n\n const barHeight = yHeights.length\n ? stackedTotals\n ? yHeights.reduce((a, b) => a + b, 0)\n : Math.max(...yHeights)\n : 0;\n const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));\n const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));\n\n const barTopPositionPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;\n const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);\n const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);\n\n return {\n top: `${barHeightPercent}%`,\n transform: `translateY(${popoverOffsetPx}px)`,\n position: 'absolute',\n ...(isChartLeftSide\n ? { left: `${svgStore.fpx(posX + 2)}%` }\n : { right: `${svgStore.fpx(102 - posX)}%` }),\n };\n }, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);\n\n if (hoveredIndex < 0 || hoveredIndex >= periods.length) {\n return null;\n }\n\n const period = periods[hoveredIndex]!;\n const partialWeek = !!period.partial;\n\n return (\n <div\n ref={popRef}\n className={classNames(Styles.popover, 'border border-radius-1 p-1')}\n style={popoverStyle}\n >\n <div\n className={classNames(\n Styles.arrow,\n isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight\n )}\n />\n <Text size=\"small\" variant=\"headline\" el=\"h6\">\n {stackedTotals\n ? `${formattedTotalAt(hoveredIndex)} ${totalLabel} | ${formatDateTitle(period)}`\n : formatDateTitle(period)}\n </Text>\n {partialWeek && (\n <BodyText size=\"xsmall\" subdued>\n Partial week\n </BodyText>\n )}\n {metrics.map(\n m =>\n m.values[hoveredIndex] !== undefined && (\n <ColorTag\n small\n label={formatValue(m.title, m.values[hoveredIndex], m.isRight)}\n color={m.color}\n key={m.title}\n className=\"m-t-1\"\n dashed={m.opts?.dashed}\n pattern={m.opts?.pattern}\n outlineColor={m.opts?.outlineColor}\n strokeColor={m.opts?.strokeColor}\n colorTagClassName={\n m.opts?.pattern === 'outline'\n ? Styles.colorTagOutlined\n : Styles.colorTag\n }\n />\n )\n )}\n </div>\n );\n});\n"],"names":["useCallback","useMemo","useRef","useLayoutEffect","useState","classNames","observer","useDependencies","BodyText","LineChartStore","SvgStore","getFormatter","periodDateTitleFormatter","Styles","ColorTag","Text","CHART_HEIGHT_PX","OFFSET_PX","HoverPopover","periods","resolution","hoveredIndex","metrics","display","formattedTotalAt","totalLabel","stackedTotals","svgStore","isChartLeftSide","length","formatDateTitle","formatValue","title","value","isRight","metricsRightFormat","metricsLeftFormat","popRef","popH","setPopH","current","rect","getBoundingClientRect","height","Math","abs","popoverStyle","posX","periodX","yHeights","filter","m","values","undefined","map","periodY","barHeight","reduce","a","b","max","barHeightPercentRaw","fpy","min","barHeightPercent","Number","barTopPositionPx","availableSpaceBelow","popoverOffsetPx","top","transform","position","left","fpx","right","period","partialWeek","partial","div","ref","className","popover","style","arrow","arrowLeft","arrowRight","size","variant","el","subdued","small","label","color","dashed","opts","pattern","outlineColor","strokeColor","colorTagClassName","colorTagOutlined","colorTag"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC } from 'react';\nimport classNames from 'classnames';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { BodyText } from '@servicetitan/design-system';\nimport { LineChartStore } from '../stores/line-chart.store';\nimport { SvgStore } from '../stores/svg.store';\nimport { getFormatter } from '../utils/formatters';\nimport { periodDateTitleFormatter } from '../utils/labels';\nimport * as Styles from './hover-popover.module.less';\nimport { ColorTag } from '../../common';\nimport { Text } from '@servicetitan/anvil2';\n\nconst CHART_HEIGHT_PX = 400;\nconst OFFSET_PX = 16;\n\nexport const HoverPopover: FC = observer(() => {\n const [\n {\n periods,\n resolution,\n hoveredIndex,\n metrics,\n display,\n formattedTotalAt,\n totalLabel,\n stackedTotals,\n },\n svgStore,\n ] = useDependencies(LineChartStore, SvgStore);\n const isChartLeftSide = hoveredIndex < periods.length / 2;\n\n const formatDateTitle = useMemo(() => periodDateTitleFormatter[resolution], [resolution]);\n const formatValue = useCallback(\n (title: string, value: number, isRight: boolean) =>\n getFormatter(isRight ? display.metricsRightFormat : display.metricsLeftFormat)(value) +\n ' ' +\n title,\n [display]\n );\n\n const popRef = useRef<HTMLDivElement | null>(null);\n const [popH, setPopH] = useState(0);\n\n useLayoutEffect(() => {\n if (!popRef.current) {\n return;\n }\n const rect = popRef.current.getBoundingClientRect();\n if (rect.height && Math.abs(rect.height - popH) > 0.5) {\n setPopH(rect.height);\n }\n }, [hoveredIndex, metrics, display, popH]);\n\n const { popoverStyle, arrowPosition } = useMemo(() => {\n const posX = svgStore.periodX(hoveredIndex);\n\n const yHeights = metrics\n .filter(m => m.values[hoveredIndex] !== undefined)\n .map(m => svgStore.periodY(m, hoveredIndex));\n\n const barHeight = yHeights.length\n ? stackedTotals\n ? yHeights.reduce((a, b) => a + b, 0)\n : Math.max(...yHeights)\n : 0;\n const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));\n const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));\n\n const barTopPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;\n const idealTopPx = barTopPx + OFFSET_PX;\n const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);\n const popoverRepositioned = idealTopPx > maxTopPx;\n const clampedTopPx = Math.min(idealTopPx, maxTopPx);\n const topPercent = (clampedTopPx / CHART_HEIGHT_PX) * 100;\n\n return {\n popoverStyle: {\n top: `${topPercent.toFixed(1)}%`,\n transform: 'translateY(0)',\n position: 'absolute' as const,\n ...(isChartLeftSide\n ? { left: `${svgStore.fpx(posX + 2)}%` }\n : { right: `${svgStore.fpx(102 - posX)}%` }),\n },\n arrowPosition: popoverRepositioned ? 'bottom' : 'top',\n };\n }, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);\n\n if (hoveredIndex < 0 || hoveredIndex >= periods.length) {\n return null;\n }\n\n if (stackedTotals?.[hoveredIndex] === 0) {\n return null;\n }\n\n const period = periods[hoveredIndex]!;\n const partialWeek = !!period.partial;\n\n return (\n <div\n ref={popRef}\n className={classNames(Styles.popover, 'border border-radius-1 p-1')}\n style={popoverStyle}\n >\n <div\n className={classNames(\n Styles.arrow,\n isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight,\n arrowPosition === 'bottom' && Styles.arrowBottom\n )}\n />\n <Text size=\"small\" variant=\"headline\" el=\"h6\">\n {stackedTotals\n ? `${formattedTotalAt(hoveredIndex)} ${totalLabel} | ${formatDateTitle(period)}`\n : formatDateTitle(period)}\n </Text>\n {partialWeek && (\n <BodyText size=\"xsmall\" subdued>\n Partial week\n </BodyText>\n )}\n {metrics.map(\n m =>\n m.values[hoveredIndex] !== undefined && (\n <ColorTag\n small\n label={formatValue(m.title, m.values[hoveredIndex], m.isRight)}\n color={m.color}\n key={m.title}\n className=\"m-t-1\"\n dashed={m.opts?.dashed}\n pattern={m.opts?.pattern}\n outlineColor={m.opts?.outlineColor}\n strokeColor={m.opts?.strokeColor}\n colorTagClassName={\n m.opts?.pattern === 'outline'\n ? Styles.colorTagOutlined\n : Styles.colorTag\n }\n />\n )\n )}\n </div>\n );\n});\n"],"names":["useCallback","useMemo","useRef","useLayoutEffect","useState","classNames","observer","useDependencies","BodyText","LineChartStore","SvgStore","getFormatter","periodDateTitleFormatter","Styles","ColorTag","Text","CHART_HEIGHT_PX","OFFSET_PX","HoverPopover","periods","resolution","hoveredIndex","metrics","display","formattedTotalAt","totalLabel","stackedTotals","svgStore","isChartLeftSide","length","formatDateTitle","formatValue","title","value","isRight","metricsRightFormat","metricsLeftFormat","popRef","popH","setPopH","current","rect","getBoundingClientRect","height","Math","abs","popoverStyle","arrowPosition","posX","periodX","yHeights","filter","m","values","undefined","map","periodY","barHeight","reduce","a","b","max","barHeightPercentRaw","fpy","min","barHeightPercent","Number","barTopPx","idealTopPx","maxTopPx","popoverRepositioned","clampedTopPx","topPercent","top","toFixed","transform","position","left","fpx","right","period","partialWeek","partial","div","ref","className","popover","style","arrow","arrowLeft","arrowRight","arrowBottom","size","variant","el","subdued","small","label","color","dashed","opts","pattern","outlineColor","strokeColor","colorTagClassName","colorTagOutlined","colorTag"],"mappings":"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"}
@@ -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 +1 @@
1
- {"version":3,"file":"svg-bars.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAuMpC,CAAC;AAEF,UAAU,iBAAiB;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAyB7C,CAAC"}
1
+ {"version":3,"file":"svg-bars.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAsLpC,CAAC;AAEF,UAAU,iBAAiB;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAyB7C,CAAC"}
@@ -55,23 +55,13 @@ export const SvgBars = observer(({ metrics, isStackedBarChart, isGroupedBarChart
55
55
  };
56
56
  });
57
57
  if (isStackedBarChart) {
58
- // Use ORIGINAL calculations - keep all spacing/positioning unchanged
59
58
  const spacingBetweenSegments = 1;
60
- const totalSpacing = (values.length - 1) * spacingBetweenSegments;
61
- let stackedBarHeight = values.reduce((sum, curr)=>sum + curr.val, 0) + totalSpacing;
62
- // Find first/last non-zero indices for visual styling
59
+ const totalValue = values.reduce((sum, curr)=>sum + curr.value, 0);
60
+ const totalYValue = values.reduce((sum, curr)=>sum + curr.val, 0);
63
61
  const firstNonZeroIdx = values.findIndex((v)=>v.value > 0);
64
62
  const lastNonZeroIdx = values.reduce((last, v, idx)=>v.value > 0 ? idx : last, -1);
65
- // Count 0-value segments below first non-zero (for text position adjustment)
66
- const zeroSegmentsBelowFirst = firstNonZeroIdx >= 0 ? values.slice(firstNonZeroIdx + 1).filter((v)=>v.value <= 0).length : 0;
67
- const totalValue = values.reduce((sum, curr)=>sum + curr.value, 0);
68
63
  if (totalValue > 0) {
69
- /*
70
- * Adjust text position to maintain consistent gap with first rendered segment:
71
- * 1. Subtract spacing for skipped segments from fpy argument
72
- * 2. Add pixel offset for zeros below first non-zero
73
- */ const textStackedBarHeight = stackedBarHeight - (firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;
74
- const yTop = +fpy(textStackedBarHeight) + zeroSegmentsBelowFirst * spacingBetweenSegments;
64
+ const yTop = +fpy(totalYValue) - 2;
75
65
  const scaleX = 0.3;
76
66
  const scaleY = 1;
77
67
  paths.push(/*#__PURE__*/ _jsx("g", {
@@ -93,21 +83,25 @@ export const SvgBars = observer(({ metrics, isStackedBarChart, isGroupedBarChart
93
83
  })
94
84
  }, `total-${i}`));
95
85
  }
86
+ let stackedBarHeight = totalYValue;
87
+ let isFirstRendered = true;
88
+ const nonZeroCount = values.filter((v)=>v.value > 0).length;
89
+ const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;
96
90
  for(let j = 0; j < values.length; j++){
97
91
  var _value_strokeColor;
98
92
  const value = values[j];
99
- stackedBarHeight -= spacingBetweenSegments;
100
- const TOP_RADIUS = 1;
101
- const xLeft = +fpx(x - barWidth / 2);
102
- const width = +fpx(barWidth);
103
93
  if (value.value <= 0) {
104
- stackedBarHeight -= value.val;
105
94
  continue;
106
95
  }
107
- const zeroSegments = values.slice(j + 1).filter((v)=>v.value <= 0).length;
108
- // Adjust yTop: move down by the space 0-value segments would occupy
109
- const yTop = +fpy(stackedBarHeight) + (values.length - 2) + zeroSegments * spacingBetweenSegments;
110
- const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);
96
+ if (!isFirstRendered) {
97
+ stackedBarHeight -= spacingBetweenSegments;
98
+ }
99
+ isFirstRendered = false;
100
+ const TOP_RADIUS = 1;
101
+ const xLeft = +fpx(x - barWidth / 2);
102
+ const width = +fpx(barWidth);
103
+ const yTop = +fpy(stackedBarHeight);
104
+ const height = j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);
111
105
  const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;
112
106
  const d = [
113
107
  `M ${xLeft} ${yTop + height}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"sourcesContent":["import { FC } from 'react';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { ChartMetric } from '../utils/internal-interfaces';\nimport { keyVal } from '../utils/key';\nimport { SvgStore } from '../stores/svg.store';\n\ninterface SvgBarsProps {\n metrics: ChartMetric[];\n isStackedBarChart?: boolean;\n isGroupedBarChart?: boolean;\n}\n\nexport const SvgBars: FC<SvgBarsProps> = observer(\n ({ metrics, isStackedBarChart, isGroupedBarChart }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n const patternDefs = metrics\n .filter(m => m.opts?.pattern === 'striped')\n .map(m => {\n const rotation = 20;\n const tileW = 0.6;\n const tileH = 0.6;\n const stripeWidth = Math.max(0.1, Math.floor(tileW / 20));\n const tintOpacity = 0.06;\n\n return (\n <pattern\n key={`pattern-${m.id}`}\n id={`stripe-pattern-${m.id}`}\n patternUnits=\"userSpaceOnUse\"\n width={tileW}\n height={tileH}\n patternTransform={`rotate(${rotation})`}\n >\n <rect width={tileW} height={tileH} fill={m.color} opacity={tintOpacity} />\n <rect width={stripeWidth} height={tileH} fill={m.color} />\n </pattern>\n );\n });\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n const values = metrics.map(m => ({\n id: m.id,\n color: m.valuesOpts?.[i]?.color ?? m.color,\n opacity: m.opacity,\n val: store.periodY(m, i),\n pattern: m.opts?.pattern,\n strokeColor: m.opts?.strokeColor,\n outlineColor: m.opts?.outlineColor,\n value: m.values[i],\n }));\n\n if (isStackedBarChart) {\n // Use ORIGINAL calculations - keep all spacing/positioning unchanged\n const spacingBetweenSegments = 1;\n const totalSpacing = (values.length - 1) * spacingBetweenSegments;\n let stackedBarHeight =\n values.reduce((sum, curr) => sum + curr.val, 0) + totalSpacing;\n\n // Find first/last non-zero indices for visual styling\n const firstNonZeroIdx = values.findIndex(v => v.value > 0);\n const lastNonZeroIdx = values.reduce(\n (last, v, idx) => (v.value > 0 ? idx : last),\n -1\n );\n\n // Count 0-value segments below first non-zero (for text position adjustment)\n const zeroSegmentsBelowFirst =\n firstNonZeroIdx >= 0\n ? values.slice(firstNonZeroIdx + 1).filter(v => v.value <= 0).length\n : 0;\n\n const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);\n if (totalValue > 0) {\n /*\n * Adjust text position to maintain consistent gap with first rendered segment:\n * 1. Subtract spacing for skipped segments from fpy argument\n * 2. Add pixel offset for zeros below first non-zero\n */\n const textStackedBarHeight =\n stackedBarHeight -\n (firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;\n const yTop =\n +fpy(textStackedBarHeight) +\n zeroSegmentsBelowFirst * spacingBetweenSegments;\n const scaleX = 0.3;\n const scaleY = 1;\n\n paths.push(\n <g\n key={`total-${i}`}\n transform={`translate(${x},${yTop}) scale(${scaleX},${scaleY})`}\n pointerEvents=\"none\"\n >\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n dominantBaseline=\"alphabetic\"\n fontSize=\"2.5\"\n fontWeight={600}\n fill=\"#111827\"\n stroke=\"white\"\n strokeWidth={0.8}\n paintOrder=\"stroke\"\n fontFamily=\"Nunito Sans\"\n >\n {Math.round(totalValue)}\n </text>\n </g>\n );\n }\n\n for (let j = 0; j < values.length; j++) {\n const value = values[j];\n stackedBarHeight -= spacingBetweenSegments;\n\n const TOP_RADIUS = 1;\n const xLeft = +fpx(x - barWidth / 2);\n const width = +fpx(barWidth);\n\n if (value.value <= 0) {\n stackedBarHeight -= value.val;\n continue;\n }\n const zeroSegments = values.slice(j + 1).filter(v => v.value <= 0).length;\n\n // Adjust yTop: move down by the space 0-value segments would occupy\n const yTop =\n +fpy(stackedBarHeight) +\n (values.length - 2) +\n zeroSegments * spacingBetweenSegments;\n const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);\n const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;\n\n const d = [\n `M ${xLeft} ${yTop + height}`, // bottom-left\n `L ${xLeft} ${yTop + r}`, // up left edge\n `Q ${xLeft} ${yTop} ${xLeft + r / 2} ${yTop}`, // top-left corner\n `L ${xLeft + width - r / 2} ${yTop}`, // across top\n `Q ${xLeft + width} ${yTop} ${xLeft + width} ${yTop + r}`, // top-right corner\n `L ${xLeft + width} ${yTop + height}`, // down right edge\n 'Z',\n ].join(' ');\n paths.push(\n <path\n key={keyVal(value.id, i)}\n d={d}\n fill={\n value.pattern === 'striped'\n ? `url(#stripe-pattern-${value.id})`\n : value.color\n }\n stroke={\n value.pattern === 'outline'\n ? value.outlineColor\n : (value.strokeColor ?? value.color)\n }\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n strokeLinejoin=\"round\"\n />\n );\n\n stackedBarHeight -= value.val;\n }\n } else if (isGroupedBarChart) {\n for (let j = 0; j < values.length; j++) {\n const groupedBarX = (j * barWidth) / values.length;\n const value = values[j];\n\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={x + groupedBarX - barWidthHalf}\n y={fpy(value.val)}\n width={barWidth / values.length - 0.1}\n height={fpx(value.val)}\n fill={value.color}\n opacity={value.opacity}\n />\n );\n }\n } else {\n values.sort((a, b) => b.val - a.val);\n for (const value of values) {\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(value.val)}\n width={fpx(barWidth)}\n height={fpx(value.val)}\n fill={value.color}\n />\n );\n }\n }\n }\n\n return (\n <g>\n {patternDefs.length > 0 && <defs>{patternDefs}</defs>}\n {paths}\n </g>\n );\n }\n);\n\ninterface SvgBarsHoverProps {\n onHover(ind: number): void;\n onLeave(ind: number): void;\n}\n\nexport const SvgBarsHover: FC<SvgBarsHoverProps> = observer(({ onHover, onLeave }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n\n paths.push(\n <rect\n key={keyVal('_', i)}\n onMouseEnter={() => onHover(i)}\n onMouseLeave={() => onLeave(i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(100)}\n width={fpx(barWidth)}\n height=\"100%\"\n fill=\"white\"\n fillOpacity=\"0\"\n />\n );\n }\n\n return <g>{paths}</g>;\n});\n"],"names":["observer","useDependencies","keyVal","SvgStore","SvgBars","metrics","isStackedBarChart","isGroupedBarChart","store","fpx","fpy","barWidth","length","barWidthHalf","paths","patternDefs","filter","m","opts","pattern","map","rotation","tileW","tileH","stripeWidth","Math","max","floor","tintOpacity","id","patternUnits","width","height","patternTransform","rect","fill","color","opacity","i","x","periodX","values","valuesOpts","val","periodY","strokeColor","outlineColor","value","spacingBetweenSegments","totalSpacing","stackedBarHeight","reduce","sum","curr","firstNonZeroIdx","findIndex","v","lastNonZeroIdx","last","idx","zeroSegmentsBelowFirst","slice","totalValue","textStackedBarHeight","yTop","scaleX","scaleY","push","g","transform","pointerEvents","text","y","textAnchor","dominantBaseline","fontSize","fontWeight","stroke","strokeWidth","paintOrder","fontFamily","round","j","TOP_RADIUS","xLeft","zeroSegments","r","d","join","path","vectorEffect","strokeLinejoin","groupedBarX","sort","a","b","defs","SvgBarsHover","onHover","onLeave","onMouseEnter","onMouseLeave","fillOpacity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"sourcesContent":["import { FC } from 'react';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { ChartMetric } from '../utils/internal-interfaces';\nimport { keyVal } from '../utils/key';\nimport { SvgStore } from '../stores/svg.store';\n\ninterface SvgBarsProps {\n metrics: ChartMetric[];\n isStackedBarChart?: boolean;\n isGroupedBarChart?: boolean;\n}\n\nexport const SvgBars: FC<SvgBarsProps> = observer(\n ({ metrics, isStackedBarChart, isGroupedBarChart }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n const patternDefs = metrics\n .filter(m => m.opts?.pattern === 'striped')\n .map(m => {\n const rotation = 20;\n const tileW = 0.6;\n const tileH = 0.6;\n const stripeWidth = Math.max(0.1, Math.floor(tileW / 20));\n const tintOpacity = 0.06;\n\n return (\n <pattern\n key={`pattern-${m.id}`}\n id={`stripe-pattern-${m.id}`}\n patternUnits=\"userSpaceOnUse\"\n width={tileW}\n height={tileH}\n patternTransform={`rotate(${rotation})`}\n >\n <rect width={tileW} height={tileH} fill={m.color} opacity={tintOpacity} />\n <rect width={stripeWidth} height={tileH} fill={m.color} />\n </pattern>\n );\n });\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n const values = metrics.map(m => ({\n id: m.id,\n color: m.valuesOpts?.[i]?.color ?? m.color,\n opacity: m.opacity,\n val: store.periodY(m, i),\n pattern: m.opts?.pattern,\n strokeColor: m.opts?.strokeColor,\n outlineColor: m.opts?.outlineColor,\n value: m.values[i],\n }));\n\n if (isStackedBarChart) {\n const spacingBetweenSegments = 1;\n const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);\n const totalYValue = values.reduce((sum, curr) => sum + curr.val, 0);\n\n const firstNonZeroIdx = values.findIndex(v => v.value > 0);\n const lastNonZeroIdx = values.reduce(\n (last, v, idx) => (v.value > 0 ? idx : last),\n -1\n );\n\n if (totalValue > 0) {\n const yTop = +fpy(totalYValue) - 2;\n const scaleX = 0.3;\n const scaleY = 1;\n\n paths.push(\n <g\n key={`total-${i}`}\n transform={`translate(${x},${yTop}) scale(${scaleX},${scaleY})`}\n pointerEvents=\"none\"\n >\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n dominantBaseline=\"alphabetic\"\n fontSize=\"2.5\"\n fontWeight={600}\n fill=\"#111827\"\n stroke=\"white\"\n strokeWidth={0.8}\n paintOrder=\"stroke\"\n fontFamily=\"Nunito Sans\"\n >\n {Math.round(totalValue)}\n </text>\n </g>\n );\n }\n\n let stackedBarHeight = totalYValue;\n let isFirstRendered = true;\n const nonZeroCount = values.filter(v => v.value > 0).length;\n const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;\n\n for (let j = 0; j < values.length; j++) {\n const value = values[j];\n\n if (value.value <= 0) {\n continue;\n }\n\n if (!isFirstRendered) {\n stackedBarHeight -= spacingBetweenSegments;\n }\n isFirstRendered = false;\n\n const TOP_RADIUS = 1;\n const xLeft = +fpx(x - barWidth / 2);\n const width = +fpx(barWidth);\n const yTop = +fpy(stackedBarHeight);\n const height =\n j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);\n const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;\n\n const d = [\n `M ${xLeft} ${yTop + height}`, // bottom-left\n `L ${xLeft} ${yTop + r}`, // up left edge\n `Q ${xLeft} ${yTop} ${xLeft + r / 2} ${yTop}`, // top-left corner\n `L ${xLeft + width - r / 2} ${yTop}`, // across top\n `Q ${xLeft + width} ${yTop} ${xLeft + width} ${yTop + r}`, // top-right corner\n `L ${xLeft + width} ${yTop + height}`, // down right edge\n 'Z',\n ].join(' ');\n paths.push(\n <path\n key={keyVal(value.id, i)}\n d={d}\n fill={\n value.pattern === 'striped'\n ? `url(#stripe-pattern-${value.id})`\n : value.color\n }\n stroke={\n value.pattern === 'outline'\n ? value.outlineColor\n : (value.strokeColor ?? value.color)\n }\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n strokeLinejoin=\"round\"\n />\n );\n\n stackedBarHeight -= value.val;\n }\n } else if (isGroupedBarChart) {\n for (let j = 0; j < values.length; j++) {\n const groupedBarX = (j * barWidth) / values.length;\n const value = values[j];\n\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={x + groupedBarX - barWidthHalf}\n y={fpy(value.val)}\n width={barWidth / values.length - 0.1}\n height={fpx(value.val)}\n fill={value.color}\n opacity={value.opacity}\n />\n );\n }\n } else {\n values.sort((a, b) => b.val - a.val);\n for (const value of values) {\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(value.val)}\n width={fpx(barWidth)}\n height={fpx(value.val)}\n fill={value.color}\n />\n );\n }\n }\n }\n\n return (\n <g>\n {patternDefs.length > 0 && <defs>{patternDefs}</defs>}\n {paths}\n </g>\n );\n }\n);\n\ninterface SvgBarsHoverProps {\n onHover(ind: number): void;\n onLeave(ind: number): void;\n}\n\nexport const SvgBarsHover: FC<SvgBarsHoverProps> = observer(({ onHover, onLeave }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n\n paths.push(\n <rect\n key={keyVal('_', i)}\n onMouseEnter={() => onHover(i)}\n onMouseLeave={() => onLeave(i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(100)}\n width={fpx(barWidth)}\n height=\"100%\"\n fill=\"white\"\n fillOpacity=\"0\"\n />\n );\n }\n\n return <g>{paths}</g>;\n});\n"],"names":["observer","useDependencies","keyVal","SvgStore","SvgBars","metrics","isStackedBarChart","isGroupedBarChart","store","fpx","fpy","barWidth","length","barWidthHalf","paths","patternDefs","filter","m","opts","pattern","map","rotation","tileW","tileH","stripeWidth","Math","max","floor","tintOpacity","id","patternUnits","width","height","patternTransform","rect","fill","color","opacity","i","x","periodX","values","valuesOpts","val","periodY","strokeColor","outlineColor","value","spacingBetweenSegments","totalValue","reduce","sum","curr","totalYValue","firstNonZeroIdx","findIndex","v","lastNonZeroIdx","last","idx","yTop","scaleX","scaleY","push","g","transform","pointerEvents","text","y","textAnchor","dominantBaseline","fontSize","fontWeight","stroke","strokeWidth","paintOrder","fontFamily","round","stackedBarHeight","isFirstRendered","nonZeroCount","bottomTrim","j","TOP_RADIUS","xLeft","r","d","join","path","vectorEffect","strokeLinejoin","groupedBarX","sort","a","b","defs","SvgBarsHover","onHover","onLeave","onMouseEnter","onMouseLeave","fillOpacity"],"mappings":"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"}
@@ -28,6 +28,8 @@ export interface StatCardProps {
28
28
  valueOnly?: boolean;
29
29
  className?: string;
30
30
  diffPercentOnly?: boolean;
31
+ centered?: boolean;
32
+ loading?: boolean;
31
33
  }
32
34
  export declare const StatCard: FC<StatCardProps>;
33
35
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../../src/components/stat/stat-card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,SAAS,EAAY,MAAM,OAAO,CAAC;AAEhD,OAAO,EAEH,mBAAmB,EAKtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAe,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAmCtE,UAAU,aAAa;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC"}
1
+ {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../../src/components/stat/stat-card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAY,SAAS,EAAY,MAAM,OAAO,CAAC;AAE1D,OAAO,EAEH,mBAAmB,EAMtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAe,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAqCtE,UAAU,aAAa;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA6FtC,CAAC"}
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { Fragment, useState } from 'react';
3
3
  import classNames from 'classnames';
4
- import { BodyText, Eyebrow, Popover, Stack, Tooltip } from '@servicetitan/design-system';
4
+ import { BodyText, Eyebrow, Headline, Popover, Stack, Tooltip } from '@servicetitan/design-system';
5
5
  import * as Styles from './stat-card.module.less';
6
6
  import { formatValue } from '../../utils/formatters';
7
- import { Icon } from '@servicetitan/anvil2';
7
+ import { Icon, Skeleton } from '@servicetitan/anvil2';
8
8
  import TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';
9
9
  import TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';
10
+ import InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';
11
+ import { tokens } from '@servicetitan/tokens/core/index';
10
12
  const calculateDiff = (value, prev, percents)=>{
11
13
  const diff = (value - prev) * (percents ? 100 : 1);
12
14
  const absDiff = Math.abs(diff);
@@ -78,50 +80,83 @@ export const StatDiff = ({ value, prev, size, format, inverted, neutral, classNa
78
80
  ]
79
81
  });
80
82
  };
81
- export const StatCard = ({ title, description, popoverContent, value, percent, money, rate, prev, clean, inverted, neutral, fill, valueOnly, className, diffPercentOnly = false })=>{
83
+ export const StatCard = ({ title, description, popoverContent, value, percent, money, rate, prev, clean, inverted, neutral, fill, valueOnly, className, diffPercentOnly = false, centered, loading })=>{
82
84
  const [popoverShown, setPopoverShown] = useState(false);
83
85
  const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';
84
86
  const val = value === undefined ? '\u00A0' : formatValue(value, format);
87
+ const hasInfo = !!description || !!popoverContent;
85
88
  const eyebrow = /*#__PURE__*/ _jsx(Eyebrow, {
86
- className: classNames(Styles.title, 'ta-center'),
87
89
  "data-cy": `marketing-stat-${title}-title`,
88
- onMouseEnter: ()=>{
89
- setPopoverShown(true);
90
- },
91
90
  children: title
92
91
  });
93
- return /*#__PURE__*/ _jsxs(Stack, {
92
+ const infoIcon = /*#__PURE__*/ _jsx(Icon, {
93
+ svg: InfoSVG,
94
+ color: tokens.colorNeutral100
95
+ });
96
+ const infoContent = popoverContent ? /*#__PURE__*/ _jsx("div", {
97
+ className: Styles.infoIcon,
98
+ children: /*#__PURE__*/ _jsx(Popover, {
99
+ open: popoverShown,
100
+ trigger: infoIcon,
101
+ onMouseEnter: ()=>setPopoverShown(true),
102
+ children: popoverContent
103
+ })
104
+ }) : description ? /*#__PURE__*/ _jsx("div", {
105
+ className: Styles.infoIcon,
106
+ children: /*#__PURE__*/ _jsx(Tooltip, {
107
+ text: description,
108
+ "data-cy": `marketing-stat-${title}-tooltip`,
109
+ children: infoIcon
110
+ })
111
+ }) : null;
112
+ return /*#__PURE__*/ _jsx(Stack, {
94
113
  direction: "column",
95
- alignItems: "center",
96
- className: classNames('p-y-3', {
97
- 'bg-white border-radius-2 border': !clean,
114
+ className: classNames('p-2', {
115
+ 'bg-white border': !clean,
116
+ [Styles.card]: !clean,
98
117
  'flex-grow-1 flex-basis-0': fill
99
118
  }, className),
100
119
  onMouseLeave: ()=>setPopoverShown(false),
101
- children: [
102
- popoverContent ? /*#__PURE__*/ _jsx(Popover, {
103
- open: popoverShown,
104
- trigger: eyebrow,
105
- children: popoverContent
106
- }) : description ? /*#__PURE__*/ _jsx(Tooltip, {
107
- text: description,
108
- "data-cy": `marketing-stat-${title}-tooltip`,
109
- children: eyebrow
110
- }) : eyebrow,
111
- /*#__PURE__*/ _jsx(BodyText, {
112
- className: "fs-6-i ff-display",
113
- "data-cy": `marketing-stat-${title}-value`,
114
- children: val
115
- }),
116
- !valueOnly && /*#__PURE__*/ _jsx(StatDiff, {
117
- value: value,
118
- prev: prev,
119
- format: format,
120
- inverted: inverted,
121
- neutral: neutral,
122
- diffPercentOnly: diffPercentOnly
123
- })
124
- ]
120
+ children: /*#__PURE__*/ _jsxs("div", {
121
+ className: classNames('p-3', Styles.content),
122
+ children: [
123
+ /*#__PURE__*/ _jsxs("div", {
124
+ className: classNames(Styles.titleRow, {
125
+ [Styles.centered]: centered
126
+ }),
127
+ children: [
128
+ eyebrow,
129
+ hasInfo && infoContent
130
+ ]
131
+ }),
132
+ /*#__PURE__*/ _jsx("div", {
133
+ className: classNames(Styles.valueRow, {
134
+ [Styles.centered]: centered
135
+ }),
136
+ children: loading ? /*#__PURE__*/ _jsx(Skeleton.Text, {
137
+ variant: "headline",
138
+ className: "w-33"
139
+ }) : /*#__PURE__*/ _jsxs(Fragment, {
140
+ children: [
141
+ /*#__PURE__*/ _jsx(Headline, {
142
+ size: "xlarge",
143
+ className: "m-b-0-i",
144
+ "data-cy": `marketing-stat-${title}-value`,
145
+ children: val
146
+ }),
147
+ !valueOnly && /*#__PURE__*/ _jsx(StatDiff, {
148
+ value: value,
149
+ prev: prev,
150
+ format: format,
151
+ inverted: inverted,
152
+ neutral: neutral,
153
+ diffPercentOnly: diffPercentOnly
154
+ })
155
+ ]
156
+ })
157
+ })
158
+ ]
159
+ })
125
160
  });
126
161
  };
127
162
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/stat/stat-card.tsx"],"sourcesContent":["import { FC, ReactNode, useState } from 'react';\nimport classNames from 'classnames';\nimport {\n BodyText,\n BodyTextPropsStrict,\n Eyebrow,\n Popover,\n Stack,\n Tooltip,\n} from '@servicetitan/design-system';\nimport * as Styles from './stat-card.module.less';\nimport { formatValue, NumberFormatter } from '../../utils/formatters';\nimport { Icon } from '@servicetitan/anvil2';\nimport TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';\nimport TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';\n\nconst calculateDiff = (\n value: number,\n prev: number,\n percents?: boolean\n): [number, number, boolean] => {\n const diff = (value - prev) * (percents ? 100 : 1);\n const absDiff = Math.abs(diff);\n let diffPercent = 0;\n\n if (percents) {\n diffPercent = diff;\n } else if (absDiff) {\n diffPercent = prev ? (100 * absDiff) / prev : 100;\n }\n\n return [absDiff, diffPercent, diff >= 0];\n};\n\nconst formatDifference = (value: number, isPlus: boolean, format: NumberFormatter): string => {\n return (isPlus ? '+' : '-') + formatValue(value, format);\n};\n\nconst formatDifferencePercentage = (value: number, isPlus: boolean): string => {\n if (!value) {\n return '';\n }\n\n return (isPlus ? '+' : '-') + formatValue(value, 'percent-100');\n};\n\ninterface StatDiffProps {\n value?: number;\n prev?: number;\n size?: BodyTextPropsStrict['size'];\n format: NumberFormatter;\n inverted?: boolean;\n neutral?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatDiff: FC<StatDiffProps> = ({\n value,\n prev,\n size,\n format,\n inverted,\n neutral,\n className,\n diffPercentOnly = false,\n}) => {\n const percents = format === 'percent';\n const [absDiff, diffPercent, isIncrease] = calculateDiff(value ?? 0, prev ?? 0, percents);\n const diff =\n absDiff === 0 ? (\n ''\n ) : (\n <Icon\n svg={isIncrease ? TrendingUpSVG : TrendingDownSVG}\n color={\n neutral\n ? 'neutral-200'\n : inverted\n ? isIncrease\n ? 'red'\n : 'green'\n : isIncrease\n ? 'green'\n : 'red'\n }\n />\n );\n let text = '';\n\n if (percents) {\n text += formatDifferencePercentage(absDiff, isIncrease);\n } else {\n const diffPercentage = formatDifferencePercentage(diffPercent, isIncrease);\n\n if (diffPercentOnly) {\n text += `${diffPercentage}`;\n } else {\n text += `${formatDifference(absDiff, isIncrease, format)}`;\n\n if (diffPercent !== 0) {\n text += ` (${diffPercentage})`;\n }\n }\n }\n\n return (\n <Stack\n className={classNames(Styles.statDiff, className)}\n justifyContent=\"center\"\n alignItems=\"center\"\n >\n <Stack.Item className=\"m-r-half m-t-half\">\n <span>{diff}</span>\n </Stack.Item>\n <Stack.Item>\n <BodyText\n size={size ?? 'small'}\n data-cy=\"stat-diff-value\"\n className={classNames({\n 'c-red-500': !neutral && (inverted ? isIncrease : !isIncrease),\n 'c-green-500': !neutral && (inverted ? !isIncrease : isIncrease),\n 'c-neutral-200': !!neutral,\n })}\n >\n {value === undefined ? '\\u00A0' : text}\n </BodyText>\n </Stack.Item>\n </Stack>\n );\n};\n\nexport interface StatCardProps {\n title: string;\n description?: string;\n popoverContent?: ReactNode;\n value?: number;\n prev?: number;\n percent?: boolean;\n money?: boolean;\n rate?: boolean;\n clean?: boolean;\n inverted?: boolean;\n neutral?: boolean;\n fill?: boolean;\n valueOnly?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n title,\n description,\n popoverContent,\n value,\n percent,\n money,\n rate,\n prev,\n clean,\n inverted,\n neutral,\n fill,\n valueOnly,\n className,\n diffPercentOnly = false,\n}) => {\n const [popoverShown, setPopoverShown] = useState(false);\n const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';\n const val = value === undefined ? '\\u00A0' : formatValue(value, format);\n\n const eyebrow = (\n <Eyebrow\n className={classNames(Styles.title, 'ta-center')}\n data-cy={`marketing-stat-${title}-title`}\n onMouseEnter={() => {\n setPopoverShown(true);\n }}\n >\n {title}\n </Eyebrow>\n );\n\n return (\n <Stack\n direction=\"column\"\n alignItems=\"center\"\n className={classNames(\n 'p-y-3',\n {\n 'bg-white border-radius-2 border': !clean,\n 'flex-grow-1 flex-basis-0': fill,\n },\n className\n )}\n onMouseLeave={() => setPopoverShown(false)}\n >\n {popoverContent ? (\n <Popover open={popoverShown} trigger={eyebrow}>\n {popoverContent}\n </Popover>\n ) : description ? (\n <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>\n {eyebrow}\n </Tooltip>\n ) : (\n eyebrow\n )}\n <BodyText className=\"fs-6-i ff-display\" data-cy={`marketing-stat-${title}-value`}>\n {val}\n </BodyText>\n {!valueOnly && (\n <StatDiff\n value={value}\n prev={prev}\n format={format}\n inverted={inverted}\n neutral={neutral}\n diffPercentOnly={diffPercentOnly}\n />\n )}\n </Stack>\n );\n};\n"],"names":["useState","classNames","BodyText","Eyebrow","Popover","Stack","Tooltip","Styles","formatValue","Icon","TrendingUpSVG","TrendingDownSVG","calculateDiff","value","prev","percents","diff","absDiff","Math","abs","diffPercent","formatDifference","isPlus","format","formatDifferencePercentage","StatDiff","size","inverted","neutral","className","diffPercentOnly","isIncrease","svg","color","text","diffPercentage","statDiff","justifyContent","alignItems","Item","span","data-cy","undefined","StatCard","title","description","popoverContent","percent","money","rate","clean","fill","valueOnly","popoverShown","setPopoverShown","val","eyebrow","onMouseEnter","direction","onMouseLeave","open","trigger"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/stat/stat-card.tsx"],"sourcesContent":["import { FC, Fragment, ReactNode, useState } from 'react';\nimport classNames from 'classnames';\nimport {\n BodyText,\n BodyTextPropsStrict,\n Eyebrow,\n Headline,\n Popover,\n Stack,\n Tooltip,\n} from '@servicetitan/design-system';\nimport * as Styles from './stat-card.module.less';\nimport { formatValue, NumberFormatter } from '../../utils/formatters';\nimport { Icon, Skeleton } from '@servicetitan/anvil2';\nimport TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';\nimport TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';\nimport InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';\nimport { tokens } from '@servicetitan/tokens/core/index';\n\nconst calculateDiff = (\n value: number,\n prev: number,\n percents?: boolean\n): [number, number, boolean] => {\n const diff = (value - prev) * (percents ? 100 : 1);\n const absDiff = Math.abs(diff);\n let diffPercent = 0;\n\n if (percents) {\n diffPercent = diff;\n } else if (absDiff) {\n diffPercent = prev ? (100 * absDiff) / prev : 100;\n }\n\n return [absDiff, diffPercent, diff >= 0];\n};\n\nconst formatDifference = (value: number, isPlus: boolean, format: NumberFormatter): string => {\n return (isPlus ? '+' : '-') + formatValue(value, format);\n};\n\nconst formatDifferencePercentage = (value: number, isPlus: boolean): string => {\n if (!value) {\n return '';\n }\n\n return (isPlus ? '+' : '-') + formatValue(value, 'percent-100');\n};\n\ninterface StatDiffProps {\n value?: number;\n prev?: number;\n size?: BodyTextPropsStrict['size'];\n format: NumberFormatter;\n inverted?: boolean;\n neutral?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatDiff: FC<StatDiffProps> = ({\n value,\n prev,\n size,\n format,\n inverted,\n neutral,\n className,\n diffPercentOnly = false,\n}) => {\n const percents = format === 'percent';\n const [absDiff, diffPercent, isIncrease] = calculateDiff(value ?? 0, prev ?? 0, percents);\n const diff =\n absDiff === 0 ? (\n ''\n ) : (\n <Icon\n svg={isIncrease ? TrendingUpSVG : TrendingDownSVG}\n color={\n neutral\n ? 'neutral-200'\n : inverted\n ? isIncrease\n ? 'red'\n : 'green'\n : isIncrease\n ? 'green'\n : 'red'\n }\n />\n );\n let text = '';\n\n if (percents) {\n text += formatDifferencePercentage(absDiff, isIncrease);\n } else {\n const diffPercentage = formatDifferencePercentage(diffPercent, isIncrease);\n\n if (diffPercentOnly) {\n text += `${diffPercentage}`;\n } else {\n text += `${formatDifference(absDiff, isIncrease, format)}`;\n\n if (diffPercent !== 0) {\n text += ` (${diffPercentage})`;\n }\n }\n }\n\n return (\n <Stack\n className={classNames(Styles.statDiff, className)}\n justifyContent=\"center\"\n alignItems=\"center\"\n >\n <Stack.Item className=\"m-r-half m-t-half\">\n <span>{diff}</span>\n </Stack.Item>\n <Stack.Item>\n <BodyText\n size={size ?? 'small'}\n data-cy=\"stat-diff-value\"\n className={classNames({\n 'c-red-500': !neutral && (inverted ? isIncrease : !isIncrease),\n 'c-green-500': !neutral && (inverted ? !isIncrease : isIncrease),\n 'c-neutral-200': !!neutral,\n })}\n >\n {value === undefined ? '\\u00A0' : text}\n </BodyText>\n </Stack.Item>\n </Stack>\n );\n};\n\nexport interface StatCardProps {\n title: string;\n description?: string;\n popoverContent?: ReactNode;\n value?: number;\n prev?: number;\n percent?: boolean;\n money?: boolean;\n rate?: boolean;\n clean?: boolean;\n inverted?: boolean;\n neutral?: boolean;\n fill?: boolean;\n valueOnly?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n centered?: boolean;\n loading?: boolean;\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n title,\n description,\n popoverContent,\n value,\n percent,\n money,\n rate,\n prev,\n clean,\n inverted,\n neutral,\n fill,\n valueOnly,\n className,\n diffPercentOnly = false,\n centered,\n loading,\n}) => {\n const [popoverShown, setPopoverShown] = useState(false);\n const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';\n const val = value === undefined ? '\\u00A0' : formatValue(value, format);\n const hasInfo = !!description || !!popoverContent;\n\n const eyebrow = <Eyebrow data-cy={`marketing-stat-${title}-title`}>{title}</Eyebrow>;\n\n const infoIcon = <Icon svg={InfoSVG} color={tokens.colorNeutral100} />;\n\n const infoContent = popoverContent ? (\n <div className={Styles.infoIcon}>\n <Popover\n open={popoverShown}\n trigger={infoIcon}\n onMouseEnter={() => setPopoverShown(true)}\n >\n {popoverContent}\n </Popover>\n </div>\n ) : description ? (\n <div className={Styles.infoIcon}>\n <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>\n {infoIcon}\n </Tooltip>\n </div>\n ) : null;\n\n return (\n <Stack\n direction=\"column\"\n className={classNames(\n 'p-2',\n {\n 'bg-white border': !clean,\n [Styles.card]: !clean,\n 'flex-grow-1 flex-basis-0': fill,\n },\n className\n )}\n onMouseLeave={() => setPopoverShown(false)}\n >\n <div className={classNames('p-3', Styles.content)}>\n <div className={classNames(Styles.titleRow, { [Styles.centered]: centered })}>\n {eyebrow}\n {hasInfo && infoContent}\n </div>\n <div className={classNames(Styles.valueRow, { [Styles.centered]: centered })}>\n {loading ? (\n <Skeleton.Text variant=\"headline\" className=\"w-33\" />\n ) : (\n <Fragment>\n <Headline\n size=\"xlarge\"\n className=\"m-b-0-i\"\n data-cy={`marketing-stat-${title}-value`}\n >\n {val}\n </Headline>\n {!valueOnly && (\n <StatDiff\n value={value}\n prev={prev}\n format={format}\n inverted={inverted}\n neutral={neutral}\n diffPercentOnly={diffPercentOnly}\n />\n )}\n </Fragment>\n )}\n </div>\n </div>\n </Stack>\n );\n};\n"],"names":["Fragment","useState","classNames","BodyText","Eyebrow","Headline","Popover","Stack","Tooltip","Styles","formatValue","Icon","Skeleton","TrendingUpSVG","TrendingDownSVG","InfoSVG","tokens","calculateDiff","value","prev","percents","diff","absDiff","Math","abs","diffPercent","formatDifference","isPlus","format","formatDifferencePercentage","StatDiff","size","inverted","neutral","className","diffPercentOnly","isIncrease","svg","color","text","diffPercentage","statDiff","justifyContent","alignItems","Item","span","data-cy","undefined","StatCard","title","description","popoverContent","percent","money","rate","clean","fill","valueOnly","centered","loading","popoverShown","setPopoverShown","val","hasInfo","eyebrow","infoIcon","colorNeutral100","infoContent","div","open","trigger","onMouseEnter","direction","card","onMouseLeave","content","titleRow","valueRow","Text","variant"],"mappings":"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"}
@@ -11,6 +11,37 @@
11
11
  margin-bottom: 2px;
12
12
  }
13
13
 
14
- .title {
15
- border-bottom: 1px dashed @color-neutral-90;
14
+ .content {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 8px;
18
+ }
19
+
20
+ .title-row {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 8px;
24
+ }
25
+
26
+ .value-row {
27
+ display: flex;
28
+ flex-wrap: wrap;
29
+ align-items: flex-end;
30
+ gap: 8px;
31
+ min-height: 40px;
32
+ }
33
+
34
+ .info-icon {
35
+ display: flex;
36
+ align-items: center;
37
+ line-height: 0;
38
+ }
39
+
40
+ .centered {
41
+ justify-content: center;
42
+ }
43
+
44
+ .card {
45
+ border-radius: 12px;
46
+ min-width: min-content;
16
47
  }
@@ -1,5 +1,10 @@
1
1
  export const __esModule: true;
2
+ export const card: string;
3
+ export const centered: string;
4
+ export const content: string;
5
+ export const infoIcon: string;
2
6
  export const statDiff: string;
3
7
  export const statExtendedDiff: string;
4
- export const title: string;
8
+ export const titleRow: string;
9
+ export const valueRow: string;
5
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/marketing-ui",
3
- "version": "7.3.0",
3
+ "version": "7.5.0",
4
4
  "description": "Marketing UI component and utils",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,5 +53,5 @@
53
53
  "less": true,
54
54
  "webpack": false
55
55
  },
56
- "gitHead": "b013c9f39c05c44f888a5bca5ff6aadb89cd7a6f"
56
+ "gitHead": "eda155d4c358a9d29f685892a1e07dac8e9e526d"
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,37 @@
11
11
  margin-bottom: 2px;
12
12
  }
13
13
 
14
- .title {
15
- border-bottom: 1px dashed @color-neutral-90;
14
+ .content {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 8px;
18
+ }
19
+
20
+ .title-row {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 8px;
24
+ }
25
+
26
+ .value-row {
27
+ display: flex;
28
+ flex-wrap: wrap;
29
+ align-items: flex-end;
30
+ gap: 8px;
31
+ min-height: 40px;
32
+ }
33
+
34
+ .info-icon {
35
+ display: flex;
36
+ align-items: center;
37
+ line-height: 0;
38
+ }
39
+
40
+ .centered {
41
+ justify-content: center;
42
+ }
43
+
44
+ .card {
45
+ border-radius: 12px;
46
+ min-width: min-content;
16
47
  }
@@ -1,5 +1,10 @@
1
1
  export const __esModule: true;
2
+ export const card: string;
3
+ export const centered: string;
4
+ export const content: string;
5
+ export const infoIcon: string;
2
6
  export const statDiff: string;
3
7
  export const statExtendedDiff: string;
4
- export const title: string;
8
+ export const titleRow: string;
9
+ export const valueRow: string;
5
10
 
@@ -1,18 +1,21 @@
1
- import { FC, ReactNode, useState } from 'react';
1
+ import { FC, Fragment, ReactNode, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import {
4
4
  BodyText,
5
5
  BodyTextPropsStrict,
6
6
  Eyebrow,
7
+ Headline,
7
8
  Popover,
8
9
  Stack,
9
10
  Tooltip,
10
11
  } from '@servicetitan/design-system';
11
12
  import * as Styles from './stat-card.module.less';
12
13
  import { formatValue, NumberFormatter } from '../../utils/formatters';
13
- import { Icon } from '@servicetitan/anvil2';
14
+ import { Icon, Skeleton } 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,
@@ -146,6 +149,8 @@ export interface StatCardProps {
146
149
  valueOnly?: boolean;
147
150
  className?: string;
148
151
  diffPercentOnly?: boolean;
152
+ centered?: boolean;
153
+ loading?: boolean;
149
154
  }
150
155
 
151
156
  export const StatCard: FC<StatCardProps> = ({
@@ -164,61 +169,81 @@ export const StatCard: FC<StatCardProps> = ({
164
169
  valueOnly,
165
170
  className,
166
171
  diffPercentOnly = false,
172
+ centered,
173
+ loading,
167
174
  }) => {
168
175
  const [popoverShown, setPopoverShown] = useState(false);
169
176
  const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';
170
177
  const val = value === undefined ? '\u00A0' : formatValue(value, format);
171
-
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
- );
178
+ const hasInfo = !!description || !!popoverContent;
179
+
180
+ const eyebrow = <Eyebrow data-cy={`marketing-stat-${title}-title`}>{title}</Eyebrow>;
181
+
182
+ const infoIcon = <Icon svg={InfoSVG} color={tokens.colorNeutral100} />;
183
+
184
+ const infoContent = popoverContent ? (
185
+ <div className={Styles.infoIcon}>
186
+ <Popover
187
+ open={popoverShown}
188
+ trigger={infoIcon}
189
+ onMouseEnter={() => setPopoverShown(true)}
190
+ >
191
+ {popoverContent}
192
+ </Popover>
193
+ </div>
194
+ ) : description ? (
195
+ <div className={Styles.infoIcon}>
196
+ <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
197
+ {infoIcon}
198
+ </Tooltip>
199
+ </div>
200
+ ) : null;
183
201
 
184
202
  return (
185
203
  <Stack
186
204
  direction="column"
187
- alignItems="center"
188
205
  className={classNames(
189
- 'p-y-3',
206
+ 'p-2',
190
207
  {
191
- 'bg-white border-radius-2 border': !clean,
208
+ 'bg-white border': !clean,
209
+ [Styles.card]: !clean,
192
210
  'flex-grow-1 flex-basis-0': fill,
193
211
  },
194
212
  className
195
213
  )}
196
214
  onMouseLeave={() => setPopoverShown(false)}
197
215
  >
198
- {popoverContent ? (
199
- <Popover open={popoverShown} trigger={eyebrow}>
200
- {popoverContent}
201
- </Popover>
202
- ) : description ? (
203
- <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
216
+ <div className={classNames('p-3', Styles.content)}>
217
+ <div className={classNames(Styles.titleRow, { [Styles.centered]: centered })}>
204
218
  {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
- )}
219
+ {hasInfo && infoContent}
220
+ </div>
221
+ <div className={classNames(Styles.valueRow, { [Styles.centered]: centered })}>
222
+ {loading ? (
223
+ <Skeleton.Text variant="headline" className="w-33" />
224
+ ) : (
225
+ <Fragment>
226
+ <Headline
227
+ size="xlarge"
228
+ className="m-b-0-i"
229
+ data-cy={`marketing-stat-${title}-value`}
230
+ >
231
+ {val}
232
+ </Headline>
233
+ {!valueOnly && (
234
+ <StatDiff
235
+ value={value}
236
+ prev={prev}
237
+ format={format}
238
+ inverted={inverted}
239
+ neutral={neutral}
240
+ diffPercentOnly={diffPercentOnly}
241
+ />
242
+ )}
243
+ </Fragment>
244
+ )}
245
+ </div>
246
+ </div>
222
247
  </Stack>
223
248
  );
224
249
  };