Repository: barvian/number-flow Branch: main Commit: 04945dc4071d Files: 340 Total size: 376.9 KB Directory structure: gitextract_gla770jw/ ├── .changeset/ │ ├── README.md │ └── config.json ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 0-bug.yml │ │ ├── 1-docs.yml │ │ └── config.yml │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .vscode/ │ └── settings.json ├── LICENSE.md ├── README.md ├── lib/ │ └── playwright.ts ├── package.json ├── packages/ │ ├── number-flow/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── csp.ts │ │ │ ├── env.d.ts │ │ │ ├── formatter.ts │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── lite.ts │ │ │ ├── plugins/ │ │ │ │ ├── continuous.ts │ │ │ │ └── index.ts │ │ │ ├── ssr.ts │ │ │ ├── styles.ts │ │ │ └── util/ │ │ │ ├── dom.ts │ │ │ ├── iterable.ts │ │ │ ├── math.ts │ │ │ ├── string.ts │ │ │ └── types.ts │ │ ├── test/ │ │ │ └── apps/ │ │ │ └── astro/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── astro.config.mjs │ │ │ ├── package.json │ │ │ ├── playwright.config.ts │ │ │ ├── src/ │ │ │ │ ├── layouts/ │ │ │ │ │ └── Layout.astro │ │ │ │ ├── middleware.ts │ │ │ │ ├── pages/ │ │ │ │ │ ├── can-animate.astro │ │ │ │ │ ├── group-1-unchanged.astro │ │ │ │ │ ├── hashes.astro │ │ │ │ │ ├── index.astro │ │ │ │ │ ├── nonce.astro │ │ │ │ │ └── thrashing.astro │ │ │ │ └── styles/ │ │ │ │ └── global.css │ │ │ └── tsconfig.json │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── vite.config.mjs │ ├── react/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── NumberFlow.tsx │ │ │ └── index.tsx │ │ ├── test/ │ │ │ └── apps/ │ │ │ ├── react-18/ │ │ │ │ ├── .gitignore │ │ │ │ ├── app/ │ │ │ │ │ ├── can-animate/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── globals.css │ │ │ │ │ ├── group-1-unchanged/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── hashes/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── nonce/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── next.config.mjs │ │ │ │ ├── package.json │ │ │ │ ├── playwright.config.ts │ │ │ │ ├── postcss.config.mjs │ │ │ │ ├── tailwind.config.ts │ │ │ │ └── tsconfig.json │ │ │ └── react-19/ │ │ │ ├── .gitignore │ │ │ ├── app/ │ │ │ │ ├── can-animate/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── globals.css │ │ │ │ ├── group-1-unchanged/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── hashes/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── nonce/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── sc/ │ │ │ │ └── page.tsx │ │ │ ├── next.config.ts │ │ │ ├── package.json │ │ │ ├── playwright.config.ts │ │ │ ├── postcss.config.mjs │ │ │ ├── tailwind.config.ts │ │ │ └── tsconfig.json │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── svelte/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── app.css │ │ │ ├── app.d.ts │ │ │ ├── app.html │ │ │ ├── lib/ │ │ │ │ ├── NumberFlow.svelte │ │ │ │ ├── NumberFlowGroup.svelte │ │ │ │ ├── group.ts │ │ │ │ └── index.ts │ │ │ └── routes/ │ │ │ ├── +layout.svelte │ │ │ ├── +page.svelte │ │ │ ├── can-animate/ │ │ │ │ └── +page.svelte │ │ │ ├── group-1-unchanged/ │ │ │ │ └── +page.svelte │ │ │ ├── hashes/ │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ │ └── nonce/ │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── svelte.config.js │ │ ├── tailwind.config.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── vue/ │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── NumberFlowGroup.vue │ │ ├── group.ts │ │ ├── index.ts │ │ └── index.vue │ ├── test/ │ │ └── apps/ │ │ └── nuxt3/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── src/ │ │ │ ├── assets/ │ │ │ │ └── css/ │ │ │ │ └── main.css │ │ │ ├── pages/ │ │ │ │ ├── can-animate.vue │ │ │ │ ├── group-1-unchanged.vue │ │ │ │ ├── hashes.vue │ │ │ │ ├── index.vue │ │ │ │ └── nonce.vue │ │ │ └── server/ │ │ │ └── tsconfig.json │ │ └── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.mjs ├── pnpm-workspace.yaml ├── prettier.config.js ├── site/ │ ├── .gitignore │ ├── .vscode/ │ │ ├── extensions.json │ │ └── launch.json │ ├── astro.config.mjs │ ├── highlighter-theme.json │ ├── package.json │ ├── postcss.config.cjs │ ├── src/ │ │ ├── assets/ │ │ │ └── main.css │ │ ├── components/ │ │ │ ├── Alert.astro │ │ │ ├── AnimateHeightFragment.tsx │ │ │ ├── AnimationsOnTheWeb.astro │ │ │ ├── Code.astro │ │ │ ├── Comp.mdx │ │ │ ├── Demo.tsx │ │ │ ├── FrameworkMenu.tsx │ │ │ ├── Freeze.tsx │ │ │ ├── GroupComp.mdx │ │ │ ├── Heading.astro │ │ │ ├── Link.astro │ │ │ ├── Link.tsx │ │ │ ├── LogoWall/ │ │ │ │ ├── LogoWall.astro │ │ │ │ └── Wall.astro │ │ │ ├── Match.astro │ │ │ ├── Meta.astro │ │ │ ├── Nav.astro │ │ │ ├── Nav.tsx │ │ │ ├── Note.astro │ │ │ ├── Pre.astro │ │ │ ├── Snapshotter.tsx │ │ │ ├── Supported.tsx │ │ │ ├── TOC.astro │ │ │ ├── TOC.tsx │ │ │ ├── Tweet/ │ │ │ │ ├── Tweet.astro │ │ │ │ ├── TweetContent.astro │ │ │ │ ├── api/ │ │ │ │ │ ├── get-oembed.ts │ │ │ │ │ ├── get-tweet.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types/ │ │ │ │ │ ├── edit.ts │ │ │ │ │ ├── entities.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── media.ts │ │ │ │ │ ├── photo.ts │ │ │ │ │ ├── tweet.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── video.ts │ │ │ │ ├── twitter-theme/ │ │ │ │ │ ├── AvatarImg.astro │ │ │ │ │ ├── EmbeddedTweet.astro │ │ │ │ │ ├── MediaImg.astro │ │ │ │ │ ├── Skeleton.astro │ │ │ │ │ ├── TweetActions.astro │ │ │ │ │ ├── TweetBody.astro │ │ │ │ │ ├── TweetContainer.astro │ │ │ │ │ ├── TweetHeader.astro │ │ │ │ │ ├── TweetInReplyTo.astro │ │ │ │ │ ├── TweetInfo.astro │ │ │ │ │ ├── TweetInfoCreatedAt.astro │ │ │ │ │ ├── TweetLink.astro │ │ │ │ │ ├── TweetMedia.astro │ │ │ │ │ ├── TweetMediaVideo.astro │ │ │ │ │ ├── TweetMediaVideo.tsx │ │ │ │ │ ├── TweetNotFound.astro │ │ │ │ │ ├── TweetReplies.astro │ │ │ │ │ ├── TweetSkeleton.astro │ │ │ │ │ ├── VerifiedBadge.astro │ │ │ │ │ ├── components.ts │ │ │ │ │ ├── icons/ │ │ │ │ │ │ ├── Verified.astro │ │ │ │ │ │ ├── VerifiedBusiness.astro │ │ │ │ │ │ ├── VerifiedGovernment.astro │ │ │ │ │ │ ├── icons.module.css │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── quoted-tweet/ │ │ │ │ │ │ ├── QuotedTweet.astro │ │ │ │ │ │ ├── QuotedTweetBody.astro │ │ │ │ │ │ ├── QuotedTweetContainer.astro │ │ │ │ │ │ ├── QuotedTweetHeader.astro │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── quoted-tweet-body.module.css │ │ │ │ │ │ ├── quoted-tweet-container.module.css │ │ │ │ │ │ └── quoted-tweet-header.module.css │ │ │ │ │ ├── skeleton.module.css │ │ │ │ │ ├── theme.css │ │ │ │ │ ├── tweet-actions.module.css │ │ │ │ │ ├── tweet-body.module.css │ │ │ │ │ ├── tweet-container.module.css │ │ │ │ │ ├── tweet-header.module.css │ │ │ │ │ ├── tweet-in-reply-to.module.css │ │ │ │ │ ├── tweet-info-created-at.module.css │ │ │ │ │ ├── tweet-info.module.css │ │ │ │ │ ├── tweet-link.module.css │ │ │ │ │ ├── tweet-media-video.module.css │ │ │ │ │ ├── tweet-media.module.css │ │ │ │ │ ├── tweet-not-found.module.css │ │ │ │ │ ├── tweet-replies.module.css │ │ │ │ │ ├── tweet-skeleton.module.css │ │ │ │ │ ├── types.ts │ │ │ │ │ └── verified-badge.module.css │ │ │ │ └── utils.ts │ │ │ ├── Tweet.astro │ │ │ ├── Type.astro │ │ │ ├── Union.astro │ │ │ ├── code.module.css │ │ │ └── icons/ │ │ │ └── frameworks/ │ │ │ ├── react.tsx │ │ │ ├── svelte.tsx │ │ │ ├── vanilla.tsx │ │ │ └── vue.tsx │ │ ├── context/ │ │ │ └── toc.ts │ │ ├── env.d.ts │ │ ├── hooks/ │ │ │ └── useCycle.ts │ │ ├── layouts/ │ │ │ ├── Docs.astro │ │ │ ├── Layout.astro │ │ │ └── TOC.astro │ │ ├── lib/ │ │ │ ├── dom.ts │ │ │ ├── framework.ts │ │ │ ├── spring.ts │ │ │ ├── stores.ts │ │ │ ├── types.ts │ │ │ └── url.ts │ │ ├── middleware.ts │ │ ├── pages/ │ │ │ ├── [...framework]/ │ │ │ │ ├── _CSP.astro │ │ │ │ ├── _Digits.astro │ │ │ │ ├── _Hero.tsx │ │ │ │ ├── _Home.astro │ │ │ │ ├── _csp.txt │ │ │ │ ├── _demos/ │ │ │ │ │ ├── Continuous.tsx │ │ │ │ │ ├── Isolate.tsx │ │ │ │ │ ├── Styling.tsx │ │ │ │ │ ├── Suffix.tsx │ │ │ │ │ ├── TabularNums.tsx │ │ │ │ │ ├── Timings.tsx │ │ │ │ │ └── Trend.tsx │ │ │ │ ├── examples/ │ │ │ │ │ ├── _Activity/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── stores.ts │ │ │ │ │ │ ├── svelte/ │ │ │ │ │ │ │ ├── Component.svelte │ │ │ │ │ │ │ └── index.svelte │ │ │ │ │ │ ├── vanilla/ │ │ │ │ │ │ │ ├── Component.astro │ │ │ │ │ │ │ └── index.astro │ │ │ │ │ │ └── vue/ │ │ │ │ │ │ ├── Component.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── _ColoredTrends/ │ │ │ │ │ │ ├── Example.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── _Countdown/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── stores.ts │ │ │ │ │ │ ├── svelte/ │ │ │ │ │ │ │ ├── Component.svelte │ │ │ │ │ │ │ └── index.svelte │ │ │ │ │ │ ├── vanilla/ │ │ │ │ │ │ │ └── index.astro │ │ │ │ │ │ └── vue/ │ │ │ │ │ │ ├── Component.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── _Examples.astro │ │ │ │ │ ├── _Group/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── stores.ts │ │ │ │ │ │ ├── svelte/ │ │ │ │ │ │ │ ├── Component.svelte │ │ │ │ │ │ │ └── index.svelte │ │ │ │ │ │ ├── vanilla/ │ │ │ │ │ │ │ └── index.astro │ │ │ │ │ │ └── vue/ │ │ │ │ │ │ ├── Component.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── _Input/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── svelte/ │ │ │ │ │ │ │ ├── Component.svelte │ │ │ │ │ │ │ └── index.svelte │ │ │ │ │ │ └── vue/ │ │ │ │ │ │ ├── Component.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ ├── _Motion/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── stores.ts │ │ │ │ │ ├── _Slider/ │ │ │ │ │ │ ├── index.astro │ │ │ │ │ │ ├── react/ │ │ │ │ │ │ │ ├── Component.tsx │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── svelte/ │ │ │ │ │ │ │ ├── Component.svelte │ │ │ │ │ │ │ └── index.svelte │ │ │ │ │ │ └── vue/ │ │ │ │ │ │ ├── Component.vue │ │ │ │ │ │ └── index.vue │ │ │ │ │ └── index.mdx │ │ │ │ └── index.mdx │ │ │ └── showcase.astro │ │ ├── react.d.ts │ │ └── stores/ │ │ └── url.ts │ ├── svelte.config.mjs │ ├── tailwind.config.ts │ └── tsconfig.json ├── test-suites/ │ └── wrapper/ │ ├── can-animate.test.ts │ ├── group-1-unchanged.test.ts │ ├── hashes.test.ts │ ├── nonce.test.ts │ ├── parts.test.ts │ ├── render.test.ts │ ├── ssr.test.ts │ └── update.test.ts ├── tsconfig.build.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", "changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "barvian/number-flow" }], "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["!(@number-flow/*|number-flow)"] } ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [barvian] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/0-bug.yml ================================================ name: '🐛 Report a bug' description: Report a reproducible bug or regression. body: - type: markdown attributes: value: | Thanks for reporting :wave:. Before filing, please [search](https://github.com/barvian/number-flow/issues?q=is%3Aissue) open & closed issues to see if a similar one exists. # - type: input # id: numberflow-version # attributes: # label: NumberFlow version # description: | # - Please update to the [latest version](https://github.com/barvian/number-flow/releases) before filing to see if your bug has already been fixed. # placeholder: | # @number-flow/react@x.x.x # validations: # required: true # - type: input # id: framework-library-version # attributes: # label: Framework version # # description: Which framework (and version) are you using? # placeholder: | # react@x.x.x # validations: # required: false - type: input id: link attributes: label: Minimal reproduction description: | - Links to starter sandboxes can be found in the [site](https://number-flow.barvian.me/) header - Tips for creating minimal examples: https://stackoverflow.com/help/mcve placeholder: | https://codesandbox.io/p/... validations: required: true - type: textarea id: description attributes: label: Describe the bug and the steps to reproduce it placeholder: | You can drag screenshots or videos into this editor ↓ validations: required: true # - type: textarea # id: screenshots_or_videos # attributes: # label: Screenshots or videos # description: | # For more information on the supported file image/file types and the file size limits, see [GitHub docs](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files). # placeholder: | # You can drag your video or image files inside of this editor ↓ # - type: dropdown # attributes: # options: # - No, because I do not know how # - No, because I do not have time to dig into it # - Maybe, I'll investigate and start debugging # - Yes, I think I know how to fix it and will discuss it in the comments of this issue # - Yes, I am also opening a PR that solves the problem along side this issue # label: Do you intend to try to help solve this bug with your own PR? # description: | # If you think you know the cause of the problem, the fastest way to get it fixed is to suggest a fix, or fix it yourself! However, it is ok if you cannot solve this yourself and are just wanting help. # - type: checkboxes # id: agrees-to-terms # attributes: # label: Terms & Code of Conduct # description: By submitting this issue, you agree to follow our Code of Conduct and can verify that you have followed the requirements outlined above to the best of your ability. # options: # - label: I agree to follow this project's Code of Conduct # required: true # - label: I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed. # required: true ================================================ FILE: .github/ISSUE_TEMPLATE/1-docs.yml ================================================ name: "📖 Docs issue" description: "Report a typo or other issue on the docs." title: "[Docs]: " # labels: ["type: documentation"] body: - type: textarea attributes: label: Summary description: | A clear and concise summary of the issue. placeholder: | Example: The Motion for React example isn't linked to the correct page. validations: required: true - type: input attributes: label: Affected page # description: | # What page does this concern? placeholder: | https://number-flow.barvian.me validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: '💬 Get help' url: https://github.com/barvian/number-flow/discussions/new?category=help about: If you can't get something to work the way you'd like, open a question in the discussion forums. - name: '💡 Request a feature' url: https://github.com/barvian/number-flow/discussions/new?category=ideas about: 'Suggest any ideas you have using the discussion forums.' # - name: Bug Report # url: https://github.com/barvian/number-flow/issues/new # about: If something is clearly broken or not working as documented, create a bug report. # - name: Documentation Issue # url: https://github.com/tailwindlabs/tailwindcss.com # about: 'For documentation issues, suggest changes on our documentation repository.' ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: env: # we call `pnpm playwright install` instead PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' # cancel in-progress runs on new commits to same PR (gitub.event.number) concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.sha }} cancel-in-progress: true permissions: contents: read # to fetch code (actions/checkout) jobs: test: timeout-minutes: 60 runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4.0.0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 18.x - run: pnpm install --frozen-lockfile - run: pnpm playwright install --with-deps - run: pnpm build:packages - run: pnpm test || exit 1 - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: '**/playwright-report/' retention-days: 30 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main jobs: release: # prevents this action from running on forks if: github.repository == 'barvian/number-flow' name: Release runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - uses: pnpm/action-setup@v4.0.0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22.x - run: pnpm install --frozen-lockfile - name: Create Release Pull Request or Publish to npm id: changesets uses: changesets/action@v1 with: publish: pnpm release version: pnpm run version commit: Version packages env: GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ .DS_Store node_modules .turbo dist/ .env build/ .astro/ .env.* !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* .vercel ================================================ FILE: .npmrc ================================================ link-workspace-packages = true ================================================ FILE: .prettierignore ================================================ pnpm-lock.yaml **/*.mdx ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2024 Maxwell Barvian Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ https://github.com/user-attachments/assets/fb49ac50-039e-41e6-a19b-64e74ebb5930 # NumberFlow An animated number component for React, Vue, Svelte, and TS/JS. [![NPM Version](https://img.shields.io/npm/v/number-flow.svg)](https://npmjs.com/package/number-flow) [![Follow @mbarvian](https://img.shields.io/twitter/follow/mbarvian.svg?style=social&label=Follow)](https://x.com/mbarvian) ## Documentation For full documentation, visit [number-flow.barvian.me](https://number-flow.barvian.me). ## You may also like * [TextMorph](https://github.com/lochie/torph) - An animated text component by [Lochie Axon](https://x.com/lochieaxon). ================================================ FILE: lib/playwright.ts ================================================ import { defineConfig, devices } from '@playwright/test' /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ // import dotenv from 'dotenv'; // import path from 'path'; // dotenv.config({ path: path.resolve(__dirname, '.env') }); /** * See https://playwright.dev/docs/test-configuration. */ export const config = defineConfig({ testDir: './tests', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: 'http://localhost:3039', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' }, /* Configure projects for major browsers */ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'chromium-no-js', use: { ...devices['Desktop Chrome'], javaScriptEnabled: false } }, { name: 'chromium-reduced-motion', use: { ...devices['Desktop Chrome'], contextOptions: { reducedMotion: 'reduce' } } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } } /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', // use: { ...devices['Pixel 5'] }, // }, // { // name: 'Mobile Safari', // use: { ...devices['iPhone 12'] }, // }, /* Test against branded browsers. */ // { // name: 'Microsoft Edge', // use: { ...devices['Desktop Edge'], channel: 'msedge' }, // }, // { // name: 'Google Chrome', // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], /* Build and run production server before starting tests */ webServer: { command: 'pnpm build && pnpm start', url: 'http://localhost:3039', cwd: '.', reuseExistingServer: !process.env.CI } }) ================================================ FILE: package.json ================================================ { "private": true, "type": "module", "pnpm": { "overrides": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3" } }, "devDependencies": { "@changesets/cli": "^2.27.9", "@playwright/test": "^1.48.0", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "playwright": "^1.48.0", "prettier": "^3.3.3", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-svelte": "^3.2.7", "prettier-plugin-tailwindcss": "^0.6.5", "typescript": "^5.6.2" }, "scripts": { "build": "pnpm -r --filter=\"!./packages/**/test/**\" build", "build:packages": "pnpm -r --filter=\"./packages/*\" build", "test": "pnpm -r --workspace-concurrency=1 test", "format": "prettier --write .", "version": "changeset version && git add --all", "release": "pnpm build:packages && changeset publish" }, "packageManager": "pnpm@9.12.1", "engines": { "pnpm": "^9.0.0" } } ================================================ FILE: packages/number-flow/CHANGELOG.md ================================================ # number-flow ## 0.6.0 ### Minor Changes - Remove `--number-flow-char-height` CSS property in favor of `line-height` ([`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)) ## 0.5.12 ### Patch Changes - Only animate when ownerDocument is visible (see [#165](https://github.com/barvian/number-flow/issues/165)) ([#173](https://github.com/barvian/number-flow/pull/173)) ## 0.5.11 ### Patch Changes - Add CSP strategies (see [#170](https://github.com/barvian/number-flow/issues/170)) ([`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)) ## 0.5.10 ### Patch Changes - Fix Safari text alignment (see [#84](https://github.com/barvian/number-flow/issues/84)) ([`4a6c26e`](https://github.com/barvian/number-flow/commit/4a6c26efe13d6ffc1b84ea75accf511f63669eb9)) ## 0.5.9 ### Patch Changes - Fix Safari visual bug (see [#147](https://github.com/barvian/number-flow/issues/147)) ([`bdf8ce9`](https://github.com/barvian/number-flow/commit/bdf8ce92df67d6147d7f56998c625fc29e1b7571)) ## 0.5.8 ### Patch Changes - Fix "custom element already defined" bugs ([`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)) ## 0.5.7 ### Patch Changes - Attempted fix for old versions of Safari (see [#131](https://github.com/barvian/number-flow/issues/131)) ([`ee6a0a2`](https://github.com/barvian/number-flow/commit/ee6a0a2f2f09ba187b8df24cdfe0992ee7883192)) ## 0.5.6 ### Patch Changes - Fix errors in browsers that don't support attachInternals (see [#127](https://github.com/barvian/number-flow/issues/127)) ([`2539c4b`](https://github.com/barvian/number-flow/commit/2539c4b653fd4aaa17ef6b2ffd77b7a41454da08)) ## 0.5.5 ### Patch Changes - Expose value on custom element ([`bc6476f`](https://github.com/barvian/number-flow/commit/bc6476f910ad58625491c23ed0a8768217f9ab57)) ## 0.5.4 ### Patch Changes - Release vanilla JS version ([`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)) ## 0.5.3 ### Patch Changes - Revert mask-image change due to <1em char heights ([`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)) ## 0.5.2 ### Patch Changes - Improve `::selection` display and accessibility during transitions ([`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)) ## 0.5.1 ### Patch Changes - Add missing symbol part to SSR ([`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)) ## 0.5.0 ### Minor Changes - Move `continuous` prop into importable plugin ([`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)) ## 0.4.2 ### Patch Changes - Add symbol part for styling all symbols ([`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)) ## 0.4.1 ### Patch Changes - Reduce bundle size ([`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)) ## 0.4.0 ### Minor Changes - More flexible trend prop ([`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0)) ### Patch Changes - Add digits prop ([`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94)) - Fix cursor and improve text selection ([`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)) ## 0.3.10 ### Patch Changes - Switch to TS private properties to reduce bundle size ([`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)) ## 0.3.9 ### Patch Changes - Expose parts for styling support ([`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)) ## 0.3.8 ### Patch Changes - Minor performance optimizations ([`9854f77`](https://github.com/barvian/number-flow/commit/9854f77e11561fe119bf9009ae1369389a64ba15)) - Add prefix & suffix props ([`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)) ## 0.3.7 ### Patch Changes - Expose `created` property ([`0fac2f6`](https://github.com/barvian/number-flow/commit/0fac2f69b239048054755c556afc3f0eb65767c9)) ## 0.3.6 ### Patch Changes - More defensive checks on browser globals (see [#58](https://github.com/barvian/number-flow/issues/58)) ([#59](https://github.com/barvian/number-flow/pull/59)) ## 0.3.5 ### Patch Changes - Rename number-flow element to avoid conflicts between wrappers ([`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)) ## 0.3.4 ### Patch Changes - automatically disable animation when hidden (see [#9](https://github.com/barvian/number-flow/issues/9)) ([`ff966f4`](https://github.com/barvian/number-flow/commit/ff966f489eaeeacc72b35a8ee4c8cc13fe894eb6)) ## 0.3.3 ### Patch Changes - attempted fix for #45 ([`be3f7da`](https://github.com/barvian/number-flow/commit/be3f7da7ee88b6ab35f67736c98edcfb6909543d)) ================================================ FILE: packages/number-flow/README.md ================================================ [![NumberFlow](https://number-flow.barvian.me/preview.webp)](https://number-flow.barvian.me/vanilla) # NumberFlow An animated number component. [![NPM Version](https://img.shields.io/npm/v/number-flow.svg)](https://npmjs.com/package/number-flow) [![Bundle size](https://badgen.net/bundlephobia/minzip/number-flow@latest)](https://bundlephobia.com/package/number-flow@latest) [![Follow @mbarvian](https://img.shields.io/twitter/follow/mbarvian.svg?style=social&label=Follow)](https://x.com/mbarvian) ## Documentation For full documentation, visit [number-flow.barvian.me/vanilla](https://number-flow.barvian.me/vanilla). ================================================ FILE: packages/number-flow/package.json ================================================ { "name": "number-flow", "publishConfig": { "access": "public" }, "version": "0.6.0", "author": { "name": "Maxwell Barvian", "email": "max@barvian.me", "url": "https://barvian.me" }, "description": "A component to transition and format numbers.", "license": "MIT", "homepage": "https://number-flow.barvian.me/vanilla", "repository": { "type": "git", "url": "https://github.com/barvian/number-flow", "directory": "src" }, "bugs": { "url": "https://github.com/barvian/number-flow/issues" }, "keywords": [ "accessible", "odometer", "animation", "number-format", "number-animation", "animated-number" ], "files": [ "dist", "README.md" ], "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" }, "./lite": { "types": "./dist/lite.d.ts", "import": "./dist/lite.mjs", "require": "./dist/lite.js" }, "./csp": { "types": "./dist/csp.d.ts", "import": "./dist/csp.mjs", "require": "./dist/csp.js" }, "./group": { "types": "./dist/group.d.ts", "import": "./dist/group.mjs", "require": "./dist/group.js" }, "./plugins": { "types": "./dist/plugins/index.d.ts", "import": "./dist/plugins.mjs", "require": "./dist/plugins.js" } }, "scripts": { "build": "vite build --mode production", "dev": "vite build --mode development --watch", "test": "pnpm -r --workspace-concurrency 1 --filter=\"./test/apps/*\" test" }, "devDependencies": { "@rollup/plugin-typescript": "^12.1.0", "@testing-library/dom": "^10.4.0", "@vitest/browser": "^2.1.2", "babel-plugin-styled-components": "^2.1.4", "magic-string": "^0.30.11", "parse-literals": "^1.2.1", "playwright": "^1.48.0", "rollup-plugin-minify-html-literals-v3": "^1.3.4", "tslib": "^2.7.0", "typescript": "^5.6.2", "vite": "^5.4.3", "vitest": "^2.1.2" }, "dependencies": { "esm-env": "^1.1.4" } } ================================================ FILE: packages/number-flow/src/csp.ts ================================================ import runtimeStyles from './styles' import { styles as ssrStyles, renderFallbackStyles } from './ssr' export const buildStyles = (elementSuffix?: string) => [ssrStyles, renderFallbackStyles(elementSuffix), runtimeStyles] as const ================================================ FILE: packages/number-flow/src/env.d.ts ================================================ // Fix types for Intl.NumberFormat declare namespace Intl { interface NumberFormat { formatToParts(number?: number | bigint | string): NumberFormatPart[] } } ================================================ FILE: packages/number-flow/src/formatter.ts ================================================ // Merge the plus and minus sign types export type NumberPartType = | Exclude | 'sign' | 'prefix' | 'suffix' // These need to be separated for the discriminated union to work: // https://www.typescriptlang.org/play/?target=99&ssl=8&ssc=1&pln=9&pc=1#code/C4TwDgpgBAIglgczsKBeKBvKpIC4oDkcAdsBAhAE4FQA+hAZpQIYDGwcA9sQQNxQA3ZgBsArhHzFRAWwBGVKAF8AsACgc0AMIALZpTSZs4CYVa7q-QSPH4AzsEokEStWoaji7LsSgATTgDKwKIMDAAUYHrA+PBIKPQ6egCUmGpQUHAMUBFRAHQaaKjoRKTkVDS09JGUwPnGhcVMbBzcBCkYaelQ1cCdilAQwrbQHapd3VFQAPRTUAA8ALTY2nC2GbY8KImUADRQwnAA1tAAkgS+AwAekOzZAPxJfWqKQA type IntegerPart = { type: NumberPartType & 'integer'; value: number } type FractionPart = { type: NumberPartType & 'fraction'; value: number } type DigitPart = IntegerPart | FractionPart type SymbolPart = { type: Exclude value: string } export type NumberPartKey = string type KeyedPart = { key: NumberPartKey } export type KeyedDigitPart = DigitPart & KeyedPart & { pos: number } export type KeyedSymbolPart = SymbolPart & KeyedPart export type KeyedNumberPart = KeyedDigitPart | KeyedSymbolPart export type Format = Omit & { notation?: Exclude } export type Value = Exclude< Parameters[0], bigint | undefined > export function formatToData( value: Value, formatter: Intl.NumberFormat, prefix?: string, suffix?: string ) { const parts: Array< Omit & { type: Intl.NumberFormatPartTypes | 'prefix' | 'suffix' } > = formatter.formatToParts(value) if (prefix) parts.unshift({ type: 'prefix', value: prefix }) if (suffix) parts.push({ type: 'suffix', value: suffix }) const pre: KeyedNumberPart[] = [] const _integer: Array = [] // we do a second pass to key these from RTL const fraction: KeyedNumberPart[] = [] const post: KeyedNumberPart[] = [] const counts: Partial> = {} const generateKey = (type: NumberPartType) => `${type}:${(counts[type] = (counts[type] ?? -1) + 1)}` let valueAsString = '' let seenInteger = false, seenDecimal = false for (const part of parts) { valueAsString += part.value // Merge plus and minus sign types (doing it this way appeases TypeScript) const type: NumberPartType = part.type === 'minusSign' || part.type === 'plusSign' ? 'sign' : part.type if (type === 'integer') { seenInteger = true _integer.push(...part.value.split('').map((d) => ({ type, value: parseInt(d) }))) } else if (type === 'group') { _integer.push({ type, value: part.value }) } else if (type === 'decimal') { seenDecimal = true fraction.push({ type, value: part.value, key: generateKey(type) }) } else if (type === 'fraction') { fraction.push( ...part.value.split('').map((d) => ({ type, value: parseInt(d), key: generateKey(type), pos: -1 - counts[type]! })) ) } else { ;(seenInteger || seenDecimal ? post : pre).push({ type, value: part.value, key: generateKey(type) }) } } const integer: KeyedNumberPart[] = [] // Key the integer parts RTL, for better layout animations for (let i = _integer.length - 1; i >= 0; i--) { const p = _integer[i]! integer.unshift( p.type === 'integer' ? { ...p, key: generateKey(p.type), pos: counts[p.type]! } : { ...p, key: generateKey(p.type) } ) } return { pre, integer, fraction, post, valueAsString, value: typeof value == 'string' ? parseFloat(value) : value } } export type Data = ReturnType ================================================ FILE: packages/number-flow/src/group.ts ================================================ import { define } from './util/dom' import { ServerSafeHTMLElement } from './ssr' import { CONNECT_EVENT, UPDATE_EVENT } from '.' import NumberFlow from '.' export default class NumberFlowGroup extends ServerSafeHTMLElement { private _mutationObserver?: MutationObserver connectedCallback() { // The descendants are probably already connected, so query the DOM first. // Note: this won't work with a custom-defined element, if that ever exists: this.querySelectorAll('number-flow').forEach((flow) => { this._addDescendant(flow) }) this.addEventListener(CONNECT_EVENT, this._onDescendantConnected) this.addEventListener(UPDATE_EVENT, this._onDescendantUpdate) // We can't emit disconnection events, so use a mutation observer to track those this._mutationObserver ??= new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node instanceof NumberFlow) { this._removeDescendant(node) } }) }) }) this._mutationObserver.observe(this, { childList: true, subtree: true }) } private _flows = new Set() private _addDescendant = (flow: NumberFlow) => { flow.batched = true this._flows.add(flow) } private _removeDescendant = (flow: NumberFlow) => { flow.batched = false this._flows.delete(flow) } private _onDescendantConnected = (event: Event) => { this._addDescendant(event.target as NumberFlow) } private _updating = false private _onDescendantUpdate = () => { if (this._updating) return this._updating = true this._flows.forEach((flow) => { if (!flow.created) return flow.willUpdate() queueMicrotask(() => { if (flow.connected) flow.didUpdate() }) }) queueMicrotask(() => { this._updating = false }) } disconnectedCallback() { this.removeEventListener(CONNECT_EVENT, this._onDescendantConnected) this.removeEventListener(UPDATE_EVENT, this._onDescendantUpdate) this._mutationObserver?.disconnect() } } define('number-flow-group', NumberFlowGroup) declare global { interface HTMLElementTagNameMap { 'number-flow-group': NumberFlowGroup } } ================================================ FILE: packages/number-flow/src/index.ts ================================================ import NumberFlowLite from './lite' import { define } from './util/dom' import { renderInnerHTML as defaultRenderInnerHTML } from './ssr' import { formatToData, type Value, type Format } from './formatter' import { buildStyles } from './csp' export const styles = buildStyles() export * from './lite' export const CONNECT_EVENT = 'number-flow-connect' export const UPDATE_EVENT = 'number-flow-update' // Override the export from ./lite export const renderInnerHTML = ( value: Value, { locales, format, numberPrefix: prefix, numberSuffix: suffix, nonce }: { locales?: Intl.LocalesArgument format?: Intl.NumberFormatOptions numberPrefix?: string numberSuffix?: string nonce?: string } = {} ) => { const data = formatToData(value, new Intl.NumberFormat(locales, format), prefix, suffix) return defaultRenderInnerHTML(data, { nonce }) } export default class NumberFlow extends NumberFlowLite { /** * @internal for grouping */ connected = false connectedCallback() { this.connected = true this.dispatchEvent(new Event(CONNECT_EVENT, { bubbles: true })) } disconnectedCallback() { this.connected = false } format?: Format locales?: Intl.LocalesArgument // This can't be called prefix because that conflicts: // https://developer.mozilla.org/en-US/docs/Web/API/Element/prefix numberPrefix?: string numberSuffix?: string private _formatter?: Intl.NumberFormat private _prevFormat?: Format private _prevLocales?: Intl.LocalesArgument private _value?: Value get value() { return this._value } update(value?: Value) { // Might want to do a deep-equal check here: if ( !this._formatter || this._prevFormat !== this.format || this._prevLocales !== this.locales ) { this._formatter = new Intl.NumberFormat(this.locales, this.format) this._prevFormat = this.format this._prevLocales = this.locales } if (value != null) { this._value = value } // For group, has to be before setting data: this.dispatchEvent(new Event(UPDATE_EVENT, { bubbles: true })) this.data = formatToData(this._value!, this._formatter!, this.numberPrefix, this.numberSuffix) } } define('number-flow', NumberFlow) declare global { interface HTMLElementTagNameMap { 'number-flow': NumberFlow } } ================================================ FILE: packages/number-flow/src/lite.ts ================================================ import { createElement, offset, visible, type HTMLProps, type Justify } from './util/dom' import { forEach } from './util/iterable' import { type KeyedDigitPart, type KeyedNumberPart, type KeyedSymbolPart, type NumberPartKey, type Data } from './formatter' import { ServerSafeHTMLElement } from './ssr' import styles, { supportsMod, supportsLinear, dxVar, opacityDeltaVar, prefersReducedMotion, supportsAtProperty, widthDeltaVar, deltaVar } from './styles' import type { Mutable as MakeMutable } from './util/types' import type { Plugin } from './plugins' export { define } from './util/dom' export { prefersReducedMotion } from './styles' export { renderInnerHTML } from './ssr' export * from './plugins' export * from './formatter' export const canAnimate = supportsMod && supportsLinear && supportsAtProperty // Hoping to use -1 | 0 | 1 in the future if Math.sign types ever get fixed. // Don't do ReturnType cause it breaks Vue prop types: export type Trend = number | ((oldValue: number, value: number) => number) export type DigitOptions = { max?: number } export type Digits = Record export interface Props { transformTiming: EffectTiming spinTiming: EffectTiming | undefined opacityTiming: EffectTiming animated: boolean respectMotionPreference: boolean trend: Trend plugins?: Plugin[] digits: Digits | undefined } // Workaround for Object.assign in constructor and TS: // https://github.com/microsoft/TypeScript/issues/26792#issuecomment-617541464 export default interface NumberFlowLite extends Props {} // Workaround for no outside-readable/inside-writable members in TS: // https://github.com/microsoft/TypeScript/issues/37487 type Mutable = MakeMutable /** * @internal Used for framework wrappers */ export default class NumberFlowLite extends ServerSafeHTMLElement implements Props { /** * Use `private _private` properties instead of `#private` to avoid # polyfill and * reduce bundle size. Also, use `readonly` properties instead of getters to save on bundle * size, even though you have to do gross stuff like `(this as Mutable<...>)` until TS * supports e.g. https://github.com/microsoft/TypeScript/issues/37487 */ static defaultProps: Props = { transformTiming: { duration: 900, // Make sure to keep this minified: easing: `linear(0,.005,.019,.039,.066,.096,.129,.165,.202,.24,.278,.316,.354,.39,.426,.461,.494,.526,.557,.586,.614,.64,.665,.689,.711,.731,.751,.769,.786,.802,.817,.831,.844,.856,.867,.877,.887,.896,.904,.912,.919,.925,.931,.937,.942,.947,.951,.955,.959,.962,.965,.968,.971,.973,.976,.978,.98,.981,.983,.984,.986,.987,.988,.989,.99,.991,.992,.992,.993,.994,.994,.995,.995,.996,.996,.9963,.9967,.9969,.9972,.9975,.9977,.9979,.9981,.9982,.9984,.9985,.9987,.9988,.9989,1)` }, spinTiming: undefined, opacityTiming: { duration: 450, easing: 'ease-out' }, animated: true, trend: (oldValue, value) => Math.sign(value - oldValue), respectMotionPreference: true, plugins: undefined, digits: undefined } constructor() { super() const { animated, ...props } = (this.constructor as typeof NumberFlowLite).defaultProps this._animated = this.computedAnimated = animated Object.assign(this, props) } private _animated: boolean get animated() { return this._animated } set animated(val: boolean) { if (this.animated === val) return this._animated = val // Finish any in-flight animations (instead of cancel, which won't trigger their finish events): this.shadowRoot?.getAnimations().forEach((a) => a.finish()) } readonly created: boolean = false private _pre?: SymbolSection private _num?: Num private _post?: SymbolSection readonly computedTrend?: number readonly computedAnimated: boolean private _internals?: ElementInternals private _data?: Data /** * @internal */ batched = false /** * @internal */ set data(data: Data | undefined) { if (data == null) { return } const { pre, integer, fraction, post, value } = data // Initialize if needed if (!this.created) { this._data = data // This will overwrite the DSD if any: this.attachShadow({ mode: 'open' }) try { this._internals ??= this.attachInternals() this._internals.role = 'img' } catch { // Don't error in old browsers that don't support ElementInternals // Try/catch is less code than an if check. } // Add stylesheet; don't use adoptedStylesheets because it works unreliably in Safari: const style = document.createElement('style') if (this.nonce) style.nonce = this.nonce style.textContent = styles this.shadowRoot!.appendChild(style) this._pre = new SymbolSection(this, pre, { justify: 'right', part: 'left' }) this.shadowRoot!.appendChild(this._pre.el) this._num = new Num(this, integer, fraction) this.shadowRoot!.appendChild(this._num.el) this._post = new SymbolSection(this, post, { justify: 'left', part: 'right' }) this.shadowRoot!.appendChild(this._post.el) ;(this as Mutable).created = true } else { const prev = this._data! this._data = data // Compute trend ;(this as Mutable).computedTrend = typeof this.trend === 'function' ? this.trend(prev.value, value) : this.trend ;(this as Mutable).computedAnimated = canAnimate && this._animated && (!this.respectMotionPreference || !prefersReducedMotion?.matches) && // https://github.com/barvian/number-flow/issues/9 visible(this) && // https://github.com/barvian/number-flow/issues/165 this.ownerDocument.visibilityState === 'visible' this.plugins?.forEach((p) => p.onUpdate?.(data, prev, this)) if (!this.batched) this.willUpdate() this._pre!.update(pre) this._num!.update({ integer, fraction }) this._post!.update(post) if (!this.batched) this.didUpdate() } try { this._internals!.ariaLabel = data.valueAsString } catch { // Don't error in old browsers that don't support ElementInternals // Try/catch is less code than an if check. } } /** * @internal */ willUpdate() { // Not super safe to check animated here, b/c the prop may not have been updated yet: this._pre!.willUpdate() this._num!.willUpdate() this._post!.willUpdate() } private _abortAnimationsFinish?: AbortController /** * @internal */ didUpdate() { // Safe to call this here because we know the animated prop is up-to-date if (!this.computedAnimated) return // If we're already animating, cancel the previous animationsfinish event: if (this._abortAnimationsFinish) this._abortAnimationsFinish.abort() // Otherwise, dispatch a start event: else this.dispatchEvent(new Event('animationsstart')) this._pre!.didUpdate() this._num!.didUpdate() this._post!.didUpdate() const controller = new AbortController() Promise.all(this.shadowRoot!.getAnimations().map((a) => a.finished)).then(() => { if (!controller.signal.aborted) { this.dispatchEvent(new Event('animationsfinish')) this._abortAnimationsFinish = undefined } }) this._abortAnimationsFinish = controller } } class Num { readonly el: HTMLSpanElement readonly _inner: HTMLSpanElement private _integer: NumberSection private _fraction: NumberSection constructor( readonly flow: NumberFlowLite, integer: KeyedNumberPart[], fraction: KeyedNumberPart[], { className, ...props }: HTMLProps<'span'> = {} ) { this._integer = new NumberSection(flow, integer, { justify: 'right', part: 'integer' }) this._fraction = new NumberSection(flow, fraction, { justify: 'left', part: 'fraction' }) this._inner = createElement( 'span', { className: `number__inner` }, [this._integer.el, this._fraction.el] ) this.el = createElement( 'span', { ...props, part: 'number', className: `number ${className ?? ''}` }, [this._inner] ) } private _prevWidth?: number private _prevLeft?: number willUpdate() { this._prevWidth = this.el.offsetWidth this._prevLeft = this.el.getBoundingClientRect().left this._integer.willUpdate() this._fraction.willUpdate() } update({ integer, fraction }: Pick) { this._integer.update(integer) this._fraction.update(fraction) } didUpdate() { const rect = this.el.getBoundingClientRect() // Do this before starting to animate: this._integer.didUpdate() this._fraction.didUpdate() const dx = this._prevLeft! - rect.left const width = this.el.offsetWidth // We convert scale to width delta in px to better handle interruptions and keep them in // sync with translations: const dWidth = this._prevWidth! - width this.el.style.setProperty('--width', String(width)) this.el.animate( { [dxVar]: [`${dx}px`, '0px'], [widthDeltaVar]: [dWidth, 0] }, { ...this.flow.transformTiming, composite: 'accumulate' } ) } } type SectionProps = { justify: Justify } & HTMLProps<'span'> abstract class Section { readonly el: HTMLSpanElement readonly justify: Justify // All children in the DOM: protected children = new Map() constructor( readonly flow: NumberFlowLite, parts: KeyedNumberPart[], { justify, className, ...props }: SectionProps, children?: (chars: Node[]) => Node[] ) { this.justify = justify const chars = parts.map((p) => this.addChar(p).el) this.el = createElement( 'span', { ...props, className: `section section--justify-${justify} ${className ?? ''}` }, children ? children(chars) : chars ) } protected addChar( part: KeyedNumberPart, { startDigitsAtZero = false, ...props }: { startDigitsAtZero?: boolean } & Pick = {} ) { const comp = part.type === 'integer' || part.type === 'fraction' ? new Digit(this, part.type, startDigitsAtZero ? 0 : part.value, part.pos, { ...props, onRemove: this.onCharRemove(part.key) }) : new Sym(this, part.type, part.value, { ...props, onRemove: this.onCharRemove(part.key) }) this.children.set(part.key, comp) return comp } private onCharRemove = (key: NumberPartKey): OnRemove => () => { this.children.delete(key) } protected unpop(char: Char) { char.el.removeAttribute('inert') char.el.style.top = '' char.el.style[this.justify] = '' } protected pop(chars: Map) { // Calculate offsets for removed before popping, to avoid layout thrashing: chars.forEach((char) => { char.el.style.top = `${char.el.offsetTop}px` char.el.style[this.justify] = `${offset(char.el, this.justify)}px` }) chars.forEach((char) => { char.el.setAttribute('inert', '') char.present = false }) } protected addNewAndUpdateExisting(parts: KeyedNumberPart[]) { const added = new Map() const updated = new Map() // Add new parts before any other updates, so we can save their position correctly: const reverse = this.justify === 'left' const op = reverse ? 'prepend' : 'append' forEach( parts, (part) => { let comp: Char // Already exists/needs update, so set aside for now if (this.children.has(part.key)) { comp = this.children.get(part.key)! updated.set(part, comp) this.unpop(comp) comp.present = true } else { // New part comp = this.addChar(part, { startDigitsAtZero: true, animateIn: true }) added.set(part, comp) } this.el[op](comp.el) }, { reverse } ) if (this.flow.computedAnimated) { const rect = this.el.getBoundingClientRect() // this should only cause a layout if there were added children (?) added.forEach((comp) => { comp.willUpdate(rect) }) } // Update added children to their initial value (we start them at 0) added.forEach((comp, part) => { comp.update(part.value) }) // Update any updated children updated.forEach((comp, part) => { comp.update(part.value) }) } private _prevOffset?: number willUpdate() { const rect = this.el.getBoundingClientRect() this._prevOffset = rect[this.justify] this.children.forEach((comp) => comp.willUpdate(rect)) } didUpdate() { const rect = this.el.getBoundingClientRect() // Make sure to pass this in before starting to animate: this.children.forEach((comp) => comp.didUpdate(rect)) const offset = rect[this.justify] const dx = this._prevOffset! - offset // Technically checking for children could get weird during multiple interruptions // but probably still worth it; if (dx && this.children.size) this.el.animate( { transform: [`translateX(${dx}px)`, 'none'] }, { ...this.flow.transformTiming, composite: 'accumulate' } ) } } class NumberSection extends Section { update(parts: KeyedNumberPart[]) { const removed = new Map() this.children.forEach((comp, key) => { // Keep track of removed children: if (!parts.find((p) => p.key === key)) { removed.set(key, comp) } // Put everything back into the flow briefly, to recompute offsets: this.unpop(comp) }) this.addNewAndUpdateExisting(parts) // Set all removed digits to 0, for mathematical correctness: removed.forEach((comp) => { if (comp instanceof Digit) comp.update(0) }) // Then end with them popped out again: this.pop(removed) } } class SymbolSection extends Section { update(parts: KeyedNumberPart[]) { const removed = new Map() this.children.forEach((comp, key) => { // Keep track of removed children: if (!parts.find((p) => p.key === key)) { removed.set(key, comp) } }) // Pop them, before any additions this.pop(removed) this.addNewAndUpdateExisting(parts) } } type OnRemove = () => void interface AnimatePresenceProps { onRemove?: OnRemove animateIn?: boolean } class AnimatePresence { private _present = true private _onRemove?: OnRemove constructor( readonly flow: NumberFlowLite, readonly el: HTMLElement, { onRemove, animateIn = false }: AnimatePresenceProps = {} ) { this.el.classList.add('animate-presence') // This craziness is the only way I could figure out how to get the opacity // accumulation to work in all browsers. Accumulating -1 onto opacity directly // failed in both FF and Safari, and setting a delta to -1 still failed in FF if (this.flow.computedAnimated && animateIn) { this.el.animate( { [opacityDeltaVar]: [-0.9999, 0] }, { ...this.flow.opacityTiming, composite: 'accumulate' } ) } this._onRemove = onRemove } get present() { return this._present } private _remove = () => { this.el.remove() this._onRemove?.() } set present(val) { if (this._present === val) return this._present = val if (val) this.el.removeAttribute('inert') else this.el.setAttribute('inert', '') if (!this.flow.computedAnimated) { if (!val) this._remove() return } this.el.style.setProperty('--_number-flow-d-opacity', val ? '0' : '-.999') this.el.animate( { [opacityDeltaVar]: val ? [-0.9999, 0] : [0.999, 0] }, { ...this.flow.opacityTiming, composite: 'accumulate' } ) if (val) this.flow.removeEventListener('animationsfinish', this._remove) else this.flow.addEventListener('animationsfinish', this._remove, { once: true }) } } interface CharProps extends AnimatePresenceProps {} abstract class Char

extends AnimatePresence { constructor( readonly section: Section, protected value: P['value'], override readonly el: HTMLSpanElement, props?: AnimatePresenceProps ) { super(section.flow, el, props) } abstract willUpdate(parentRect: DOMRect): void abstract update(value: P['value']): void abstract didUpdate(parentRect: DOMRect): void } export class Digit extends Char { private _numbers: HTMLSpanElement[] readonly length: number constructor( section: Section, type: KeyedDigitPart['type'], value: KeyedDigitPart['value'], readonly pos: number, props?: CharProps ) { const length = (section.flow.digits?.[pos]?.max ?? 9) + 1 const numbers = Array.from({ length }).map((_, i) => { const num = createElement('span', { className: `digit__num` }, [ document.createTextNode(String(i)) ]) // Use the attribute for now because it has a little better browser support: if (i !== value) num.setAttribute('inert', '') num.style.setProperty('--n', String(i)) return num }) const el = createElement( 'span', { part: `digit ${type}-digit`, className: `digit` }, numbers ) el.style.setProperty('--current', String(value)) el.style.setProperty('--length', String(length)) super(section, value, el, props) this._numbers = numbers this.length = length } private _prevValue?: KeyedDigitPart['value'] // Relative to parent: private _prevCenter?: number willUpdate(parentRect: DOMRect) { const rect = this.el.getBoundingClientRect() this._prevValue = this.value const prevOffset = rect[this.section.justify] - parentRect[this.section.justify] const halfWidth = rect.width / 2 this._prevCenter = this.section.justify === 'left' ? prevOffset + halfWidth : prevOffset - halfWidth } update(value: KeyedDigitPart['value']) { this.el.style.setProperty('--current', String(value)) this._numbers.forEach((num, i) => i === value ? num.removeAttribute('inert') : num.setAttribute('inert', '') ) this.value = value } didUpdate(parentRect: DOMRect) { const rect = this.el.getBoundingClientRect() const offset = rect[this.section.justify] - parentRect[this.section.justify] const halfWidth = rect.width / 2 const center = this.section.justify === 'left' ? offset + halfWidth : offset - halfWidth const dx = this._prevCenter! - center if (dx) this.el.animate( { transform: [`translateX(${dx}px)`, 'none'] }, { ...this.flow.transformTiming, composite: 'accumulate' } ) const delta = this.getDelta() if (!delta) return this.el.classList.add('is-spinning') this.el.animate( { [deltaVar]: [-delta, 0] }, { ...(this.flow.spinTiming ?? this.flow.transformTiming), composite: 'accumulate' } ) // Hoisting the callback out prevents duplicates: this.flow.addEventListener('animationsfinish', this._onAnimationsFinish, { once: true }) } getDelta() { if (this.flow.plugins) for (const plugin of this.flow.plugins) { const diff = plugin.getDelta?.(this.value, this._prevValue!, this) if (diff != null) return diff } const diff = this.value - this._prevValue! // Make it per-digit if no root trend: const trend = this.flow.computedTrend || Math.sign(diff) // Loop around if need be: if (trend < 0 && this.value > this._prevValue!) return this.value - this.length - this._prevValue! else if (trend > 0 && this.value < this._prevValue!) return this.length - this._prevValue! + this.value return diff } private _onAnimationsFinish = () => { this.el.classList.remove('is-spinning') } } class Sym extends Char { constructor( section: Section, private type: KeyedSymbolPart['type'], value: KeyedSymbolPart['value'], props?: CharProps ) { const val = createElement('span', { className: 'symbol__value', textContent: value }) super( section, value, createElement( 'span', { part: `symbol ${type}`, className: `symbol` }, [val] ), props ) this._children.set( value, new AnimatePresence(this.flow, val, { onRemove: this._onChildRemove(value) }) ) } private _children = new Map() private _prevOffset?: number willUpdate(parentRect: DOMRect) { if (this.type === 'decimal') return // decimal never needs animation b/c it's the first in a left aligned section and never moves const rect = this.el.getBoundingClientRect() this._prevOffset = rect[this.section.justify] - parentRect[this.section.justify] } private _onChildRemove = (key: KeyedSymbolPart['value']): OnRemove => () => { this._children.delete(key) } update(value: KeyedSymbolPart['value']) { if (this.value !== value) { // Pop the current value: const current = this._children.get(this.value) if (current) current.present = false // If we already have the new value and it hasn't finished removing, reclaim it: const prev = this._children.get(value) if (prev) { prev.present = true } else { // Otherwise, create a new one: const newVal = createElement('span', { className: 'symbol__value', textContent: value }) this.el.appendChild(newVal) this._children.set( value, new AnimatePresence(this.flow, newVal, { animateIn: true, onRemove: this._onChildRemove(value) }) ) } } this.value = value } didUpdate(parentRect: DOMRect) { if (this.type === 'decimal') return const rect = this.el.getBoundingClientRect() const offset = rect[this.section.justify] - parentRect[this.section.justify] const dx = this._prevOffset! - offset if (dx) this.el.animate( { transform: [`translateX(${dx}px)`, 'none'] }, { ...this.flow.transformTiming, composite: 'accumulate' } ) } } ================================================ FILE: packages/number-flow/src/plugins/continuous.ts ================================================ import { max } from '../util/math' import type { Plugin } from '.' import type NumberFlowLite from '../lite' const startingPos = new WeakMap() /** * Makes number transitions appear to pass through in between numbers. */ export const continuous: Plugin = { onUpdate(data, prev, flow) { startingPos.set(flow, undefined) if (!flow.computedTrend) return // Find the starting pos based on the parts, not the value, // to handle e.g. compact notation where value = 1000 and integer part = 1 const prevNumber = prev.integer .concat(prev.fraction) .filter((p) => p.type === 'integer' || p.type === 'fraction') const number = data.integer .concat(data.fraction) .filter((p) => p.type === 'integer' || p.type === 'fraction') const firstChangedPrev = prevNumber.find( (pp) => !number.find((p) => p.pos === pp.pos && p.value === pp.value) ) const firstChanged = number.find( (p) => !prevNumber.find((pp) => p.pos === pp.pos && p.value === pp.value) ) startingPos.set(flow, max(firstChangedPrev?.pos, firstChanged?.pos)) }, getDelta(value, prev, digit) { const diff = value - prev const starting = startingPos.get(digit.flow) // Loop once if it's continuous: if (!diff && starting != null && starting >= digit.pos) { return digit.length * digit.flow.computedTrend! // trend must exist if there's starting } } } ================================================ FILE: packages/number-flow/src/plugins/index.ts ================================================ import type NumberFlowLite from '../lite' import type { Digit } from '../lite' import type { Data } from '../formatter' export type Plugin = { onUpdate?(data: Data, prev: Data, context: NumberFlowLite): void getDelta?(value: number, prev: number, context: Digit): number | void } export { continuous } from './continuous' ================================================ FILE: packages/number-flow/src/ssr.ts ================================================ import type { Data, KeyedNumberPart } from './formatter' import { css, html } from './util/string' import { halfMaskHeight, maskHeight } from './styles' import { BROWSER } from 'esm-env' export const ServerSafeHTMLElement = BROWSER ? HTMLElement : (class {} as unknown as typeof HTMLElement) // for types export const styles = css` :host { display: inline-block; direction: ltr; white-space: nowrap; line-height: 1; } span { display: inline-block; } :host([data-will-change]) span { will-change: transform; } .number, .digit { padding: ${halfMaskHeight} 0; } .symbol { white-space: pre; /* some symbols are spaces or thin spaces */ } ` const renderPart = (part: KeyedNumberPart) => `${part.value}` const renderSection = (section: KeyedNumberPart[], part: string) => `${section.reduce((str, p) => str + renderPart(p), '')}` export const renderFallbackStyles = (elementSuffix = '') => css` :where(number-flow${elementSuffix}) { line-height: 1; } number-flow${elementSuffix} > span { font-kerning: none; display: inline-block; padding: ${maskHeight} 0; } ` export const renderInnerHTML = ( data: Data, { nonce, elementSuffix }: { nonce?: string; elementSuffix?: string } = {} ) => // shadowroot="open" non-standard attribute for old Chrome: html`${renderFallbackStyles(elementSuffix)}${data.valueAsString}` ================================================ FILE: packages/number-flow/src/styles.ts ================================================ import { BROWSER } from 'esm-env' import { css } from './util/string' export const supportsLinear = BROWSER && (() => { try { // We can't use CSS.supports because it sometimes gives // false positives compared to .animate support: document.createElement('div').animate({ opacity: 0 }, { easing: 'linear(0, 1)' }) } catch (e) { return false } return true })() export const supportsMod = BROWSER && typeof CSS !== 'undefined' && CSS.supports && CSS.supports('line-height', 'mod(1,1)') export const prefersReducedMotion = BROWSER && typeof matchMedia !== 'undefined' ? matchMedia('(prefers-reduced-motion: reduce)') : null // Register animated vars: export const opacityDeltaVar = '--_number-flow-d-opacity' export const widthDeltaVar = '--_number-flow-d-width' export const dxVar = '--_number-flow-dx' export const deltaVar = '--_number-flow-d' export const supportsAtProperty = (() => { try { CSS.registerProperty({ name: opacityDeltaVar, syntax: '', inherits: false, initialValue: '0' }) CSS.registerProperty({ name: dxVar, syntax: '', inherits: true, initialValue: '0px' }) CSS.registerProperty({ name: widthDeltaVar, syntax: '', inherits: false, initialValue: '0' }) CSS.registerProperty({ name: deltaVar, syntax: '', inherits: true, initialValue: '0' }) return true } catch { return false } })() // Don't use CSS.registerProperty for vars needed during SSR: // Mask technique taken from: // https://expensive.toys/blog/blur-vignette // Use round() to avoid fractional pixels which fixes alignment in Safari: export const halfMaskHeight = `round(nearest, calc(var(--number-flow-mask-height, 0.25em) / 2), 1px)` export const maskHeight = `calc(${halfMaskHeight} * 2)` const maskWidth = 'var(--number-flow-mask-width, 0.5em)' const scaledMaskWidth = `calc(${maskWidth} / var(--scale-x))` const cornerGradient = `#000 0, transparent 71%` // or transparent ${maskWidth} const styles = css` :host { display: inline-block; direction: ltr; white-space: nowrap; isolation: isolate; /* for .number z-index */ /* Technically this is only needed on the .number, but applying it here makes the ::selection the same height for the whole element: */ line-height: 1; } .number, .number__inner { display: inline-block; transform-origin: left top; } :host([data-will-change]) :is(.number, .number__inner, .section, .digit, .digit__num, .symbol) { will-change: transform; } .number { --scale-x: calc(1 + var(${widthDeltaVar}) / var(--width)); transform: translateX(var(${dxVar})) scaleX(var(--scale-x)); margin: 0 calc(-1 * ${maskWidth}); position: relative; /* for z-index */ /* overflow: clip; /* helpful to not affect page layout, but breaks baseline alignment in Safari :/ */ /* -webkit- prefixed properties have better support than unprefixed ones: */ -webkit-mask-image: /* Horizontal: */ linear-gradient( to right, transparent 0, #000 ${scaledMaskWidth}, #000 calc(100% - ${scaledMaskWidth}), transparent ), /* Vertical: */ linear-gradient( to bottom, transparent 0, #000 ${maskHeight}, #000 calc(100% - ${maskHeight}), transparent 100% ), /* TL corner */ radial-gradient(at bottom right, ${cornerGradient}), /* TR corner */ radial-gradient(at bottom left, ${cornerGradient}), /* BR corner */ radial-gradient(at top left, ${cornerGradient}), /* BL corner */ radial-gradient(at top right, ${cornerGradient}); -webkit-mask-size: 100% calc(100% - ${maskHeight} * 2), calc(100% - ${scaledMaskWidth} * 2) 100%, ${scaledMaskWidth} ${maskHeight}, ${scaledMaskWidth} ${maskHeight}, ${scaledMaskWidth} ${maskHeight}, ${scaledMaskWidth} ${maskHeight}; -webkit-mask-position: center, center, top left, top right, bottom right, bottom left; -webkit-mask-repeat: no-repeat; } /* Small improvement for ::selection when not animating: */ /* Reverted because you can see it change when char height < 1em: */ /*.number:not(:has(.digit.is-spinning)) { -webkit-mask-image: none; }*/ .number__inner { padding: ${halfMaskHeight} ${maskWidth}; /* invert parent's: */ transform: scaleX(calc(1 / var(--scale-x))) translateX(calc(-1 * var(${dxVar}))); } /* Put number underneath other sections. Negative z-index messed up text cursor and selection, weirdly: */ :host > :not(.number) { z-index: 5; } .section, .symbol { display: inline-block; /* for exiting (> [inert]): */ position: relative; isolation: isolate; /* also helpful for mix-blend-mode in symbol__value */ } .section::after { /* * We seem to need some type of character to ensure baseline alignment continues working * even when empty */ content: '\200b'; /* zero-width space */ display: inline-block; } .section--justify-left { transform-origin: center left; } .section--justify-right { transform-origin: center right; } .section > [inert], .symbol > [inert] { margin: 0 !important; /* to override any user styles */ position: absolute !important; /* ^ */ z-index: -1; } .digit { display: inline-block; position: relative; --c: var(--current) + var(${deltaVar}); } .digit__num, .number .section::after { padding: ${halfMaskHeight} 0; } .digit__num { display: inline-block; /* Claude + https://buildui.com/recipes/animated-counter */ --offset-raw: mod(var(--length) + var(--n) - mod(var(--c), var(--length)), var(--length)); --offset: calc( var(--offset-raw) - var(--length) * round(down, var(--offset-raw) / (var(--length) / 2), 1) ); /* Technically we just need var(--offset)*100%, but clamping should reduce the layer size: */ --y: clamp(-100%, var(--offset) * 100%, 100%); transform: translateY(var(--y)); } .digit__num[inert] { position: absolute; top: 0; left: 50%; transform: translateX(-50%) translateY(var(--y)); } .digit:not(.is-spinning) .digit__num[inert] { display: none; } .symbol__value { display: inline-block; mix-blend-mode: plus-lighter; /* better crossfades e.g. + <-> - */ white-space: pre; /* some symbols are spaces or thin spaces */ } .section--justify-left .symbol > [inert] { left: 0; } .section--justify-right .symbol > [inert] { right: 0; } .animate-presence { opacity: calc(1 + var(${opacityDeltaVar})); } ` export default styles ================================================ FILE: packages/number-flow/src/util/dom.ts ================================================ import { BROWSER } from 'esm-env' type ExcludeReadonly = { -readonly [K in keyof T as T[K] extends Readonly ? never : K]: T[K] } export type HTMLProps = Partial< ExcludeReadonly & { part: string } > export const createElement = ( tagName: K, optionsOrChildren?: HTMLProps | Node[], _children?: Node[] ): HTMLElementTagNameMap[K] => { const element = document.createElement(tagName) const [options, children] = Array.isArray(optionsOrChildren) ? [undefined, optionsOrChildren] : [optionsOrChildren, _children] if (options) Object.assign(element, options) children?.forEach((child) => element.appendChild(child)) return element } export type Justify = 'left' | 'right' // Makeshift .offsetRight export const offset = (el: HTMLElement, justify: Justify) => { return justify === 'left' ? el.offsetLeft : ((el.offsetParent instanceof HTMLElement ? el.offsetParent : null)?.offsetWidth ?? 0) - el.offsetWidth - el.offsetLeft } export const visible = (el: HTMLElement) => el.offsetWidth > 0 && el.offsetHeight > 0 // HMR-safe customElements.define export const define = (name: string, constructor: CustomElementConstructor) => { // Opt for the simpler check, the constructor check breaks in Next.js force-static, // Svelte REPL, and Webpack Module Federation: if (BROWSER && !customElements.get(name) /* !== constructor*/) customElements.define(name, constructor) } ================================================ FILE: packages/number-flow/src/util/iterable.ts ================================================ export function forEach( arr: T[], fn: (item: T, index: number) => void, { reverse = false } = {} ) { const len = arr.length for (let i = reverse ? len - 1 : 0; reverse ? i >= 0 : i < len; reverse ? i-- : i++) { fn(arr[i]!, i) } } ================================================ FILE: packages/number-flow/src/util/math.ts ================================================ // Math.max that handles nullish numbers export const max = (n1?: number, n2?: number) => { if (n1 == null) return n2 if (n2 == null) return n1 return Math.max(n1, n2) } ================================================ FILE: packages/number-flow/src/util/string.ts ================================================ export const html = String.raw export const css = String.raw ================================================ FILE: packages/number-flow/src/util/types.ts ================================================ export type Mutable = { -readonly [P in keyof T]: T[P] } ================================================ FILE: packages/number-flow/test/apps/astro/.gitignore ================================================ # build output dist/ # generated types .astro/ # testing /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store # jetbrains setting folder .idea/ ================================================ FILE: packages/number-flow/test/apps/astro/README.md ================================================ # Astro Starter Kit: Basics ```sh pnpm create astro@latest -- --template basics ``` [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) ## 🚀 Project Structure Inside of your Astro project, you'll see the following folders and files: ```text / ├── public/ │ └── favicon.svg ├── src/ │ ├── layouts/ │ │ └── Layout.astro │ └── pages/ │ └── index.astro └── package.json ``` To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). ## 🧞 Commands All commands are run from the root of the project, from a terminal: | Command | Action | | :------------------------ | :----------------------------------------------- | | `pnpm install` | Installs dependencies | | `pnpm dev` | Starts local dev server at `localhost:4321` | | `pnpm build` | Build your production site to `./dist/` | | `pnpm preview` | Preview your build locally, before deploying | | `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | | `pnpm astro -- --help` | Get help using the Astro CLI | ## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). ================================================ FILE: packages/number-flow/test/apps/astro/astro.config.mjs ================================================ import { defineConfig } from 'astro/config' import tailwindcss from '@tailwindcss/vite' import node from '@astrojs/node' // https://astro.build/config export default defineConfig({ devToolbar: { enabled: false }, vite: { plugins: [tailwindcss()] }, adapter: node({ mode: 'standalone' }) }) ================================================ FILE: packages/number-flow/test/apps/astro/package.json ================================================ { "name": "test", "type": "module", "private": true, "version": "0.0.1", "scripts": { "dev": "astro dev --port 3039", "build": "astro build", "start": "astro preview --port 3039", "preview": "astro preview --port 3039", "astro": "astro", "test": "playwright test", "test:ui": "playwright test --ui", "test:update": "playwright test --update-snapshots" }, "dependencies": { "number-flow": "workspace:*", "@astrojs/node": "^9.5.4", "@tailwindcss/vite": "^4.0.9", "astro": "^5.17.3", "tailwindcss": "^4.0.9" } } ================================================ FILE: packages/number-flow/test/apps/astro/playwright.config.ts ================================================ export { config as default } from '../../../../../lib/playwright' ================================================ FILE: packages/number-flow/test/apps/astro/src/layouts/Layout.astro ================================================ --- import '../styles/global.css' --- ================================================ FILE: packages/number-flow/test/apps/astro/src/middleware.ts ================================================ import { defineMiddleware } from 'astro:middleware' import { createHash } from 'node:crypto' import { styles } from 'number-flow' const nonceCsp = "style-src 'nonce-test-nonce'" const hash = (style: string) => `'sha256-${createHash('sha256').update(style).digest('base64')}'` const hashesCsp = `style-src ${styles.map(hash).join(' ')}` export const onRequest = defineMiddleware(async ({ url }, next) => { const response = await next() if (url.pathname === '/nonce') { response.headers.set('Content-Security-Policy', nonceCsp) } if (url.pathname === '/hashes') { response.headers.set('Content-Security-Policy', hashesCsp) } return response }) ================================================ FILE: packages/number-flow/test/apps/astro/src/pages/can-animate.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { canAnimate, prefersReducedMotion } from 'number-flow' ---

{String(!(prefersReducedMotion?.matches ?? false) && canAnimate)}

{String(canAnimate)}

================================================ FILE: packages/number-flow/test/apps/astro/src/pages/group-1-unchanged.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { renderInnerHTML } from 'number-flow' ---

================================================ FILE: packages/number-flow/test/apps/astro/src/pages/hashes.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { renderInnerHTML } from 'number-flow' export const prerender = false --- ================================================ FILE: packages/number-flow/test/apps/astro/src/pages/index.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { renderInnerHTML } from 'number-flow' ---
Text node{' '}

================================================ FILE: packages/number-flow/test/apps/astro/src/pages/nonce.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { renderInnerHTML } from 'number-flow' export const prerender = false --- ================================================ FILE: packages/number-flow/test/apps/astro/src/pages/thrashing.astro ================================================ --- import Layout from '../layouts/Layout.astro' import { renderInnerHTML } from 'number-flow' --- ================================================ FILE: packages/number-flow/test/apps/astro/src/styles/global.css ================================================ @import 'tailwindcss'; ================================================ FILE: packages/number-flow/test/apps/astro/tsconfig.json ================================================ { "extends": "astro/tsconfigs/strictest", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"] } ================================================ FILE: packages/number-flow/tsconfig.build.json ================================================ { "extends": ["./tsconfig.json", "../../tsconfig.build.json"] } ================================================ FILE: packages/number-flow/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { "jsx": "react", "declaration": true, "types": ["@vitest/browser/providers/playwright"], "outDir": "./dist" } } ================================================ FILE: packages/number-flow/vite.config.mjs ================================================ import { resolve } from 'path' import { defineConfig, createFilter } from 'vite' import typescript from '@rollup/plugin-typescript' // import minifyLiterals from 'rollup-plugin-minify-html-literals-v3' import { minifyRaw as minifyCSS } from 'babel-plugin-styled-components/lib/minify' import MagicString from 'magic-string' import * as pl from 'parse-literals' const outDir = resolve(__dirname, 'dist') export default defineConfig(({ mode }) => ({ build: { outDir, lib: { entry: { index: resolve(__dirname, 'src/index.ts'), lite: resolve(__dirname, 'src/lite.ts'), csp: resolve(__dirname, 'src/csp.ts'), group: resolve(__dirname, 'src/group.ts'), plugins: resolve(__dirname, 'src/plugins/index.ts') }, formats: ['es', 'cjs'] }, rollupOptions: { external: ['esm-env'], plugins: [ // Caused issues with CSS: // minifyLiterals({ // // Couldn't get the plugin to work with css``, so disable it and handle it separately: // minifyOptions: { // minifyCSS: false // } // }), minifyCSSLiterals(), typescript({ tsconfig: resolve(__dirname, `./tsconfig${mode === 'production' ? '.build' : ''}.json`) }) ] } } })) /** @satisfies {import('vite').Plugin} */ function minifyCSSLiterals() { const filter = createFilter(/\.[jt]sx?$/) return { name: 'vite-plugin-minify-css-literals', enforce: 'pre', apply: 'build', transform(code, id) { if (!filter(id)) return null const templates = pl.parseLiterals(code) if (!templates.length) return code const ms = new MagicString(code) templates.forEach((template) => { if (template.tag !== 'css') return template.parts.forEach((part) => { if (part.start < part.end) { const mini = minifyCSS(part.text)[0] .replaceAll(';}', '}') // .replaceAll(/\s+!important/g, '!important') .replaceAll(/linear-gradient\(\s+/g, 'linear-gradient(') ms.overwrite(part.start, part.end, mini) } }) }) return { code: ms.toString(), map: ms.generateMap({ hires: 'boundary' }) } } } } ================================================ FILE: packages/react/CHANGELOG.md ================================================ # @number-flow/react ## 0.6.0 ### Minor Changes - Remove `--number-flow-char-height` CSS property in favor of `line-height` ([`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)) ### Patch Changes - Updated dependencies [[`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)]: - number-flow@0.6.0 ## 0.5.14 ### Patch Changes - Updated dependencies [[`5cc3c9b`](https://github.com/barvian/number-flow/commit/5cc3c9b7f7c223719047b964b47dd9d3a42fa371)]: - number-flow@0.5.12 ## 0.5.13 ### Patch Changes - Add CSP strategies (see [#170](https://github.com/barvian/number-flow/issues/170)) ([`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)) - Updated dependencies [[`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)]: - number-flow@0.5.11 ## 0.5.12 ### Patch Changes - Updated dependencies [[`4a6c26e`](https://github.com/barvian/number-flow/commit/4a6c26efe13d6ffc1b84ea75accf511f63669eb9)]: - number-flow@0.5.10 ## 0.5.11 ### Patch Changes - Updated dependencies [[`bdf8ce9`](https://github.com/barvian/number-flow/commit/bdf8ce92df67d6147d7f56998c625fc29e1b7571)]: - number-flow@0.5.9 ## 0.5.10 ### Patch Changes - Fix "custom element already defined" bugs ([`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)) - Updated dependencies [[`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)]: - number-flow@0.5.8 ## 0.5.9 ### Patch Changes - Updated dependencies [[`ee6a0a2`](https://github.com/barvian/number-flow/commit/ee6a0a2f2f09ba187b8df24cdfe0992ee7883192)]: - number-flow@0.5.7 ## 0.5.8 ### Patch Changes - Updated dependencies [[`2539c4b`](https://github.com/barvian/number-flow/commit/2539c4b653fd4aaa17ef6b2ffd77b7a41454da08)]: - number-flow@0.5.6 ## 0.5.7 ### Patch Changes - Updated dependencies [[`bc6476f`](https://github.com/barvian/number-flow/commit/bc6476f910ad58625491c23ed0a8768217f9ab57)]: - number-flow@0.5.5 ## 0.5.6 ### Patch Changes - Release vanilla JS version ([`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)) - Updated dependencies [[`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)]: - number-flow@0.5.4 ## 0.5.5 ### Patch Changes - Revert mask-image change due to <1em char heights ([`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)) - Updated dependencies [[`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)]: - number-flow@0.5.3 ## 0.5.4 ### Patch Changes - Fix use within Server Components ([`1f27875`](https://github.com/barvian/number-flow/commit/1f278754acef7863fd81a27b22e739e2fdb009c0)) ## 0.5.3 ### Patch Changes - Fix Next.js 15.1.4 build errors (see [#95](https://github.com/barvian/number-flow/issues/95)) ([`4244809`](https://github.com/barvian/number-flow/commit/42448099b1652e658b0d5e4cb1968185753a16b6)) ## 0.5.2 ### Patch Changes - Improve `::selection` display and accessibility during transitions ([`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)) - Updated dependencies [[`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)]: - number-flow@0.5.2 ## 0.5.1 ### Patch Changes - Add missing symbol part to SSR ([`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)) - Updated dependencies [[`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)]: - number-flow@0.5.1 ## 0.5.0 ### Minor Changes - Move `continuous` prop into importable plugin ([`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)) ### Patch Changes - Updated dependencies [[`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)]: - number-flow@0.5.0 ## 0.4.4 ### Patch Changes - Add symbol part for styling all symbols ([`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)) - Use useSyncExternalStore for hooks ([`e626a3f`](https://github.com/barvian/number-flow/commit/e626a3fc3776f06ad3b92d8f1afdb8586bf66a13)) - Updated dependencies [[`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)]: - number-flow@0.4.2 ## 0.4.3 ### Patch Changes - Bump React peers ([`215a6bf`](https://github.com/barvian/number-flow/commit/215a6bf6f4d90a40114c5dd588fbf05c812b91f5)) ## 0.4.2 ### Patch Changes - Reduce bundle size ([`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)) - Fix group coming through as attribute ([`0efd6cd`](https://github.com/barvian/number-flow/commit/0efd6cd92af03d15e0010412b147d96eb30b1967)) - Updated dependencies [[`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)]: - number-flow@0.4.1 ## 0.4.1 ### Patch Changes - Fix useCanAnimate hydration ([`e775131`](https://github.com/barvian/number-flow/commit/e775131a09628b98724dd1ec905d06ba78d06e21)) ## 0.4.0 ### Minor Changes - More flexible trend prop ([`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0)) ### Patch Changes - Add digits prop ([`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94)) - Add `` ([`228110c`](https://github.com/barvian/number-flow/commit/228110cf189e81fa030ffc61766290f02d273ff7)) - Fix cursor and improve text selection ([`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)) - Updated dependencies [[`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0), [`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94), [`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)]: - number-flow@0.4.0 ## 0.3.5 ### Patch Changes - Switch to TS private properties to reduce bundle size ([`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)) - Updated dependencies [[`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)]: - number-flow@0.3.10 ## 0.3.4 ### Patch Changes - Expose parts for styling support ([`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)) - Updated dependencies [[`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)]: - number-flow@0.3.9 ## 0.3.3 ### Patch Changes - Add prefix & suffix props ([`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)) - Updated dependencies [[`9854f77`](https://github.com/barvian/number-flow/commit/9854f77e11561fe119bf9009ae1369389a64ba15), [`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)]: - number-flow@0.3.8 ## 0.3.2 ### Patch Changes - Updated dependencies [[`0fac2f6`](https://github.com/barvian/number-flow/commit/0fac2f69b239048054755c556afc3f0eb65767c9)]: - number-flow@0.3.7 ## 0.3.1 ### Patch Changes - More defensive checks on browser globals (see [#58](https://github.com/barvian/number-flow/issues/58)) ([#59](https://github.com/barvian/number-flow/pull/59)) - Updated dependencies [[`13a66fb`](https://github.com/barvian/number-flow/commit/13a66fba336c53687664ad9b859ec705891fce2a)]: - number-flow@0.3.6 ## 0.3.0 ### Minor Changes - Rename number-flow element to avoid conflicts between wrappers ([`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)) ### Patch Changes - fix animation handler types ([`7534f2e`](https://github.com/barvian/number-flow/commit/7534f2e35d6e871b1b13a008b8a923c23e1077e6)) - Updated dependencies [[`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)]: - number-flow@0.3.5 ## 0.2.6 ### Patch Changes - Updated dependencies [[`ff966f4`](https://github.com/barvian/number-flow/commit/ff966f489eaeeacc72b35a8ee4c8cc13fe894eb6)]: - number-flow@0.3.4 ## 0.2.5 ### Patch Changes - remove unused esm-env dep ([`46ffa56`](https://github.com/barvian/number-flow/commit/46ffa569d1a0fefec0219d0f5808be51b7f3f612)) ## 0.2.4 ### Patch Changes - Updated dependencies [[`be3f7da`](https://github.com/barvian/number-flow/commit/be3f7da7ee88b6ab35f67736c98edcfb6909543d)]: - number-flow@0.3.3 ================================================ FILE: packages/react/README.md ================================================ [![NumberFlow for React](https://number-flow.barvian.me/preview.webp)](https://number-flow.barvian.me) # NumberFlow for React An animated number component. [![NPM Version](https://img.shields.io/npm/v/@number-flow/react.svg)](https://npmjs.com/package/@number-flow/react) [![Bundle size](https://badgen.net/bundlephobia/minzip/@number-flow/react@latest)](https://bundlephobia.com/package/@number-flow/react@latest) [![Follow @mbarvian](https://img.shields.io/twitter/follow/mbarvian.svg?style=social&label=Follow)](https://x.com/mbarvian) ## Documentation For full documentation, visit [number-flow.barvian.me](https://number-flow.barvian.me). ================================================ FILE: packages/react/package.json ================================================ { "name": "@number-flow/react", "publishConfig": { "access": "public" }, "version": "0.6.0", "author": { "name": "Maxwell Barvian", "email": "max@barvian.me", "url": "https://barvian.me" }, "description": "A component to transition and format numbers.", "license": "MIT", "homepage": "https://number-flow.barvian.me", "repository": { "type": "git", "url": "https://github.com/barvian/number-flow", "directory": "src" }, "bugs": { "url": "https://github.com/barvian/number-flow/issues" }, "keywords": [ "accessible", "odometer", "animation", "number-format", "number-animation", "animated-number" ], "files": [ "dist", "README.md" ], "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "exports": { ".": { "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" }, "require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } } }, "scripts": { "build": "bunchee --tsconfig tsconfig.build.json", "dev": "bunchee --watch", "test": "pnpm -r --workspace-concurrency 1 --filter=\"./test/apps/*\" test" }, "dependencies": { "esm-env": "^1.1.4", "number-flow": "workspace:*" }, "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^22.7.9", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "bunchee": "^6.3.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } } ================================================ FILE: packages/react/src/NumberFlow.tsx ================================================ 'use client' // This has to be in a separate file for #95. // Make sure tsup outputs both files. import * as React from 'react' import NumberFlowLite, { type Value, type Format, type Props, renderInnerHTML, formatToData, type Data, prefersReducedMotion as _prefersReducedMotion, canAnimate as _canAnimate, define } from 'number-flow/lite' import { BROWSER } from 'esm-env' const REACT_MAJOR = parseInt(React.version.match(/^(\d+)\./)?.[1]!) const isReact19 = REACT_MAJOR >= 19 // Can't wait to not have to do this in React 19: const OBSERVED_ATTRIBUTES = ['data', 'digits'] as const type ObservedAttribute = (typeof OBSERVED_ATTRIBUTES)[number] export class NumberFlowElement extends NumberFlowLite { static observedAttributes = isReact19 ? [] : OBSERVED_ATTRIBUTES attributeChangedCallback(attr: ObservedAttribute, _oldValue: string, newValue: string) { this[attr] = JSON.parse(newValue) } } define('number-flow-react', NumberFlowElement) type BaseProps = React.HTMLAttributes & Partial & { isolate?: boolean willChange?: boolean onAnimationsStart?: (e: CustomEvent) => void onAnimationsFinish?: (e: CustomEvent) => void } type NumberFlowImplProps = BaseProps & { innerRef: React.MutableRefObject group?: GroupContext data: Data } // You're supposed to cache these between uses: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString // Serialize to strings b/c React: const formatters: Record = {} // Tiny workaround to support React 19 until it's released: function identity(v: T) { return v } const serialize = isReact19 ? identity : JSON.stringify function splitProps>( props: T ): [Omit, Omit>] { const { transformTiming, spinTiming, opacityTiming, animated, respectMotionPreference, trend, plugins, ...rest } = props return [ { transformTiming, spinTiming, opacityTiming, animated, respectMotionPreference, trend, plugins }, rest ] } type NumberFlowImplState = {} type NumberFlowImplSnapshot = (() => void) | null // React doesn't like undefined // We need a class component to use getSnapshotBeforeUpdate: class NumberFlowImpl extends React.Component< NumberFlowImplProps, NumberFlowImplState, NumberFlowImplSnapshot > { constructor(props: NumberFlowImplProps) { super(props) this.handleRef = this.handleRef.bind(this) } // Update the non-`data` props to avoid JSON serialization // Data needs to be set in render still: updateProperties(prevProps?: Readonly) { if (!this.el) return this.el.batched = !this.props.isolate const [nonData] = splitProps(this.props) Object.entries(nonData).forEach(([k, v]) => { // @ts-ignore this.el![k] = v ?? NumberFlowElement.defaultProps[k] }) if (prevProps?.onAnimationsStart) this.el.removeEventListener('animationsstart', prevProps.onAnimationsStart as EventListener) if (this.props.onAnimationsStart) this.el.addEventListener('animationsstart', this.props.onAnimationsStart as EventListener) if (prevProps?.onAnimationsFinish) this.el.removeEventListener('animationsfinish', prevProps.onAnimationsFinish as EventListener) if (this.props.onAnimationsFinish) this.el.addEventListener('animationsfinish', this.props.onAnimationsFinish as EventListener) } override componentDidMount() { this.updateProperties() if (isReact19 && this.el) { // React 19 needs this because the attributeChangedCallback isn't called: this.el.digits = this.props.digits this.el.data = this.props.data } } override getSnapshotBeforeUpdate(prevProps: Readonly) { this.updateProperties(prevProps) if (prevProps.data !== this.props.data) { if (this.props.group) { this.props.group.willUpdate() return () => this.props.group?.didUpdate() } if (!this.props.isolate) { this.el?.willUpdate() return () => this.el?.didUpdate() } } return null } override componentDidUpdate( _: Readonly, __: NumberFlowImplState, didUpdate: NumberFlowImplSnapshot ) { didUpdate?.() } private el?: NumberFlowElement handleRef(el: NumberFlowElement) { if (this.props.innerRef) this.props.innerRef.current = el this.el = el } override render() { const [ _, { innerRef, className, data, nonce, willChange, isolate, group, digits, onAnimationsStart, onAnimationsFinish, ...rest } ] = splitProps(this.props) return ( // @ts-expect-error missing types ) } } export type NumberFlowProps = BaseProps & { value: Value locales?: Intl.LocalesArgument format?: Format prefix?: string suffix?: string } const NumberFlow = React.forwardRef(function NumberFlow( { value, locales, format, prefix, suffix, ...props }, _ref ) { React.useImperativeHandle(_ref, () => ref.current!, []) const ref = React.useRef(undefined) const group = React.useContext(NumberFlowGroupContext) group?.useRegister(ref) const localesString = React.useMemo(() => (locales ? JSON.stringify(locales) : ''), [locales]) const formatString = React.useMemo(() => (format ? JSON.stringify(format) : ''), [format]) const data = React.useMemo(() => { const formatter = (formatters[`${localesString}:${formatString}`] ??= new Intl.NumberFormat( locales, format )) return formatToData(value, formatter, prefix, suffix) }, [value, localesString, formatString, prefix, suffix]) return }) export default NumberFlow // NumberFlowGroup type GroupContext = { useRegister: (ref: React.MutableRefObject) => void willUpdate: () => void didUpdate: () => void } const NumberFlowGroupContext = React.createContext(undefined) export function NumberFlowGroup({ children }: { children: React.ReactNode }) { const flows = React.useRef(new Set>()) const updating = React.useRef(false) const pending = React.useRef(new WeakMap()) const value = React.useMemo( () => ({ useRegister(ref) { React.useEffect(() => { flows.current.add(ref) return () => { flows.current.delete(ref) } }, []) }, willUpdate() { if (updating.current) return updating.current = true flows.current.forEach((ref) => { const f = ref.current if (!f || !f.created) return f.willUpdate() pending.current.set(f, true) }) }, didUpdate() { flows.current.forEach((ref) => { const f = ref.current if (!f || !pending.current.get(f)) return f.didUpdate() pending.current.delete(f) }) updating.current = false } }), [] ) return {children} } ================================================ FILE: packages/react/src/index.tsx ================================================ import * as React from 'react' import { prefersReducedMotion as _prefersReducedMotion, canAnimate as _canAnimate } from 'number-flow/lite' import { buildStyles } from 'number-flow/csp' export const styles = buildStyles('-react') export * from 'number-flow/plugins' export { default } from './NumberFlow' export * from './NumberFlow' export type { Value, Format, Trend, NumberPartType } from 'number-flow/lite' export const useIsSupported = () => React.useSyncExternalStore( () => () => {}, // this value doesn't change, but it's useful to specify a different SSR value: () => _canAnimate, () => false ) export const usePrefersReducedMotion = () => React.useSyncExternalStore( (cb) => { _prefersReducedMotion?.addEventListener('change', cb) return () => _prefersReducedMotion?.removeEventListener('change', cb) }, () => _prefersReducedMotion!.matches, () => false ) export function useCanAnimate({ respectMotionPreference = true } = {}) { const isSupported = useIsSupported() const reducedMotion = usePrefersReducedMotion() return isSupported && (!respectMotionPreference || !reducedMotion) } ================================================ FILE: packages/react/test/apps/react-18/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: packages/react/test/apps/react-18/app/can-animate/page.tsx ================================================ 'use client' import * as React from 'react' import { useCanAnimate } from '@number-flow/react' export default function Page() { const canAnimate = useCanAnimate() const disrespectMotionPreference = useCanAnimate({ respectMotionPreference: false }) return (

{String(canAnimate)}

{String(disrespectMotionPreference)}

) } ================================================ FILE: packages/react/test/apps/react-18/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: packages/react/test/apps/react-18/app/group-1-unchanged/page.tsx ================================================ 'use client' import * as React from 'react' import NumberFlow, { NumberFlowElement, NumberFlowGroup } from '@number-flow/react' import { flushSync } from 'react-dom' export default function Page() { const [value, setValue] = React.useState(42) const ref1 = React.useRef(null) const ref2 = React.useRef(null) return ( <>

) } ================================================ FILE: packages/react/test/apps/react-18/app/hashes/page.tsx ================================================ import NumberFlow from '@number-flow/react' export default function Page() { return } ================================================ FILE: packages/react/test/apps/react-18/app/layout.tsx ================================================ import type { Metadata } from 'next' // import { Inter } from 'next/font/google' import './globals.css' // const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'NumberFlow tests' } export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( {/* {children} */} {children} ) } ================================================ FILE: packages/react/test/apps/react-18/app/nonce/page.tsx ================================================ import NumberFlow from '@number-flow/react' export default function Page() { return ( <> ) } ================================================ FILE: packages/react/test/apps/react-18/app/page.tsx ================================================ 'use client' import * as React from 'react' import NumberFlow, { NumberFlowElement, NumberFlowGroup, continuous } from '@number-flow/react' import { flushSync } from 'react-dom' export default function Page() { const [value, setValue] = React.useState(42) const ref1 = React.useRef(null) const ref2 = React.useRef(null) return ( <>
Text node{' '} -1} prefix=":" suffix="/mo" onAnimationsStart={() => console.log('start')} onAnimationsFinish={() => console.log('finish')} transformTiming={{ easing: 'linear', duration: 500 }} spinTiming={{ easing: 'linear', duration: 800 }} opacityTiming={{ easing: 'linear', duration: 500 }} />

) } ================================================ FILE: packages/react/test/apps/react-18/next.config.mjs ================================================ import webpack from 'webpack' import { createHash } from 'node:crypto' import { styles } from '@number-flow/react' const nonceCsp = "style-src 'nonce-test-nonce'" const hashesCsp = `style-src ${styles.map((style) => `'sha256-${createHash('sha256').update(style).digest('base64')}'`).join(' ')}` /** @type {import('next').NextConfig} */ const nextConfig = { async headers() { return [ { source: '/nonce', headers: [ { key: 'Content-Security-Policy', value: nonceCsp } ] }, { source: '/hashes', headers: [ { key: 'Content-Security-Policy', value: hashesCsp } ] } ] }, webpack(config, context) { config.plugins.push( new webpack.DefinePlugin({ __REACT_DEVTOOLS_GLOBAL_HOOK__: '({ isDisabled: true })' }) ) return config } } export default nextConfig ================================================ FILE: packages/react/test/apps/react-18/package.json ================================================ { "name": "test", "version": "0.1.0", "private": true, "scripts": { "build": "next build", "start": "next start --port 3039", "dev": "next dev --port 3039", "lint": "next lint", "test": "playwright test", "test:ui": "playwright test --ui", "test:update": "playwright test --update-snapshots" }, "dependencies": { "@number-flow/react": "workspace:^", "next": "14.2.15", "react": "^18", "react-dom": "^18" }, "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^22.7.5", "@types/react": "^18", "@types/react-dom": "^18", "postcss": "^8", "tailwindcss": "^3.4.1", "tw-reset": "^0.0.5", "typescript": "^5", "webpack": "^5.96.1" }, "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" } ================================================ FILE: packages/react/test/apps/react-18/playwright.config.ts ================================================ export { config as default } from '../../../../../lib/playwright' ================================================ FILE: packages/react/test/apps/react-18/postcss.config.mjs ================================================ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {} } } export default config ================================================ FILE: packages/react/test/apps/react-18/tailwind.config.ts ================================================ import type { Config } from 'tailwindcss' import reset from 'tw-reset' const config: Config = { presets: [reset], content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}' ], theme: { extend: { colors: { background: 'var(--background)', foreground: 'var(--foreground)' } } }, plugins: [] } export default config ================================================ FILE: packages/react/test/apps/react-18/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: packages/react/test/apps/react-19/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # testing /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # env files (can opt-in for commiting if needed) .env* # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: packages/react/test/apps/react-19/app/can-animate/page.tsx ================================================ 'use client' import * as React from 'react' import { useCanAnimate } from '@number-flow/react' export default function Page() { const canAnimate = useCanAnimate() const disrespectMotionPreference = useCanAnimate({ respectMotionPreference: false }) return (

