@sproutsocial/seeds-react-numeral 1.0.50 → 1.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +20 -0
- package/dist/esm/index.js +82 -29
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +82 -29
- package/dist/index.js.map +1 -1
- package/dist/numeral.css +21 -0
- package/package.json +8 -8
- package/src/Numeral.stories.tsx +21 -0
- package/src/Numeral.tsx +3 -207
- package/src/NumeralHybrid.tsx +29 -0
- package/src/NumeralTailwind.tsx +60 -0
- package/src/__tests__/features/routing.test.tsx +48 -0
- package/src/css.d.ts +1 -0
- package/src/getNumeral.tsx +220 -0
- package/src/numeral.css +21 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
yarn run v1.22.22
|
|
2
|
-
$ tsup --dts
|
|
2
|
+
$ tsup --dts && cp src/numeral.css dist/numeral.css
|
|
3
3
|
[34mCLI[39m Building entry: src/index.ts
|
|
4
4
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
5
5
|
[34mCLI[39m tsup v8.5.0
|
|
@@ -8,14 +8,14 @@ $ tsup --dts
|
|
|
8
8
|
[34mCLI[39m Cleaning output folder
|
|
9
9
|
[34mCJS[39m Build start
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
12
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
13
|
-
[32mCJS[39m ⚡️ Build success in
|
|
14
|
-
[32mESM[39m [1mdist/esm/index.js [22m[
|
|
15
|
-
[32mESM[39m [1mdist/esm/index.js.map [22m[
|
|
16
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mCJS[39m [1mdist/index.js [22m[32m8.30 KB[39m
|
|
12
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m14.48 KB[39m
|
|
13
|
+
[32mCJS[39m ⚡️ Build success in 14ms
|
|
14
|
+
[32mESM[39m [1mdist/esm/index.js [22m[32m5.98 KB[39m
|
|
15
|
+
[32mESM[39m [1mdist/esm/index.js.map [22m[32m14.33 KB[39m
|
|
16
|
+
[32mESM[39m ⚡️ Build success in 14ms
|
|
17
17
|
[34mDTS[39m Build start
|
|
18
|
-
[32mDTS[39m ⚡️ Build success in
|
|
19
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.
|
|
20
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.
|
|
21
|
-
Done in 2.
|
|
18
|
+
[32mDTS[39m ⚡️ Build success in 1660ms
|
|
19
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.82 KB[39m
|
|
20
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.82 KB[39m
|
|
21
|
+
Done in 2.37s.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @sproutsocial/seeds-react-numeral
|
|
2
2
|
|
|
3
|
+
## 1.0.52
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [d21ae54]
|
|
8
|
+
- @sproutsocial/seeds-react-theme@4.2.1
|
|
9
|
+
- @sproutsocial/seeds-react-tooltip@1.1.31
|
|
10
|
+
- @sproutsocial/seeds-react-visually-hidden@1.0.26
|
|
11
|
+
|
|
12
|
+
## 1.0.51
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 6611f6a: Migrate Numeral to the hybrid Tailwind pattern: a pure CSS-class Tailwind implementation routed against styled-components via `needsStyledComponents`, with component-scoped classes in `numeral.css` under `@layer components` so Tailwind utilities reliably override them. `seeds-react-system-props` now also detects styled() extensions via the generated `sc-*` className so wrapped components take the styled-components path.
|
|
17
|
+
- Updated dependencies [6611f6a]
|
|
18
|
+
- @sproutsocial/seeds-react-system-props@3.1.2
|
|
19
|
+
- @sproutsocial/seeds-react-tooltip@1.1.30
|
|
20
|
+
- @sproutsocial/seeds-react-text@1.5.1
|
|
21
|
+
- @sproutsocial/seeds-react-visually-hidden@1.0.25
|
|
22
|
+
|
|
3
23
|
## 1.0.50
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/NumeralHybrid.tsx
|
|
2
|
+
import "react";
|
|
3
|
+
|
|
4
|
+
// src/getNumeral.tsx
|
|
2
5
|
import "react";
|
|
3
6
|
import memoize from "lru-memoize";
|
|
4
7
|
|
|
@@ -15,23 +18,9 @@ var DefaultPrecisions = {
|
|
|
15
18
|
};
|
|
16
19
|
var EM_DASH = "\u2014";
|
|
17
20
|
|
|
18
|
-
// src/
|
|
21
|
+
// src/getNumeral.tsx
|
|
19
22
|
import Tooltip from "@sproutsocial/seeds-react-tooltip";
|
|
20
23
|
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
21
|
-
|
|
22
|
-
// src/styles.ts
|
|
23
|
-
import styled from "styled-components";
|
|
24
|
-
import Text from "@sproutsocial/seeds-react-text";
|
|
25
|
-
var Container = styled(Text)`
|
|
26
|
-
font-variant-numeric: tabular-nums;
|
|
27
|
-
`;
|
|
28
|
-
var AbbrContainer = styled(Text)`
|
|
29
|
-
font-variant-numeric: tabular-nums;
|
|
30
|
-
border-bottom: 1px dotted
|
|
31
|
-
${(props) => props.theme.colors.container.border.base};
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
// src/Numeral.tsx
|
|
35
24
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
36
25
|
var _getNumberFormatters = (options) => {
|
|
37
26
|
const { locale, format, currency, min, max } = options;
|
|
@@ -110,17 +99,13 @@ var normalizeArgs = (props) => {
|
|
|
110
99
|
rest
|
|
111
100
|
};
|
|
112
101
|
};
|
|
113
|
-
var
|
|
114
|
-
returnType,
|
|
115
|
-
props
|
|
116
|
-
}) => {
|
|
117
|
-
const isReturnTypeString = returnType === "string";
|
|
102
|
+
var renderNumeral = (props, { Container: Container2, AbbrContainer: AbbrContainer2 }) => {
|
|
118
103
|
const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } = normalizeArgs(props);
|
|
119
104
|
if (!isValidNumber(props.number)) {
|
|
120
|
-
return
|
|
105
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
106
|
invalidNumberLabel && // Give screen readers something useful to read off + hide the em dash
|
|
122
107
|
/* @__PURE__ */ jsx(VisuallyHidden, { children: invalidNumberLabel }),
|
|
123
|
-
/* @__PURE__ */ jsx(
|
|
108
|
+
/* @__PURE__ */ jsx(Container2, { "aria-hidden": true, ...qa, children: EM_DASH })
|
|
124
109
|
] });
|
|
125
110
|
}
|
|
126
111
|
const formatters = getNumberFormatters(options);
|
|
@@ -128,18 +113,86 @@ var getNumeral = ({
|
|
|
128
113
|
if (canAbbreviate) {
|
|
129
114
|
const abbreviatedText = formatters.abbreviated.format(value);
|
|
130
115
|
if (abbreviatedText !== fullText) {
|
|
131
|
-
return
|
|
116
|
+
return /* @__PURE__ */ jsx(Tooltip, { content: fullText, children: /* @__PURE__ */ jsx(AbbrContainer2, { ...qa, ...rest, children: abbreviatedText }) });
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
|
-
return
|
|
119
|
+
return /* @__PURE__ */ jsx(Container2, { ...qa, ...rest, children: fullText });
|
|
135
120
|
};
|
|
136
121
|
var formatNumeral = (props) => {
|
|
137
|
-
|
|
122
|
+
const { value, canAbbreviate, options } = normalizeArgs(props);
|
|
123
|
+
if (!isValidNumber(props.number)) {
|
|
124
|
+
return EM_DASH;
|
|
125
|
+
}
|
|
126
|
+
const formatters = getNumberFormatters(options);
|
|
127
|
+
const fullText = formatters.standard.format(value);
|
|
128
|
+
if (canAbbreviate) {
|
|
129
|
+
const abbreviatedText = formatters.abbreviated.format(value);
|
|
130
|
+
if (abbreviatedText !== fullText) {
|
|
131
|
+
return abbreviatedText;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return fullText;
|
|
138
135
|
};
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
|
|
137
|
+
// src/styles.ts
|
|
138
|
+
import styled from "styled-components";
|
|
139
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
140
|
+
var Container = styled(Text)`
|
|
141
|
+
font-variant-numeric: tabular-nums;
|
|
142
|
+
`;
|
|
143
|
+
var AbbrContainer = styled(Text)`
|
|
144
|
+
font-variant-numeric: tabular-nums;
|
|
145
|
+
border-bottom: 1px dotted
|
|
146
|
+
${(props) => props.theme.colors.container.border.base};
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
// src/NumeralTailwind.tsx
|
|
150
|
+
import "react";
|
|
151
|
+
import Text2 from "@sproutsocial/seeds-react-text";
|
|
152
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
153
|
+
function cn(...inputs) {
|
|
154
|
+
const classes = [];
|
|
155
|
+
for (const input of inputs) {
|
|
156
|
+
if (!input) continue;
|
|
157
|
+
if (typeof input === "string") {
|
|
158
|
+
classes.push(input);
|
|
159
|
+
} else if (typeof input === "object") {
|
|
160
|
+
for (const [key, value] of Object.entries(input)) {
|
|
161
|
+
if (value) {
|
|
162
|
+
classes.push(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return classes.join(" ");
|
|
168
|
+
}
|
|
169
|
+
var TwContainer = ({ className, children, ...rest }) => /* @__PURE__ */ jsx2(Text2, { className: cn("seeds-numeral", className), ...rest, children });
|
|
170
|
+
var TwAbbrContainer = ({ className, children, ...rest }) => /* @__PURE__ */ jsx2(
|
|
171
|
+
Text2,
|
|
172
|
+
{
|
|
173
|
+
className: cn("seeds-numeral", "seeds-numeral-abbr", className),
|
|
174
|
+
...rest,
|
|
175
|
+
children
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
var NumeralTailwind = (props) => renderNumeral(props, {
|
|
179
|
+
Container: TwContainer,
|
|
180
|
+
AbbrContainer: TwAbbrContainer
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// src/NumeralHybrid.tsx
|
|
184
|
+
import { needsStyledComponents } from "@sproutsocial/seeds-react-system-props";
|
|
185
|
+
var NumeralHybrid = (props) => {
|
|
186
|
+
if (needsStyledComponents(props)) {
|
|
187
|
+
return renderNumeral(props, { Container, AbbrContainer });
|
|
188
|
+
}
|
|
189
|
+
return NumeralTailwind(props);
|
|
141
190
|
};
|
|
142
|
-
|
|
191
|
+
NumeralHybrid.displayName = "Numeral";
|
|
192
|
+
var NumeralHybrid_default = NumeralHybrid;
|
|
193
|
+
|
|
194
|
+
// src/Numeral.tsx
|
|
195
|
+
var Numeral_default = NumeralHybrid_default;
|
|
143
196
|
|
|
144
197
|
// src/index.ts
|
|
145
198
|
var index_default = Numeral_default;
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Numeral.tsx","../../src/constants.ts","../../src/styles.ts","../../src/index.ts"],"sourcesContent":["import * as React from \"react\";\n// @ts-expect-error lru-memoize is not typed\nimport memoize from \"lru-memoize\";\nimport { EM_DASH } from \"./constants\";\nimport Tooltip from \"@sproutsocial/seeds-react-tooltip\";\nimport type { TypeTextProps } from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\n\nimport {\n DEFAULT_THRESHOLD,\n MEMO_CACHE_SIZE,\n COMPARE_OBJECTS,\n MAX_PRECISION,\n ABBREV_PRECISION,\n DefaultPrecisions,\n} from \"./constants\";\nimport { AbbrContainer, Container } from \"./styles\";\nimport type { EnumNumeralFormat, TypeNumeralProps } from \"./NumeralTypes\";\n\ninterface TypeFormatOptions {\n locale: string;\n format: EnumNumeralFormat;\n currency: string;\n min: number;\n max: number;\n}\n\ninterface TypeFormatters {\n standard: Intl.NumberFormat;\n abbreviated: Intl.NumberFormat;\n}\n\ninterface TypeArgs {\n value: number;\n canAbbreviate: boolean;\n invalidNumberLabel?: string;\n ariaLabel?: string;\n options: TypeFormatOptions;\n qa: object | null | undefined;\n rest: Omit<TypeTextProps, \"children\">;\n}\n\nconst _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {\n const { locale, format, currency, min, max } = options;\n const compactPrecision = min === max ? min : ABBREV_PRECISION;\n\n const _currency = format === \"currency\" ? currency : undefined;\n\n const standard: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: min,\n maximumFractionDigits: max,\n currency: _currency,\n };\n const compact: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: compactPrecision,\n maximumFractionDigits: compactPrecision,\n currency: _currency,\n notation: \"compact\",\n };\n // Safari 14.1 is currently throwing errors when trying to use the compact\n // options of NumberFormat\n // https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687\n let abbreviated;\n\n try {\n abbreviated = new Intl.NumberFormat(locale, compact);\n } catch (error) {\n abbreviated = new Intl.NumberFormat(locale, standard);\n }\n\n return {\n standard: new Intl.NumberFormat(locale, standard),\n abbreviated,\n };\n};\n\n// Memoize to reduce the energy of creating new instances of Intl.NumberFormat\nconst memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);\nconst getNumberFormatters = memoizer(_getNumberFormatters);\n\nconst getThreshold = (abbreviate: boolean | number): number => {\n if (typeof abbreviate === \"number\")\n return Math.max(1000, Math.abs(abbreviate));\n if (abbreviate) return DEFAULT_THRESHOLD;\n return Infinity;\n};\n\nconst getMinMaxPrecision = (\n precision: TypeNumeralProps[\"precision\"],\n format: EnumNumeralFormat\n): [number, number] => {\n if (typeof precision === \"number\") return [precision, precision];\n if (precision === \"none\") return [0, MAX_PRECISION];\n return DefaultPrecisions[format];\n};\n\nconst isValidNumber = (value: unknown): boolean => {\n return typeof value === \"number\" && isFinite(value);\n};\n\nconst normalizeArgs = (props: TypeNumeralProps): TypeArgs => {\n const {\n number,\n locale = \"us-EN\",\n format = props.currency ? \"currency\" : \"decimal\",\n currency = \"USD\",\n abbreviate = true,\n invalidNumberLabel,\n precision,\n qa,\n ...rest\n } = props;\n const threshold = getThreshold(abbreviate);\n const [min, max] = getMinMaxPrecision(precision, format);\n\n const _number = number || 0;\n\n const value = _number * (format === \"percent\" ? 0.01 : 1);\n const canAbbreviate = Math.abs(_number) >= threshold;\n const options = {\n locale,\n format,\n currency,\n min,\n max,\n };\n\n return {\n value,\n canAbbreviate,\n invalidNumberLabel,\n options,\n qa,\n rest,\n };\n};\n\nconst getNumeral = ({\n returnType,\n props,\n}: {\n returnType: \"string\" | \"component\";\n props: TypeNumeralProps;\n}): string | React.ReactNode => {\n const isReturnTypeString = returnType === \"string\";\n const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =\n normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return isReturnTypeString ? (\n EM_DASH\n ) : (\n <>\n {invalidNumberLabel && (\n // Give screen readers something useful to read off + hide the em dash\n <VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>\n )}\n <Container aria-hidden {...qa}>\n {EM_DASH}\n </Container>\n </>\n );\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n\n // The following are used to debug the skipped tests which are misbehaving!!!\n // console.log({ fullText, abbreviatedText });\n // console.log({ abbreviated: formatters.abbreviated.resolvedOptions() });\n // The following check is necessary because each locale may have differing thresholds\n // for which abbreviation begins.\n if (abbreviatedText !== fullText) {\n return isReturnTypeString ? (\n abbreviatedText\n ) : (\n <Tooltip content={fullText}>\n <AbbrContainer {...qa} {...rest}>\n {abbreviatedText}\n </AbbrContainer>\n </Tooltip>\n );\n }\n }\n\n return isReturnTypeString ? (\n fullText\n ) : (\n <Container {...qa} {...rest}>\n {fullText}\n </Container>\n );\n};\n\nexport const formatNumeral = (props: TypeNumeralProps): string => {\n return getNumeral({ returnType: \"string\", props }) as string;\n};\n\nconst Numeral = (props: TypeNumeralProps) => {\n return getNumeral({ returnType: \"component\", props });\n};\n\nexport default Numeral;\n","import type { EnumNumeralFormat } from \"./NumeralTypes\";\n\nexport const DEFAULT_THRESHOLD = 10000;\nexport const MEMO_CACHE_SIZE = 10;\nexport const COMPARE_OBJECTS = true;\nexport const MAX_PRECISION = 20;\nexport const ABBREV_PRECISION = 2;\nexport const DefaultPrecisions: {\n [key in EnumNumeralFormat]: [number, number];\n} = {\n decimal: [0, 2],\n percent: [0, 1],\n currency: [2, 2],\n};\n\nexport const EM_DASH = \"—\"; // shift + option + hyphen on a mac keyboard\n","import styled from \"styled-components\";\nimport Text from \"@sproutsocial/seeds-react-text\";\n\nexport const Container = styled(Text)`\n font-variant-numeric: tabular-nums;\n`;\n\nexport const AbbrContainer = styled(Text)`\n font-variant-numeric: tabular-nums;\n border-bottom: 1px dotted\n ${(props) => props.theme.colors.container.border.base};\n`;\n","import Numeral, { formatNumeral } from \"./Numeral\";\n\nexport default Numeral;\nexport { Numeral };\nexport { formatNumeral };\nexport * from \"./NumeralTypes\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/NumeralHybrid.tsx","../../src/getNumeral.tsx","../../src/constants.ts","../../src/styles.ts","../../src/NumeralTailwind.tsx","../../src/Numeral.tsx","../../src/index.ts"],"sourcesContent":["/**\n * Hybrid Numeral Component\n * Automatically chooses between Tailwind and styled-components based on props.\n *\n * Routes to styled-components when the consumer uses styled-system props or a\n * styled(Numeral) extension (which injects a `theme` prop); otherwise renders\n * the lighter Tailwind path. Gated on needsStyledComponents — a superset of\n * Button's hasStyledProps gate — so a styled(Numeral) extension reliably hits\n * the SC path, as the ticket's DoD requires.\n */\n\nimport * as React from \"react\";\nimport { renderNumeral } from \"./getNumeral\";\nimport { Container, AbbrContainer } from \"./styles\"; // Styled-components version\nimport { NumeralTailwind } from \"./NumeralTailwind\"; // Tailwind version\nimport { needsStyledComponents } from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeNumeralProps } from \"./NumeralTypes\";\n\nconst NumeralHybrid = (props: TypeNumeralProps): React.ReactNode => {\n if (needsStyledComponents(props)) {\n return renderNumeral(props, { Container, AbbrContainer });\n }\n\n // Use Tailwind version (preferred - better performance)\n return NumeralTailwind(props);\n};\n\nNumeralHybrid.displayName = \"Numeral\";\nexport default NumeralHybrid;\n","import * as React from \"react\";\n// @ts-expect-error lru-memoize is not typed\nimport memoize from \"lru-memoize\";\nimport { EM_DASH } from \"./constants\";\nimport Tooltip from \"@sproutsocial/seeds-react-tooltip\";\nimport type { TypeTextProps } from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\n\nimport {\n DEFAULT_THRESHOLD,\n MEMO_CACHE_SIZE,\n COMPARE_OBJECTS,\n MAX_PRECISION,\n ABBREV_PRECISION,\n DefaultPrecisions,\n} from \"./constants\";\nimport type { EnumNumeralFormat, TypeNumeralProps } from \"./NumeralTypes\";\n\ninterface TypeFormatOptions {\n locale: string;\n format: EnumNumeralFormat;\n currency: string;\n min: number;\n max: number;\n}\n\ninterface TypeFormatters {\n standard: Intl.NumberFormat;\n abbreviated: Intl.NumberFormat;\n}\n\ninterface TypeArgs {\n value: number;\n canAbbreviate: boolean;\n invalidNumberLabel?: string;\n ariaLabel?: string;\n options: TypeFormatOptions;\n qa: object | null | undefined;\n rest: Omit<TypeTextProps, \"children\">;\n}\n\n/**\n * The two container components used to render a numeral. The SC path supplies\n * the styled(Text) wrappers from ./styles; the Tailwind path supplies plain\n * Text wrappers carrying the seeds-numeral CSS classes.\n */\nexport interface TypeNumeralContainers {\n Container: React.ComponentType<any>;\n AbbrContainer: React.ComponentType<any>;\n}\n\nconst _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {\n const { locale, format, currency, min, max } = options;\n const compactPrecision = min === max ? min : ABBREV_PRECISION;\n\n const _currency = format === \"currency\" ? currency : undefined;\n\n const standard: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: min,\n maximumFractionDigits: max,\n currency: _currency,\n };\n const compact: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: compactPrecision,\n maximumFractionDigits: compactPrecision,\n currency: _currency,\n notation: \"compact\",\n };\n // Safari 14.1 is currently throwing errors when trying to use the compact\n // options of NumberFormat\n // https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687\n let abbreviated;\n\n try {\n abbreviated = new Intl.NumberFormat(locale, compact);\n } catch (error) {\n abbreviated = new Intl.NumberFormat(locale, standard);\n }\n\n return {\n standard: new Intl.NumberFormat(locale, standard),\n abbreviated,\n };\n};\n\n// Memoize to reduce the energy of creating new instances of Intl.NumberFormat\nconst memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);\nconst getNumberFormatters = memoizer(_getNumberFormatters);\n\nconst getThreshold = (abbreviate: boolean | number): number => {\n if (typeof abbreviate === \"number\")\n return Math.max(1000, Math.abs(abbreviate));\n if (abbreviate) return DEFAULT_THRESHOLD;\n return Infinity;\n};\n\nconst getMinMaxPrecision = (\n precision: TypeNumeralProps[\"precision\"],\n format: EnumNumeralFormat\n): [number, number] => {\n if (typeof precision === \"number\") return [precision, precision];\n if (precision === \"none\") return [0, MAX_PRECISION];\n return DefaultPrecisions[format];\n};\n\nconst isValidNumber = (value: unknown): boolean => {\n return typeof value === \"number\" && isFinite(value);\n};\n\nconst normalizeArgs = (props: TypeNumeralProps): TypeArgs => {\n const {\n number,\n locale = \"us-EN\",\n format = props.currency ? \"currency\" : \"decimal\",\n currency = \"USD\",\n abbreviate = true,\n invalidNumberLabel,\n precision,\n qa,\n ...rest\n } = props;\n const threshold = getThreshold(abbreviate);\n const [min, max] = getMinMaxPrecision(precision, format);\n\n const _number = number || 0;\n\n const value = _number * (format === \"percent\" ? 0.01 : 1);\n const canAbbreviate = Math.abs(_number) >= threshold;\n const options = {\n locale,\n format,\n currency,\n min,\n max,\n };\n\n return {\n value,\n canAbbreviate,\n invalidNumberLabel,\n options,\n qa,\n rest,\n };\n};\n\n/**\n * Path-independent component renderer. Produces the three render branches\n * (invalid, abbreviated-with-Tooltip, standard) using whichever container\n * components are passed in. The SC and Tailwind paths share this logic so the\n * formatting engine is never duplicated.\n */\nexport const renderNumeral = (\n props: TypeNumeralProps,\n { Container, AbbrContainer }: TypeNumeralContainers\n): React.ReactNode => {\n const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =\n normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return (\n <>\n {invalidNumberLabel && (\n // Give screen readers something useful to read off + hide the em dash\n <VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>\n )}\n <Container aria-hidden {...qa}>\n {EM_DASH}\n </Container>\n </>\n );\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n\n // The following check is necessary because each locale may have differing thresholds\n // for which abbreviation begins.\n if (abbreviatedText !== fullText) {\n return (\n <Tooltip content={fullText}>\n <AbbrContainer {...qa} {...rest}>\n {abbreviatedText}\n </AbbrContainer>\n </Tooltip>\n );\n }\n }\n\n return (\n <Container {...qa} {...rest}>\n {fullText}\n </Container>\n );\n};\n\nexport const formatNumeral = (props: TypeNumeralProps): string => {\n const { value, canAbbreviate, options } = normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return EM_DASH;\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n if (abbreviatedText !== fullText) {\n return abbreviatedText;\n }\n }\n\n return fullText;\n};\n","import type { EnumNumeralFormat } from \"./NumeralTypes\";\n\nexport const DEFAULT_THRESHOLD = 10000;\nexport const MEMO_CACHE_SIZE = 10;\nexport const COMPARE_OBJECTS = true;\nexport const MAX_PRECISION = 20;\nexport const ABBREV_PRECISION = 2;\nexport const DefaultPrecisions: {\n [key in EnumNumeralFormat]: [number, number];\n} = {\n decimal: [0, 2],\n percent: [0, 1],\n currency: [2, 2],\n};\n\nexport const EM_DASH = \"—\"; // shift + option + hyphen on a mac keyboard\n","import styled from \"styled-components\";\nimport Text from \"@sproutsocial/seeds-react-text\";\n\nexport const Container = styled(Text)`\n font-variant-numeric: tabular-nums;\n`;\n\nexport const AbbrContainer = styled(Text)`\n font-variant-numeric: tabular-nums;\n border-bottom: 1px dotted\n ${(props) => props.theme.colors.container.border.base};\n`;\n","/**\n * Tailwind CSS implementation of Numeral component\n * Uses Seeds numeral CSS classes from numeral.css\n *\n * NOTE: mirror Button — do NOT `import \"./numeral.css\"`. The CSS ships via a\n * build-step copy (cp src/numeral.css dist/numeral.css) and the global racine\n * aggregation, not via a source-level import.\n */\n\nimport * as React from \"react\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport { renderNumeral } from \"./getNumeral\";\nimport type { TypeNumeralProps } from \"./NumeralTypes\";\n\n// Utility for merging class names properly\nfunction cn(\n ...inputs: (string | undefined | null | false | Record<string, boolean>)[]\n): string {\n const classes: string[] = [];\n\n for (const input of inputs) {\n if (!input) continue;\n\n if (typeof input === \"string\") {\n classes.push(input);\n } else if (typeof input === \"object\") {\n for (const [key, value] of Object.entries(input)) {\n if (value) {\n classes.push(key);\n }\n }\n }\n }\n\n return classes.join(\" \");\n}\n\n// Plain Text wrappers carrying the numeral.css classes. Text forwards\n// className and the remaining props (incl. non-styled typography props like\n// fontSize/fontWeight) to its DOM span, so the classes land on the span.\nconst TwContainer = ({ className, children, ...rest }: any) => (\n <Text className={cn(\"seeds-numeral\", className)} {...rest}>\n {children}\n </Text>\n);\n\nconst TwAbbrContainer = ({ className, children, ...rest }: any) => (\n <Text\n className={cn(\"seeds-numeral\", \"seeds-numeral-abbr\", className)}\n {...rest}\n >\n {children}\n </Text>\n);\n\nexport const NumeralTailwind = (props: TypeNumeralProps): React.ReactNode =>\n renderNumeral(props, {\n Container: TwContainer,\n AbbrContainer: TwAbbrContainer,\n });\n","import NumeralHybrid from \"./NumeralHybrid\";\n\nexport { formatNumeral } from \"./getNumeral\";\nexport default NumeralHybrid;\n","import Numeral, { formatNumeral } from \"./Numeral\";\n\nexport default Numeral;\nexport { Numeral };\nexport { formatNumeral };\nexport * from \"./NumeralTypes\";\n"],"mappings":"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","names":["Container","AbbrContainer","Text","jsx","Text"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -19,7 +19,22 @@ interface TypeNumeralProps extends Omit<TypeTextProps, "children"> {
|
|
|
19
19
|
qa?: object;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Hybrid Numeral Component
|
|
24
|
+
* Automatically chooses between Tailwind and styled-components based on props.
|
|
25
|
+
*
|
|
26
|
+
* Routes to styled-components when the consumer uses styled-system props or a
|
|
27
|
+
* styled(Numeral) extension (which injects a `theme` prop); otherwise renders
|
|
28
|
+
* the lighter Tailwind path. Gated on needsStyledComponents — a superset of
|
|
29
|
+
* Button's hasStyledProps gate — so a styled(Numeral) extension reliably hits
|
|
30
|
+
* the SC path, as the ticket's DoD requires.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const NumeralHybrid: {
|
|
34
|
+
(props: TypeNumeralProps): React.ReactNode;
|
|
35
|
+
displayName: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
22
38
|
declare const formatNumeral: (props: TypeNumeralProps) => string;
|
|
23
|
-
declare const Numeral: (props: TypeNumeralProps) => React.ReactNode;
|
|
24
39
|
|
|
25
|
-
export { type EnumNumeralFormat, Numeral, type TypeNumeralProps,
|
|
40
|
+
export { type EnumNumeralFormat, NumeralHybrid as Numeral, type TypeNumeralProps, NumeralHybrid as default, formatNumeral };
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,22 @@ interface TypeNumeralProps extends Omit<TypeTextProps, "children"> {
|
|
|
19
19
|
qa?: object;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Hybrid Numeral Component
|
|
24
|
+
* Automatically chooses between Tailwind and styled-components based on props.
|
|
25
|
+
*
|
|
26
|
+
* Routes to styled-components when the consumer uses styled-system props or a
|
|
27
|
+
* styled(Numeral) extension (which injects a `theme` prop); otherwise renders
|
|
28
|
+
* the lighter Tailwind path. Gated on needsStyledComponents — a superset of
|
|
29
|
+
* Button's hasStyledProps gate — so a styled(Numeral) extension reliably hits
|
|
30
|
+
* the SC path, as the ticket's DoD requires.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
declare const NumeralHybrid: {
|
|
34
|
+
(props: TypeNumeralProps): React.ReactNode;
|
|
35
|
+
displayName: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
22
38
|
declare const formatNumeral: (props: TypeNumeralProps) => string;
|
|
23
|
-
declare const Numeral: (props: TypeNumeralProps) => React.ReactNode;
|
|
24
39
|
|
|
25
|
-
export { type EnumNumeralFormat, Numeral, type TypeNumeralProps,
|
|
40
|
+
export { type EnumNumeralFormat, NumeralHybrid as Numeral, type TypeNumeralProps, NumeralHybrid as default, formatNumeral };
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,10 @@ __export(index_exports, {
|
|
|
36
36
|
});
|
|
37
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
38
|
|
|
39
|
-
// src/
|
|
39
|
+
// src/NumeralHybrid.tsx
|
|
40
|
+
var React3 = require("react");
|
|
41
|
+
|
|
42
|
+
// src/getNumeral.tsx
|
|
40
43
|
var React = require("react");
|
|
41
44
|
var import_lru_memoize = __toESM(require("lru-memoize"));
|
|
42
45
|
|
|
@@ -53,23 +56,9 @@ var DefaultPrecisions = {
|
|
|
53
56
|
};
|
|
54
57
|
var EM_DASH = "\u2014";
|
|
55
58
|
|
|
56
|
-
// src/
|
|
59
|
+
// src/getNumeral.tsx
|
|
57
60
|
var import_seeds_react_tooltip = __toESM(require("@sproutsocial/seeds-react-tooltip"));
|
|
58
61
|
var import_seeds_react_visually_hidden = require("@sproutsocial/seeds-react-visually-hidden");
|
|
59
|
-
|
|
60
|
-
// src/styles.ts
|
|
61
|
-
var import_styled_components = __toESM(require("styled-components"));
|
|
62
|
-
var import_seeds_react_text = __toESM(require("@sproutsocial/seeds-react-text"));
|
|
63
|
-
var Container = (0, import_styled_components.default)(import_seeds_react_text.default)`
|
|
64
|
-
font-variant-numeric: tabular-nums;
|
|
65
|
-
`;
|
|
66
|
-
var AbbrContainer = (0, import_styled_components.default)(import_seeds_react_text.default)`
|
|
67
|
-
font-variant-numeric: tabular-nums;
|
|
68
|
-
border-bottom: 1px dotted
|
|
69
|
-
${(props) => props.theme.colors.container.border.base};
|
|
70
|
-
`;
|
|
71
|
-
|
|
72
|
-
// src/Numeral.tsx
|
|
73
62
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
74
63
|
var _getNumberFormatters = (options) => {
|
|
75
64
|
const { locale, format, currency, min, max } = options;
|
|
@@ -148,17 +137,13 @@ var normalizeArgs = (props) => {
|
|
|
148
137
|
rest
|
|
149
138
|
};
|
|
150
139
|
};
|
|
151
|
-
var
|
|
152
|
-
returnType,
|
|
153
|
-
props
|
|
154
|
-
}) => {
|
|
155
|
-
const isReturnTypeString = returnType === "string";
|
|
140
|
+
var renderNumeral = (props, { Container: Container2, AbbrContainer: AbbrContainer2 }) => {
|
|
156
141
|
const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } = normalizeArgs(props);
|
|
157
142
|
if (!isValidNumber(props.number)) {
|
|
158
|
-
return
|
|
143
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
159
144
|
invalidNumberLabel && // Give screen readers something useful to read off + hide the em dash
|
|
160
145
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_visually_hidden.VisuallyHidden, { children: invalidNumberLabel }),
|
|
161
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Container2, { "aria-hidden": true, ...qa, children: EM_DASH })
|
|
162
147
|
] });
|
|
163
148
|
}
|
|
164
149
|
const formatters = getNumberFormatters(options);
|
|
@@ -166,18 +151,86 @@ var getNumeral = ({
|
|
|
166
151
|
if (canAbbreviate) {
|
|
167
152
|
const abbreviatedText = formatters.abbreviated.format(value);
|
|
168
153
|
if (abbreviatedText !== fullText) {
|
|
169
|
-
return
|
|
154
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_tooltip.default, { content: fullText, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AbbrContainer2, { ...qa, ...rest, children: abbreviatedText }) });
|
|
170
155
|
}
|
|
171
156
|
}
|
|
172
|
-
return
|
|
157
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Container2, { ...qa, ...rest, children: fullText });
|
|
173
158
|
};
|
|
174
159
|
var formatNumeral = (props) => {
|
|
175
|
-
|
|
160
|
+
const { value, canAbbreviate, options } = normalizeArgs(props);
|
|
161
|
+
if (!isValidNumber(props.number)) {
|
|
162
|
+
return EM_DASH;
|
|
163
|
+
}
|
|
164
|
+
const formatters = getNumberFormatters(options);
|
|
165
|
+
const fullText = formatters.standard.format(value);
|
|
166
|
+
if (canAbbreviate) {
|
|
167
|
+
const abbreviatedText = formatters.abbreviated.format(value);
|
|
168
|
+
if (abbreviatedText !== fullText) {
|
|
169
|
+
return abbreviatedText;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return fullText;
|
|
176
173
|
};
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
|
|
175
|
+
// src/styles.ts
|
|
176
|
+
var import_styled_components = __toESM(require("styled-components"));
|
|
177
|
+
var import_seeds_react_text = __toESM(require("@sproutsocial/seeds-react-text"));
|
|
178
|
+
var Container = (0, import_styled_components.default)(import_seeds_react_text.default)`
|
|
179
|
+
font-variant-numeric: tabular-nums;
|
|
180
|
+
`;
|
|
181
|
+
var AbbrContainer = (0, import_styled_components.default)(import_seeds_react_text.default)`
|
|
182
|
+
font-variant-numeric: tabular-nums;
|
|
183
|
+
border-bottom: 1px dotted
|
|
184
|
+
${(props) => props.theme.colors.container.border.base};
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
// src/NumeralTailwind.tsx
|
|
188
|
+
var React2 = require("react");
|
|
189
|
+
var import_seeds_react_text2 = __toESM(require("@sproutsocial/seeds-react-text"));
|
|
190
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
191
|
+
function cn(...inputs) {
|
|
192
|
+
const classes = [];
|
|
193
|
+
for (const input of inputs) {
|
|
194
|
+
if (!input) continue;
|
|
195
|
+
if (typeof input === "string") {
|
|
196
|
+
classes.push(input);
|
|
197
|
+
} else if (typeof input === "object") {
|
|
198
|
+
for (const [key, value] of Object.entries(input)) {
|
|
199
|
+
if (value) {
|
|
200
|
+
classes.push(key);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return classes.join(" ");
|
|
206
|
+
}
|
|
207
|
+
var TwContainer = ({ className, children, ...rest }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_seeds_react_text2.default, { className: cn("seeds-numeral", className), ...rest, children });
|
|
208
|
+
var TwAbbrContainer = ({ className, children, ...rest }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
209
|
+
import_seeds_react_text2.default,
|
|
210
|
+
{
|
|
211
|
+
className: cn("seeds-numeral", "seeds-numeral-abbr", className),
|
|
212
|
+
...rest,
|
|
213
|
+
children
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
var NumeralTailwind = (props) => renderNumeral(props, {
|
|
217
|
+
Container: TwContainer,
|
|
218
|
+
AbbrContainer: TwAbbrContainer
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/NumeralHybrid.tsx
|
|
222
|
+
var import_seeds_react_system_props = require("@sproutsocial/seeds-react-system-props");
|
|
223
|
+
var NumeralHybrid = (props) => {
|
|
224
|
+
if ((0, import_seeds_react_system_props.needsStyledComponents)(props)) {
|
|
225
|
+
return renderNumeral(props, { Container, AbbrContainer });
|
|
226
|
+
}
|
|
227
|
+
return NumeralTailwind(props);
|
|
179
228
|
};
|
|
180
|
-
|
|
229
|
+
NumeralHybrid.displayName = "Numeral";
|
|
230
|
+
var NumeralHybrid_default = NumeralHybrid;
|
|
231
|
+
|
|
232
|
+
// src/Numeral.tsx
|
|
233
|
+
var Numeral_default = NumeralHybrid_default;
|
|
181
234
|
|
|
182
235
|
// src/index.ts
|
|
183
236
|
var index_default = Numeral_default;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/Numeral.tsx","../src/constants.ts","../src/styles.ts"],"sourcesContent":["import Numeral, { formatNumeral } from \"./Numeral\";\n\nexport default Numeral;\nexport { Numeral };\nexport { formatNumeral };\nexport * from \"./NumeralTypes\";\n","import * as React from \"react\";\n// @ts-expect-error lru-memoize is not typed\nimport memoize from \"lru-memoize\";\nimport { EM_DASH } from \"./constants\";\nimport Tooltip from \"@sproutsocial/seeds-react-tooltip\";\nimport type { TypeTextProps } from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\n\nimport {\n DEFAULT_THRESHOLD,\n MEMO_CACHE_SIZE,\n COMPARE_OBJECTS,\n MAX_PRECISION,\n ABBREV_PRECISION,\n DefaultPrecisions,\n} from \"./constants\";\nimport { AbbrContainer, Container } from \"./styles\";\nimport type { EnumNumeralFormat, TypeNumeralProps } from \"./NumeralTypes\";\n\ninterface TypeFormatOptions {\n locale: string;\n format: EnumNumeralFormat;\n currency: string;\n min: number;\n max: number;\n}\n\ninterface TypeFormatters {\n standard: Intl.NumberFormat;\n abbreviated: Intl.NumberFormat;\n}\n\ninterface TypeArgs {\n value: number;\n canAbbreviate: boolean;\n invalidNumberLabel?: string;\n ariaLabel?: string;\n options: TypeFormatOptions;\n qa: object | null | undefined;\n rest: Omit<TypeTextProps, \"children\">;\n}\n\nconst _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {\n const { locale, format, currency, min, max } = options;\n const compactPrecision = min === max ? min : ABBREV_PRECISION;\n\n const _currency = format === \"currency\" ? currency : undefined;\n\n const standard: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: min,\n maximumFractionDigits: max,\n currency: _currency,\n };\n const compact: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: compactPrecision,\n maximumFractionDigits: compactPrecision,\n currency: _currency,\n notation: \"compact\",\n };\n // Safari 14.1 is currently throwing errors when trying to use the compact\n // options of NumberFormat\n // https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687\n let abbreviated;\n\n try {\n abbreviated = new Intl.NumberFormat(locale, compact);\n } catch (error) {\n abbreviated = new Intl.NumberFormat(locale, standard);\n }\n\n return {\n standard: new Intl.NumberFormat(locale, standard),\n abbreviated,\n };\n};\n\n// Memoize to reduce the energy of creating new instances of Intl.NumberFormat\nconst memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);\nconst getNumberFormatters = memoizer(_getNumberFormatters);\n\nconst getThreshold = (abbreviate: boolean | number): number => {\n if (typeof abbreviate === \"number\")\n return Math.max(1000, Math.abs(abbreviate));\n if (abbreviate) return DEFAULT_THRESHOLD;\n return Infinity;\n};\n\nconst getMinMaxPrecision = (\n precision: TypeNumeralProps[\"precision\"],\n format: EnumNumeralFormat\n): [number, number] => {\n if (typeof precision === \"number\") return [precision, precision];\n if (precision === \"none\") return [0, MAX_PRECISION];\n return DefaultPrecisions[format];\n};\n\nconst isValidNumber = (value: unknown): boolean => {\n return typeof value === \"number\" && isFinite(value);\n};\n\nconst normalizeArgs = (props: TypeNumeralProps): TypeArgs => {\n const {\n number,\n locale = \"us-EN\",\n format = props.currency ? \"currency\" : \"decimal\",\n currency = \"USD\",\n abbreviate = true,\n invalidNumberLabel,\n precision,\n qa,\n ...rest\n } = props;\n const threshold = getThreshold(abbreviate);\n const [min, max] = getMinMaxPrecision(precision, format);\n\n const _number = number || 0;\n\n const value = _number * (format === \"percent\" ? 0.01 : 1);\n const canAbbreviate = Math.abs(_number) >= threshold;\n const options = {\n locale,\n format,\n currency,\n min,\n max,\n };\n\n return {\n value,\n canAbbreviate,\n invalidNumberLabel,\n options,\n qa,\n rest,\n };\n};\n\nconst getNumeral = ({\n returnType,\n props,\n}: {\n returnType: \"string\" | \"component\";\n props: TypeNumeralProps;\n}): string | React.ReactNode => {\n const isReturnTypeString = returnType === \"string\";\n const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =\n normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return isReturnTypeString ? (\n EM_DASH\n ) : (\n <>\n {invalidNumberLabel && (\n // Give screen readers something useful to read off + hide the em dash\n <VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>\n )}\n <Container aria-hidden {...qa}>\n {EM_DASH}\n </Container>\n </>\n );\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n\n // The following are used to debug the skipped tests which are misbehaving!!!\n // console.log({ fullText, abbreviatedText });\n // console.log({ abbreviated: formatters.abbreviated.resolvedOptions() });\n // The following check is necessary because each locale may have differing thresholds\n // for which abbreviation begins.\n if (abbreviatedText !== fullText) {\n return isReturnTypeString ? (\n abbreviatedText\n ) : (\n <Tooltip content={fullText}>\n <AbbrContainer {...qa} {...rest}>\n {abbreviatedText}\n </AbbrContainer>\n </Tooltip>\n );\n }\n }\n\n return isReturnTypeString ? (\n fullText\n ) : (\n <Container {...qa} {...rest}>\n {fullText}\n </Container>\n );\n};\n\nexport const formatNumeral = (props: TypeNumeralProps): string => {\n return getNumeral({ returnType: \"string\", props }) as string;\n};\n\nconst Numeral = (props: TypeNumeralProps) => {\n return getNumeral({ returnType: \"component\", props });\n};\n\nexport default Numeral;\n","import type { EnumNumeralFormat } from \"./NumeralTypes\";\n\nexport const DEFAULT_THRESHOLD = 10000;\nexport const MEMO_CACHE_SIZE = 10;\nexport const COMPARE_OBJECTS = true;\nexport const MAX_PRECISION = 20;\nexport const ABBREV_PRECISION = 2;\nexport const DefaultPrecisions: {\n [key in EnumNumeralFormat]: [number, number];\n} = {\n decimal: [0, 2],\n percent: [0, 1],\n currency: [2, 2],\n};\n\nexport const EM_DASH = \"—\"; // shift + option + hyphen on a mac keyboard\n","import styled from \"styled-components\";\nimport Text from \"@sproutsocial/seeds-react-text\";\n\nexport const Container = styled(Text)`\n font-variant-numeric: tabular-nums;\n`;\n\nexport const AbbrContainer = styled(Text)`\n font-variant-numeric: tabular-nums;\n border-bottom: 1px dotted\n ${(props) => props.theme.colors.container.border.base};\n`;\n"],"mappings":"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","names":["styled","Text","memoize","Tooltip"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/NumeralHybrid.tsx","../src/getNumeral.tsx","../src/constants.ts","../src/styles.ts","../src/NumeralTailwind.tsx","../src/Numeral.tsx"],"sourcesContent":["import Numeral, { formatNumeral } from \"./Numeral\";\n\nexport default Numeral;\nexport { Numeral };\nexport { formatNumeral };\nexport * from \"./NumeralTypes\";\n","/**\n * Hybrid Numeral Component\n * Automatically chooses between Tailwind and styled-components based on props.\n *\n * Routes to styled-components when the consumer uses styled-system props or a\n * styled(Numeral) extension (which injects a `theme` prop); otherwise renders\n * the lighter Tailwind path. Gated on needsStyledComponents — a superset of\n * Button's hasStyledProps gate — so a styled(Numeral) extension reliably hits\n * the SC path, as the ticket's DoD requires.\n */\n\nimport * as React from \"react\";\nimport { renderNumeral } from \"./getNumeral\";\nimport { Container, AbbrContainer } from \"./styles\"; // Styled-components version\nimport { NumeralTailwind } from \"./NumeralTailwind\"; // Tailwind version\nimport { needsStyledComponents } from \"@sproutsocial/seeds-react-system-props\";\nimport type { TypeNumeralProps } from \"./NumeralTypes\";\n\nconst NumeralHybrid = (props: TypeNumeralProps): React.ReactNode => {\n if (needsStyledComponents(props)) {\n return renderNumeral(props, { Container, AbbrContainer });\n }\n\n // Use Tailwind version (preferred - better performance)\n return NumeralTailwind(props);\n};\n\nNumeralHybrid.displayName = \"Numeral\";\nexport default NumeralHybrid;\n","import * as React from \"react\";\n// @ts-expect-error lru-memoize is not typed\nimport memoize from \"lru-memoize\";\nimport { EM_DASH } from \"./constants\";\nimport Tooltip from \"@sproutsocial/seeds-react-tooltip\";\nimport type { TypeTextProps } from \"@sproutsocial/seeds-react-text\";\nimport { VisuallyHidden } from \"@sproutsocial/seeds-react-visually-hidden\";\n\nimport {\n DEFAULT_THRESHOLD,\n MEMO_CACHE_SIZE,\n COMPARE_OBJECTS,\n MAX_PRECISION,\n ABBREV_PRECISION,\n DefaultPrecisions,\n} from \"./constants\";\nimport type { EnumNumeralFormat, TypeNumeralProps } from \"./NumeralTypes\";\n\ninterface TypeFormatOptions {\n locale: string;\n format: EnumNumeralFormat;\n currency: string;\n min: number;\n max: number;\n}\n\ninterface TypeFormatters {\n standard: Intl.NumberFormat;\n abbreviated: Intl.NumberFormat;\n}\n\ninterface TypeArgs {\n value: number;\n canAbbreviate: boolean;\n invalidNumberLabel?: string;\n ariaLabel?: string;\n options: TypeFormatOptions;\n qa: object | null | undefined;\n rest: Omit<TypeTextProps, \"children\">;\n}\n\n/**\n * The two container components used to render a numeral. The SC path supplies\n * the styled(Text) wrappers from ./styles; the Tailwind path supplies plain\n * Text wrappers carrying the seeds-numeral CSS classes.\n */\nexport interface TypeNumeralContainers {\n Container: React.ComponentType<any>;\n AbbrContainer: React.ComponentType<any>;\n}\n\nconst _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {\n const { locale, format, currency, min, max } = options;\n const compactPrecision = min === max ? min : ABBREV_PRECISION;\n\n const _currency = format === \"currency\" ? currency : undefined;\n\n const standard: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: min,\n maximumFractionDigits: max,\n currency: _currency,\n };\n const compact: Intl.NumberFormatOptions = {\n style: format,\n minimumFractionDigits: compactPrecision,\n maximumFractionDigits: compactPrecision,\n currency: _currency,\n notation: \"compact\",\n };\n // Safari 14.1 is currently throwing errors when trying to use the compact\n // options of NumberFormat\n // https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687\n let abbreviated;\n\n try {\n abbreviated = new Intl.NumberFormat(locale, compact);\n } catch (error) {\n abbreviated = new Intl.NumberFormat(locale, standard);\n }\n\n return {\n standard: new Intl.NumberFormat(locale, standard),\n abbreviated,\n };\n};\n\n// Memoize to reduce the energy of creating new instances of Intl.NumberFormat\nconst memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);\nconst getNumberFormatters = memoizer(_getNumberFormatters);\n\nconst getThreshold = (abbreviate: boolean | number): number => {\n if (typeof abbreviate === \"number\")\n return Math.max(1000, Math.abs(abbreviate));\n if (abbreviate) return DEFAULT_THRESHOLD;\n return Infinity;\n};\n\nconst getMinMaxPrecision = (\n precision: TypeNumeralProps[\"precision\"],\n format: EnumNumeralFormat\n): [number, number] => {\n if (typeof precision === \"number\") return [precision, precision];\n if (precision === \"none\") return [0, MAX_PRECISION];\n return DefaultPrecisions[format];\n};\n\nconst isValidNumber = (value: unknown): boolean => {\n return typeof value === \"number\" && isFinite(value);\n};\n\nconst normalizeArgs = (props: TypeNumeralProps): TypeArgs => {\n const {\n number,\n locale = \"us-EN\",\n format = props.currency ? \"currency\" : \"decimal\",\n currency = \"USD\",\n abbreviate = true,\n invalidNumberLabel,\n precision,\n qa,\n ...rest\n } = props;\n const threshold = getThreshold(abbreviate);\n const [min, max] = getMinMaxPrecision(precision, format);\n\n const _number = number || 0;\n\n const value = _number * (format === \"percent\" ? 0.01 : 1);\n const canAbbreviate = Math.abs(_number) >= threshold;\n const options = {\n locale,\n format,\n currency,\n min,\n max,\n };\n\n return {\n value,\n canAbbreviate,\n invalidNumberLabel,\n options,\n qa,\n rest,\n };\n};\n\n/**\n * Path-independent component renderer. Produces the three render branches\n * (invalid, abbreviated-with-Tooltip, standard) using whichever container\n * components are passed in. The SC and Tailwind paths share this logic so the\n * formatting engine is never duplicated.\n */\nexport const renderNumeral = (\n props: TypeNumeralProps,\n { Container, AbbrContainer }: TypeNumeralContainers\n): React.ReactNode => {\n const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =\n normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return (\n <>\n {invalidNumberLabel && (\n // Give screen readers something useful to read off + hide the em dash\n <VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>\n )}\n <Container aria-hidden {...qa}>\n {EM_DASH}\n </Container>\n </>\n );\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n\n // The following check is necessary because each locale may have differing thresholds\n // for which abbreviation begins.\n if (abbreviatedText !== fullText) {\n return (\n <Tooltip content={fullText}>\n <AbbrContainer {...qa} {...rest}>\n {abbreviatedText}\n </AbbrContainer>\n </Tooltip>\n );\n }\n }\n\n return (\n <Container {...qa} {...rest}>\n {fullText}\n </Container>\n );\n};\n\nexport const formatNumeral = (props: TypeNumeralProps): string => {\n const { value, canAbbreviate, options } = normalizeArgs(props);\n\n if (!isValidNumber(props.number)) {\n return EM_DASH;\n }\n\n const formatters = getNumberFormatters(options);\n const fullText = formatters.standard.format(value);\n\n if (canAbbreviate) {\n const abbreviatedText = formatters.abbreviated.format(value);\n if (abbreviatedText !== fullText) {\n return abbreviatedText;\n }\n }\n\n return fullText;\n};\n","import type { EnumNumeralFormat } from \"./NumeralTypes\";\n\nexport const DEFAULT_THRESHOLD = 10000;\nexport const MEMO_CACHE_SIZE = 10;\nexport const COMPARE_OBJECTS = true;\nexport const MAX_PRECISION = 20;\nexport const ABBREV_PRECISION = 2;\nexport const DefaultPrecisions: {\n [key in EnumNumeralFormat]: [number, number];\n} = {\n decimal: [0, 2],\n percent: [0, 1],\n currency: [2, 2],\n};\n\nexport const EM_DASH = \"—\"; // shift + option + hyphen on a mac keyboard\n","import styled from \"styled-components\";\nimport Text from \"@sproutsocial/seeds-react-text\";\n\nexport const Container = styled(Text)`\n font-variant-numeric: tabular-nums;\n`;\n\nexport const AbbrContainer = styled(Text)`\n font-variant-numeric: tabular-nums;\n border-bottom: 1px dotted\n ${(props) => props.theme.colors.container.border.base};\n`;\n","/**\n * Tailwind CSS implementation of Numeral component\n * Uses Seeds numeral CSS classes from numeral.css\n *\n * NOTE: mirror Button — do NOT `import \"./numeral.css\"`. The CSS ships via a\n * build-step copy (cp src/numeral.css dist/numeral.css) and the global racine\n * aggregation, not via a source-level import.\n */\n\nimport * as React from \"react\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport { renderNumeral } from \"./getNumeral\";\nimport type { TypeNumeralProps } from \"./NumeralTypes\";\n\n// Utility for merging class names properly\nfunction cn(\n ...inputs: (string | undefined | null | false | Record<string, boolean>)[]\n): string {\n const classes: string[] = [];\n\n for (const input of inputs) {\n if (!input) continue;\n\n if (typeof input === \"string\") {\n classes.push(input);\n } else if (typeof input === \"object\") {\n for (const [key, value] of Object.entries(input)) {\n if (value) {\n classes.push(key);\n }\n }\n }\n }\n\n return classes.join(\" \");\n}\n\n// Plain Text wrappers carrying the numeral.css classes. Text forwards\n// className and the remaining props (incl. non-styled typography props like\n// fontSize/fontWeight) to its DOM span, so the classes land on the span.\nconst TwContainer = ({ className, children, ...rest }: any) => (\n <Text className={cn(\"seeds-numeral\", className)} {...rest}>\n {children}\n </Text>\n);\n\nconst TwAbbrContainer = ({ className, children, ...rest }: any) => (\n <Text\n className={cn(\"seeds-numeral\", \"seeds-numeral-abbr\", className)}\n {...rest}\n >\n {children}\n </Text>\n);\n\nexport const NumeralTailwind = (props: TypeNumeralProps): React.ReactNode =>\n renderNumeral(props, {\n Container: TwContainer,\n AbbrContainer: TwAbbrContainer,\n });\n","import NumeralHybrid from \"./NumeralHybrid\";\n\nexport { formatNumeral } from \"./getNumeral\";\nexport default NumeralHybrid;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,IAAAA,SAAuB;;;ACXvB,YAAuB;AAEvB,yBAAoB;;;ACAb,IAAM,oBAAoB;AAC1B,IAAM,kBAAkB;AACxB,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AACtB,IAAM,mBAAmB;AACzB,IAAM,oBAET;AAAA,EACF,SAAS,CAAC,GAAG,CAAC;AAAA,EACd,SAAS,CAAC,GAAG,CAAC;AAAA,EACd,UAAU,CAAC,GAAG,CAAC;AACjB;AAEO,IAAM,UAAU;;;ADXvB,iCAAoB;AAEpB,yCAA+B;AA6JzB;AAhHN,IAAM,uBAAuB,CAAC,YAA+C;AAC3E,QAAM,EAAE,QAAQ,QAAQ,UAAU,KAAK,IAAI,IAAI;AAC/C,QAAM,mBAAmB,QAAQ,MAAM,MAAM;AAE7C,QAAM,YAAY,WAAW,aAAa,WAAW;AAErD,QAAM,WAAqC;AAAA,IACzC,OAAO;AAAA,IACP,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,IACvB,UAAU;AAAA,EACZ;AACA,QAAM,UAAoC;AAAA,IACxC,OAAO;AAAA,IACP,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,IACvB,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAIA,MAAI;AAEJ,MAAI;AACF,kBAAc,IAAI,KAAK,aAAa,QAAQ,OAAO;AAAA,EACrD,SAAS,OAAO;AACd,kBAAc,IAAI,KAAK,aAAa,QAAQ,QAAQ;AAAA,EACtD;AAEA,SAAO;AAAA,IACL,UAAU,IAAI,KAAK,aAAa,QAAQ,QAAQ;AAAA,IAChD;AAAA,EACF;AACF;AAGA,IAAM,eAAW,mBAAAC,SAAQ,iBAAiB,eAAe;AACzD,IAAM,sBAAsB,SAAS,oBAAoB;AAEzD,IAAM,eAAe,CAAC,eAAyC;AAC7D,MAAI,OAAO,eAAe;AACxB,WAAO,KAAK,IAAI,KAAM,KAAK,IAAI,UAAU,CAAC;AAC5C,MAAI,WAAY,QAAO;AACvB,SAAO;AACT;AAEA,IAAM,qBAAqB,CACzB,WACA,WACqB;AACrB,MAAI,OAAO,cAAc,SAAU,QAAO,CAAC,WAAW,SAAS;AAC/D,MAAI,cAAc,OAAQ,QAAO,CAAC,GAAG,aAAa;AAClD,SAAO,kBAAkB,MAAM;AACjC;AAEA,IAAM,gBAAgB,CAAC,UAA4B;AACjD,SAAO,OAAO,UAAU,YAAY,SAAS,KAAK;AACpD;AAEA,IAAM,gBAAgB,CAAC,UAAsC;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA,SAAS;AAAA,IACT,SAAS,MAAM,WAAW,aAAa;AAAA,IACvC,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,YAAY,aAAa,UAAU;AACzC,QAAM,CAAC,KAAK,GAAG,IAAI,mBAAmB,WAAW,MAAM;AAEvD,QAAM,UAAU,UAAU;AAE1B,QAAM,QAAQ,WAAW,WAAW,YAAY,OAAO;AACvD,QAAM,gBAAgB,KAAK,IAAI,OAAO,KAAK;AAC3C,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAQO,IAAM,gBAAgB,CAC3B,OACA,EAAE,WAAAC,YAAW,eAAAC,eAAc,MACP;AACpB,QAAM,EAAE,OAAO,eAAe,oBAAoB,SAAS,IAAI,KAAK,IAClE,cAAc,KAAK;AAErB,MAAI,CAAC,cAAc,MAAM,MAAM,GAAG;AAChC,WACE,4EACG;AAAA;AAAA,MAEC,4CAAC,qDAAgB,8BAAmB;AAAA,MAEtC,4CAACD,YAAA,EAAU,eAAW,MAAE,GAAG,IACxB,mBACH;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,aAAa,oBAAoB,OAAO;AAC9C,QAAM,WAAW,WAAW,SAAS,OAAO,KAAK;AAEjD,MAAI,eAAe;AACjB,UAAM,kBAAkB,WAAW,YAAY,OAAO,KAAK;AAI3D,QAAI,oBAAoB,UAAU;AAChC,aACE,4CAAC,2BAAAE,SAAA,EAAQ,SAAS,UAChB,sDAACD,gBAAA,EAAe,GAAG,IAAK,GAAG,MACxB,2BACH,GACF;AAAA,IAEJ;AAAA,EACF;AAEA,SACE,4CAACD,YAAA,EAAW,GAAG,IAAK,GAAG,MACpB,oBACH;AAEJ;AAEO,IAAM,gBAAgB,CAAC,UAAoC;AAChE,QAAM,EAAE,OAAO,eAAe,QAAQ,IAAI,cAAc,KAAK;AAE7D,MAAI,CAAC,cAAc,MAAM,MAAM,GAAG;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,oBAAoB,OAAO;AAC9C,QAAM,WAAW,WAAW,SAAS,OAAO,KAAK;AAEjD,MAAI,eAAe;AACjB,UAAM,kBAAkB,WAAW,YAAY,OAAO,KAAK;AAC3D,QAAI,oBAAoB,UAAU;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;;;AE3NA,+BAAmB;AACnB,8BAAiB;AAEV,IAAM,gBAAY,yBAAAG,SAAO,wBAAAC,OAAI;AAAA;AAAA;AAI7B,IAAM,oBAAgB,yBAAAD,SAAO,wBAAAC,OAAI;AAAA;AAAA;AAAA,MAGlC,CAAC,UAAU,MAAM,MAAM,OAAO,UAAU,OAAO,IAAI;AAAA;;;ACDzD,IAAAC,SAAuB;AACvB,IAAAC,2BAAiB;AA+Bf,IAAAC,sBAAA;AA1BF,SAAS,MACJ,QACK;AACR,QAAM,UAAoB,CAAC;AAE3B,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,MAAO;AAEZ,QAAI,OAAO,UAAU,UAAU;AAC7B,cAAQ,KAAK,KAAK;AAAA,IACpB,WAAW,OAAO,UAAU,UAAU;AACpC,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,YAAI,OAAO;AACT,kBAAQ,KAAK,GAAG;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,QAAQ,KAAK,GAAG;AACzB;AAKA,IAAM,cAAc,CAAC,EAAE,WAAW,UAAU,GAAG,KAAK,MAClD,6CAAC,yBAAAC,SAAA,EAAK,WAAW,GAAG,iBAAiB,SAAS,GAAI,GAAG,MAClD,UACH;AAGF,IAAM,kBAAkB,CAAC,EAAE,WAAW,UAAU,GAAG,KAAK,MACtD;AAAA,EAAC,yBAAAA;AAAA,EAAA;AAAA,IACC,WAAW,GAAG,iBAAiB,sBAAsB,SAAS;AAAA,IAC7D,GAAG;AAAA,IAEH;AAAA;AACH;AAGK,IAAM,kBAAkB,CAAC,UAC9B,cAAc,OAAO;AAAA,EACnB,WAAW;AAAA,EACX,eAAe;AACjB,CAAC;;;AJ5CH,sCAAsC;AAGtC,IAAM,gBAAgB,CAAC,UAA6C;AAClE,UAAI,uDAAsB,KAAK,GAAG;AAChC,WAAO,cAAc,OAAO,EAAE,WAAW,cAAc,CAAC;AAAA,EAC1D;AAGA,SAAO,gBAAgB,KAAK;AAC9B;AAEA,cAAc,cAAc;AAC5B,IAAO,wBAAQ;;;AKzBf,IAAO,kBAAQ;;;ANDf,IAAO,gBAAQ;","names":["React","memoize","Container","AbbrContainer","Tooltip","styled","Text","React","import_seeds_react_text","import_jsx_runtime","Text"]}
|
package/dist/numeral.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Numeral component classes
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
6
|
+
* CSS variable definitions and dark mode support.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <span class="seeds-numeral">1,234</span>
|
|
10
|
+
* <span class="seeds-numeral seeds-numeral-abbr">1.2K</span>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.seeds-numeral {
|
|
15
|
+
font-variant-numeric: tabular-nums;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.seeds-numeral-abbr {
|
|
19
|
+
border-bottom: 1px dotted var(--color-container-border-base);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-numeral",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.52",
|
|
4
4
|
"description": "Seeds React Numeral",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"module": "dist/esm/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsup --dts",
|
|
12
|
-
"build:debug": "tsup --dts --metafile",
|
|
11
|
+
"build": "tsup --dts && cp src/numeral.css dist/numeral.css",
|
|
12
|
+
"build:debug": "tsup --dts --metafile && cp src/numeral.css dist/numeral.css",
|
|
13
13
|
"dev": "tsup --watch --dts",
|
|
14
14
|
"clean": "rm -rf .turbo dist",
|
|
15
15
|
"clean:modules": "rm -rf node_modules",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"test:watch": "jest --watch --coverage=false"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@sproutsocial/seeds-react-theme": "^4.2.
|
|
22
|
-
"@sproutsocial/seeds-react-system-props": "^3.1.
|
|
23
|
-
"@sproutsocial/seeds-react-tooltip": "^1.1.
|
|
24
|
-
"@sproutsocial/seeds-react-text": "^1.5.
|
|
25
|
-
"@sproutsocial/seeds-react-visually-hidden": "^1.0.
|
|
21
|
+
"@sproutsocial/seeds-react-theme": "^4.2.1",
|
|
22
|
+
"@sproutsocial/seeds-react-system-props": "^3.1.2",
|
|
23
|
+
"@sproutsocial/seeds-react-tooltip": "^1.1.31",
|
|
24
|
+
"@sproutsocial/seeds-react-text": "^1.5.1",
|
|
25
|
+
"@sproutsocial/seeds-react-visually-hidden": "^1.0.26",
|
|
26
26
|
"lru-memoize": "~1.0.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/src/Numeral.stories.tsx
CHANGED
|
@@ -167,3 +167,24 @@ export const AbbreviatedNegative: Story = {
|
|
|
167
167
|
abbreviate: true,
|
|
168
168
|
},
|
|
169
169
|
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Demonstrates that Tailwind utility classes override the component's own
|
|
173
|
+
* `numeral.css` rules. Because those rules now live in `@layer components`,
|
|
174
|
+
* plain utilities like `mx-200`, `p-300`, and `inline-flex` win the cascade
|
|
175
|
+
* and visibly take effect on the rendered numeral.
|
|
176
|
+
*
|
|
177
|
+
* `color` is cleared (it is set as a default in `meta.args`); any styled-system
|
|
178
|
+
* prop would route Numeral onto the styled-components path, where the
|
|
179
|
+
* `seeds-numeral` class is never emitted and the override demo would prove
|
|
180
|
+
* nothing. Plain utility classes do not match `SC_CLASS_PATTERN` (`sc-` prefix
|
|
181
|
+
* required), so passing them via `className` keeps the component on the
|
|
182
|
+
* Tailwind path.
|
|
183
|
+
*/
|
|
184
|
+
export const TailwindOverrides: Story = {
|
|
185
|
+
args: {
|
|
186
|
+
number: 1234.5,
|
|
187
|
+
color: undefined,
|
|
188
|
+
className: "mx-200 p-300 inline-flex bg-decorative-blue-100",
|
|
189
|
+
},
|
|
190
|
+
};
|
package/src/Numeral.tsx
CHANGED
|
@@ -1,208 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
// @ts-expect-error lru-memoize is not typed
|
|
3
|
-
import memoize from "lru-memoize";
|
|
4
|
-
import { EM_DASH } from "./constants";
|
|
5
|
-
import Tooltip from "@sproutsocial/seeds-react-tooltip";
|
|
6
|
-
import type { TypeTextProps } from "@sproutsocial/seeds-react-text";
|
|
7
|
-
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
1
|
+
import NumeralHybrid from "./NumeralHybrid";
|
|
8
2
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
MEMO_CACHE_SIZE,
|
|
12
|
-
COMPARE_OBJECTS,
|
|
13
|
-
MAX_PRECISION,
|
|
14
|
-
ABBREV_PRECISION,
|
|
15
|
-
DefaultPrecisions,
|
|
16
|
-
} from "./constants";
|
|
17
|
-
import { AbbrContainer, Container } from "./styles";
|
|
18
|
-
import type { EnumNumeralFormat, TypeNumeralProps } from "./NumeralTypes";
|
|
19
|
-
|
|
20
|
-
interface TypeFormatOptions {
|
|
21
|
-
locale: string;
|
|
22
|
-
format: EnumNumeralFormat;
|
|
23
|
-
currency: string;
|
|
24
|
-
min: number;
|
|
25
|
-
max: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface TypeFormatters {
|
|
29
|
-
standard: Intl.NumberFormat;
|
|
30
|
-
abbreviated: Intl.NumberFormat;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface TypeArgs {
|
|
34
|
-
value: number;
|
|
35
|
-
canAbbreviate: boolean;
|
|
36
|
-
invalidNumberLabel?: string;
|
|
37
|
-
ariaLabel?: string;
|
|
38
|
-
options: TypeFormatOptions;
|
|
39
|
-
qa: object | null | undefined;
|
|
40
|
-
rest: Omit<TypeTextProps, "children">;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {
|
|
44
|
-
const { locale, format, currency, min, max } = options;
|
|
45
|
-
const compactPrecision = min === max ? min : ABBREV_PRECISION;
|
|
46
|
-
|
|
47
|
-
const _currency = format === "currency" ? currency : undefined;
|
|
48
|
-
|
|
49
|
-
const standard: Intl.NumberFormatOptions = {
|
|
50
|
-
style: format,
|
|
51
|
-
minimumFractionDigits: min,
|
|
52
|
-
maximumFractionDigits: max,
|
|
53
|
-
currency: _currency,
|
|
54
|
-
};
|
|
55
|
-
const compact: Intl.NumberFormatOptions = {
|
|
56
|
-
style: format,
|
|
57
|
-
minimumFractionDigits: compactPrecision,
|
|
58
|
-
maximumFractionDigits: compactPrecision,
|
|
59
|
-
currency: _currency,
|
|
60
|
-
notation: "compact",
|
|
61
|
-
};
|
|
62
|
-
// Safari 14.1 is currently throwing errors when trying to use the compact
|
|
63
|
-
// options of NumberFormat
|
|
64
|
-
// https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687
|
|
65
|
-
let abbreviated;
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
abbreviated = new Intl.NumberFormat(locale, compact);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
abbreviated = new Intl.NumberFormat(locale, standard);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
standard: new Intl.NumberFormat(locale, standard),
|
|
75
|
-
abbreviated,
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Memoize to reduce the energy of creating new instances of Intl.NumberFormat
|
|
80
|
-
const memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);
|
|
81
|
-
const getNumberFormatters = memoizer(_getNumberFormatters);
|
|
82
|
-
|
|
83
|
-
const getThreshold = (abbreviate: boolean | number): number => {
|
|
84
|
-
if (typeof abbreviate === "number")
|
|
85
|
-
return Math.max(1000, Math.abs(abbreviate));
|
|
86
|
-
if (abbreviate) return DEFAULT_THRESHOLD;
|
|
87
|
-
return Infinity;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const getMinMaxPrecision = (
|
|
91
|
-
precision: TypeNumeralProps["precision"],
|
|
92
|
-
format: EnumNumeralFormat
|
|
93
|
-
): [number, number] => {
|
|
94
|
-
if (typeof precision === "number") return [precision, precision];
|
|
95
|
-
if (precision === "none") return [0, MAX_PRECISION];
|
|
96
|
-
return DefaultPrecisions[format];
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const isValidNumber = (value: unknown): boolean => {
|
|
100
|
-
return typeof value === "number" && isFinite(value);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const normalizeArgs = (props: TypeNumeralProps): TypeArgs => {
|
|
104
|
-
const {
|
|
105
|
-
number,
|
|
106
|
-
locale = "us-EN",
|
|
107
|
-
format = props.currency ? "currency" : "decimal",
|
|
108
|
-
currency = "USD",
|
|
109
|
-
abbreviate = true,
|
|
110
|
-
invalidNumberLabel,
|
|
111
|
-
precision,
|
|
112
|
-
qa,
|
|
113
|
-
...rest
|
|
114
|
-
} = props;
|
|
115
|
-
const threshold = getThreshold(abbreviate);
|
|
116
|
-
const [min, max] = getMinMaxPrecision(precision, format);
|
|
117
|
-
|
|
118
|
-
const _number = number || 0;
|
|
119
|
-
|
|
120
|
-
const value = _number * (format === "percent" ? 0.01 : 1);
|
|
121
|
-
const canAbbreviate = Math.abs(_number) >= threshold;
|
|
122
|
-
const options = {
|
|
123
|
-
locale,
|
|
124
|
-
format,
|
|
125
|
-
currency,
|
|
126
|
-
min,
|
|
127
|
-
max,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
value,
|
|
132
|
-
canAbbreviate,
|
|
133
|
-
invalidNumberLabel,
|
|
134
|
-
options,
|
|
135
|
-
qa,
|
|
136
|
-
rest,
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const getNumeral = ({
|
|
141
|
-
returnType,
|
|
142
|
-
props,
|
|
143
|
-
}: {
|
|
144
|
-
returnType: "string" | "component";
|
|
145
|
-
props: TypeNumeralProps;
|
|
146
|
-
}): string | React.ReactNode => {
|
|
147
|
-
const isReturnTypeString = returnType === "string";
|
|
148
|
-
const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =
|
|
149
|
-
normalizeArgs(props);
|
|
150
|
-
|
|
151
|
-
if (!isValidNumber(props.number)) {
|
|
152
|
-
return isReturnTypeString ? (
|
|
153
|
-
EM_DASH
|
|
154
|
-
) : (
|
|
155
|
-
<>
|
|
156
|
-
{invalidNumberLabel && (
|
|
157
|
-
// Give screen readers something useful to read off + hide the em dash
|
|
158
|
-
<VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>
|
|
159
|
-
)}
|
|
160
|
-
<Container aria-hidden {...qa}>
|
|
161
|
-
{EM_DASH}
|
|
162
|
-
</Container>
|
|
163
|
-
</>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const formatters = getNumberFormatters(options);
|
|
168
|
-
const fullText = formatters.standard.format(value);
|
|
169
|
-
|
|
170
|
-
if (canAbbreviate) {
|
|
171
|
-
const abbreviatedText = formatters.abbreviated.format(value);
|
|
172
|
-
|
|
173
|
-
// The following are used to debug the skipped tests which are misbehaving!!!
|
|
174
|
-
// console.log({ fullText, abbreviatedText });
|
|
175
|
-
// console.log({ abbreviated: formatters.abbreviated.resolvedOptions() });
|
|
176
|
-
// The following check is necessary because each locale may have differing thresholds
|
|
177
|
-
// for which abbreviation begins.
|
|
178
|
-
if (abbreviatedText !== fullText) {
|
|
179
|
-
return isReturnTypeString ? (
|
|
180
|
-
abbreviatedText
|
|
181
|
-
) : (
|
|
182
|
-
<Tooltip content={fullText}>
|
|
183
|
-
<AbbrContainer {...qa} {...rest}>
|
|
184
|
-
{abbreviatedText}
|
|
185
|
-
</AbbrContainer>
|
|
186
|
-
</Tooltip>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return isReturnTypeString ? (
|
|
192
|
-
fullText
|
|
193
|
-
) : (
|
|
194
|
-
<Container {...qa} {...rest}>
|
|
195
|
-
{fullText}
|
|
196
|
-
</Container>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export const formatNumeral = (props: TypeNumeralProps): string => {
|
|
201
|
-
return getNumeral({ returnType: "string", props }) as string;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const Numeral = (props: TypeNumeralProps) => {
|
|
205
|
-
return getNumeral({ returnType: "component", props });
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
export default Numeral;
|
|
3
|
+
export { formatNumeral } from "./getNumeral";
|
|
4
|
+
export default NumeralHybrid;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybrid Numeral Component
|
|
3
|
+
* Automatically chooses between Tailwind and styled-components based on props.
|
|
4
|
+
*
|
|
5
|
+
* Routes to styled-components when the consumer uses styled-system props or a
|
|
6
|
+
* styled(Numeral) extension (which injects a `theme` prop); otherwise renders
|
|
7
|
+
* the lighter Tailwind path. Gated on needsStyledComponents — a superset of
|
|
8
|
+
* Button's hasStyledProps gate — so a styled(Numeral) extension reliably hits
|
|
9
|
+
* the SC path, as the ticket's DoD requires.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import { renderNumeral } from "./getNumeral";
|
|
14
|
+
import { Container, AbbrContainer } from "./styles"; // Styled-components version
|
|
15
|
+
import { NumeralTailwind } from "./NumeralTailwind"; // Tailwind version
|
|
16
|
+
import { needsStyledComponents } from "@sproutsocial/seeds-react-system-props";
|
|
17
|
+
import type { TypeNumeralProps } from "./NumeralTypes";
|
|
18
|
+
|
|
19
|
+
const NumeralHybrid = (props: TypeNumeralProps): React.ReactNode => {
|
|
20
|
+
if (needsStyledComponents(props)) {
|
|
21
|
+
return renderNumeral(props, { Container, AbbrContainer });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use Tailwind version (preferred - better performance)
|
|
25
|
+
return NumeralTailwind(props);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
NumeralHybrid.displayName = "Numeral";
|
|
29
|
+
export default NumeralHybrid;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS implementation of Numeral component
|
|
3
|
+
* Uses Seeds numeral CSS classes from numeral.css
|
|
4
|
+
*
|
|
5
|
+
* NOTE: mirror Button — do NOT `import "./numeral.css"`. The CSS ships via a
|
|
6
|
+
* build-step copy (cp src/numeral.css dist/numeral.css) and the global racine
|
|
7
|
+
* aggregation, not via a source-level import.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import Text from "@sproutsocial/seeds-react-text";
|
|
12
|
+
import { renderNumeral } from "./getNumeral";
|
|
13
|
+
import type { TypeNumeralProps } from "./NumeralTypes";
|
|
14
|
+
|
|
15
|
+
// Utility for merging class names properly
|
|
16
|
+
function cn(
|
|
17
|
+
...inputs: (string | undefined | null | false | Record<string, boolean>)[]
|
|
18
|
+
): string {
|
|
19
|
+
const classes: string[] = [];
|
|
20
|
+
|
|
21
|
+
for (const input of inputs) {
|
|
22
|
+
if (!input) continue;
|
|
23
|
+
|
|
24
|
+
if (typeof input === "string") {
|
|
25
|
+
classes.push(input);
|
|
26
|
+
} else if (typeof input === "object") {
|
|
27
|
+
for (const [key, value] of Object.entries(input)) {
|
|
28
|
+
if (value) {
|
|
29
|
+
classes.push(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return classes.join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Plain Text wrappers carrying the numeral.css classes. Text forwards
|
|
39
|
+
// className and the remaining props (incl. non-styled typography props like
|
|
40
|
+
// fontSize/fontWeight) to its DOM span, so the classes land on the span.
|
|
41
|
+
const TwContainer = ({ className, children, ...rest }: any) => (
|
|
42
|
+
<Text className={cn("seeds-numeral", className)} {...rest}>
|
|
43
|
+
{children}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const TwAbbrContainer = ({ className, children, ...rest }: any) => (
|
|
48
|
+
<Text
|
|
49
|
+
className={cn("seeds-numeral", "seeds-numeral-abbr", className)}
|
|
50
|
+
{...rest}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</Text>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const NumeralTailwind = (props: TypeNumeralProps): React.ReactNode =>
|
|
57
|
+
renderNumeral(props, {
|
|
58
|
+
Container: TwContainer,
|
|
59
|
+
AbbrContainer: TwAbbrContainer,
|
|
60
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-env jest, node */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
import { render } from "@sproutsocial/seeds-react-testing-library";
|
|
5
|
+
import Numeral, { formatNumeral } from "../../Numeral";
|
|
6
|
+
|
|
7
|
+
jest.mock("../../constants.ts", () => ({
|
|
8
|
+
...jest.requireActual("../../constants.ts"),
|
|
9
|
+
MEMO_CACHE_SIZE: 0,
|
|
10
|
+
COMPARE_OBJECTS: false,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("Numeral hybrid routing", () => {
|
|
14
|
+
it("renders the Tailwind path (seeds-numeral class) for a plain numeral", () => {
|
|
15
|
+
const { container } = render(<Numeral number={5} />);
|
|
16
|
+
expect(container.querySelector(".seeds-numeral")).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders the Tailwind abbreviated branch with a tooltip", () => {
|
|
20
|
+
const { container } = render(<Numeral number={12345} abbreviate />);
|
|
21
|
+
expect(container.querySelector(".seeds-numeral-abbr")).toBeTruthy();
|
|
22
|
+
expect(container.querySelector("[data-qa-tooltip]")).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("routes to the styled-components path when a styled-system prop is used", () => {
|
|
26
|
+
const { container } = render(<Numeral number={5} color="teal.500" />);
|
|
27
|
+
expect(container.querySelector(".seeds-numeral")).toBeFalsy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("routes to the styled-components path for a styled(Numeral) extension", () => {
|
|
31
|
+
const Extended = styled(Numeral)``;
|
|
32
|
+
const { container } = render(<Extended number={5} />);
|
|
33
|
+
expect(container.querySelector(".seeds-numeral")).toBeFalsy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps a plain utility className on the Tailwind path and applies it to the seeds-numeral element", () => {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<Numeral number={5} className="mx-200 p-300 inline-flex" />
|
|
39
|
+
);
|
|
40
|
+
const el = container.querySelector(".seeds-numeral");
|
|
41
|
+
expect(el).toBeTruthy();
|
|
42
|
+
expect(el).toHaveClass("mx-200", "p-300", "inline-flex");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("keeps formatNumeral working as a named export", () => {
|
|
46
|
+
expect(formatNumeral({ number: 12345 })).toEqual("12.35K");
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/css.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
// @ts-expect-error lru-memoize is not typed
|
|
3
|
+
import memoize from "lru-memoize";
|
|
4
|
+
import { EM_DASH } from "./constants";
|
|
5
|
+
import Tooltip from "@sproutsocial/seeds-react-tooltip";
|
|
6
|
+
import type { TypeTextProps } from "@sproutsocial/seeds-react-text";
|
|
7
|
+
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_THRESHOLD,
|
|
11
|
+
MEMO_CACHE_SIZE,
|
|
12
|
+
COMPARE_OBJECTS,
|
|
13
|
+
MAX_PRECISION,
|
|
14
|
+
ABBREV_PRECISION,
|
|
15
|
+
DefaultPrecisions,
|
|
16
|
+
} from "./constants";
|
|
17
|
+
import type { EnumNumeralFormat, TypeNumeralProps } from "./NumeralTypes";
|
|
18
|
+
|
|
19
|
+
interface TypeFormatOptions {
|
|
20
|
+
locale: string;
|
|
21
|
+
format: EnumNumeralFormat;
|
|
22
|
+
currency: string;
|
|
23
|
+
min: number;
|
|
24
|
+
max: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TypeFormatters {
|
|
28
|
+
standard: Intl.NumberFormat;
|
|
29
|
+
abbreviated: Intl.NumberFormat;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TypeArgs {
|
|
33
|
+
value: number;
|
|
34
|
+
canAbbreviate: boolean;
|
|
35
|
+
invalidNumberLabel?: string;
|
|
36
|
+
ariaLabel?: string;
|
|
37
|
+
options: TypeFormatOptions;
|
|
38
|
+
qa: object | null | undefined;
|
|
39
|
+
rest: Omit<TypeTextProps, "children">;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The two container components used to render a numeral. The SC path supplies
|
|
44
|
+
* the styled(Text) wrappers from ./styles; the Tailwind path supplies plain
|
|
45
|
+
* Text wrappers carrying the seeds-numeral CSS classes.
|
|
46
|
+
*/
|
|
47
|
+
export interface TypeNumeralContainers {
|
|
48
|
+
Container: React.ComponentType<any>;
|
|
49
|
+
AbbrContainer: React.ComponentType<any>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const _getNumberFormatters = (options: TypeFormatOptions): TypeFormatters => {
|
|
53
|
+
const { locale, format, currency, min, max } = options;
|
|
54
|
+
const compactPrecision = min === max ? min : ABBREV_PRECISION;
|
|
55
|
+
|
|
56
|
+
const _currency = format === "currency" ? currency : undefined;
|
|
57
|
+
|
|
58
|
+
const standard: Intl.NumberFormatOptions = {
|
|
59
|
+
style: format,
|
|
60
|
+
minimumFractionDigits: min,
|
|
61
|
+
maximumFractionDigits: max,
|
|
62
|
+
currency: _currency,
|
|
63
|
+
};
|
|
64
|
+
const compact: Intl.NumberFormatOptions = {
|
|
65
|
+
style: format,
|
|
66
|
+
minimumFractionDigits: compactPrecision,
|
|
67
|
+
maximumFractionDigits: compactPrecision,
|
|
68
|
+
currency: _currency,
|
|
69
|
+
notation: "compact",
|
|
70
|
+
};
|
|
71
|
+
// Safari 14.1 is currently throwing errors when trying to use the compact
|
|
72
|
+
// options of NumberFormat
|
|
73
|
+
// https://community.atlassian.com/t5/Trello-questions/Trello-stuck-at-loading-after-Safari-14-1-update-on-macOS-Mojave/qaq-p/1675577#M45687
|
|
74
|
+
let abbreviated;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
abbreviated = new Intl.NumberFormat(locale, compact);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
abbreviated = new Intl.NumberFormat(locale, standard);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
standard: new Intl.NumberFormat(locale, standard),
|
|
84
|
+
abbreviated,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Memoize to reduce the energy of creating new instances of Intl.NumberFormat
|
|
89
|
+
const memoizer = memoize(MEMO_CACHE_SIZE, COMPARE_OBJECTS);
|
|
90
|
+
const getNumberFormatters = memoizer(_getNumberFormatters);
|
|
91
|
+
|
|
92
|
+
const getThreshold = (abbreviate: boolean | number): number => {
|
|
93
|
+
if (typeof abbreviate === "number")
|
|
94
|
+
return Math.max(1000, Math.abs(abbreviate));
|
|
95
|
+
if (abbreviate) return DEFAULT_THRESHOLD;
|
|
96
|
+
return Infinity;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getMinMaxPrecision = (
|
|
100
|
+
precision: TypeNumeralProps["precision"],
|
|
101
|
+
format: EnumNumeralFormat
|
|
102
|
+
): [number, number] => {
|
|
103
|
+
if (typeof precision === "number") return [precision, precision];
|
|
104
|
+
if (precision === "none") return [0, MAX_PRECISION];
|
|
105
|
+
return DefaultPrecisions[format];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const isValidNumber = (value: unknown): boolean => {
|
|
109
|
+
return typeof value === "number" && isFinite(value);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const normalizeArgs = (props: TypeNumeralProps): TypeArgs => {
|
|
113
|
+
const {
|
|
114
|
+
number,
|
|
115
|
+
locale = "us-EN",
|
|
116
|
+
format = props.currency ? "currency" : "decimal",
|
|
117
|
+
currency = "USD",
|
|
118
|
+
abbreviate = true,
|
|
119
|
+
invalidNumberLabel,
|
|
120
|
+
precision,
|
|
121
|
+
qa,
|
|
122
|
+
...rest
|
|
123
|
+
} = props;
|
|
124
|
+
const threshold = getThreshold(abbreviate);
|
|
125
|
+
const [min, max] = getMinMaxPrecision(precision, format);
|
|
126
|
+
|
|
127
|
+
const _number = number || 0;
|
|
128
|
+
|
|
129
|
+
const value = _number * (format === "percent" ? 0.01 : 1);
|
|
130
|
+
const canAbbreviate = Math.abs(_number) >= threshold;
|
|
131
|
+
const options = {
|
|
132
|
+
locale,
|
|
133
|
+
format,
|
|
134
|
+
currency,
|
|
135
|
+
min,
|
|
136
|
+
max,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
value,
|
|
141
|
+
canAbbreviate,
|
|
142
|
+
invalidNumberLabel,
|
|
143
|
+
options,
|
|
144
|
+
qa,
|
|
145
|
+
rest,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Path-independent component renderer. Produces the three render branches
|
|
151
|
+
* (invalid, abbreviated-with-Tooltip, standard) using whichever container
|
|
152
|
+
* components are passed in. The SC and Tailwind paths share this logic so the
|
|
153
|
+
* formatting engine is never duplicated.
|
|
154
|
+
*/
|
|
155
|
+
export const renderNumeral = (
|
|
156
|
+
props: TypeNumeralProps,
|
|
157
|
+
{ Container, AbbrContainer }: TypeNumeralContainers
|
|
158
|
+
): React.ReactNode => {
|
|
159
|
+
const { value, canAbbreviate, invalidNumberLabel, options, qa, rest } =
|
|
160
|
+
normalizeArgs(props);
|
|
161
|
+
|
|
162
|
+
if (!isValidNumber(props.number)) {
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
{invalidNumberLabel && (
|
|
166
|
+
// Give screen readers something useful to read off + hide the em dash
|
|
167
|
+
<VisuallyHidden>{invalidNumberLabel}</VisuallyHidden>
|
|
168
|
+
)}
|
|
169
|
+
<Container aria-hidden {...qa}>
|
|
170
|
+
{EM_DASH}
|
|
171
|
+
</Container>
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const formatters = getNumberFormatters(options);
|
|
177
|
+
const fullText = formatters.standard.format(value);
|
|
178
|
+
|
|
179
|
+
if (canAbbreviate) {
|
|
180
|
+
const abbreviatedText = formatters.abbreviated.format(value);
|
|
181
|
+
|
|
182
|
+
// The following check is necessary because each locale may have differing thresholds
|
|
183
|
+
// for which abbreviation begins.
|
|
184
|
+
if (abbreviatedText !== fullText) {
|
|
185
|
+
return (
|
|
186
|
+
<Tooltip content={fullText}>
|
|
187
|
+
<AbbrContainer {...qa} {...rest}>
|
|
188
|
+
{abbreviatedText}
|
|
189
|
+
</AbbrContainer>
|
|
190
|
+
</Tooltip>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<Container {...qa} {...rest}>
|
|
197
|
+
{fullText}
|
|
198
|
+
</Container>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const formatNumeral = (props: TypeNumeralProps): string => {
|
|
203
|
+
const { value, canAbbreviate, options } = normalizeArgs(props);
|
|
204
|
+
|
|
205
|
+
if (!isValidNumber(props.number)) {
|
|
206
|
+
return EM_DASH;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const formatters = getNumberFormatters(options);
|
|
210
|
+
const fullText = formatters.standard.format(value);
|
|
211
|
+
|
|
212
|
+
if (canAbbreviate) {
|
|
213
|
+
const abbreviatedText = formatters.abbreviated.format(value);
|
|
214
|
+
if (abbreviatedText !== fullText) {
|
|
215
|
+
return abbreviatedText;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return fullText;
|
|
220
|
+
};
|
package/src/numeral.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Numeral component classes
|
|
3
|
+
* Use these instead of writing out individual Tailwind utility classes.
|
|
4
|
+
*
|
|
5
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
6
|
+
* CSS variable definitions and dark mode support.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <span class="seeds-numeral">1,234</span>
|
|
10
|
+
* <span class="seeds-numeral seeds-numeral-abbr">1.2K</span>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.seeds-numeral {
|
|
15
|
+
font-variant-numeric: tabular-nums;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.seeds-numeral-abbr {
|
|
19
|
+
border-bottom: 1px dotted var(--color-container-border-base);
|
|
20
|
+
}
|
|
21
|
+
}
|