@sproutsocial/seeds-react-numeral 1.0.49 → 1.0.51

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.
@@ -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
  CLI Building entry: src/index.ts
4
4
  CLI Using tsconfig: tsconfig.json
5
5
  CLI tsup v8.5.0
@@ -8,14 +8,14 @@ $ tsup --dts
8
8
  CLI Cleaning output folder
9
9
  CJS Build start
10
10
  ESM Build start
11
- CJS dist/index.js 6.55 KB
12
- CJS dist/index.js.map 9.75 KB
13
- CJS ⚡️ Build success in 14ms
14
- ESM dist/esm/index.js 4.44 KB
15
- ESM dist/esm/index.js.map 9.64 KB
16
- ESM ⚡️ Build success in 15ms
11
+ CJS dist/index.js 8.30 KB
12
+ CJS dist/index.js.map 14.48 KB
13
+ CJS ⚡️ Build success in 37ms
14
+ ESM dist/esm/index.js 5.98 KB
15
+ ESM dist/esm/index.js.map 14.33 KB
16
+ ESM ⚡️ Build success in 37ms
17
17
  DTS Build start
18
- DTS ⚡️ Build success in 1884ms
19
- DTS dist/index.d.ts 1.29 KB
20
- DTS dist/index.d.mts 1.29 KB
21
- Done in 2.71s.
18
+ DTS ⚡️ Build success in 2296ms
19
+ DTS dist/index.d.ts 1.82 KB
20
+ DTS dist/index.d.mts 1.82 KB
21
+ Done in 3.11s.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @sproutsocial/seeds-react-numeral
2
2
 
3
+ ## 1.0.51
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+ - Updated dependencies [6611f6a]
9
+ - @sproutsocial/seeds-react-system-props@3.1.2
10
+ - @sproutsocial/seeds-react-tooltip@1.1.30
11
+ - @sproutsocial/seeds-react-text@1.5.1
12
+ - @sproutsocial/seeds-react-visually-hidden@1.0.25
13
+
14
+ ## 1.0.50
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [85f9728]
19
+ - Updated dependencies [fa7d859]
20
+ - @sproutsocial/seeds-react-theme@4.2.0
21
+ - @sproutsocial/seeds-react-tooltip@1.1.29
22
+ - @sproutsocial/seeds-react-visually-hidden@1.0.24
23
+
3
24
  ## 1.0.49
4
25
 
5
26
  ### Patch Changes
package/dist/esm/index.js CHANGED
@@ -1,4 +1,7 @@
1
- // src/Numeral.tsx
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/Numeral.tsx
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 getNumeral = ({
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 isReturnTypeString ? EM_DASH : /* @__PURE__ */ jsxs(Fragment, { children: [
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(Container, { "aria-hidden": true, ...qa, children: EM_DASH })
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 isReturnTypeString ? abbreviatedText : /* @__PURE__ */ jsx(Tooltip, { content: fullText, children: /* @__PURE__ */ jsx(AbbrContainer, { ...qa, ...rest, children: abbreviatedText }) });
116
+ return /* @__PURE__ */ jsx(Tooltip, { content: fullText, children: /* @__PURE__ */ jsx(AbbrContainer2, { ...qa, ...rest, children: abbreviatedText }) });
132
117
  }
133
118
  }
134
- return isReturnTypeString ? fullText : /* @__PURE__ */ jsx(Container, { ...qa, ...rest, children: fullText });
119
+ return /* @__PURE__ */ jsx(Container2, { ...qa, ...rest, children: fullText });
135
120
  };
136
121
  var formatNumeral = (props) => {
137
- return getNumeral({ returnType: "string", props });
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
- var Numeral = (props) => {
140
- return getNumeral({ returnType: "component", props });
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
- var Numeral_default = Numeral;
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;
@@ -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, Numeral as default, formatNumeral };
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, Numeral as default, formatNumeral };
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/Numeral.tsx
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/Numeral.tsx
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 getNumeral = ({
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 isReturnTypeString ? EM_DASH : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
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)(Container, { "aria-hidden": true, ...qa, children: EM_DASH })
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 isReturnTypeString ? abbreviatedText : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_tooltip.default, { content: fullText, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AbbrContainer, { ...qa, ...rest, children: abbreviatedText }) });
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 isReturnTypeString ? fullText : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Container, { ...qa, ...rest, children: fullText });
157
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Container2, { ...qa, ...rest, children: fullText });
173
158
  };
174
159
  var formatNumeral = (props) => {
175
- return getNumeral({ returnType: "string", props });
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
- var Numeral = (props) => {
178
- return getNumeral({ returnType: "component", props });
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
- var Numeral_default = Numeral;
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":"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","names":["React","memoize","Container","AbbrContainer","Tooltip","styled","Text","React","import_seeds_react_text","import_jsx_runtime","Text"]}
@@ -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.49",
3
+ "version": "1.0.51",
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.1.1",
22
- "@sproutsocial/seeds-react-system-props": "^3.1.1",
23
- "@sproutsocial/seeds-react-tooltip": "^1.1.28",
24
- "@sproutsocial/seeds-react-text": "^1.5.0",
25
- "@sproutsocial/seeds-react-visually-hidden": "^1.0.23",
21
+ "@sproutsocial/seeds-react-theme": "^4.2.0",
22
+ "@sproutsocial/seeds-react-system-props": "^3.1.2",
23
+ "@sproutsocial/seeds-react-tooltip": "^1.1.30",
24
+ "@sproutsocial/seeds-react-text": "^1.5.1",
25
+ "@sproutsocial/seeds-react-visually-hidden": "^1.0.25",
26
26
  "lru-memoize": "~1.0.2"
27
27
  },
28
28
  "devDependencies": {
@@ -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 * 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";
1
+ import NumeralHybrid from "./NumeralHybrid";
8
2
 
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 { 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
+ };
@@ -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
+ }