{String(canAnimate)}

{String(disrespectMotionPreference)}

) } ================================================ FILE: packages/react/test/apps/react-19/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: packages/react/test/apps/react-19/app/group-1-unchanged/page.tsx ================================================ 'use client' import * as React from 'react' import NumberFlow, { NumberFlowElement, NumberFlowGroup } from '@number-flow/react' import { flushSync } from 'react-dom' export default function Page() { const [value, setValue] = React.useState(42) const ref1 = React.useRef(null) const ref2 = React.useRef(null) return ( <>

) } ================================================ FILE: packages/react/test/apps/react-19/app/hashes/page.tsx ================================================ import NumberFlow from '@number-flow/react' export default function Page() { return } ================================================ FILE: packages/react/test/apps/react-19/app/layout.tsx ================================================ import type { Metadata } from 'next' // import { Inter } from 'next/font/google' import './globals.css' // const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'NumberFlow tests' } export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( {/* {children} */} {children} ) } ================================================ FILE: packages/react/test/apps/react-19/app/nonce/page.tsx ================================================ import NumberFlow from '@number-flow/react' export default function Page() { return ( <> ) } ================================================ FILE: packages/react/test/apps/react-19/app/page.tsx ================================================ 'use client' import * as React from 'react' import NumberFlow, { NumberFlowElement, NumberFlowGroup, continuous } from '@number-flow/react' import { flushSync } from 'react-dom' export default function Page() { const [value, setValue] = React.useState(42) const ref1 = React.useRef(null) const ref2 = React.useRef(null) return ( <>
Text node{' '} -1} prefix=":" suffix="/mo" onAnimationsStart={() => console.log('start')} onAnimationsFinish={() => console.log('finish')} transformTiming={{ easing: 'linear', duration: 500 }} spinTiming={{ easing: 'linear', duration: 800 }} opacityTiming={{ easing: 'linear', duration: 500 }} />

) } ================================================ FILE: packages/react/test/apps/react-19/app/sc/page.tsx ================================================ import NumberFlow from '@number-flow/react' export default function SC() { return } ================================================ FILE: packages/react/test/apps/react-19/next.config.ts ================================================ import type { NextConfig } from 'next' import webpack from 'webpack' import { createHash } from 'node:crypto' import { styles } from '@number-flow/react' const nonceCsp = "style-src 'nonce-test-nonce'" const hash = (style: string) => `'sha256-${createHash('sha256').update(style).digest('base64')}'` const hashesCsp = `style-src ${styles.map(hash).join(' ')}` const nextConfig: NextConfig = { async headers() { return [ { source: '/nonce', headers: [ { key: 'Content-Security-Policy', value: nonceCsp } ] }, { source: '/hashes', headers: [ { key: 'Content-Security-Policy', value: hashesCsp } ] } ] }, webpack(config, context) { config.plugins.push( new webpack.DefinePlugin({ __REACT_DEVTOOLS_GLOBAL_HOOK__: '({ isDisabled: true })' }) ) return config }, devIndicators: { appIsrStatus: false } } export default nextConfig ================================================ FILE: packages/react/test/apps/react-19/package.json ================================================ { "name": "react-19", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --port 3039", "build": "next build", "start": "next start --port 3039", "lint": "next lint", "test": "playwright test", "test:ui": "playwright test --ui", "test:update": "playwright test --update-snapshots" }, "dependencies": { "@number-flow/react": "workspace:^", "next": "15.1.4", "react": "19.0.0", "react-dom": "19.0.0" }, "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5", "webpack": "^5.96.1" } } ================================================ FILE: packages/react/test/apps/react-19/playwright.config.ts ================================================ export { config as default } from '../../../../../lib/playwright' ================================================ FILE: packages/react/test/apps/react-19/postcss.config.mjs ================================================ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {} } } export default config ================================================ FILE: packages/react/test/apps/react-19/tailwind.config.ts ================================================ import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}' ], theme: { extend: { colors: { background: 'var(--background)', foreground: 'var(--foreground)' } } }, plugins: [] } export default config ================================================ FILE: packages/react/test/apps/react-19/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: packages/react/tsconfig.build.json ================================================ { "extends": ["./tsconfig.json", "../../tsconfig.build.json"] } ================================================ FILE: packages/react/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { "jsx": "react" } } ================================================ FILE: packages/svelte/.gitignore ================================================ test-results node_modules # Output .output .vercel /.svelte-kit /build /dist # testing /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ # OS .DS_Store Thumbs.db # Env .env .env.* !.env.example !.env.test # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* ================================================ FILE: packages/svelte/.npmrc ================================================ engine-strict=true ================================================ FILE: packages/svelte/CHANGELOG.md ================================================ # @number-flow/svelte ## 0.4.0 ### Minor Changes - Remove `--number-flow-char-height` CSS property in favor of `line-height` ([`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)) ### Patch Changes - Updated dependencies [[`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)]: - number-flow@0.6.0 ## 0.3.13 ### Patch Changes - Updated dependencies [[`5cc3c9b`](https://github.com/barvian/number-flow/commit/5cc3c9b7f7c223719047b964b47dd9d3a42fa371)]: - number-flow@0.5.12 ## 0.3.12 ### Patch Changes - Add CSP strategies (see [#170](https://github.com/barvian/number-flow/issues/170)) ([`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)) - Updated dependencies [[`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)]: - number-flow@0.5.11 ## 0.3.11 ### Patch Changes - Updated dependencies [[`4a6c26e`](https://github.com/barvian/number-flow/commit/4a6c26efe13d6ffc1b84ea75accf511f63669eb9)]: - number-flow@0.5.10 ## 0.3.10 ### Patch Changes - Updated dependencies [[`bdf8ce9`](https://github.com/barvian/number-flow/commit/bdf8ce92df67d6147d7f56998c625fc29e1b7571)]: - number-flow@0.5.9 ## 0.3.9 ### Patch Changes - Fix Svelte 5 props (see [#136](https://github.com/barvian/number-flow/issues/136)) ([`605008d`](https://github.com/barvian/number-flow/commit/605008da694cb56ec4047daa06fef9a1d74ef0b6)) ## 0.3.8 ### Patch Changes - Fix "custom element already defined" bugs ([`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)) - Updated dependencies [[`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)]: - number-flow@0.5.8 ## 0.3.7 ### Patch Changes - Updated dependencies [[`ee6a0a2`](https://github.com/barvian/number-flow/commit/ee6a0a2f2f09ba187b8df24cdfe0992ee7883192)]: - number-flow@0.5.7 ## 0.3.6 ### Patch Changes - Updated dependencies [[`2539c4b`](https://github.com/barvian/number-flow/commit/2539c4b653fd4aaa17ef6b2ffd77b7a41454da08)]: - number-flow@0.5.6 ## 0.3.5 ### Patch Changes - Updated dependencies [[`bc6476f`](https://github.com/barvian/number-flow/commit/bc6476f910ad58625491c23ed0a8768217f9ab57)]: - number-flow@0.5.5 ## 0.3.4 ### Patch Changes - Release vanilla JS version ([`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)) - Updated dependencies [[`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)]: - number-flow@0.5.4 ## 0.3.3 ### Patch Changes - Revert mask-image change due to <1em char heights ([`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)) - Updated dependencies [[`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)]: - number-flow@0.5.3 ## 0.3.2 ### Patch Changes - Improve `::selection` display and accessibility during transitions ([`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)) - Updated dependencies [[`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)]: - number-flow@0.5.2 ## 0.3.1 ### Patch Changes - Add missing symbol part to SSR ([`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)) - Updated dependencies [[`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)]: - number-flow@0.5.1 ## 0.3.0 ### Minor Changes - Move `continuous` prop into importable plugin ([`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)) ### Patch Changes - Updated dependencies [[`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)]: - number-flow@0.5.0 ## 0.2.3 ### Patch Changes - Add symbol part for styling all symbols ([`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)) - Updated dependencies [[`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)]: - number-flow@0.4.2 ## 0.2.2 ### Patch Changes - Reduce bundle size ([`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)) - Updated dependencies [[`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)]: - number-flow@0.4.1 ## 0.2.1 ### Patch Changes - Fix getCanAnimate hydration ([`e775131`](https://github.com/barvian/number-flow/commit/e775131a09628b98724dd1ec905d06ba78d06e21)) ## 0.2.0 ### Minor Changes - More flexible trend prop ([`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0)) ### Patch Changes - Add digits prop ([`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94)) - Fix cursor and improve text selection ([`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)) - Updated dependencies [[`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0), [`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94), [`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)]: - number-flow@0.4.0 ## 0.1.7 ### Patch Changes - Switch to TS private properties to reduce bundle size ([`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)) - Updated dependencies [[`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)]: - number-flow@0.3.10 ## 0.1.6 ### Patch Changes - Expose parts for styling support ([`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)) - Updated dependencies [[`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)]: - number-flow@0.3.9 ## 0.1.5 ### Patch Changes - Add prefix & suffix props ([`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)) - Updated dependencies [[`9854f77`](https://github.com/barvian/number-flow/commit/9854f77e11561fe119bf9009ae1369389a64ba15), [`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)]: - number-flow@0.3.8 ## 0.1.4 ### Patch Changes - Add `` ([`b291400`](https://github.com/barvian/number-flow/commit/b2914009cf54d58604d3e34f0b6f16dc4b912a6a)) - Updated dependencies [[`0fac2f6`](https://github.com/barvian/number-flow/commit/0fac2f69b239048054755c556afc3f0eb65767c9)]: - number-flow@0.3.7 ## 0.1.3 ### Patch Changes - More defensive checks on browser globals (see [#58](https://github.com/barvian/number-flow/issues/58)) ([#59](https://github.com/barvian/number-flow/pull/59)) - Updated dependencies [[`13a66fb`](https://github.com/barvian/number-flow/commit/13a66fba336c53687664ad9b859ec705891fce2a)]: - number-flow@0.3.6 ## 0.1.2 ### Patch Changes - Use main field for bundlephobia ([`790e0c4`](https://github.com/barvian/number-flow/commit/790e0c4c672ffb473614b8c8eed33e4dece3aa2f)) ## 0.1.1 ### Patch Changes - Remove unused code ([`d2a1c7b`](https://github.com/barvian/number-flow/commit/d2a1c7b7fcc1523e0efd693acc8b971e35aac102)) ## 0.1.0 ### Minor Changes - Rename number-flow element to avoid conflicts between wrappers ([`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)) ### Patch Changes - Updated dependencies [[`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)]: - number-flow@0.3.5 ================================================ FILE: packages/svelte/README.md ================================================ [![NumberFlow for Svelte](https://number-flow.barvian.me/preview.webp)](https://number-flow.barvian.me/svelte) # NumberFlow for Svelte An animated number component. [![NPM Version](https://img.shields.io/npm/v/@number-flow/svelte.svg)](https://npmjs.com/package/@number-flow/svelte) [![Bundle size](https://badgen.net/bundlephobia/minzip/@number-flow/svelte@latest)](https://bundlephobia.com/package/@number-flow/svelte@latest) [![Follow @mbarvian](https://img.shields.io/twitter/follow/mbarvian.svg?style=social&label=Follow)](https://x.com/mbarvian) ## Documentation For full documentation, visit [number-flow.barvian.me/svelte](https://number-flow.barvian.me/svelte). ================================================ FILE: packages/svelte/package.json ================================================ { "name": "@number-flow/svelte", "publishConfig": { "access": "public" }, "version": "0.4.0", "description": "A component to transition and format numbers.", "license": "MIT", "homepage": "https://number-flow.barvian.me/svelte", "repository": { "type": "git", "url": "https://github.com/barvian/number-flow", "directory": "src" }, "bugs": { "url": "https://github.com/barvian/number-flow/issues" }, "keywords": [ "accessible", "odometer", "animation", "number-format", "number-animation", "animated-number" ], "scripts": { "dev": "vite dev", "build:watch": "svelte-package --watch", "build": "svelte-kit sync && svelte-package && publint", "build:site": "vite build && pnpm build", "preview": "vite preview --port 3039", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test": "playwright test", "test:ui": "playwright test --ui", "test:update": "playwright test --update-snapshots" }, "files": [ "dist", "!dist/**/*.test.*", "!dist/**/*.spec.*", "README.md" ], "main": "./dist/index.js", "svelte": "./dist/index.js", "types": "./dist/index.d.ts", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "default": "./dist/index.js" } }, "dependencies": { "esm-env": "^1.1.4", "number-flow": "workspace:*" }, "peerDependencies": { "svelte": "^4 || ^5" }, "devDependencies": { "@playwright/test": "^1.45.3", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "autoprefixer": "^10.4.20", "publint": "^0.2.0", "svelte": "^4.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^3.4.9", "typescript": "^5.0.0", "vite": "^5.0.11" } } ================================================ FILE: packages/svelte/playwright.config.ts ================================================ import { defineConfig } from '@playwright/test' import { config } from '../../lib/playwright' // Use prod build cause it includes hydration errors but excludes random Vite stuff: export default defineConfig({ ...config, webServer: { ...config.webServer, command: 'pnpm build:site && pnpm preview' } }) ================================================ FILE: packages/svelte/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {} } } ================================================ FILE: packages/svelte/src/app.css ================================================ @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; ================================================ FILE: packages/svelte/src/app.d.ts ================================================ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} } } export {} ================================================ FILE: packages/svelte/src/app.html ================================================ %sveltekit.head%
%sveltekit.body%
================================================ FILE: packages/svelte/src/lib/NumberFlow.svelte ================================================ {@html BROWSER ? undefined : renderInnerHTML(data, { nonce, elementSuffix: '-svelte' })} ================================================ FILE: packages/svelte/src/lib/NumberFlowGroup.svelte ================================================ ================================================ FILE: packages/svelte/src/lib/group.ts ================================================ import type NumberFlowLite from 'number-flow/lite' import { getContext, setContext } from 'svelte' import type { Readable } from 'svelte/store' let groupKey = Symbol('group') export type RegisterWithGroup = (el: Readable) => void export type GroupContext = { register: RegisterWithGroup } export function setGroupContext(ctx: GroupContext) { setContext(groupKey, ctx) } export function getGroupContext() { return getContext(groupKey) as GroupContext | undefined } ================================================ FILE: packages/svelte/src/lib/index.ts ================================================ import { canAnimate as _canAnimate, prefersReducedMotion as _prefersReducedMotion } from 'number-flow/lite' import { onMount } from 'svelte' import { derived, readable } from 'svelte/store' import { buildStyles } from 'number-flow/csp' export const styles = buildStyles('-svelte') export type { Value, Format, Trend } from 'number-flow/lite' export * from 'number-flow/plugins' export { default as NumberFlowGroup } from './NumberFlowGroup.svelte' export { default, NumberFlowElement } from './NumberFlow.svelte' const canAnimate = readable(false, (set) => { onMount(() => { set(_canAnimate) }) }) const prefersReducedMotion = readable(false, (set) => { onMount(() => { set(_prefersReducedMotion?.matches ?? false) const onChange = ({ matches }: MediaQueryListEvent) => { set(matches) } _prefersReducedMotion?.addEventListener('change', onChange) return () => { _prefersReducedMotion?.removeEventListener('change', onChange) } }) }) const canAnimateWithPreference = derived( [canAnimate, prefersReducedMotion], ([canAnimate, prefersReducedMotion]) => canAnimate && !prefersReducedMotion ) export const getCanAnimate = ({ respectMotionPreference = true } = {}) => respectMotionPreference ? canAnimateWithPreference : canAnimate ================================================ FILE: packages/svelte/src/routes/+layout.svelte ================================================ ================================================ FILE: packages/svelte/src/routes/+page.svelte ================================================
Text node -1} prefix=":" suffix="/mo" on:animationsstart={() => console.log('start')} on:animationsfinish={() => console.log('finish')} transformTiming={{ easing: 'linear', duration: 500 }} spinTiming={{ easing: 'linear', duration: 800 }} opacityTiming={{ easing: 'linear', duration: 500 }} />

