diff --git a/docs/core/shared/_useLive.mdx b/docs/core/shared/_useLive.mdx index 0c9528ff6c44..eddd0cf97324 100644 --- a/docs/core/shared/_useLive.mdx +++ b/docs/core/shared/_useLive.mdx @@ -1,6 +1,6 @@ import HooksPlayground from '@site/src/components/HooksPlayground'; - + ```typescript title="Ticker" {32} collapsed import { Entity, RestEndpoint } from '@data-client/rest'; @@ -47,7 +47,10 @@ function AssetPrice({ productId }: Props) { return (
{productId}{' '} - +
); } diff --git a/docs/rest/guides/network-transform.md b/docs/rest/guides/network-transform.md index 5100771b120e..ee0a6279c131 100644 --- a/docs/rest/guides/network-transform.md +++ b/docs/rest/guides/network-transform.md @@ -249,7 +249,10 @@ function AssetPrice({ productId }: Props) { return (
{productId}{' '} - +
); } diff --git a/examples/nextjs/components/AssetPrice.tsx b/examples/nextjs/components/AssetPrice.tsx index 137721efbfcc..627291eb9147 100644 --- a/examples/nextjs/components/AssetPrice.tsx +++ b/examples/nextjs/components/AssetPrice.tsx @@ -1,17 +1,20 @@ 'use client'; import { useLive } from '@data-client/react'; +import NumberFlow from '@number-flow/react'; import { getTicker } from '@/resources/Ticker'; -import { Formatted } from './Formatted'; - export default function AssetPrice({ symbol }: Props) { const product_id = `${symbol}-USD`; // Learn more about Reactive Data Client: https://dataclient.io/docs const ticker = useLive(getTicker, { product_id }); return ( - {symbol} + {symbol}{' '} + ); } diff --git a/examples/nextjs/components/Formatted.tsx b/examples/nextjs/components/Formatted.tsx deleted file mode 100644 index 87ef52beb5d2..000000000000 --- a/examples/nextjs/components/Formatted.tsx +++ /dev/null @@ -1,133 +0,0 @@ -'use client'; -import clsx from 'clsx'; -import React from 'react'; - -import { formatters, Formatter } from './formatters'; - -export enum FlashDirection { - Down = 'down', - Up = 'up', -} - -export interface Props { - /** - * Color value when the component flashes 'down'. - */ - downColor?: string; - /** - * One of the built in formatters. - */ - formatter?: 'currency' | 'percentage' | 'number'; - /** - * Pass your own formatter function. - */ - formatterFn?: Formatter; - /** - * Prefix for the CSS selectors in the DOM. - */ - stylePrefix?: string; - /** - * Amount of time the flashed state is visible for, in milliseconds. - */ - timeout?: number; - /** - * Custom CSS transition property. - */ - transition?: string; - /** - * Transition length, in milliseconds. - */ - transitionLength?: number; - /** - * Color value when the component flashes 'up'. - */ - upColor?: string; - /** - * Value to display. The only required prop. - */ - value: number; -} - -/** - * Flash component. - * - * `react-value-flash` will display a flashed value on screen based - * on some value change. This pattern is extremely common in financial - * applications, and at Lab49, we're focused on the finance industry. - * - * Incorporate this component into your application and pass along a - * number. As that number changes, this component will briefly flash - * a color, letting the user know the number has changed. By default, - * this component will flash green when the value changes up, or red - * when the value changes down. - * - * Not only are these colors configurable, but the properties of the - * flash itself and the formatting of the value are configurable as well. - * - * Furthermore, this component doesn't come with any styles, but does - * provide plenty of hooks to add your own styles. Even though flash - * color and transition properties are configurable as props, you can - * still use the generated classnames (which are also configurable) to - * add your own unique styles. - */ -export const Formatted = ({ - downColor = '#d43215', - formatter, - formatterFn, - timeout = 300, - transition, - transitionLength = 300, - upColor = '#00d865', - value, - stylePrefix = 'rvf_Flash', -}: Props) => { - const ref = React.useRef(value); - const [flash, setFlash] = React.useState(null); - const style = { - transition: - transition || `background-color ${transitionLength}ms ease-in-out`, - ...(flash ? - { backgroundColor: flash === FlashDirection.Up ? upColor : downColor } - : null), - }; - const cls = clsx(stylePrefix, { - [`${stylePrefix}--flashing`]: flash != null, - [`${stylePrefix}--flashing-${flash}`]: flash != null, - [`${stylePrefix}--even`]: value === 0, - [`${stylePrefix}--negative`]: value < 0, - [`${stylePrefix}--positive`]: value > 0, - }); - const valueFormatter = - formatterFn ?? (formatter ? formatters[formatter] : formatters.default); - - React.useEffect(() => { - // If there's no change, only reset (this prevents flash on first render). - // TODO (brianmcallister) - Which, maybe, people might want? - if (ref.current === value) { - setFlash(null); - - return () => {}; - } - - // Set the flash direction. - setFlash(value > ref.current ? FlashDirection.Up : FlashDirection.Down); - - // Reset the flash state after `timeout`. - const timeoutInterval = setTimeout(() => { - setFlash(null); - }, timeout); - - // Update the ref to reflect the new `value`. - ref.current = value; - - return () => { - clearTimeout(timeoutInterval); - }; - }, [value, timeout]); - - return ( - - {valueFormatter(value)} - - ); -}; diff --git a/examples/nextjs/components/formatters/index.ts b/examples/nextjs/components/formatters/index.ts deleted file mode 100644 index 5dcb27e76474..000000000000 --- a/examples/nextjs/components/formatters/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Props } from '../Formatted'; - -export type Formatter = (value: Props['value']) => string; - -type Formatters = { - [K in Extract]: (value: Props['value']) => string; -} & { - default: Formatter; -}; - -// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat -export const formatPrice = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}); -export const formatLargePrice = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - notation: 'compact', - maximumSignificantDigits: 4, - minimumSignificantDigits: 4, -}); - -const numberFormatter = (value: number) => - Intl.NumberFormat('en').format(value); - -const currencyFormatter = (value: number) => formatPrice.format(value); - -const percentageFormatter = (value: number) => - Intl.NumberFormat('en', { - style: 'percent', - // See: https://github.com/microsoft/TypeScript/issues/36533 - // @ts-ignore - signDisplay: 'exceptZero', - }).format(value); - -const defaultFormatter = (value: number) => `${value}`; - -export const formatters: Formatters = { - default: defaultFormatter, - number: numberFormatter, - currency: currencyFormatter, - percentage: percentageFormatter, -}; diff --git a/examples/nextjs/package-lock.json b/examples/nextjs/package-lock.json index 7b7531486a0d..5781bd594dae 100644 --- a/examples/nextjs/package-lock.json +++ b/examples/nextjs/package-lock.json @@ -12,6 +12,7 @@ "@babel/core": "^7.24.7", "@data-client/react": "^0.14.0", "@data-client/rest": "^0.14.0", + "@number-flow/react": "^0.2.1", "@types/node": "22.7.7", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", @@ -2592,6 +2593,19 @@ "node": ">= 10" } }, + "node_modules/@number-flow/react": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@number-flow/react/-/react-0.2.1.tgz", + "integrity": "sha512-gU1jIy6ORk7YhNiyrvOsiyfgewzw+a0X8xZRIn8b5Q3sJXRWZqxV3XkAoKh/XOuAASKo7I+i3LS6mOiM/LT3bg==", + "dependencies": { + "esm-env": "^1.0.0", + "number-flow": "0.3.0" + }, + "peerDependencies": { + "react": "^18 || ^19.0.0-rc-915b914b3a-20240515", + "react-dom": "^18" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -3056,6 +3070,11 @@ "node": ">=0.8.0" } }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3400,6 +3419,14 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, + "node_modules/number-flow": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.3.0.tgz", + "integrity": "sha512-Aog+XsC2App/t/nM8mICf5z+dHXAVUaBfWAb7YgPQX5XHxBc5O5ho6dZsvGEn6nfcI3Ty+NUCpqfQ9zDQ/5+rA==", + "dependencies": { + "esm-env": "^1.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index b55c3e648c3c..0f15fa4ad08f 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.24.7", "@data-client/react": "^0.14.0", "@data-client/rest": "^0.14.0", + "@number-flow/react": "^0.2.1", "@types/node": "22.7.7", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", diff --git a/website/package.json b/website/package.json index d1c91bee1c52..b960b6c2cd77 100644 --- a/website/package.json +++ b/website/package.json @@ -46,6 +46,7 @@ "@docusaurus/theme-mermaid": "^3.0.1", "@js-temporal/polyfill": "^0.4.4", "@monaco-editor/react": "^4.4.6", + "@number-flow/react": "^0.2.1", "bignumber.js": "9.1.2", "clsx": "2.1.1", "monaco-editor": "^0.52.0", diff --git a/website/src/components/Demo/code/live-app/polling/AssetPrice.tsx b/website/src/components/Demo/code/live-app/polling/AssetPrice.tsx index 2ba23ebf11a2..7cc25babfb89 100644 --- a/website/src/components/Demo/code/live-app/polling/AssetPrice.tsx +++ b/website/src/components/Demo/code/live-app/polling/AssetPrice.tsx @@ -6,8 +6,11 @@ export default function AssetPrice({ symbol }: { symbol: string }) { return ( {symbol} - - + + ); diff --git a/website/src/components/Playground/DesignSystem/index.ts b/website/src/components/Playground/DesignSystem/index.ts index 74323d70568b..2756e0caa366 100644 --- a/website/src/components/Playground/DesignSystem/index.ts +++ b/website/src/components/Playground/DesignSystem/index.ts @@ -7,3 +7,4 @@ export { TextInput } from './TextInput'; export { TextArea } from './TextArea'; export { SearchIcon } from './SearchIcon'; export { Loading } from './Loading'; +export { default as NumberFlow } from '@number-flow/react'; diff --git a/website/src/components/Playground/PreviewWithScope.tsx b/website/src/components/Playground/PreviewWithScope.tsx index e301f77945d1..3030e669f256 100644 --- a/website/src/components/Playground/PreviewWithScope.tsx +++ b/website/src/components/Playground/PreviewWithScope.tsx @@ -5,7 +5,6 @@ import * as rest from '@data-client/rest'; import type { Fixture, Interceptor } from '@data-client/test'; import { Temporal, Intl as PolyIntl } from '@js-temporal/polyfill'; import BigNumber from 'bignumber.js'; -import React from 'react'; import { LiveProvider } from 'react-live'; import { v4 as uuid } from 'uuid'; diff --git a/website/src/components/Playground/editor-types/@number-flow/react.d.ts b/website/src/components/Playground/editor-types/@number-flow/react.d.ts new file mode 100644 index 000000000000..bb45ec81105c --- /dev/null +++ b/website/src/components/Playground/editor-types/@number-flow/react.d.ts @@ -0,0 +1,59 @@ +import { NumberFlowLite, Value, Format } from 'number-flow'; +import * as React from 'react'; +export { Format, Trend, Value } from 'number-flow'; + +declare const OBSERVED_ATTRIBUTES: readonly ['parts']; +type ObservedAttribute = (typeof OBSERVED_ATTRIBUTES)[number]; +declare class NumberFlowElement extends NumberFlowLite { + static observedAttributes: readonly ['parts']; + attributeChangedCallback( + attr: ObservedAttribute, + _oldValue: string, + newValue: string, + ): void; +} +type NumberFlowProps = React.HTMLAttributes & { + value: Value; + locales?: Intl.LocalesArgument; + format?: Format; + isolate?: boolean; + animated?: boolean; + respectMotionPreference?: boolean; + willChange?: boolean; + onAnimationsStart?: () => void; + onAnimationsFinish?: () => void; + trend?: (typeof NumberFlowElement)['prototype']['trend']; + opacityTiming?: (typeof NumberFlowElement)['prototype']['opacityTiming']; + transformTiming?: (typeof NumberFlowElement)['prototype']['transformTiming']; + spinTiming?: (typeof NumberFlowElement)['prototype']['spinTiming']; +}; +declare const NumberFlow: ( + options: React.HTMLAttributes & { + value: Value; + locales?: Intl.LocalesArgument; + format?: Format; + isolate?: boolean; + animated?: boolean; + respectMotionPreference?: boolean; + willChange?: boolean; + onAnimationsStart?: () => void; + onAnimationsFinish?: () => void; + trend?: (typeof NumberFlowElement)['prototype']['trend']; + opacityTiming?: (typeof NumberFlowElement)['prototype']['opacityTiming']; + transformTiming?: (typeof NumberFlowElement)['prototype']['transformTiming']; + spinTiming?: (typeof NumberFlowElement)['prototype']['spinTiming']; + } & React.RefAttributes, +) => JSX.Element; + +declare function useCanAnimate({ + respectMotionPreference, +}?: { + respectMotionPreference?: boolean | undefined; +}): boolean; + +export { + NumberFlowElement, + type NumberFlowProps, + NumberFlow as default, + useCanAnimate, +}; diff --git a/website/src/components/Playground/monaco-init.ts b/website/src/components/Playground/monaco-init.ts index 15e12a96c76a..7c4002b1f991 100644 --- a/website/src/components/Playground/monaco-init.ts +++ b/website/src/components/Playground/monaco-init.ts @@ -52,6 +52,7 @@ if ( monaco.languages.typescript.ModuleResolutionKind.NodeJs, allowSyntheticDefaultImports: true, skipLibCheck: true, + skipDefaultLibCheck: true, noImplicitAny: false, }); // TODO: load theme from docusaurus config so we eliminate DRY violation @@ -214,6 +215,9 @@ if ( import( /* webpackChunkName: 'bignumberDTS', webpackPreload: true */ '!!raw-loader?esModule=false!./editor-types/bignumber.d.ts' ), + import( + /* webpackChunkName: 'bignumberDTS', webpackPreload: true */ '!!raw-loader?esModule=false!./editor-types/@number-flow/react.d.ts' + ), import( /* webpackChunkName: 'temporalDTS', webpackPreload: true */ '!!raw-loader?esModule=false!./editor-types/temporal.d.ts' ), @@ -235,10 +239,18 @@ if ( ]).then(([mPromise, ...settles]) => { if (mPromise.status !== 'fulfilled' || !mPromise.value) return; const monaco = mPromise.value; - const [react, bignumber, temporal, uuid, qs, globals, ...rhLibs] = - settles.map(result => - result.status === 'fulfilled' ? result.value.default : '', - ); + const [ + react, + bignumber, + numberFlow, + temporal, + uuid, + qs, + globals, + ...rhLibs + ] = settles.map(result => + result.status === 'fulfilled' ? result.value.default : '', + ); monaco.languages.typescript.typescriptDefaults.addExtraLib( `declare module "react/jsx-runtime" { @@ -318,6 +330,10 @@ if ( `declare module "bignumber.js" { ${bignumber} }`, 'file:///node_modules/bignumber.js/index.d.ts', ); + monaco.languages.typescript.typescriptDefaults.addExtraLib( + `declare module "@number-flow/react" { ${numberFlow} };`, + 'file:///node_modules/@number-flow/react/index.d.ts', + ); monaco.languages.typescript.typescriptDefaults.addExtraLib( `declare module "@js-temporal/polyfill" { ${temporal} }`, 'file:///node_modules/@js-temporal/polyfill/index.d.ts', @@ -333,6 +349,9 @@ if ( monaco.languages.typescript.typescriptDefaults.addExtraLib( `declare globals { ${react} }`, ); + monaco.languages.typescript.typescriptDefaults.addExtraLib( + `declare globals { export { default as NumberFlow } from '@number-flow/react'; }`, + ); monaco.languages.typescript.typescriptDefaults.addExtraLib( `declare globals { export { Temporal, DateTimeFormat } from '@js-temporal/polyfill'; }`, ); diff --git a/website/src/css/root.css b/website/src/css/root.css index bb85b462d013..df91888ec535 100644 --- a/website/src/css/root.css +++ b/website/src/css/root.css @@ -68,4 +68,5 @@ html { table { display: table; width: 100%; -} \ No newline at end of file + font-variant-numeric: tabular-nums; +} diff --git a/yarn.lock b/yarn.lock index a6e6b004fbbc..722996c9af74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5132,6 +5132,19 @@ __metadata: languageName: node linkType: hard +"@number-flow/react@npm:^0.2.1": + version: 0.2.1 + resolution: "@number-flow/react@npm:0.2.1" + dependencies: + esm-env: "npm:^1.0.0" + number-flow: "npm:0.3.0" + peerDependencies: + react: ^18 || ^19.0.0-rc-915b914b3a-20240515 + react-dom: ^18 + checksum: 10c0/ab8cfb6802416d65a21cafda06be171ef07b712829067c8d1da9a85b232076e8c5033981da25085c2de39a6a1ac4d377c72fc40fddde06c13b90750b6c0bdda8 + languageName: node + linkType: hard + "@octokit/auth-token@npm:^2.4.4": version: 2.5.0 resolution: "@octokit/auth-token@npm:2.5.0" @@ -13792,6 +13805,13 @@ __metadata: languageName: node linkType: hard +"esm-env@npm:^1.0.0": + version: 1.0.0 + resolution: "esm-env@npm:1.0.0" + checksum: 10c0/6ea0001410224ebc18de4a83ce97dbdca6abc83ea4bbe91625aa3aead70793bb98dfa089f38e2cc5c13b7b025668d0649d5e25f2f9e8cca0f4aa3ad3406870d0 + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.2.0": version: 10.2.0 resolution: "espree@npm:10.2.0" @@ -22187,6 +22207,15 @@ __metadata: languageName: node linkType: hard +"number-flow@npm:0.3.0": + version: 0.3.0 + resolution: "number-flow@npm:0.3.0" + dependencies: + esm-env: "npm:^1.0.0" + checksum: 10c0/bee143652fefea2a006fb228558d478169e93b9b39106da84c68c5b48c90a0d791974b92ae7deb8d1c0d64e5ad9dcba48b9472c3838aa2e86ae7a2a293c5d98f + languageName: node + linkType: hard + "number-is-nan@npm:^1.0.0": version: 1.0.1 resolution: "number-is-nan@npm:1.0.1" @@ -25171,6 +25200,7 @@ __metadata: "@docusaurus/theme-mermaid": "npm:^3.0.1" "@js-temporal/polyfill": "npm:^0.4.4" "@monaco-editor/react": "npm:^4.4.6" + "@number-flow/react": "npm:^0.2.1" "@tsconfig/docusaurus": "npm:^2.0.0" "@types/react": "npm:18.3.11" "@types/react-dom": "npm:^18.2.7"