================================================ FILE: packages/svelte/src/routes/can-animate/+page.svelte ================================================

{String($canAnimate)}

{String($disrespectMotionPreference)}

================================================ FILE: packages/svelte/src/routes/group-1-unchanged/+page.svelte ================================================

================================================ FILE: packages/svelte/src/routes/hashes/+page.server.ts ================================================ import type { PageServerLoad } from './$types' import { createHash } from 'node:crypto' import { styles } from '$lib/index.js' const hash = (style: string) => `'sha256-${createHash('sha256').update(style).digest('base64')}'` export const load: PageServerLoad = ({ setHeaders }) => { setHeaders({ 'Content-Security-Policy': `style-src ${styles.map(hash).join(' ')}` }) return {} } ================================================ FILE: packages/svelte/src/routes/hashes/+page.svelte ================================================ ================================================ FILE: packages/svelte/src/routes/nonce/+page.server.ts ================================================ import type { PageServerLoad } from './$types' export const load: PageServerLoad = ({ setHeaders }) => { setHeaders({ 'Content-Security-Policy': "style-src 'nonce-test-nonce'" }) return {} } ================================================ FILE: packages/svelte/src/routes/nonce/+page.svelte ================================================ ================================================ FILE: packages/svelte/svelte.config.js ================================================ import adapter from '@sveltejs/adapter-auto' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors preprocess: vitePreprocess(), kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter() } } export default config ================================================ FILE: packages/svelte/tailwind.config.ts ================================================ import type { Config } from 'tailwindcss' export default { content: ['./src/**/*.{html,js,svelte,ts}'], theme: { extend: {} }, plugins: [] } as Config ================================================ FILE: packages/svelte/tsconfig.json ================================================ { "extends": ["./.svelte-kit/tsconfig.json", "../../tsconfig.json"], "include": ["src"] } ================================================ FILE: packages/svelte/vite.config.ts ================================================ import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [sveltekit()], server: { port: 3039, strictPort: true } }) ================================================ FILE: packages/vue/CHANGELOG.md ================================================ # @number-flow/vue ## 0.5.0 ### Minor Changes - Remove `--number-flow-char-height` CSS property in favor of `line-height` ([`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)) ### Patch Changes - Updated dependencies [[`e8a8904`](https://github.com/barvian/number-flow/commit/e8a890432ef7f78661fce88ce53ac8e277ba3aa6)]: - number-flow@0.6.0 ## 0.4.12 ### Patch Changes - Updated dependencies [[`5cc3c9b`](https://github.com/barvian/number-flow/commit/5cc3c9b7f7c223719047b964b47dd9d3a42fa371)]: - number-flow@0.5.12 ## 0.4.11 ### Patch Changes - Add CSP strategies (see [#170](https://github.com/barvian/number-flow/issues/170)) ([`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)) - Updated dependencies [[`a7b3b0b`](https://github.com/barvian/number-flow/commit/a7b3b0b581fc05b914ea9e1ab1441da75b30bb67)]: - number-flow@0.5.11 ## 0.4.10 ### Patch Changes - Updated dependencies [[`4a6c26e`](https://github.com/barvian/number-flow/commit/4a6c26efe13d6ffc1b84ea75accf511f63669eb9)]: - number-flow@0.5.10 ## 0.4.9 ### Patch Changes - Updated dependencies [[`bdf8ce9`](https://github.com/barvian/number-flow/commit/bdf8ce92df67d6147d7f56998c625fc29e1b7571)]: - number-flow@0.5.9 ## 0.4.8 ### Patch Changes - Fix "custom element already defined" bugs ([`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)) - Updated dependencies [[`a0d2a09`](https://github.com/barvian/number-flow/commit/a0d2a0901c06c647152654068163202e988d1f5d)]: - number-flow@0.5.8 ## 0.4.7 ### Patch Changes - Updated dependencies [[`ee6a0a2`](https://github.com/barvian/number-flow/commit/ee6a0a2f2f09ba187b8df24cdfe0992ee7883192)]: - number-flow@0.5.7 ## 0.4.6 ### Patch Changes - Updated dependencies [[`2539c4b`](https://github.com/barvian/number-flow/commit/2539c4b653fd4aaa17ef6b2ffd77b7a41454da08)]: - number-flow@0.5.6 ## 0.4.5 ### Patch Changes - Updated dependencies [[`bc6476f`](https://github.com/barvian/number-flow/commit/bc6476f910ad58625491c23ed0a8768217f9ab57)]: - number-flow@0.5.5 ## 0.4.4 ### Patch Changes - Release vanilla JS version ([`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)) - Updated dependencies [[`3929e33`](https://github.com/barvian/number-flow/commit/3929e33e8dcef03462593428639d66134f84c51d)]: - number-flow@0.5.4 ## 0.4.3 ### Patch Changes - Revert mask-image change due to <1em char heights ([`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)) - Updated dependencies [[`e5be284`](https://github.com/barvian/number-flow/commit/e5be2840dfd0858894463beb8e3ebcffefb48d5d)]: - number-flow@0.5.3 ## 0.4.2 ### Patch Changes - Improve `::selection` display and accessibility during transitions ([`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)) - Updated dependencies [[`301a755`](https://github.com/barvian/number-flow/commit/301a755edd8bde8ad8a6fe680c1882e8f6230393)]: - number-flow@0.5.2 ## 0.4.1 ### Patch Changes - Add missing symbol part to SSR ([`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)) - Updated dependencies [[`34ea785`](https://github.com/barvian/number-flow/commit/34ea7856d6a75fba420bf379656dc3c8a7018948)]: - number-flow@0.5.1 ## 0.4.0 ### Minor Changes - Move `continuous` prop into importable plugin ([`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)) ### Patch Changes - Updated dependencies [[`e40a15e`](https://github.com/barvian/number-flow/commit/e40a15e3df55727a196ba1dc9a1230139f4d69ff)]: - number-flow@0.5.0 ## 0.3.4 ### Patch Changes - Use UMD for CJS build (see [#88](https://github.com/barvian/number-flow/issues/88)) ([`2f9495d`](https://github.com/barvian/number-flow/commit/2f9495dd4b69dbd4716cfbeb2a1cfb2d9ecd0a00)) ## 0.3.3 ### Patch Changes - Add symbol part for styling all symbols ([`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)) - Updated dependencies [[`46ab8bd`](https://github.com/barvian/number-flow/commit/46ab8bd96467b1e27383546ce67a9889263ad0eb)]: - number-flow@0.4.2 ## 0.3.2 ### Patch Changes - Reduce bundle size ([`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)) - Updated dependencies [[`efd355d`](https://github.com/barvian/number-flow/commit/efd355dda6c5005f5dec8bba0c4a0ff705144ee3)]: - number-flow@0.4.1 ## 0.3.1 ### Patch Changes - Fix useCanAnimate hydration ([`e775131`](https://github.com/barvian/number-flow/commit/e775131a09628b98724dd1ec905d06ba78d06e21)) ## 0.3.0 ### Minor Changes - More flexible trend prop ([`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0)) ### Patch Changes - Add digits prop ([`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94)) - Fix cursor and improve text selection ([`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)) - Updated dependencies [[`6f53990`](https://github.com/barvian/number-flow/commit/6f539906a439f567d50667d9fe9d52de4e2a4bd0), [`05423bb`](https://github.com/barvian/number-flow/commit/05423bbe4f0f4dab8caf442032fae9ecfccdbf94), [`8c1f922`](https://github.com/barvian/number-flow/commit/8c1f92232375bc35cf4a3b5f8136206c70918809)]: - number-flow@0.4.0 ## 0.2.7 ### Patch Changes - Switch to TS private properties to reduce bundle size ([`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)) - Updated dependencies [[`765e43b`](https://github.com/barvian/number-flow/commit/765e43b4f2670ec532b5ef69b745d5d350f51bdd)]: - number-flow@0.3.10 ## 0.2.6 ### Patch Changes - Improve tree-shaking ([`7885ecd`](https://github.com/barvian/number-flow/commit/7885ecddd717822b48dc43c4ab0cccbb8c33cf6f)) ## 0.2.5 ### Patch Changes - Expose parts for styling support ([`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)) - Updated dependencies [[`27156cc`](https://github.com/barvian/number-flow/commit/27156cc3d4750d06293b7022afca492024f4bea4)]: - number-flow@0.3.9 ## 0.2.4 ### Patch Changes - Add prefix & suffix props ([`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)) - Updated dependencies [[`9854f77`](https://github.com/barvian/number-flow/commit/9854f77e11561fe119bf9009ae1369389a64ba15), [`adcf50f`](https://github.com/barvian/number-flow/commit/adcf50f93eec1f6a469004ab58aae4b2799b3c14)]: - number-flow@0.3.8 ## 0.2.3 ### Patch Changes - Fix for `` and v-if ([`a57700c`](https://github.com/barvian/number-flow/commit/a57700c211d67d2d1d8ea228bae9bd427bee553c)) - Updated dependencies [[`0fac2f6`](https://github.com/barvian/number-flow/commit/0fac2f69b239048054755c556afc3f0eb65767c9)]: - number-flow@0.3.7 ## 0.2.2 ### Patch Changes - Add `` ([`ad220fd`](https://github.com/barvian/number-flow/commit/ad220fdb95b524b451e11bfcddd1f86e768e007d)) ## 0.2.1 ### Patch Changes - More defensive checks on browser globals (see [#58](https://github.com/barvian/number-flow/issues/58)) ([#59](https://github.com/barvian/number-flow/pull/59)) - Updated dependencies [[`13a66fb`](https://github.com/barvian/number-flow/commit/13a66fba336c53687664ad9b859ec705891fce2a)]: - number-flow@0.3.6 ## 0.2.0 ### Minor Changes - use lowercase event names ([`8406974`](https://github.com/barvian/number-flow/commit/8406974cbef1948c675336255fdfecc3a0e4107e)) - Rename number-flow element to avoid conflicts between wrappers ([`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)) ### Patch Changes - Updated dependencies [[`19abcf8`](https://github.com/barvian/number-flow/commit/19abcf88f7d7bd34332f5e1c42e647a0e81725ac)]: - number-flow@0.3.5 ## 0.1.1 ### Patch Changes - Updated dependencies [[`ff966f4`](https://github.com/barvian/number-flow/commit/ff966f489eaeeacc72b35a8ee4c8cc13fe894eb6)]: - number-flow@0.3.4 ================================================ FILE: packages/vue/README.md ================================================ [![NumberFlow for Vue](https://number-flow.barvian.me/preview.webp)](https://number-flow.barvian.me/vue) # NumberFlow for Vue An animated number component. [![NPM Version](https://img.shields.io/npm/v/@number-flow/vue.svg)](https://npmjs.com/package/@number-flow/vue) [![Bundle size](https://badgen.net/bundlephobia/minzip/@number-flow/vue@latest)](https://bundlephobia.com/package/@number-flow/vue@latest) [![Follow @mbarvian](https://img.shields.io/twitter/follow/mbarvian.svg?style=social&label=Follow)](https://x.com/mbarvian) ## Documentation For full documentation, visit [number-flow.barvian.me/vue](https://number-flow.barvian.me/vue). ================================================ FILE: packages/vue/package.json ================================================ { "name": "@number-flow/vue", "type": "module", "publishConfig": { "access": "public" }, "version": "0.5.0", "author": { "name": "Maxwell Barvian", "email": "max@barvian.me", "url": "https://barvian.me" }, "description": "A component to transition and format numbers.", "license": "MIT", "homepage": "https://number-flow.barvian.me/vue", "repository": { "type": "git", "url": "https://github.com/barvian/number-flow", "directory": "src" }, "bugs": { "url": "https://github.com/barvian/number-flow/issues" }, "keywords": [ "accessible", "odometer", "animation", "number-format", "number-animation", "animated-number" ], "files": [ "dist", "README.md" ], "main": "./dist/index.umd.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.umd.cjs" } }, "scripts": { "build": "vite build --mode production", "dev": "vite build --mode development --watch", "test": "pnpm -r --workspace-concurrency 1 --filter=\"./test/apps/*\" test" }, "dependencies": { "esm-env": "^1.1.4", "number-flow": "workspace:*" }, "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^22.7.9", "@vitejs/plugin-vue": "^5.1.4", "typescript": "^5.6.2", "vite": "^5.4.3", "vite-plugin-dts": "^4.3.0", "vue": "^3.5.12" }, "peerDependencies": { "vue": "^3" } } ================================================ FILE: packages/vue/src/NumberFlowGroup.vue ================================================ ================================================ FILE: packages/vue/src/group.ts ================================================ import type NumberFlowLite from 'number-flow/lite' import type { formatToData } from 'number-flow/lite' import type { InjectionKey, Ref, ComputedRef } from 'vue' export type RegisterWithGroup = ( el: Ref, parts: ComputedRef> ) => void export const key = Symbol() as InjectionKey ================================================ FILE: packages/vue/src/index.ts ================================================ import { computed, onMounted, ref, toValue, watchEffect, type MaybeRefOrGetter } from 'vue' import NumberFlowElement, { canAnimate as _canAnimate, define, prefersReducedMotion } from 'number-flow/lite' import { buildStyles } from 'number-flow/csp' export const styles = buildStyles('-vue') export { default as NumberFlowGroup } from './NumberFlowGroup.vue' export type { Value, Format, Trend } from 'number-flow/lite' export * from 'number-flow/plugins' export { NumberFlowElement } define('number-flow-vue', NumberFlowElement) export { default } from './index.vue' // SSR-safe canAnimate export function useCanAnimate({ respectMotionPreference = true }: { respectMotionPreference?: MaybeRefOrGetter } = {}) { const canAnimate = ref(false) const reducedMotion = ref(false) // Handle SSR: onMounted(() => { canAnimate.value = _canAnimate reducedMotion.value = prefersReducedMotion?.matches ?? false }) // Listen for reduced motion changes if needed: watchEffect((onCleanup) => { if (!toValue(respectMotionPreference)) return const onChange = ({ matches }: MediaQueryListEvent) => { reducedMotion.value = matches } prefersReducedMotion?.addEventListener('change', onChange) onCleanup(() => { prefersReducedMotion?.removeEventListener('change', onChange) }) }) return computed( () => canAnimate.value && (!toValue(respectMotionPreference) || !reducedMotion.value) ) } ================================================ FILE: packages/vue/src/index.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/.gitignore ================================================ # Nuxt dev/build outputs .output .data .nuxt .nitro .cache dist # Node dependencies node_modules # testing /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ # Logs logs *.log # Misc .DS_Store .fleet .idea # Local env files .env .env.* !.env.example ================================================ FILE: packages/vue/test/apps/nuxt3/README.md ================================================ # Nuxt Minimal Starter Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. ## Setup Make sure to install dependencies: ```bash # npm npm install # pnpm pnpm install # yarn yarn install # bun bun install ``` ## Development Server Start the development server on `http://localhost:3000`: ```bash # npm npm run dev # pnpm pnpm dev # yarn yarn dev # bun bun run dev ``` ## Production Build the application for production: ```bash # npm npm run build # pnpm pnpm build # yarn yarn build # bun bun run build ``` Locally preview production build: ```bash # npm npm run preview # pnpm pnpm preview # yarn yarn preview # bun bun run preview ``` Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. ================================================ FILE: packages/vue/test/apps/nuxt3/nuxt.config.ts ================================================ import tailwindcss from '@tailwindcss/vite' import { createHash } from 'node:crypto' import { styles } from '@number-flow/vue' const hash = (style: string) => `'sha256-${createHash('sha256').update(style).digest('base64')}'` const hashesCsp = `style-src ${styles.map(hash).join(' ')}` // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: '2024-04-03', devtools: { enabled: false }, srcDir: 'src/', devServer: { port: 3039 }, routeRules: { '/nonce': { headers: { 'Content-Security-Policy': "style-src 'nonce-test-nonce'" } }, '/hashes': { headers: { 'Content-Security-Policy': hashesCsp } } }, modules: ['@nuxt/fonts'], css: [ // CSS file in the project '~/assets/css/main.css' ], imports: { // Breaks stuff b/c monorepo? // https://github.com/nuxt/nuxt/issues/18823 autoImport: false }, fonts: { defaults: { weights: [400], styles: ['normal'], subsets: ['latin'] }, experimental: { disableLocalFallbacks: true } }, vite: { plugins: [tailwindcss()] } }) ================================================ FILE: packages/vue/test/apps/nuxt3/package.json ================================================ { "name": "nuxt-app", "private": true, "type": "module", "scripts": { "dev": "nuxt dev", "build": "nuxt build", "start": "PORT=3039 nuxt start", "generate": "nuxt generate", "test": "playwright test", "test:ui": "playwright test --ui", "test:update": "playwright test --update-snapshots" }, "dependencies": { "@number-flow/vue": "workspace:*", "@nuxt/fonts": "^0.10.2", "nuxt": "^3.15.4", "vue": "^3.5.13", "vue-router": "latest" }, "devDependencies": { "@playwright/test": "^1.48.0", "@tailwindcss/vite": "^4.0.9", "tailwindcss": "^4.0.10" }, "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" } ================================================ FILE: packages/vue/test/apps/nuxt3/playwright.config.ts ================================================ export { config as default } from '../../../../../lib/playwright' ================================================ FILE: packages/vue/test/apps/nuxt3/src/assets/css/main.css ================================================ @import 'tailwindcss'; ================================================ FILE: packages/vue/test/apps/nuxt3/src/pages/can-animate.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/src/pages/group-1-unchanged.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/src/pages/hashes.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/src/pages/index.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/src/pages/nonce.vue ================================================ ================================================ FILE: packages/vue/test/apps/nuxt3/src/server/tsconfig.json ================================================ { "extends": "../../.nuxt/tsconfig.server.json" } ================================================ FILE: packages/vue/test/apps/nuxt3/tsconfig.json ================================================ { // https://nuxt.com/docs/guide/concepts/typescript "extends": "./.nuxt/tsconfig.json" } ================================================ FILE: packages/vue/tsconfig.build.json ================================================ { "extends": ["./tsconfig.json", "../../tsconfig.build.json"] } ================================================ FILE: packages/vue/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { "declaration": true, "outDir": "./dist" } } ================================================ FILE: packages/vue/vite.config.mjs ================================================ import { resolve } from 'path' import { defineConfig } from 'vite' import dts from 'vite-plugin-dts' import vue from '@vitejs/plugin-vue' const outDir = resolve(__dirname, 'dist') export default defineConfig(({ mode }) => ({ plugins: [ vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'number-flow-vue' } } }), dts({ tsconfigPath: resolve(__dirname, `./tsconfig${mode === 'production' ? '.build' : ''}.json`), rollupTypes: true, include: ['src'] }) ], build: { outDir, lib: { name: 'number-flow-vue', // required for UMD build entry: { index: resolve(__dirname, 'src/index.ts') }, fileName: (format, name) => { return `${name}.${format === 'es' ? 'js' : 'umd.cjs'}` } }, rollupOptions: { external: ['vue', 'number-flow/lite', 'number-flow/plugins', 'esm-env'], output: { // Names for UMD builds globals: { vue: 'Vue', 'esm-env': 'Env', 'number-flow': 'NumberFlow' }, exports: 'named' } } } })) ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - 'packages/*' - 'packages/number-flow/test/apps/*' - 'packages/react/test/apps/*' - 'packages/vue/test/apps/*' - 'site' ================================================ FILE: prettier.config.js ================================================ /** @type {import('prettier').Config} */ export default { useTabs: true, singleQuote: true, semi: false, trailingComma: 'none', printWidth: 100, plugins: ['prettier-plugin-astro', 'prettier-plugin-svelte', 'prettier-plugin-tailwindcss'], overrides: [ { files: '*.astro', options: { parser: 'astro' } }, { files: '*.svelte', options: { parser: 'svelte' } } ] } ================================================ FILE: site/.gitignore ================================================ # build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store # jetbrains setting folder .idea/ .vercel ================================================ FILE: site/.vscode/extensions.json ================================================ { "recommendations": ["astro-build.astro-vscode"], "unwantedRecommendations": [] } ================================================ FILE: site/.vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "command": "./node_modules/.bin/astro dev", "name": "Development server", "request": "launch", "type": "node-terminal" } ] } ================================================ FILE: site/astro.config.mjs ================================================ import { defineConfig, envField } from 'astro/config' import pkg from '/../packages/number-flow/package.json' import mdx from '@astrojs/mdx' import vercel from '@astrojs/vercel' import shikiTheme from './highlighter-theme.json' import react from '@astrojs/react' import vue from '@astrojs/vue' import svelte from '@astrojs/svelte' // https://astro.build/config export default defineConfig({ site: pkg.homepage, markdown: { shikiConfig: { // @ts-ignore theme: shikiTheme } }, vite: { plugins: [ { name: 'transform-vanilla-examples', transform(code, id) { // Make .astro examples look like valid HTML: if (id.endsWith('.astro?raw')) { return ( code .replaceAll(' ================================================ FILE: site/src/layouts/TOC.astro ================================================ { (async function () { // Render TOC last (for context to work) but return its markup first const def = await Astro.slots.render('default') const toc = await Astro.slots.render('toc') return })() } ================================================ FILE: site/src/lib/dom.ts ================================================ // Used for vanilla JS examples: export const onReady = (cb: () => () => void) => { document.addEventListener('astro:page-load', () => { // Lazy try/catch to prevent errors on inapplicable pages: try { const destroy = cb() // Use after-preparation because our hydratable nanostores use before-swap: document.addEventListener('astro:after-preparation', destroy, { once: true }) } catch {} }) } ================================================ FILE: site/src/lib/framework.ts ================================================ import { trimSlash } from './url' import { name as vanillaPkgName } from '/../packages/number-flow/package.json' import { name as reactPkgName } from '/../packages/react/package.json' import { name as vuePkgName } from '/../packages/vue/package.json' import { name as sveltePkgName } from '/../packages/svelte/package.json' export type FrameworkData = { name: string | undefined pkg: string pkgName: string componentType: string sandbox: string lightColor: string darkColor: string } export const FRAMEWORKS = { react: { name: 'React', pkg: reactPkgName, pkgName: 'NumberFlow for React', componentType: 'React component', sandbox: 'https://codesandbox.io/p/sandbox/r47dcw', lightColor: '#0A7EA4', darkColor: '#58C4DC' }, vue: { name: 'Vue', pkg: vuePkgName, pkgName: 'NumberFlow for Vue', componentType: 'Vue component', sandbox: 'https://stackblitz.com/edit/vitejs-vite-4prbhc?file=src%2FApp.vue', lightColor: '#42B883', darkColor: '#42B883' }, svelte: { name: 'Svelte', pkg: sveltePkgName, pkgName: 'NumberFlow for Svelte', componentType: 'Svelte component', sandbox: 'https://stackblitz.com/edit/vitejs-vite-5czxuc?file=src%2FApp.svelte', lightColor: '#FF3E00', darkColor: '#F96844' }, vanilla: { name: 'Vanilla', pkg: vanillaPkgName, pkgName: 'NumberFlow', componentType: 'web component', sandbox: 'https://stackblitz.com/edit/vitejs-vite-ec8hg3dz?file=index.html,src%2Fmain.ts', lightColor: '#F7DF1E', darkColor: '#F7DF1E' } } satisfies Record export type Framework = keyof typeof FRAMEWORKS export const DEFAULT_FRAMEWORK: Framework = 'react' export const getFramework = (params: Record) => 'framework' in params ? ((params.framework as Framework) ?? DEFAULT_FRAMEWORK) : null export const getStaticPaths = () => Object.keys(FRAMEWORKS).map((id) => ({ params: { framework: id === DEFAULT_FRAMEWORK ? undefined : id } })) export const toFrameworkPath = ( urlOrPathname?: string | URL | Location | null, id?: Framework | false | null ) => { if (!urlOrPathname) return const path = typeof urlOrPathname === 'string' ? urlOrPathname : urlOrPathname.pathname if (!id) return path const [_, firstSegment, ...segments] = path.split('/') // New prefix to prepend, based on new framework: const prefix = id === DEFAULT_FRAMEWORK ? '' : '/' + id if (firstSegment && Object.keys(FRAMEWORKS).includes(firstSegment)) { return trimSlash(prefix + '/' + segments.join('/')) } // It was on the default framework return trimSlash(prefix + path) } ================================================ FILE: site/src/lib/spring.ts ================================================ import { spring as motionSpring } from 'motion' export const spring = (...args: Parameters) => { const string = motionSpring(...args).toString() const [, duration, easing] = string.match(/^(.*?ms)\s(.*)$/)! return { duration: parseFloat(duration!), easing, toString() { return string } } } ================================================ FILE: site/src/lib/stores.ts ================================================ import { atom, computed, onStart, type PreinitializedWritableAtom, type ReadableAtom } from 'nanostores' export const isCyclableAtom = Symbol() export type CyclableAtom = ReadableAtom & { cycle: () => void reset: () => void [isCyclableAtom]: true } export function cyclable(...options: Array): CyclableAtom { const $index = atom(0) const $value = computed($index, (i) => options[i]!) return Object.assign($value, { cycle: () => $index.set(($index.get() + 1) % options.length), reset: () => $index.set(0), [isCyclableAtom]: true } as const) } export function hydratable | PreinitializedWritableAtom>( $atom: A ): A { const initial = $atom.get() onStart($atom, () => { const beforeSwap = () => { if (isCyclableAtom in $atom) $atom.reset() else $atom.set(initial) } typeof document !== 'undefined' && document.addEventListener('astro:before-swap', beforeSwap) return () => { document.removeEventListener('astro:before-swap', beforeSwap) } }) return $atom } ================================================ FILE: site/src/lib/types.ts ================================================ export type Rename = { [P in keyof T as P extends K ? N : P]: T[P] } ================================================ FILE: site/src/lib/url.ts ================================================ import { DEFAULT_FRAMEWORK, toFrameworkPath } from './framework' const _isActive = (path: string | undefined, urlOrPathname: URL | string | undefined) => { if (!path || !urlOrPathname) return false const currentPath = typeof urlOrPathname === 'string' ? urlOrPathname : urlOrPathname.pathname if (currentPath === path || currentPath.startsWith(path + '/')) return true return false } export const isActive = ( path: string | undefined, urlOrPathname: URL | Location | string | undefined ) => _isActive( toFrameworkPath(path, DEFAULT_FRAMEWORK), toFrameworkPath(urlOrPathname, DEFAULT_FRAMEWORK) ) export const trimSlash = (path: string | undefined) => path?.replace(/(.)\/$/, '$1') ================================================ FILE: site/src/middleware.ts ================================================ import type { MiddlewareHandler } from 'astro' import { $url, $pageFramework } from '@/stores/url' import { getFramework } from '@/lib/framework' export const onRequest: MiddlewareHandler = ({ url, params }, next) => { // Expose the request URL for framework components SSR (equivalent to Astro.url in Astro component) $url.set(url) $pageFramework.set(getFramework(params)) return next() } ================================================ FILE: site/src/pages/[...framework]/_CSP.astro ================================================ --- import Code from '@/components/Code.astro' import { FRAMEWORKS, getFramework } from '@/lib/framework' import cspInstructions from './_csp.txt?raw' const { pkg } = FRAMEWORKS[getFramework(Astro.params)!] --- ================================================ FILE: site/src/pages/[...framework]/_Digits.astro ================================================ 324120.5-1 ================================================ FILE: site/src/pages/[...framework]/_Hero.tsx ================================================ import NumberFlow, { type Format } from '@number-flow/react' import useCycle from '@/hooks/useCycle' import { useEffect, useRef } from 'react' import { useInView } from 'motion/react' import { ArrowUpRight } from 'lucide-react' const NUMBERS = [431.1, -3243.6, 42, 398.43, -3243.5, 1435237.2, 12348.43, -3243.6, 54231.2] const LOCALES = ['en-US', 'en-US', 'fr-FR', 'en-US', 'en-US', 'zh-CN', 'en-US', 'en-US', 'fr-FR'] const FORMATS = [ { minimumFractionDigits: 2 }, { style: 'currency', currency: 'USD', currencySign: 'accounting', signDisplay: 'always' }, {}, { style: 'percent', signDisplay: 'always' }, {}, { style: 'unit', unit: 'meter', notation: 'compact', minimumFractionDigits: 2, maximumFractionDigits: 2, signDisplay: 'never' }, { style: 'currency', currency: 'USD' }, {}, { // style: "percent", signDisplay: 'always' } ] as Format[] export default function Hero({ sandbox }: { sandbox: string }) { const [value, cycleValue] = useCycle(NUMBERS) const [locale, cycleLocale] = useCycle(LOCALES) const [format, cycleFormat] = useCycle(FORMATS) const ref = useRef(null) const inView = useInView(ref, { once: true }) const timeoutRef = useRef | null>(null) useEffect(() => { if (!inView) return if (sessionStorage.getItem('hero-did-animate')) return timeoutRef.current = setTimeout(() => { sessionStorage.setItem('hero-did-animate', 'true') cycleValue() cycleLocale() cycleFormat() }, 750) return () => { if (timeoutRef.current !== null) clearTimeout(timeoutRef.current) } }, [inView]) return (

An animated number component. Dependency-free. Accessible. Customizable.

Open sandbox
) } ================================================ FILE: site/src/pages/[...framework]/_Home.astro ================================================ --- // import LogoWall from '@/components/LogoWall/LogoWall.astro' // import Match from '@/components/Match.astro' import DocsLayout from '@/layouts/Docs.astro' import type { MDXLayoutProps } from 'astro' import Hero from './_Hero' import Link from '@/components/Link.astro' import { FRAMEWORKS, getFramework } from '@/lib/framework' type Props = MDXLayoutProps<{}> const { sandbox, pkgName, componentType } = FRAMEWORKS[getFramework(Astro.params)!] ---

Built by Max Barvian. Heavily inspired by the Family app. mask-image technique by Artur Bień. Digit looping technique by Sam Selikoff.

================================================ FILE: site/src/pages/[...framework]/_csp.txt ================================================ import { styles } from '[pkg]' import { createHash } from 'node:crypto' const headers = { 'Content-Security-Policy': `style-src ${styles.map((style) => `'sha256-${createHash('sha256').update(style).digest('base64')}'`).join(' ')}` } ================================================ FILE: site/src/pages/[...framework]/_demos/Continuous.tsx ================================================ import Demo, { DemoSwitch, type DemoProps } from '@/components/Demo' import NumberFlow, { continuous } from '@number-flow/react' import * as React from 'react' import useCycle from '@/hooks/useCycle' import type { Rename } from '@/lib/types' const NUMBERS = [120, 140] export default function DemoHOC({ children, ...rest }: Rename, 'code', 'children'>) { const [value, cycleValue] = useCycle(NUMBERS) const [useContinuous, setUseContinuous] = React.useState(true) return ( continuous } onClick={cycleValue} >
) } ================================================ FILE: site/src/pages/[...framework]/_demos/Isolate.tsx ================================================ import Demo, { DemoSwitch, type DemoProps } from '@/components/Demo' import NumberFlow from '@number-flow/react' import * as React from 'react' export default function DemoHOC({ ...rest }: Omit) { const [increased, setIncreased] = React.useState(false) const [isolate, setIsolate] = React.useState(false) return ( isolate } onClick={() => setIncreased((o) => !o)} >
{increased &&
}
) } ================================================ FILE: site/src/pages/[...framework]/_demos/Styling.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import type { Rename } from '@/lib/types' import NumberFlow, { type Value } from '@number-flow/react' import useCycle from '@/hooks/useCycle' const NUMBERS: Value[] = [3, 15, 50] export default function DemoHOC({ children, ...rest }: Rename, 'code', 'children'>) { const [value, cycleValue] = useCycle(NUMBERS) return ( ) } ================================================ FILE: site/src/pages/[...framework]/_demos/Suffix.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import type { Rename } from '@/lib/types' import NumberFlow, { type Value } from '@number-flow/react' import useCycle from '@/hooks/useCycle' const NUMBERS: Value[] = [3, 15, 50] export default function DemoHOC({ children, ...rest }: Rename, 'code', 'children'>) { const [value, cycleValue] = useCycle(NUMBERS) return ( ) } ================================================ FILE: site/src/pages/[...framework]/_demos/TabularNums.tsx ================================================ import Demo, { DemoSwitch, type DemoProps } from '@/components/Demo' import NumberFlow from '@number-flow/react' import * as React from 'react' export default function DemoHOC({ ...rest }: Omit) { const [value, setValue] = React.useState(10) const [tabularNums, setTabularNums] = React.useState(false) return ( tabular-nums } onClick={() => setValue((v) => v + 1)} >
) } ================================================ FILE: site/src/pages/[...framework]/_demos/Timings.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import type { Rename } from '@/lib/types' import NumberFlow from '@number-flow/react' import useCycle from '@/hooks/useCycle' const bouncySpring: EffectTiming = { duration: 750, easing: 'linear(0 0%, 0.0058021823078800595 1.1235955056179776%, 0.022019245228978974 2.247191011235955%, 0.04697426784552192 3.370786516853933%, 0.07913194654911777 4.49438202247191%, 0.11709565568904509 5.617977528089888%, 0.15960336091615873 6.741573033707866%, 0.2055225959731031 7.865168539325843%, 0.25384469405525506 8.98876404494382%, 0.30367844589280607 10.112359550561798%, 0.35424333850373657 11.235955056179776%, 0.40486251123515354 12.359550561797754%, 0.45495554931807397 13.483146067415731%, 0.5040312197589397 14.606741573033709%, 0.5516802400120898 15.730337078651687%, 0.5975681565371418 16.853932584269664%, 0.6414283980456571 17.97752808988764%, 0.6830555569725216 19.10112359550562%, 0.7222989424488457 20.224719101123597%, 0.7590564387760397 21.348314606741575%, 0.7932686950692723 22.471910112359552%, 0.8249136643114104 23.59550561797753%, 0.8540015034900891 24.719101123595507%, 0.8805698407319795 25.842696629213485%, 0.9046794103485847 26.966292134831463%, 0.9264100524147755 28.08988764044944%, 0.9458570698619917 29.213483146067418%, 0.9631279330300166 30.337078651685395%, 0.9783393191326047 31.460674157303373%, 0.9916144721023972 32.58426966292135%, 1.0030808667404794 33.70786516853933%, 1.0128681599585279 34.831460674157306%, 1.0211064111219177 35.95505617977528%, 1.0279245530378938 37.07865168539326%, 1.033449094943999 38.20224719101124%, 1.0378030389010637 39.325842696629216%, 1.0411049912474741 40.449438202247194%, 1.0434684511951837 41.57303370786517%, 1.0450012592136182 42.69662921348315%, 1.0458051885285111 43.82022471910113%, 1.0459756638345723 44.943820224719104%, 1.0456015921619513 46.06741573033708%, 1.044765291727289 47.19101123595506%, 1.0435425055235072 48.31460674157304%, 1.0420024873433105 49.438202247191015%, 1.0402081488764594 50.56179775280899%, 1.0382162574589653 51.68539325842697%, 1.0360776749738179 52.80898876404495%, 1.0338376292996567 53.932584269662925%, 1.0315360105693312 55.0561797752809%, 1.029207685329244 56.17977528089888%, 1.0268828224786164 57.30337078651686%, 1.02458722561227 58.426966292134836%, 1.022342667089039 59.55056179775281%, 1.020167219799199 60.67415730337079%, 1.0180755832077413 61.79775280898877%, 1.0160794008059388 62.921348314606746%, 1.0141875666120557 64.04494382022472%, 1.0124065188242148 65.16853932584269%, 1.0107405191458168 66.29213483146067%, 1.0091919166781451 67.41573033707866%, 1.0077613956078837 68.53932584269663%, 1.0064482062113422 69.6629213483146%, 1.005250378954478 70.78651685393258%, 1.0041649216907218 71.91011235955057%, 1.003188000149523 73.03370786516854%, 1.002315102069899 74.15730337078651%, 1.001541185467481 75.28089887640449%, 1.0008608116329931 76.40449438202248%, 1.0002682635470859 77.52808988764045%, 0.9997576504632046 78.65168539325842%, 0.9993229994588604 79.7752808988764%, 0.998958334788324 80.89887640449439%, 0.9986577458883016 82.02247191011236%, 0.9984154448944083 83.14606741573033%, 0.9982258145219031 84.26966292134831%, 0.9980834471507738 85.3932584269663%, 0.9979831759342885 86.51685393258427%, 0.9979200987229117 87.64044943820224%, 0.9978895955632022 88.76404494382022%, 0.9978873404950641 89.88764044943821%, 0.9979093083314955 91.01123595505618%, 0.9979517770636321 92.13483146067415%, 0.9980113264912046 93.25842696629213%, 0.9980848336351612 94.38202247191012%, 0.9981694654457833 95.50561797752809%, 0.9982626692766015 96.62921348314606%, 0.9983621615522535 97.75280898876404%, 0.9984659150174638 98.87640449438203%, 1 100%)' } const opacityTiming: EffectTiming = { duration: 350, easing: 'ease-out' } const NUMBERS = [124.23, 41.75, 2125.95] export default function DemoHOC({ children, ...rest }: Rename, 'code', 'children'>) { const [value, cycleValue] = useCycle(NUMBERS) return ( ) } ================================================ FILE: site/src/pages/[...framework]/_demos/Trend.tsx ================================================ import * as React from 'react' import Demo, { DemoMenu, DemoMenuButton, DemoMenuItem, DemoMenuItems, type DemoProps } from '@/components/Demo' import NumberFlow, { type Trend } from '@number-flow/react' import useCycle from '@/hooks/useCycle' const NUMBERS = [20, 19] const TRENDS: Record = { default: undefined, '+1': 1, '0': 0, '-1': -1 } export default function DemoHOC({ ...rest }: Omit) { const [value, cycleValue] = useCycle(NUMBERS) const [option, setOption] = React.useState('default') const trend = TRENDS[option] return ( trend: {option} setOption('default')} isDisabled={option === 'default'} > default setOption('+1')} isDisabled={option === '+1'} > +1 setOption('0')} isDisabled={option === '0'}> 0 setOption('-1')} isDisabled={option === '-1'} > -1 } onClick={cycleValue} > ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/index.astro ================================================ --- import Activity from '.' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ // Could probably do a client:only={framework} but then we'd lose SSR: import React from './react' import react from './react/Component.tsx?raw' import Vue from './vue/index.vue' import vue from './vue/Component.vue?raw' import Svelte from './svelte/index.svelte' import svelte from './svelte/Component.svelte?raw' import Vanilla from './vanilla/index.astro' import vanilla from './vanilla/Component.astro?raw' --- ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/index.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import { $inView } from './stores' export default function Activity(props: DemoProps) { return $inView.set(isIntersecting)} /> } ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/react/Component.tsx ================================================ import NumberFlow, { continuous, type Format } from '@number-flow/react' import clsx from 'clsx/lite' import { Bookmark, ChartNoAxesColumn, Heart, Repeat, Share } from 'lucide-react' import type { ComponentPropsWithoutRef } from 'react' const format: Format = { notation: 'compact', compactDisplay: 'short', roundingMode: 'trunc' } type Props = ComponentPropsWithoutRef<'div'> & { likes: number reposts: number views: number bookmarks: number liked: boolean reposted: boolean bookmarked: boolean onLike: () => void onBookmark: () => void onRepost: () => void } export default function Activity({ className, likes, reposts, views, bookmarks, onLike, onRepost, onBookmark, liked, reposted, bookmarked, ...rest }: Props) { return (
) } ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/react/index.tsx ================================================ import Component from './Component' import { useStore } from '@nanostores/react' import { $bookmarks, $likes, $reposts, $views } from '../stores' export default function () { const reposts = useStore($reposts) const bookmarks = useStore($bookmarks) const likes = useStore($likes) const views = useStore($views) return ( ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/stores.ts ================================================ import { atom, map, onMount, type ReadableAtom } from 'nanostores' import { hydratable } from '@/lib/stores' export const $inView = atom(false) interface CounterState { count: number hasIncremented: boolean } function countable( initialValue: number, active: ReadableAtom, min: number, max: number, rate = 1 ) { const state = map({ count: initialValue, hasIncremented: false }) onMount(state, () => { let timeout: NodeJS.Timeout | null = null const unsubscribe = active.subscribe((active) => { if (timeout != null) clearTimeout(timeout) if (!active) return const randomlyIncrease = (delay: number) => { timeout = setTimeout(() => { state.setKey('count', state.get().count + randomBetween(min, max) * rate) randomlyIncrease(3500) }, delay) } randomlyIncrease(1500) }) return () => { if (timeout != null) clearTimeout(timeout) unsubscribe() } }) return Object.assign(state, { toggle: () => { const s = state.get() state.set({ count: s.hasIncremented ? s.count - 1 : s.count + 1, hasIncremented: !s.hasIncremented }) } }) } // Generate a random number between two numbers: function randomBetween(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min) } export const $reposts = hydratable(countable(2, $inView, 0, 2)) export const $likes = hydratable(countable(50, $inView, 0, 3, 5)) export const $bookmarks = hydratable(countable(40, $inView, 0, 3, 3)) export const $views = hydratable(countable(995, $inView, 1, 3, 50)) ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/svelte/Component.svelte ================================================
================================================ FILE: site/src/pages/[...framework]/examples/_Activity/svelte/index.svelte ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/vanilla/Component.astro ================================================
================================================ FILE: site/src/pages/[...framework]/examples/_Activity/vanilla/index.astro ================================================ --- import Component from './Component.astro' ---
================================================ FILE: site/src/pages/[...framework]/examples/_Activity/vue/Component.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Activity/vue/index.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_ColoredTrends/Example.tsx ================================================ import NumberFlow, { NumberFlowElement } from '@number-flow/react' import * as React from 'react' type Props = { value: number } export default function PriceWithColoredTrend({ value }: Props) { const ref = React.useRef(null) const prevValue = React.useRef(value) React.useEffect(() => { if (value > prevValue.current) ref.current?.animate( { color: ['unset', '#34d399', 'unset'] }, { easing: 'ease', duration: 300 } ) else if (value < prevValue.current) ref.current?.animate( { color: ['unset', '#f87171', 'unset'] }, { easing: 'ease', duration: 300 } ) return () => { prevValue.current = value } }, [value]) return ( ) } ================================================ FILE: site/src/pages/[...framework]/examples/_ColoredTrends/index.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import useCycle from '@/hooks/useCycle' import Example from './Example' import type { Rename } from '@/lib/types' const NUMBERS = [12398.432, -3243.6, 543.2] export default function DemoHOC({ children, ...rest }: Rename, 'code', 'children'>) { const [value, cycleValue] = useCycle(NUMBERS) function onClick() { cycleValue() } return ( ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/index.astro ================================================ --- import Activity from '.' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ // Could probably do a client:only={framework} but then we'd lose SSR: import React from './react' import react from './react/Component.tsx?raw' import Vue from './vue/index.vue' import vue from './vue/Component.vue?raw' import Svelte from './svelte/index.svelte' import svelte from './svelte/Component.svelte?raw' import Vanilla from './vanilla/index.astro' import vanilla from './vanilla/index.astro?raw' --- ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/index.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import { $inView } from './stores' export default function Activity(props: DemoProps) { return $inView.set(isIntersecting)} /> } ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/react/Component.tsx ================================================ import NumberFlow, { NumberFlowGroup } from '@number-flow/react' type Props = { seconds: number } export default function Countdown({ seconds }: Props) { const hh = Math.floor(seconds / 3600) const mm = Math.floor((seconds % 3600) / 60) const ss = seconds % 60 return (
) } ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/react/index.tsx ================================================ import Component from './Component' import { useStore } from '@nanostores/react' import { $seconds } from '../stores' export default function () { const seconds = useStore($seconds) return } ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/stores.ts ================================================ import { atom, onMount, type ReadableAtom } from 'nanostores' import { hydratable } from '@/lib/stores' export const $inView = atom(false) function countdownable( initialValue: number, active: ReadableAtom, rate = 1, everyMs = 1000 ) { const state = atom(initialValue) onMount(state, () => { let timeout: NodeJS.Timeout | null = null const unsubscribe = active.subscribe((active) => { if (timeout != null) clearInterval(timeout) if (!active) return timeout = setInterval(() => { state.set(state.get() - rate) }, everyMs) }) return () => { if (timeout != null) clearInterval(timeout) unsubscribe() } }) return state } export const $seconds = hydratable(countdownable(3600, $inView)) ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/svelte/Component.svelte ================================================
================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/svelte/index.svelte ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/vanilla/index.astro ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/vue/Component.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Countdown/vue/index.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Examples.astro ================================================ --- import DocsLayout from '@/layouts/Docs.astro' import type { MDXLayoutProps } from 'astro' import { ArrowUpRight } from 'lucide-react' import { FRAMEWORKS, getFramework } from '@/lib/framework' type Props = MDXLayoutProps<{}> const { sandbox, pkgName } = FRAMEWORKS[getFramework(Astro.params)!] ---

Examples

Official templates to get you started.

================================================ FILE: site/src/pages/[...framework]/examples/_Group/index.astro ================================================ --- import Group from '.' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ // Could probably do a client:only={framework} but then we'd lose SSR: import React from './react' import react from './react/Component.tsx?raw' import Vue from './vue/index.vue' import vue from './vue/Component.vue?raw' import Svelte from './svelte/index.svelte' import svelte from './svelte/Component.svelte?raw' import Vanilla from './vanilla/index.astro' import vanilla from './vanilla/index.astro?raw' --- ================================================ FILE: site/src/pages/[...framework]/examples/_Group/index.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import { $number, $diff } from './stores' export default function Group(props: DemoProps) { function onClick() { $number.cycle() $diff.cycle() } return } ================================================ FILE: site/src/pages/[...framework]/examples/_Group/react/Component.tsx ================================================ import NumberFlow, { NumberFlowGroup } from '@number-flow/react' import clsx from 'clsx/lite' type Props = { value: number diff: number } export default function PriceWithDiff({ value, diff }: Props) { return (
) } ================================================ FILE: site/src/pages/[...framework]/examples/_Group/react/index.tsx ================================================ import { useStore } from '@nanostores/react' import Component from './Component' import { $diff, $number } from '../stores' export default function () { const number = useStore($number) const diff = useStore($diff) return } ================================================ FILE: site/src/pages/[...framework]/examples/_Group/stores.ts ================================================ import { cyclable, hydratable } from '@/lib/stores' export const $number = hydratable(cyclable(124.23, 41.75, 2125.95)) export const $diff = hydratable(cyclable(0.0564, -0.3912, 0.0029)) ================================================ FILE: site/src/pages/[...framework]/examples/_Group/svelte/Component.svelte ================================================
================================================ FILE: site/src/pages/[...framework]/examples/_Group/svelte/index.svelte ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Group/vanilla/index.astro ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Group/vue/Component.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Group/vue/index.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Input/index.astro ================================================ --- export { type DemoProps as Props } from '@/components/Demo' import Demo from '@/components/Demo' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ import React from './react' import react from './react/Component.tsx?raw' import Vue from './vue/index.vue' import vue from './vue/Component.vue?raw' import Svelte from './svelte/index.svelte' import svelte from './svelte/Component.svelte?raw' --- ================================================ FILE: site/src/pages/[...framework]/examples/_Input/react/Component.tsx ================================================ import NumberFlow from '@number-flow/react' import clsx from 'clsx/lite' import { Minus, Plus } from 'lucide-react' import * as React from 'react' type Props = { value?: number min?: number max?: number onChange?: (value: number) => void } export default function Input({ value = 0, min = -Infinity, max = Infinity, onChange }: Props) { const defaultValue = React.useRef(value) const inputRef = React.useRef(null) const [animated, setAnimated] = React.useState(true) // Hide the caret during transitions so you can't see it shifting around: const [showCaret, setShowCaret] = React.useState(true) const handleInput: React.InputEventHandler = ({ currentTarget: el }) => { setAnimated(false) let next = value if (el.value === '') { next = defaultValue.current } else { const num = el.valueAsNumber if (!isNaN(num) && min <= num && num <= max) next = num } // Manually update the input.value in case the number stays the same e.g. 09 == 9 el.value = String(next) onChange?.(next) } const handlePointerDown = (diff: number) => (event: React.PointerEvent) => { setAnimated(true) if (event.pointerType === 'mouse') { event?.preventDefault() inputRef.current?.focus() } const newVal = Math.min(Math.max(value + diff, min), max) onChange?.(newVal) } return (
) } ================================================ FILE: site/src/pages/[...framework]/examples/_Input/react/index.tsx ================================================ import Component from './Component' import * as React from 'react' export default function Input() { const [value, setValue] = React.useState(0) return } ================================================ FILE: site/src/pages/[...framework]/examples/_Input/svelte/Component.svelte ================================================
================================================ FILE: site/src/pages/[...framework]/examples/_Input/svelte/index.svelte ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Input/vue/Component.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Input/vue/index.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Motion/index.astro ================================================ --- import Group from '.' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ // Could probably do a client:only={framework} but then we'd lose SSR: import React from './react' import react from './react/Component.tsx?raw' --- ================================================ FILE: site/src/pages/[...framework]/examples/_Motion/index.tsx ================================================ import Demo, { type DemoProps } from '@/components/Demo' import { $value } from './stores' export default function Group(props: DemoProps) { return $value.cycle()} /> } ================================================ FILE: site/src/pages/[...framework]/examples/_Motion/react/Component.tsx ================================================ import { motion, MotionConfig } from 'motion/react' import NumberFlow, { useCanAnimate } from '@number-flow/react' import { ArrowUp } from 'lucide-react' import clsx from 'clsx/lite' import type { CSSProperties } from 'react' const MotionNumberFlow = motion.create(NumberFlow) const MotionArrowUp = motion.create(ArrowUp) type Props = { value: number } export default function MotionExample({ value }: Props) { const canAnimate = useCanAnimate() return ( 0 ? 'bg-emerald-400' : 'bg-red-500', 'inline-flex items-center px-[0.3em] text-2xl text-white transition-colors duration-300' )} layout style={{ borderRadius: 999 }} > 0 ? 0 : -180 }} initial={false} /> ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Motion/react/index.tsx ================================================ import { useStore } from '@nanostores/react' import Component from './Component' import { $value } from '../stores' export default function () { const value = useStore($value) return } ================================================ FILE: site/src/pages/[...framework]/examples/_Motion/stores.ts ================================================ import { cyclable, hydratable } from '@/lib/stores' export const $value = hydratable(cyclable(0.0564, -0.3912, 0.0029)) ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/index.astro ================================================ --- import Demo from '@/components/Demo' import Match from '@/components/Match.astro' import Code from '@/components/Code.astro' // Can't glob these b/c Astro needs the import :/ import React from './react' import react from './react/Component.tsx?raw' import Vue from './vue/index.vue' import vue from './vue/Component.vue?raw' import Svelte from './svelte/index.svelte' import svelte from './svelte/Component.svelte?raw' import clsx from 'clsx/lite' import { getFramework } from '@/lib/framework' const framework = getFramework(Astro.params) --- ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/react/Component.tsx ================================================ import NumberFlow, { continuous } from '@number-flow/react' import * as RadixSlider from '@radix-ui/react-slider' import clsx from 'clsx/lite' export default function Slider({ value, className, ...props }: RadixSlider.SliderProps) { return ( {value?.[0] != null && ( )} ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/react/index.tsx ================================================ import Example from './Component' import * as React from 'react' export default function Slider() { const [value, setValue] = React.useState(50) return ( value != null && setValue(value)} min={0} max={100} step={1} /> ) } ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/svelte/Component.svelte ================================================
{#each thumbs as thumb} {/each} {#if value[0] != null}
{/if}
================================================ FILE: site/src/pages/[...framework]/examples/_Slider/svelte/index.svelte ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/vue/Component.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/_Slider/vue/index.vue ================================================ ================================================ FILE: site/src/pages/[...framework]/examples/index.mdx ================================================ --- layout: './_Examples.astro' --- export { getStaticPaths } from '@/lib/framework' import { DemoTitle } from '@/components/Demo' import Activity from './_Activity/index.astro' import Slider from './_Slider/index.astro' import Input from './_Input/index.astro' import Countdown from './_Countdown/index.astro' import Heading from '@/components/Heading.astro'; import Match from '@/components/Match.astro'; import Code from '@/components/Code.astro'; import Link from '@/components/Link.astro' import Note from '@/components/Note.astro' import Pre from '@/components/Pre.astro' import Motion from './_Motion/index.astro' export const components = {a: Link, pre: Pre} NumberFlow was designed to work with [Motion's layout animations](https://motion.dev/docs/react-layout-animations). You can use a `{ type: 'spring', duration: 0.9, bounce: 0 }` transition to match NumberFlow's default transform timing: When NumberFlow is used within a layout animation it should be (or be in) a [`motion` component](https://motion.dev/docs/react-motion-component) with the `layout layoutRoot` props. ================================================ FILE: site/src/pages/[...framework]/index.mdx ================================================ --- layout: './_Home.astro' --- import Pre from '@/components/Pre.astro'; import NumberFlow from '@number-flow/react' import TimingsDemo from './_demos/Timings' import IsolateDemo from './_demos/Isolate' import ContinuousDemo from './_demos/Continuous' import Comp from '@/components/Comp.mdx' import GroupComp from '@/components/GroupComp.mdx' import SuffixDemo from './_demos/Suffix' import TrendDemo from './_demos/Trend' import StylingDemo from './_demos/Styling' import Digits from './_Digits.astro' import Match from '@/components/Match.astro' import Meta from '@/components/Meta.astro' import AnimationsOnTheWeb from '@/components/AnimationsOnTheWeb.astro' import Type from '@/components/Type.astro' import Heading from '@/components/Heading.astro' import Union from '@/components/Union.astro' import Link from '@/components/Link.astro' import Note from '@/components/Note.astro' import Group from './examples/_Group/index.astro' import CSP from './_CSP.astro' export { getStaticPaths } from '@/lib/framework' export const components = {a: Link, pre: Pre} {/* We need an empty match for a complicated reason related to async and collecting the TOC headers in order */}
```jsx // Basic usage import NumberFlow from '@number-flow/react' ``` ```vue ``` ```svelte ``` ```html ```
Subsequent calls to `.update()` will trigger animations. will automatically transition when the `value` prop changes.

format: Intl.NumberFormatOptions

Formatting options for the number. ```jsx ``` ```vue ``` ```svelte ``` ```js flow.format = { notation: 'compact' } ```

locales: Intl.LocalesArgument

The locale(s) for the number.

refix: string, uffix: string

A custom prefix or suffix for the number. ```jsx ``` ```vue ``` ```svelte ``` ```js flow.format = { style: 'currency', currency: 'USD', trailingZeroDisplay: 'stripIfInteger' } flow.numberSuffix = "/mo" ``` ### Timings There are three to customize the animation timings. Each accept an [`EffectTiming`](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getTiming#return_value) object: ```jsx ``` ```vue ``` ```svelte ``` ```js // Used for layout-related transforms: flow.transformTiming = { duration: 700, easing: 'linear(...)' } // Used for the digit spin animations. // Will fall back to `transformTiming` if unset: flow.spinTiming = { duration: 700, easing: 'linear(...)' } // Used for fading in/out characters: flow.opacityTiming = { duration: 350, easing: 'ease-out' } ``` For spring-based easings, I'd recommend [Kevin Grajeda's generator](https://www.kvin.me/css-springs) or [easing.dev](https://www.easing.dev/).

trend: number"]} /> Default: `(oldValue, value) => Math.sign(value - oldValue)`

Controls the direction of the digits. If `trend` is or returns - `+1:` the digits always go up. - `0:` each digit goes up if it increases and down if it decreases. This can be useful if you want to animate number changes without conveying an overall trend ([example](https://x.com/pontusab/status/1825941664189526067)). - `-1:` The digits always go down.

isolate: boolean Default: `false`

If `isolate` is set, 's transitions are isolated from any other layout changes that may occur in the same update. Has no effect when inside a [``](#grouping).

animated: boolean Default: `true`

Can be set to `false` to disable all animations and finish any current ones. See the [input example](/examples/#input) for a usage scenario.

digits: Record{''}

Configure digits based on their position in the number (i.e. for 342.5, the positions are: ). This can be helpful for time-related displays, to ensure e.g. 59 -> 00. See the [countdown example](/examples/#countdown) for a demo. `digits` is not reactive to save on bundle size. If you need it to be reactive, please submit a [feature request](https://github.com/barvian/number-flow/discussions/new?category=ideas).

respectMotionPreference: boolean Default: `true`

Can be set to `false` to animate regardless of the user's reduced motion preference.

plugins: Plugin[]

Plugins to apply to the component. Currently there's only one plugin, `continuous`, which makes the number transitions appear to pass through in-between numbers: ```jsx import NumberFlow, { continuous } from '@number-flow/react' ``` ```vue ``` ```svelte ``` ```js import { continuous } from 'number-flow' flow.plugins = [continuous] ``` This plugin has no effect if `trend` is `0`. ---

data-will-change

willChange: boolean Default: `false`

If set, NumberFlow applies [`will-change` properties](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change) to relevant elements. This can be useful if: * Your number is guaranteed to change frequently * You experience unwanted repositioning when a transition completes Note that "excessive use of `will-change` will result in excessive memory use" (source: [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change)). ```html ```

nonce: string

Passes a [CSP nonce](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/nonce) through to NumberFlow's inline `