Showing preview only (453K chars total). Download the full file or copy to clipboard to get everything.
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.
[](https://npmjs.com/package/number-flow)
[](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
================================================
[](https://number-flow.barvian.me/vanilla)
# NumberFlow
An animated number component.
[](https://npmjs.com/package/number-flow)
[](https://bundlephobia.com/package/number-flow@latest)
[](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<Intl.NumberFormatPartTypes, 'minusSign' | 'plusSign'>
| '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<NumberPartType, 'integer' | 'fraction'>
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<Intl.NumberFormatOptions, 'notation'> & {
notation?: Exclude<Intl.NumberFormatOptions['notation'], 'scientific' | 'engineering'>
}
export type Value = Exclude<
Parameters<typeof Intl.NumberFormat.prototype.formatToParts>[0],
bigint | undefined
>
export function formatToData(
value: Value,
formatter: Intl.NumberFormat,
prefix?: string,
suffix?: string
) {
const parts: Array<
Omit<Intl.NumberFormatPart, 'type'> & { 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<IntegerPart | SymbolPart> = [] // we do a second pass to key these from RTL
const fraction: KeyedNumberPart[] = []
const post: KeyedNumberPart[] = []
const counts: Partial<Record<NumberPartType, number>> = {}
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<typeof formatToData>
================================================
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<NumberFlow>('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<NumberFlow>()
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<Math['sign']> cause it breaks Vue prop types:
export type Trend = number | ((oldValue: number, value: number) => number)
export type DigitOptions = { max?: number }
export type Digits = Record<number, DigitOptions>
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<NumberFlowLite>
/**
* @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<Data, 'integer' | 'fraction'>) {
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<NumberPartKey, Char>()
constructor(
readonly flow: NumberFlowLite,
parts: KeyedNumberPart[],
{ justify, className, ...props }: SectionProps,
children?: (chars: Node[]) => Node[]
) {
this.justify = justify
const chars = parts.map<Node>((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<AnimatePresenceProps, 'animateIn'> = {}
) {
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<any, Char>) {
// 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<KeyedNumberPart, Char>()
const updated = new Map<KeyedNumberPart, Char>()
// 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<NumberPartKey, Char>()
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<NumberPartKey, Char>()
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<P extends KeyedNumberPart = KeyedNumberPart> 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<KeyedDigitPart> {
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<KeyedSymbolPart> {
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<KeyedSymbolPart['value'], AnimatePresence>()
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<NumberFlowLite, number | undefined>()
/**
* 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) =>
`<span class="${part.type === 'integer' || part.type === 'fraction' ? 'digit' : 'symbol'}" part="${part.type === 'integer' || part.type === 'fraction' ? `digit ${part.type}-digit` : `symbol ${part.type}`}">${part.value}</span>`
const renderSection = (section: KeyedNumberPart[], part: string) =>
`<span part="${part}">${section.reduce((str, p) => str + renderPart(p), '')}</span>`
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`<template shadowroot="open" shadowrootmode="open"
><style${nonce ? ` nonce="${nonce}"` : ''}>${styles}</style
><span role="img" aria-label="${data.valueAsString}"
>${renderSection(data.pre, 'left')}<span part="number" class="number"
>${renderSection(data.integer, 'integer')}${renderSection(data.fraction, 'fraction')}</span
>${renderSection(data.post, 'right')}</span
></template
><style${nonce ? ` nonce="${nonce}"` : ''}>${renderFallbackStyles(elementSuffix)}</style
><span>${data.valueAsString}</span>`
================================================
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: '<number>',
inherits: false,
initialValue: '0'
})
CSS.registerProperty({
name: dxVar,
syntax: '<length>',
inherits: true,
initialValue: '0px'
})
CSS.registerProperty({
name: widthDeltaVar,
syntax: '<number>',
inherits: false,
initialValue: '0'
})
CSS.registerProperty({
name: deltaVar,
syntax: '<number>',
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<T> = {
-readonly [K in keyof T as T[K] extends Readonly<any> ? never : K]: T[K]
}
export type HTMLProps<K extends keyof HTMLElementTagNameMap> = Partial<
ExcludeReadonly<HTMLElementTagNameMap[K]> & { part: string }
>
export const createElement = <K extends keyof HTMLElementTagNameMap>(
tagName: K,
optionsOrChildren?: HTMLProps<K> | 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<T>(
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<T> = {
-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
```
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!

## 🚀 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'
---
<!doctype html>
<html lang="en">
<body>
<slot />
</body>
</html>
================================================
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'
---
<Layout>
<div>
<p data-testid="default">{String(!(prefersReducedMotion?.matches ?? false) && canAnimate)}</p>
<p data-testid="disrespect-motion-preference">
{String(canAnimate)}
</p>
</div>
</Layout>
<script>
import { canAnimate, prefersReducedMotion } from 'number-flow'
const def = document.querySelector('[data-testid="default"]')
const disrespect = document.querySelector('[data-testid="disrespect-motion-preference"]')
if (def) def.textContent = String(!(prefersReducedMotion?.matches ?? false) && canAnimate)
if (disrespect) disrespect.textContent = String(canAnimate)
</script>
================================================
FILE: packages/number-flow/test/apps/astro/src/pages/group-1-unchanged.astro
================================================
---
import Layout from '../layouts/Layout.astro'
import { renderInnerHTML } from 'number-flow'
---
<Layout>
<div>
<number-flow-group>
<number-flow set:html={renderInnerHTML(42)} /><number-flow set:html={renderInnerHTML(0)} />
</number-flow-group>
</div>
<button id="change"> Change and pause </button>
<br />
<button id="resume"> Resume </button>
</Layout>
<script>
import 'number-flow'
import 'number-flow/group'
const [flow1, flow2] = document.getElementsByTagName('number-flow')
const changeBtn = document.getElementById('change')
const resumeBtn = document.getElementById('resume')
flow1?.update(42)
flow2?.update(0)
changeBtn?.addEventListener('click', () => {
flow1?.update(152000)
queueMicrotask(() => {
;[
...(flow1?.shadowRoot?.getAnimations() ?? []),
...(flow2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
})
})
resumeBtn?.addEventListener('click', () => {
;[
...(flow1?.shadowRoot?.getAnimations() ?? []),
...(flow2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
})
</script>
================================================
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
---
<Layout>
<number-flow set:html={renderInnerHTML(42)} />
</Layout>
<script>
import 'number-flow'
const flow = document.querySelector('number-flow')
flow?.update(42)
</script>
================================================
FILE: packages/number-flow/test/apps/astro/src/pages/index.astro
================================================
---
import Layout from '../layouts/Layout.astro'
import { renderInnerHTML } from 'number-flow'
---
<Layout>
<div>
Text node{' '}
<number-flow-group>
<number-flow
id="flow1"
data-testid="flow1"
set:html={renderInnerHTML(42, {
format: { style: 'currency', currency: 'USD' },
locales: 'zh-CN',
numberPrefix: ':',
numberSuffix: '/mo'
})}
/><number-flow id="flow2" data-testid="flow2" set:html={renderInnerHTML(42)} />
</number-flow-group>
</div>
<button id="change"> Change and pause </button>
<br />
<button id="resume"> Resume </button>
</Layout>
<script>
import 'number-flow'
import { continuous } from 'number-flow'
import 'number-flow/group'
const [flow1, flow2] = document.getElementsByTagName('number-flow')
const changeBtn = document.getElementById('change')
const resumeBtn = document.getElementById('resume')
if (flow1 && flow2) {
flow1.addEventListener('animationsstart', () => {
console.log('start')
})
flow1.addEventListener('animationsfinish', () => {
console.log('finish')
})
flow1.transformTiming = { easing: 'linear', duration: 500 }
flow1.spinTiming = { easing: 'linear', duration: 800 }
flow1.opacityTiming = { easing: 'linear', duration: 500 }
flow1.format = { style: 'currency', currency: 'USD' }
flow1.trend = () => -1
flow1.locales = 'zh-CN'
flow1.numberPrefix = ':'
flow1.numberSuffix = '/mo'
flow1.update(42)
flow2.respectMotionPreference = false
flow2.plugins = [continuous]
flow2.digits = { 0: { max: 2 } }
flow2.transformTiming = { easing: 'linear', duration: 500 }
flow2.spinTiming = { easing: 'linear', duration: 800 }
flow2.opacityTiming = { easing: 'linear', duration: 500 }
flow2.update(42)
}
changeBtn?.addEventListener('click', () => {
flow1?.update(152)
flow2?.update(152)
queueMicrotask(() => {
;[
...(flow1?.shadowRoot?.getAnimations() ?? []),
...(flow2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
})
})
resumeBtn?.addEventListener('click', () => {
;[
...(flow1?.shadowRoot?.getAnimations() ?? []),
...(flow2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
})
</script>
================================================
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
---
<Layout>
<number-flow
nonce="test-nonce"
set:html={renderInnerHTML(42, { nonce: 'test-nonce' })}
/><number-flow nonce="test-nonce" set:html={renderInnerHTML(42, { nonce: 'test-nonce' })} />
</Layout>
<script>
import 'number-flow'
document.querySelectorAll('number-flow').forEach((flow) => {
flow.update(42)
})
</script>
================================================
FILE: packages/number-flow/test/apps/astro/src/pages/thrashing.astro
================================================
---
import Layout from '../layouts/Layout.astro'
import { renderInnerHTML } from 'number-flow'
---
<Layout>
<number-flow-group>
<number-flow
id="flow1"
data-testid="flow1"
set:html={renderInnerHTML(42)}
/><number-flow id="flow2" data-testid="flow2" set:html={renderInnerHTML(42)} />
</number-flow-group>
</Layout>
<script>
import 'number-flow'
import 'number-flow/group'
const [flow1, flow2] = document.getElementsByTagName('number-flow')
if (flow1 && flow2) {
flow1.update(42)
flow2.update(42)
window.addEventListener('load', () => {
setTimeout(() => {
flow1.format = { style: 'currency', currency: 'USD' }
flow1.numberSuffix = '/mo'
flow2.format = { style: 'currency', currency: 'USD' }
flow2.numberSuffix = '/mo'
flow1.update(1250.50)
flow2.update(1250.50)
}, 3000)
})
}
</script>
================================================
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 `<NumberFlowGroup>` ([`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
================================================
[](https://number-flow.barvian.me)
# NumberFlow for React
An animated number component.
[](https://npmjs.com/package/@number-flow/react)
[](https://bundlephobia.com/package/@number-flow/react@latest)
[](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<NumberFlowElement> &
Partial<Props> & {
isolate?: boolean
willChange?: boolean
onAnimationsStart?: (e: CustomEvent<undefined>) => void
onAnimationsFinish?: (e: CustomEvent<undefined>) => void
}
type NumberFlowImplProps = BaseProps & {
innerRef: React.MutableRefObject<NumberFlowElement | undefined>
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<string, Intl.NumberFormat> = {}
// Tiny workaround to support React 19 until it's released:
function identity<T>(v: T) {
return v
}
const serialize = isReact19 ? identity : JSON.stringify
function splitProps<T extends Record<string, any>>(
props: T
): [Omit<Props, 'digits'>, Omit<T, keyof Omit<Props, 'digits'>>] {
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<NumberFlowImplProps>) {
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<NumberFlowImplProps>) {
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<NumberFlowImplProps>,
__: 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
<number-flow-react
ref={this.handleRef}
data-will-change={willChange ? '' : undefined}
// Have to rename this:
class={className}
nonce={nonce}
{...rest}
dangerouslySetInnerHTML={{
__html: BROWSER ? '' : renderInnerHTML(data, { nonce, elementSuffix: '-react' })
}}
suppressHydrationWarning
digits={serialize(digits)}
// Make sure data is set last, everything else is updated:
data={serialize(data)}
/>
)
}
}
export type NumberFlowProps = BaseProps & {
value: Value
locales?: Intl.LocalesArgument
format?: Format
prefix?: string
suffix?: string
}
const NumberFlow = React.forwardRef<NumberFlowElement, NumberFlowProps>(function NumberFlow(
{ value, locales, format, prefix, suffix, ...props },
_ref
) {
React.useImperativeHandle(_ref, () => ref.current!, [])
const ref = React.useRef<NumberFlowElement | undefined>(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 <NumberFlowImpl {...props} group={group} data={data} innerRef={ref} />
})
export default NumberFlow
// NumberFlowGroup
type GroupContext = {
useRegister: (ref: React.MutableRefObject<NumberFlowElement | undefined>) => void
willUpdate: () => void
didUpdate: () => void
}
const NumberFlowGroupContext = React.createContext<GroupContext | undefined>(undefined)
export function NumberFlowGroup({ children }: { children: React.ReactNode }) {
const flows = React.useRef(new Set<React.MutableRefObject<NumberFlowElement | undefined>>())
const updating = React.useRef(false)
const pending = React.useRef(new WeakMap<NumberFlowElement, boolean>())
const value = React.useMemo<GroupContext>(
() => ({
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 <NumberFlowGroupContext.Provider value={value}>{children}</NumberFlowGroupContext.Provider>
}
================================================
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 (
<div>
<p data-testid="default">{String(canAnimate)}</p>
<p data-testid="disrespect-motion-preference">{String(disrespectMotionPreference)}</p>
</div>
)
}
================================================
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<NumberFlowElement>(null)
const ref2 = React.useRef<NumberFlowElement>(null)
return (
<>
<div>
<NumberFlowGroup>
<NumberFlow ref={ref1} value={value} />
<NumberFlow ref={ref2} value={0} />
</NumberFlowGroup>
</div>
<button
onClick={() => {
flushSync(() => {
setValue(152000)
})
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}}
>
Change and pause
</button>
<br />
<button
onClick={() => {
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
</>
)
}
================================================
FILE: packages/react/test/apps/react-18/app/hashes/page.tsx
================================================
import NumberFlow from '@number-flow/react'
export default function Page() {
return <NumberFlow value={42} />
}
================================================
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 (
<html lang="en">
{/* <body className={`${inter.className} antialiased`}>{children}</body> */}
<body>{children}</body>
</html>
)
}
================================================
FILE: packages/react/test/apps/react-18/app/nonce/page.tsx
================================================
import NumberFlow from '@number-flow/react'
export default function Page() {
return (
<>
<NumberFlow nonce="test-nonce" value={42} />
<NumberFlow nonce="test-nonce" value={42} />
</>
)
}
================================================
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<NumberFlowElement>(null)
const ref2 = React.useRef<NumberFlowElement>(null)
return (
<>
<div>
Text node{' '}
<NumberFlowGroup>
<NumberFlow
id="flow1"
data-testid="flow1"
ref={ref1}
value={value}
format={{ style: 'currency', currency: 'USD' }}
locales="zh-CN"
trend={() => -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 }}
/>
<NumberFlow
id="flow2"
data-testid="flow2"
ref={ref2}
value={value}
respectMotionPreference={false}
plugins={[continuous]}
digits={{ 0: { max: 2 } }}
transformTiming={{ easing: 'linear', duration: 500 }}
spinTiming={{ easing: 'linear', duration: 800 }}
opacityTiming={{ easing: 'linear', duration: 500 }}
/>
</NumberFlowGroup>
</div>
<button
onClick={() => {
flushSync(() => {
setValue(152)
})
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}}
>
Change and pause
</button>
<br />
<button
onClick={() => {
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
</>
)
}
================================================
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 (
<div>
<p data-testid="default">{String(canAnimate)}</p>
<p data-testid="disrespect-motion-preference">{String(disrespectMotionPreference)}</p>
</div>
)
}
================================================
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<NumberFlowElement>(null)
const ref2 = React.useRef<NumberFlowElement>(null)
return (
<>
<div>
<NumberFlowGroup>
<NumberFlow ref={ref1} value={value} />
<NumberFlow ref={ref2} value={0} />
</NumberFlowGroup>
</div>
<button
onClick={() => {
flushSync(() => {
setValue(152000)
})
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}}
>
Change and pause
</button>
<br />
<button
onClick={() => {
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
</>
)
}
================================================
FILE: packages/react/test/apps/react-19/app/hashes/page.tsx
================================================
import NumberFlow from '@number-flow/react'
export default function Page() {
return <NumberFlow value={42} />
}
================================================
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 (
<html lang="en">
{/* <body className={`${inter.className} antialiased`}>{children}</body> */}
<body>{children}</body>
</html>
)
}
================================================
FILE: packages/react/test/apps/react-19/app/nonce/page.tsx
================================================
import NumberFlow from '@number-flow/react'
export default function Page() {
return (
<>
<NumberFlow nonce="test-nonce" value={42} />
<NumberFlow nonce="test-nonce" value={42} />
</>
)
}
================================================
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<NumberFlowElement>(null)
const ref2 = React.useRef<NumberFlowElement>(null)
return (
<>
<div>
Text node{' '}
<NumberFlowGroup>
<NumberFlow
id="flow1"
data-testid="flow1"
ref={ref1}
value={value}
format={{ style: 'currency', currency: 'USD' }}
locales="zh-CN"
trend={() => -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 }}
/>
<NumberFlow
id="flow2"
data-testid="flow2"
ref={ref2}
value={value}
respectMotionPreference={false}
plugins={[continuous]}
digits={{ 0: { max: 2 } }}
transformTiming={{ easing: 'linear', duration: 500 }}
spinTiming={{ easing: 'linear', duration: 800 }}
opacityTiming={{ easing: 'linear', duration: 500 }}
/>
</NumberFlowGroup>
</div>
<button
onClick={() => {
flushSync(() => {
setValue(152)
})
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}}
>
Change and pause
</button>
<br />
<button
onClick={() => {
;[
...(ref1.current?.shadowRoot?.getAnimations() ?? []),
...(ref2.current?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
</>
)
}
================================================
FILE: packages/react/test/apps/react-19/app/sc/page.tsx
================================================
import NumberFlow from '@number-flow/react'
export default function SC() {
return <NumberFlow value={123} />
}
================================================
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 `<NumberFlowGroup>` ([`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
================================================
[](https://number-flow.barvian.me/svelte)
# NumberFlow for Svelte
An animated number component.
[](https://npmjs.com/package/@number-flow/svelte)
[](https://bundlephobia.com/package/@number-flow/svelte@latest)
[](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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>
================================================
FILE: packages/svelte/src/lib/NumberFlow.svelte
================================================
<script lang="ts" context="module">
import NumberFlowLite, { define, type Data } from 'number-flow/lite'
// Svelte only supports setters, but Svelte 4 didn't pick up inherited ones:
export class NumberFlowElement extends NumberFlowLite {
set __svelte_batched(batched: boolean) {
this.batched = batched
}
override set data(data: Data | undefined) {
super.data = data
}
}
Object.keys(NumberFlowElement.defaultProps).forEach((key) => {
// Use lowerCase for Svelte 5 for some reason:
Object.defineProperty(NumberFlowElement.prototype, `__svelte_${key.toLowerCase()}`, {
set(value) {
this[key] = value
},
enumerable: true,
configurable: true
})
})
define('number-flow-svelte', NumberFlowElement)
</script>
<script lang="ts">
import {
type Value,
type Format,
renderInnerHTML,
formatToData,
type Props as NumberFlowProps
} from 'number-flow/lite'
import type { HTMLAttributes } from 'svelte/elements'
import { writable } from 'svelte/store'
import { getGroupContext } from './group.js'
import { BROWSER } from 'esm-env'
export let locales: Intl.LocalesArgument = undefined
export let format: Format | undefined = undefined
export let value: Value
export let prefix: string | undefined = undefined
export let suffix: string | undefined = undefined
export let nonce: string | undefined = undefined
export let willChange = false
// Define these so they can be remapped. We set them to their defaults because
// that makes them optional in Svelte
export let transformTiming = NumberFlowElement.defaultProps.transformTiming
export let spinTiming = NumberFlowElement.defaultProps.spinTiming
export let opacityTiming = NumberFlowElement.defaultProps.opacityTiming
export let animated = NumberFlowElement.defaultProps.animated
export let respectMotionPreference = NumberFlowElement.defaultProps.respectMotionPreference
export let trend = NumberFlowElement.defaultProps.trend
export let plugins = NumberFlowElement.defaultProps.plugins
export let digits = NumberFlowElement.defaultProps.digits
type $$Props = HTMLAttributes<HTMLElement> &
Partial<NumberFlowProps> & {
el?: NumberFlowElement
locales?: Intl.LocalesArgument
format?: Format
value: Value
prefix?: string
suffix?: string
nonce?: string
willChange?: boolean
}
type $$Events = {
animationsstart: CustomEvent<undefined>
animationsfinish: CustomEvent<undefined>
}
export let el: NumberFlowElement | undefined = undefined
const elStore = writable<NumberFlowElement | undefined>(el)
$: $elStore = el
// You're supposed to cache these between uses:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
$: formatter = new Intl.NumberFormat(locales, format)
$: data = formatToData(value, formatter, prefix, suffix)
// Handle grouping. Keep as much logic in NumberFlowGroup.vue as possible
// for better tree-shaking:
const group = getGroupContext()
group?.register?.(elStore)
</script>
<number-flow-svelte
bind:this={el}
{...$$restProps}
data-will-change={willChange ? '' : undefined}
on:animationsstart
on:animationsfinish
__svelte_batched={Boolean(group)}
__svelte_transformtiming={transformTiming}
__svelte_spintiming={spinTiming}
__svelte_opacitytiming={opacityTiming}
__svelte_animated={animated}
__svelte_respectmotionpreference={respectMotionPreference}
__svelte_trend={trend}
__svelte_plugins={plugins}
__svelte_digits={digits}
{nonce}
{data}
>
{@html BROWSER ? undefined : renderInnerHTML(data, { nonce, elementSuffix: '-svelte' })}
</number-flow-svelte>
================================================
FILE: packages/svelte/src/lib/NumberFlowGroup.svelte
================================================
<script lang="ts">
import type NumberFlowLite from 'number-flow/lite'
import { type Readable, get } from 'svelte/store'
import { beforeUpdate, onDestroy, tick } from 'svelte'
import { type RegisterWithGroup, setGroupContext } from './group.js'
const flows = new Set<Readable<NumberFlowLite | undefined>>()
let updating = false
const registerWithGroup: RegisterWithGroup = (el) => {
flows.add(el)
beforeUpdate(async () => {
if (updating) return
updating = true
flows.forEach(async (flow) => {
{
const f = get(flow)
if (!f || !f.created) return
f.willUpdate()
}
await tick()
// Optional in case the element was removed after tick:
get(flow)?.didUpdate()
})
await tick()
updating = false
})
onDestroy(() => {
flows.delete(el)
})
}
setGroupContext({ register: registerWithGroup })
</script>
<slot />
================================================
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<NumberFlowLite | undefined>) => 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
================================================
<script lang="ts">
import '../app.css'
</script>
<slot></slot>
================================================
FILE: packages/svelte/src/routes/+page.svelte
================================================
<script lang="ts">
import NumberFlow, { NumberFlowGroup, NumberFlowElement, continuous } from '$lib/index.js'
import { afterUpdate, tick } from 'svelte'
const initialValue = 42
let value = initialValue
let el1: NumberFlowElement | undefined
let el2: NumberFlowElement | undefined
afterUpdate(async () => {
await tick()
if (value !== initialValue) {
;[
...(el1?.shadowRoot?.getAnimations() ?? []),
...(el2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}
})
</script>
<div>
Text node
<NumberFlowGroup>
<NumberFlow
bind:el={el1}
id="flow1"
data-testid="flow1"
{value}
format={{ style: 'currency', currency: 'USD' }}
locales="zh-CN"
trend={() => -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 }}
/><NumberFlow
bind:el={el2}
id="flow2"
data-testid="flow2"
{value}
respectMotionPreference={false}
plugins={[continuous]}
digits={{ 0: { max: 2 } }}
transformTiming={{ easing: 'linear', duration: 500 }}
spinTiming={{ easing: 'linear', duration: 800 }}
opacityTiming={{ easing: 'linear', duration: 500 }}
/>
</NumberFlowGroup>
</div>
<button on:click={() => (value = 152)}>Change and pause</button>
<br />
<button
on:click={() => {
;[
...(el1?.shadowRoot?.getAnimations() ?? []),
...(el2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
================================================
FILE: packages/svelte/src/routes/can-animate/+page.svelte
================================================
<script lang="ts">
import { getCanAnimate } from '$lib/index.js'
const canAnimate = getCanAnimate()
const disrespectMotionPreference = getCanAnimate({ respectMotionPreference: false })
// Trigger runes mode:
$: $canAnimate
</script>
<div>
<p data-testid="default">{String($canAnimate)}</p>
<p data-testid="disrespect-motion-preference">{String($disrespectMotionPreference)}</p>
</div>
================================================
FILE: packages/svelte/src/routes/group-1-unchanged/+page.svelte
================================================
<script lang="ts">
import NumberFlow, { NumberFlowGroup, NumberFlowElement } from '$lib/index.js'
import { afterUpdate, tick } from 'svelte'
const initialValue = 42
let value = initialValue
let el1: NumberFlowElement | undefined
let el2: NumberFlowElement | undefined
afterUpdate(async () => {
await tick()
if (value !== initialValue) {
;[
...(el1?.shadowRoot?.getAnimations() ?? []),
...(el2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
}
})
</script>
<div>
<NumberFlowGroup>
<NumberFlow bind:el={el1} {value} /><NumberFlow bind:el={el2} value={0} />
</NumberFlowGroup>
</div>
<button on:click={() => (value = 152000)}>Change and pause</button>
<br />
<button
on:click={() => {
;[
...(el1?.shadowRoot?.getAnimations() ?? []),
...(el2?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}}
>
Resume
</button>
================================================
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
================================================
<script lang="ts">
import NumberFlow from '$lib/index.js'
</script>
<NumberFlow value={42} />
================================================
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
================================================
<script lang="ts">
import NumberFlow from '$lib/index.js'
</script>
<NumberFlow value={42} nonce="test-nonce" /><NumberFlow value={42} nonce="test-nonce" />
================================================
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 `<NumberFlowGroup>` 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 `<NumberFlowGroup>` ([`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
================================================
[](https://number-flow.barvian.me/vue)
# NumberFlow for Vue
An animated number component.
[](https://npmjs.com/package/@number-flow/vue)
[](https://bundlephobia.com/package/@number-flow/vue@latest)
[](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
================================================
<script lang="ts" setup>
import type NumberFlowLite from 'number-flow/lite'
import { key, type RegisterWithGroup } from './group'
import { provide, watch, type Ref, nextTick, onUnmounted } from 'vue'
const flows = new Set<Ref<NumberFlowLite | undefined>>()
let updating = false
const registerWithGroup: RegisterWithGroup = (el, parts) => {
flows.add(el)
watch(
parts,
async () => {
if (updating) return
updating = true
flows.forEach(async (flow) => {
if (!flow.value || !flow.value.created) return
flow.value.willUpdate()
await nextTick()
// Optional in case the element was removed after tick:
flow.value?.didUpdate()
})
await nextTick()
updating = false
}
// { flush: 'pre' } // default
)
onUnmounted(() => {
flows.delete(el)
})
}
provide(key, registerWithGroup)
</script>
<template>
<slot />
</template>
================================================
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<NumberFlowLite | undefined>,
parts: ComputedRef<ReturnType<typeof formatToData>>
) => void
export const key = Symbol() as InjectionKey<RegisterWithGroup | undefined>
================================================
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<boolean>
} = {}) {
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
================================================
<script lang="ts" setup>
import NumberFlowLite, {
type Value,
type Format,
renderInnerHTML,
formatToData,
type Props as NumberFlowProps
} from 'number-flow/lite'
import { computed, inject, ref } from 'vue'
import { key as groupKey } from './group'
import { BROWSER } from 'esm-env'
type Props = Partial<NumberFlowProps> & {
locales?: Intl.LocalesArgument
format?: Format
value: Value
prefix?: string
suffix?: string
nonce?: string
willChange?: boolean
}
// This is repetitive but I couldn't get it any cleaner using `withDefaults`,
// because then you can't destructure,
// and if you don't set defaults Vue will use its own for e.g. booleans.
const {
locales,
format,
value,
prefix,
suffix,
nonce,
// Couldn't find docs on this, but needs wrapper function to work:
trend = () => NumberFlowLite.defaultProps.trend,
plugins = NumberFlowLite.defaultProps.plugins,
animated = NumberFlowLite.defaultProps.animated,
transformTiming = NumberFlowLite.defaultProps.transformTiming,
spinTiming = NumberFlowLite.defaultProps.spinTiming,
opacityTiming = NumberFlowLite.defaultProps.opacityTiming,
respectMotionPreference = NumberFlowLite.defaultProps.respectMotionPreference,
digits = NumberFlowLite.defaultProps.digits,
willChange = false
} = defineProps<Props>()
const el = ref<NumberFlowLite>()
defineExpose({ el })
defineOptions({
inheritAttrs: false // set them manually to ensure `parts` updates last
})
const emit = defineEmits<{
(e: 'animationsstart'): void
(e: 'animationsfinish'): void
}>()
// You're supposed to cache these between uses:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString
const formatter = computed(() => new Intl.NumberFormat(locales, format))
const data = computed(() => formatToData(value, formatter.value, prefix, suffix))
// Putting this in the v-html attribute ruined tree-shaking
const html = BROWSER ? undefined : renderInnerHTML(data.value, { nonce, elementSuffix: '-vue' })
// Handle grouping. Keep as much logic in NumberFlowGroup.vue as possible
// for better tree-shaking:
const register = inject(groupKey, undefined)
register?.(el, data)
</script>
<template>
<!-- Make sure data is set last: -->
<number-flow-vue
ref="el"
v-bind="$attrs"
:batched="Boolean(register)"
:trend
:plugins
:animated
:transformTiming
:spinTiming
:opacityTiming
:respectMotionPreference
:nonce
:data-will-change="willChange ? '' : undefined"
:digits
v-html="html"
data-allow-mismatch
@animationsstart="emit('animationsstart')"
@animationsfinish="emit('animationsfinish')"
:data
/>
</template>
================================================
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
================================================
<script lang="ts" setup>
import { useCanAnimate } from '@number-flow/vue'
const canAnimate = useCanAnimate()
const disrespectMotionPreference = useCanAnimate({ respectMotionPreference: false })
</script>
<template>
<div>
<p data-testid="default">{{ String(canAnimate) }}</p>
<p data-testid="disrespect-motion-preference">{{ String(disrespectMotionPreference) }}</p>
</div>
</template>
================================================
FILE: packages/vue/test/apps/nuxt3/src/pages/group-1-unchanged.vue
================================================
<script setup lang="ts">
import NumberFlow, { NumberFlowGroup } from '@number-flow/vue'
import { nextTick, ref, useTemplateRef, watch } from 'vue'
const flow1 = useTemplateRef('flow1')
const flow2 = useTemplateRef('flow2')
const value = ref(42)
watch(
value,
async () => {
await nextTick()
;[
...(flow1.value?.el?.shadowRoot?.getAnimations() ?? []),
...(flow2.value?.el?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
},
{ flush: 'post' }
)
</script>
<template>
<div>
<NumberFlowGroup>
<NumberFlow ref="flow1" :value />
<NumberFlow ref="flow2" :value="0" />
</NumberFlowGroup>
</div>
<button @click="value = 152000">Change and pause</button><br />
<button
@click="
() => {
;[
...(flow1?.el?.shadowRoot?.getAnimations() ?? []),
...(flow2?.el?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}
"
>
Resume
</button>
</template>
================================================
FILE: packages/vue/test/apps/nuxt3/src/pages/hashes.vue
================================================
<script setup lang="ts">
import NumberFlow from '@number-flow/vue'
</script>
<template>
<NumberFlow :value="42" />
</template>
================================================
FILE: packages/vue/test/apps/nuxt3/src/pages/index.vue
================================================
<script setup lang="ts">
import NumberFlow, { NumberFlowGroup, continuous } from '@number-flow/vue'
import { nextTick, ref, useTemplateRef, watch } from 'vue'
const flow1 = useTemplateRef('flow1')
const flow2 = useTemplateRef('flow2')
const value = ref(42)
watch(
value,
async () => {
await nextTick()
;[
...(flow1.value?.el?.shadowRoot?.getAnimations() ?? []),
...(flow2.value?.el?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.pause()
a.currentTime = 300
})
},
{ flush: 'post' }
)
const handleStart = () => console.log('start')
const handleFinish = () => console.log('finish')
</script>
<template>
<div>
Text node
<NumberFlowGroup>
<NumberFlow
id="flow1"
data-testid="flow1"
ref="flow1"
:value
:format="{ style: 'currency', currency: 'USD' }"
locales="zh-CN"
:trend="() => -1"
prefix=":"
suffix="/mo"
@animationsstart="handleStart"
@animationsfinish="handleFinish"
:transformTiming="{ easing: 'linear', duration: 500 }"
:spinTiming="{ easing: 'linear', duration: 800 }"
:opacityTiming="{ easing: 'linear', duration: 500 }"
/>
<NumberFlow
id="flow2"
data-testid="flow2"
ref="flow2"
:value
:respectMotionPreference="false"
:plugins="[continuous]"
:digits="{ 0: { max: 2 } }"
:transformTiming="{ easing: 'linear', duration: 500 }"
:spinTiming="{ easing: 'linear', duration: 800 }"
:opacityTiming="{ easing: 'linear', duration: 500 }"
/>
</NumberFlowGroup>
</div>
<button @click="value = 152">Change and pause</button><br />
<button
@click="
() => {
;[
...(flow1?.el?.shadowRoot?.getAnimations() ?? []),
...(flow2?.el?.shadowRoot?.getAnimations() ?? [])
].forEach((a) => {
a.play()
})
}
"
>
Resume
</button>
</template>
================================================
FILE: packages/vue/test/apps/nuxt3/src/pages/nonce.vue
================================================
<script setup lang="ts">
import NumberFlow from '@number-flow/vue'
</script>
<template>
<NumberFlow :value="42" nonce="test-nonce" />
<NumberFlow :value="42" nonce="test-nonce" />
</template>
================================================
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('<script>', '<script type=\\"module\\">')
// Remove @ts-nocheck comments
.replaceAll(/(\\t)*\/\/ @ts-nocheck\\n/g, '')
.replaceAll(/(\\t)*\/\/ prettier-ignore\\n/g, '')
// Clean up IDs
.replaceAll(/vanilla-example-.*?-/g, '')
)
}
}
}
],
ssr: {
// Fixes build issue on macOS
external: ['fsevents']
}
},
env: {
schema: {
GITHUB_TOKEN: envField.string({ context: 'server', access: 'secret' })
}
},
experimental: {
svg: true
},
integrations: [
react(),
mdx(),
{
name: 'watch-shiki-theme',
hooks: {
'astro:config:setup'({ addWatchFile, config }) {
addWatchFile(new URL('./highlighter-theme.json', config.root))
}
}
},
vue({
template: {
compilerOptions: {
// isCustomElement: (tag) => tag === 'number-flow'
}
}
// ...
}),
svelte()
],
output: 'static',
adapter: vercel({
webAnalytics: {
enabled: true
}
})
})
================================================
FILE: site/highlighter-theme.json
================================================
{
"name": "Lambda Studio — Blackout",
"semanticHighlighting": true,
"colors": {
"editorLink.activeForeground": "#ca8a0488",
"foreground": "#fff9",
"button.background": "#fff",
"button.foreground": "#000",
"button.hoverBackground": "#fffb",
"list.highlightForeground": "#fff",
"textLink.foreground": "#fff",
"scrollbar.shadow": "#000",
"textLink.activeForeground": "#fff9",
"editor.lineHighlightBackground": "#8881",
"editor.lineHighlightBorder": "#8882",
"editorCursor.foreground": "#fff",
"editor.findMatchBackground": "#fff9",
"editor.findMatchHighlightBackground": "#fff2",
"list.activeSelectionForeground": "#fff",
"list.focusForeground": "#fff",
"list.hoverForeground": "#fff",
"list.inactiveSelectionForeground": "#fff",
"list.inactiveSelectionBackground": "#000",
"list.focusBackground": "#000",
"list.focusAndSelectionOutline": "#000",
"list.focusHighlightForeground": "#fff",
"list.hoverBackground": "#000",
"list.focusOutline": "#000",
"list.activeSelectionBackground": "#000",
"editorIndentGuide.background": "#fff2",
"editor.background": "#000",
"editor.foreground": "#fff",
"editor.foldBackground": "#000",
"editor.hoverHighlightBackground": "#000",
"editor.selectionBackground": "#8888",
"editor.inactiveSelectionBackground": "#8882",
"gitDecoration.modifiedResourceForeground": "#fff",
"gitDecoration.untrackedResourceForeground": "#a7cb7b",
"gitDecoration.conflictingResourceForeground": "#ca8a04",
"gitDecoration.deletedResourceForeground": "#c97b89",
"listFilterWidget.background": "#000",
"input.background": "#fff1",
"titleBar.activeForeground": "#fff",
"editorWidget.background": "#000",
"editorGutter.background": "#000",
"debugToolBar.background": "#000",
"commandCenter.background": "#000",
"sideBarSectionHeader.background": "#000",
"focusBorder": "#fff9",
"titleBar.activeBackground": "#000",
"titleBar.inactiveBackground": "#000",
"breadcrumb.background": "#000",
"activityBar.background": "#000",
"activityBar.foreground": "#fff9",
"panel.background": "#000",
"sideBar.background": "#000",
"sideBarTitle.foreground": "#fff9",
"tab.hoverBackground": "#000",
"terminal.background": "#000",
"statusBar.background": "#000",
"statusBar.foreground": "#fff9",
"selection.background": "#fff2",
"editorPane.background": "#000",
"badge.background": "#000",
"banner.background": "#000",
"menu.background": "#000",
"activityBarBadge.background": "#000",
"activityBarBadge.foreground": "#fff9",
"editorLineNumber.foreground": "#fff2",
"editorLineNumber.activeForeground": "#fff9",
"statusBarItem.errorBackground": "#f43f5e"
},
"semanticTokenColors": {
"comment": {
"foreground": "#fff5"
},
"keyword": {
"foreground": "#fff9"
},
"string": {
"foreground": "#fff9"
},
"selfKeyword": {
"foreground": "#fff",
"bold": true
},
"method.declaration": {
"foreground": "#fff",
"bold": true
},
"method.definition": {
"foreground": "#fff",
"bold": true
},
"method": {
"foreground": "#fff",
"bold": false
},
"function.declaration": {
"foreground": "#fff",
"bold": true
},
"function.definition": {
"foreground": "#fff",
"bold": true
},
"function": {
"foreground": "#fff",
"bold": false
},
"property": {
"foreground": "#fff"
},
"enumMember": {
"foreground": "#fff9",
"bold": false
},
"enum": {
"foreground": "#fff",
"bold": true
},
"boolean": {
"foreground": "#fff9"
},
"number": {
"foreground": "#fff9"
},
"type": {
"foreground": "#fff",
"bold": true
},
"typeAlias": {
"foreground": "#fff",
"bold": true
},
"class": {
"foreground": "#fff",
"bold": true
},
"selfTypeKeyword": {
"foreground": "#fff",
"bold": true
},
"builtinType": {
"foreground": "#fff",
"bold": true
},
"interface": {
"foreground": "#fff9",
"bold": false
},
"typeParameter": {
"foreground": "#fff",
"bold": true
},
"lifetime": {
"foreground": "#fff9",
"italic": false,
"bold": false
},
"namespace": {
"foreground": "#fff"
},
"macro": {
"foreground": "#fff",
"bold": false
},
"decorator": {
"foreground": "#fff",
"bold": false
},
"builtinAttribute": {
"foreground": "#fff",
"bold": false
},
"generic.attribute": {
"foreground": "#fff"
},
"derive": {
"foreground": "#fff"
},
"operator": {
"foreground": "#fff9"
},
"variable": {
"foreground": "#fff"
},
"variable.readonly": {
"foreground": "#fff9"
},
"parameter": {
"foreground": "#fff"
},
"variable.mutable": {
"underline": true
},
"parameter.mutable": {
"underline": true
},
"selfKeyword.mutable": {
"underline": true
},
"variable.constant": {
"foreground": "#fff9"
},
"struct": {
"foreground": "#fff",
"bold": true
}
},
"tokenColors": [
{
"name": "Fallback Operator",
"scope": ["keyword.operator"],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback keywords",
"scope": [
"storage.type.ts",
"keyword",
"keyword.other",
"keyword.control",
"storage.type",
"storage.modifier"
],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback strings",
"scope": ["string"],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback JSON Properties",
"scope": ["support.type.property-name.json"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "Fallback string variables",
"scope": ["string variable", "string meta.interpolation"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "Fallback comments",
"scope": ["comment"],
"settings": {
"foreground": "#fff5"
}
},
{
"name": "Fallback constants",
"scope": ["constant"],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback self/this",
"scope": ["variable.language.this"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "Fallback types",
"scope": [
"entity.other.alias",
"source.php support.class",
"entity.name.type",
"meta.function-call support.class",
"keyword.other.type",
"entity.other.inherited-class"
],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback method calls",
"scope": ["meta.method-call entity.name.function"],
"settings": {
"foreground": "#fff",
"fontStyle": ""
}
},
{
"name": "Fallback function calls",
"scope": [
"meta.function-call entity.name.function",
"meta.function-call support.function",
"meta.function.call entity.name.function"
],
"settings": {
"foreground": "#fff",
"fontStyle": ""
}
},
{
"name": "Fallback enums & constants",
"scope": ["constant.enum", "constant.other"],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "Fallback Properties & func arguments",
"scope": [
"variable.other.property",
"entity.name.goto-label",
"entity.name.variable.parameter"
],
"settings": {
"foreground": "#fff"
}
},
{
"name": "Fallback functions & methods declarations",
"scope": [
"entity.name.function",
"support.function",
"support.function.constructor",
"entity.name.function meta.function-call meta.method-call"
],
"settings": {
"foreground": "#fff",
"fontStyle": "bold"
}
},
{
"name": "HTML Tags",
"scope": ["meta.tag entity.name.tag.html", "entity.name.tag.template.html"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "HTML Attributes",
"scope": ["entity.other.attribute-name.html"],
"settings": {
"foreground": "#fff9"
}
},
{
"name": "HTML Custom Tag",
"scope": ["meta.tag.other.unrecognized.html entity.name.tag.html"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "HTML Keywords",
"scope": ["text.html keyword"],
"settings": {
"foreground": "#fff"
}
},
{
"name": "Punctuations",
"scope": ["punctuation", "meta.brace"],
"settings": {
"foreground": "#fff9"
}
}
]
}
================================================
FILE: site/package.json
================================================
{
"name": "site",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build && sharp -i ./src/assets/preview.png -o ./.vercel/output/static/preview.webp -f webp",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.0.2",
"@astrojs/react": "^4.4.2",
"@astrojs/svelte": "^7.0.1",
"@astrojs/vue": "^5.0.2",
"@nanostores/react": "^0.8.2",
"@nanostores/vue": "^0.11.0",
"@number-flow/react": "workspace:*",
"@number-flow/svelte": "workspace:*",
"@number-flow/vue": "workspace:*",
"@radix-ui/react-slider": "^1.2.2",
"@react-aria/utils": "^3.31.0",
"@types/lodash": "^4.17.13",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"astro": "^5.0.5",
"astro-font": "^0.1.81",
"bits-ui": "^0.21.16",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"lucide-react": "^0.468.0",
"lucide-svelte": "^0.468.0",
"lucide-vue-next": "^0.468.0",
"motion": "^11.14.4",
"nanostores": "^0.11.3",
"number-flow": "workspace:*",
"react": "19.3.0-canary-e0cc7202-20260227",
"react-aria-components": "^1.15.1",
"react-dom": "19.3.0-canary-e0cc7202-20260227",
"svelte": "^5",
"tailwindcss": "^3.4.16",
"tailwindcss-spring": "^1.0.1",
"typescript": "^5.7.2",
"vue": "^3.5.13"
},
"devDependencies": {
"@astrojs/vercel": "^8.0.1",
"@astropub/context": "^0.1.0",
"@tailwindcss/typography": "^0.5.15",
"fluid-tailwind": "^1.0.4",
"github-slugger": "^2.0.0",
"postcss-easing-gradients": "^3.0.1",
"radix-vue": "^1.9.11",
"sharp": "^0.33.5",
"sharp-cli": "^5.1.0",
"tw-reset": "^0.0.5",
"unist-util-find-after": "^5.0.0",
"unist-util-visit": "^5.0.0"
}
}
================================================
FILE: site/postcss.config.cjs
================================================
module.exports = {
plugins: [
require('tailwindcss'),
// @ts-expect-error no types
require('postcss-easing-gradients')
]
}
================================================
FILE: site/src/assets/main.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-weight: 100 900;
font-style: normal;
font-family: Inter;
font-display: block;
src: url('./fonts/Inter-roman-latin.var.woff2');
}
/* Generated by AstroFont, but injected to avoid View Transitions weirdness with <style> tags */
@font-face {
font-family: '_font_fallback_732902278794';
size-adjust: 107.64%;
src: local('Arial');
ascent-override: 90%;
descent-override: 22.43%;
line-gap-override: 0%;
}
@layer base {
number-flow,
number-flow-react,
number-flow-vue,
number-flow-svelte {
@apply font-mac-ui !leading-[.85];
}
/* Undo Tailwind preflight button cursor pointer */
button {
cursor: default;
}
[data-rac]:focus-visible {
outline: none;
}
:focus-visible,
[data-rac][data-focus-visible] {
@apply outline;
}
}
@layer components {
.container {
@apply ~px-6/10 mx-auto w-full;
}
.link-underline {
@apply ease-out-quad hover:text-primary underline decoration-zinc-300 decoration-[1px] underline-offset-[0.3em] transition-[color,text-decoration-color] hover:decoration-current dark:decoration-zinc-600;
}
.btn {
@apply inline-flex h-11 items-center gap-2 whitespace-nowrap rounded-full px-5 text-sm font-medium transition duration-[.16s] ease-[cubic-bezier(.4,0,.2,1)] active:scale-[98%] active:brightness-[98%] active:duration-[25ms] dark:hover:brightness-125;
}
.btn-primary {
@apply bg-zinc-900 text-zinc-50 hover:brightness-125;
}
.btn-callout {
@apply btn-primary dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white dark:hover:brightness-100;
}
.btn-secondary {
@apply hover:bg-zinc-100 aria-expanded:bg-zinc-100 dark:hover:bg-zinc-900 dark:aria-expanded:bg-zinc-900;
}
/* We can't define this in the tailwind config because .Demo has .not-prose on it: */
.prose .Demo {
@apply my-5;
}
}
@layer utilities {
.outline {
@apply outline-offset [:where(&)]:rounded;
}
.text-primary {
@apply text-zinc-950 dark:text-zinc-50;
}
.caret-primary {
@apply caret-zinc-950 dark:caret-zinc-50;
}
.text-muted {
@apply text-zinc-500 dark:text-zinc-400;
}
.bg-muted {
@apply bg-zinc-500 dark:bg-zinc-400;
}
.bg-faint {
@apply bg-zinc-150 dark:bg-zinc-700;
}
.bg-mask-white {
background-image: linear-gradient(to top, theme(colors.white), ease-out, transparent);
}
.bg-mask-zinc-950 {
background-image: linear-gradient(to top, theme(colors.zinc.950), ease-out, transparent);
}
.scrollbar-none {
-ms-overflow-style: none; /* IE */
scrollbar-width: none; /* Firefox */
&::-webkit-scrollbar {
display: none;
}
}
.spin-hide {
appearance: textfield;
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
&::-webkit-outer-spin-button {
appearance: none;
margin: 0;
}
}
}
================================================
FILE: site/src/components/Alert.astro
================================================
<div
role="alert"
class:list={[
'text-primary border-faint relative rounded-lg border px-4 py-3 text-sm' /*, 'border-amber-200 bg-amber-50 dark:border-yellow-950 dark:bg-yellow-950/50'*/
]}
>
<slot />
</div>
================================================
FILE: site/src/components/AnimateHeightFragment.tsx
================================================
import { spring } from '@/lib/spring'
import { usePrefersReducedMotion } from '@number-flow/react'
import { cloneElement, useLayoutEffect, useRef } from 'react'
import { mergeRefs } from '@react-aria/utils'
import { Snapshotter, type SnapshotterProps } from './Snapshotter'
export type AnimateHeightProps = Pick<SnapshotterProps, 'dependencies'> & {
children: React.ReactElement<{ ref: React.Ref<HTMLElement> }>
}
export default function AnimateHeightFragment(props: AnimateHeightProps) {
const prefersReducedMotion = usePrefersReducedMotion()
if (prefersReducedMotion) {
return props.children
}
return <AnimateHeightImpl {...props} />
}
const timing = spring(0.15, 0)
console.log(timing)
function AnimateHeightImpl({ children, dependencies }: AnimateHeightProps) {
const ref = useRef<HTMLElement>(null)
const heightSnapshotRef = useRef<number | undefined>(undefined)
useLayoutEffect(() => {
if (!ref.current) return
let animation: Animation | undefined
const frame = requestAnimationFrame(() => {
if (!ref.current) return
const newHeight = ref.current.offsetHeight
if (heightSnapshotRef.current === newHeight) return
animation = ref.current.animate(
{
height: [`${heightSnapshotRef.current}px`, `${newHeight}px`]
},
timing
)
})
// Use WAAPI because motion writes to inline styles which complicates
// measuring:
return () => {
cancelAnimationFrame(frame)
animation?.cancel()
}
}, dependencies)
return (
<>
<Snapshotter
dependencies={dependencies}
onSnapshot={() => {
if (!ref.current) return
heightSnapshotRef.current = ref.current.offsetHeight
}}
/>
{cloneElement(children, {
ref: mergeRefs(ref, children.props.ref)
})}
</>
)
}
================================================
FILE: site/src/components/AnimationsOnTheWeb.astro
================================================
---
import { Image } from 'astro:assets'
import headshot from '@/assets/images/headshot.jpeg'
import { ArrowUpRight } from 'lucide-react'
---
<aside class="border-faint my-16 border-y py-8">
<h2 class="m-0 text-lg font-medium">Learn to build components like NumberFlow</h2>
<blockquote class="text-pretty not-italic text-[unset] [font-weight:unset]">
<!-- prettier-ignore -->
<p>Emil Kowalski's Animations on the Web course taught me everything I know about UI animation. I can't
imagine having built NumberFlow without it.</p>
</blockquote>
<p class="not-prose text-muted -mt-1 mb-7 flex items-center gap-2 text-sm">
<Image
src={headshot}
alt="Headshot of Maxwell Barvian"
width={64}
height={64}
class="size-6 rounded-full"
/>
Maxwell Barvian, NumberFlow creator
</p>
<a href="https://animations.dev" rel="external" target="_blank" class="btn btn-callout not-prose">
Take the course
<ArrowUpRight className="size-4" />
</a>
</aside>
================================================
FILE: site/src/components/Code.astro
================================================
---
import styles from './code.module.css'
import { Code as AstroCode } from 'astro:components'
import theme from '/highlighter-theme.json'
import type { ComponentProps } from 'astro/types'
export type Props = ComponentProps<typeof AstroCode> & {
firstLine?: string
}
const { code: _code, inline, firstLine, ...props } = Astro.props
// Clean up code a little bit:
const code = _code
.split('\n')
// Remove prettier-ignore comments:
.filter((line) => !/^\s*\/\/ prettier-ignore\s*$/.test(line))
if (firstLine) code.unshift(firstLine)
---
<AstroCode
{...props}
code={code.join('\n')}
{inline}
class:list={[!inline && styles.code]}
theme={theme as any}
/>
================================================
FILE: site/src/components/Comp.mdx
================================================
import Match from './Match.astro'
<Match><Fragment slot="vanilla">`<number-flow>`</Fragment>`<NumberFlow>`</Match>
================================================
FILE: site/src/components/Demo.tsx
================================================
import * as React from 'react'
// import { atom, useAtom } from 'jotai'
import { clsx } from 'clsx/lite'
import { inView, motion, MotionConfig } from 'motion/react'
import { useId } from 'react'
import {
MenuTrigger,
Button,
Popover,
Menu,
MenuItem,
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
SYMBOL INDEX (274 symbols across 88 files)
FILE: packages/number-flow/src/env.d.ts
type NumberFormat (line 3) | interface NumberFormat {
FILE: packages/number-flow/src/formatter.ts
type NumberPartType (line 2) | type NumberPartType =
type IntegerPart (line 9) | type IntegerPart = { type: NumberPartType & 'integer'; value: number }
type FractionPart (line 10) | type FractionPart = { type: NumberPartType & 'fraction'; value: number }
type DigitPart (line 11) | type DigitPart = IntegerPart | FractionPart
type SymbolPart (line 12) | type SymbolPart = {
type NumberPartKey (line 17) | type NumberPartKey = string
type KeyedPart (line 18) | type KeyedPart = { key: NumberPartKey }
type KeyedDigitPart (line 19) | type KeyedDigitPart = DigitPart & KeyedPart & { pos: number }
type KeyedSymbolPart (line 20) | type KeyedSymbolPart = SymbolPart & KeyedPart
type KeyedNumberPart (line 21) | type KeyedNumberPart = KeyedDigitPart | KeyedSymbolPart
type Format (line 23) | type Format = Omit<Intl.NumberFormatOptions, 'notation'> & {
type Value (line 27) | type Value = Exclude<
function formatToData (line 32) | function formatToData(
type Data (line 117) | type Data = ReturnType<typeof formatToData>
FILE: packages/number-flow/src/group.ts
class NumberFlowGroup (line 6) | class NumberFlowGroup extends ServerSafeHTMLElement {
method connectedCallback (line 9) | connectedCallback() {
method disconnectedCallback (line 63) | disconnectedCallback() {
type HTMLElementTagNameMap (line 73) | interface HTMLElementTagNameMap {
FILE: packages/number-flow/src/index.ts
constant CONNECT_EVENT (line 9) | const CONNECT_EVENT = 'number-flow-connect'
constant UPDATE_EVENT (line 10) | const UPDATE_EVENT = 'number-flow-update'
class NumberFlow (line 34) | class NumberFlow extends NumberFlowLite {
method connectedCallback (line 39) | connectedCallback() {
method disconnectedCallback (line 43) | disconnectedCallback() {
method value (line 60) | get value() {
method update (line 64) | update(value?: Value) {
type HTMLElementTagNameMap (line 89) | interface HTMLElementTagNameMap {
FILE: packages/number-flow/src/lite.ts
type Trend (line 34) | type Trend = number | ((oldValue: number, value: number) => number)
type DigitOptions (line 36) | type DigitOptions = { max?: number }
type Digits (line 37) | type Digits = Record<number, DigitOptions>
type Props (line 39) | interface Props {
type NumberFlowLite (line 52) | interface NumberFlowLite extends Props {}
method constructor (line 84) | constructor() {
method animated (line 92) | get animated() {
method animated (line 95) | set animated(val: boolean) {
method data (line 124) | set data(data: Data | undefined) {
method willUpdate (line 205) | willUpdate() {
method didUpdate (line 217) | didUpdate() {
type Mutable (line 56) | type Mutable = MakeMutable<NumberFlowLite>
class NumberFlowLite (line 61) | class NumberFlowLite extends ServerSafeHTMLElement implements Props {
method constructor (line 84) | constructor() {
method animated (line 92) | get animated() {
method animated (line 95) | set animated(val: boolean) {
method data (line 124) | set data(data: Data | undefined) {
method willUpdate (line 205) | willUpdate() {
method didUpdate (line 217) | didUpdate() {
class Num (line 241) | class Num {
method constructor (line 248) | constructor(
method willUpdate (line 284) | willUpdate() {
method update (line 292) | update({ integer, fraction }: Pick<Data, 'integer' | 'fraction'>) {
method didUpdate (line 297) | didUpdate() {
type SectionProps (line 325) | type SectionProps = { justify: Justify } & HTMLProps<'span'>
method constructor (line 334) | constructor(
method addChar (line 353) | protected addChar(
method unpop (line 380) | protected unpop(char: Char) {
method pop (line 386) | protected pop(chars: Map<any, Char>) {
method addNewAndUpdateExisting (line 398) | protected addNewAndUpdateExisting(parts: KeyedNumberPart[]) {
method willUpdate (line 444) | willUpdate() {
method didUpdate (line 451) | didUpdate() {
class NumberSection (line 475) | class NumberSection extends Section {
method update (line 476) | update(parts: KeyedNumberPart[]) {
class SymbolSection (line 500) | class SymbolSection extends Section {
method update (line 501) | update(parts: KeyedNumberPart[]) {
type OnRemove (line 518) | type OnRemove = () => void
type AnimatePresenceProps (line 519) | interface AnimatePresenceProps {
class AnimatePresence (line 524) | class AnimatePresence {
method constructor (line 528) | constructor(
method present (line 552) | get present() {
method present (line 561) | set present(val) {
type CharProps (line 591) | interface CharProps extends AnimatePresenceProps {}
method constructor (line 594) | constructor(
class Digit (line 608) | class Digit extends Char<KeyedDigitPart> {
method constructor (line 612) | constructor(
method willUpdate (line 651) | willUpdate(parentRect: DOMRect) {
method update (line 662) | update(value: KeyedDigitPart['value']) {
method didUpdate (line 670) | didUpdate(parentRect: DOMRect) {
method getDelta (line 705) | getDelta() {
class Sym (line 729) | class Sym extends Char<KeyedSymbolPart> {
method constructor (line 730) | constructor(
method willUpdate (line 765) | willUpdate(parentRect: DOMRect) {
method update (line 778) | update(value: KeyedSymbolPart['value']) {
method didUpdate (line 807) | didUpdate(parentRect: DOMRect) {
FILE: packages/number-flow/src/plugins/continuous.ts
method onUpdate (line 11) | onUpdate(data, prev, flow) {
method getDelta (line 31) | getDelta(value, prev, digit) {
FILE: packages/number-flow/src/plugins/index.ts
type Plugin (line 5) | type Plugin = {
FILE: packages/number-flow/src/util/dom.ts
type ExcludeReadonly (line 3) | type ExcludeReadonly<T> = {
type HTMLProps (line 7) | type HTMLProps<K extends keyof HTMLElementTagNameMap> = Partial<
type Justify (line 25) | type Justify = 'left' | 'right'
FILE: packages/number-flow/src/util/iterable.ts
function forEach (line 1) | function forEach<T>(
FILE: packages/number-flow/src/util/types.ts
type Mutable (line 1) | type Mutable<T> = {
FILE: packages/number-flow/vite.config.mjs
function minifyCSSLiterals (line 44) | function minifyCSSLiterals() {
FILE: packages/react/src/NumberFlow.tsx
constant REACT_MAJOR (line 20) | const REACT_MAJOR = parseInt(React.version.match(/^(\d+)\./)?.[1]!)
constant OBSERVED_ATTRIBUTES (line 24) | const OBSERVED_ATTRIBUTES = ['data', 'digits'] as const
type ObservedAttribute (line 25) | type ObservedAttribute = (typeof OBSERVED_ATTRIBUTES)[number]
class NumberFlowElement (line 26) | class NumberFlowElement extends NumberFlowLite {
method attributeChangedCallback (line 28) | attributeChangedCallback(attr: ObservedAttribute, _oldValue: string, n...
type BaseProps (line 35) | type BaseProps = React.HTMLAttributes<NumberFlowElement> &
type NumberFlowImplProps (line 43) | type NumberFlowImplProps = BaseProps & {
function identity (line 55) | function identity<T>(v: T) {
function splitProps (line 60) | function splitProps<T extends Record<string, any>>(
type NumberFlowImplState (line 88) | type NumberFlowImplState = {}
type NumberFlowImplSnapshot (line 89) | type NumberFlowImplSnapshot = (() => void) | null // React doesn't like ...
class NumberFlowImpl (line 91) | class NumberFlowImpl extends React.Component<
method constructor (line 96) | constructor(props: NumberFlowImplProps) {
method updateProperties (line 103) | updateProperties(prevProps?: Readonly<NumberFlowImplProps>) {
method componentDidMount (line 124) | override componentDidMount() {
method getSnapshotBeforeUpdate (line 133) | override getSnapshotBeforeUpdate(prevProps: Readonly<NumberFlowImplPro...
method componentDidUpdate (line 148) | override componentDidUpdate(
method handleRef (line 158) | handleRef(el: NumberFlowElement) {
method render (line 163) | override render() {
type NumberFlowProps (line 202) | type NumberFlowProps = BaseProps & {
type GroupContext (line 236) | type GroupContext = {
function NumberFlowGroup (line 244) | function NumberFlowGroup({ children }: { children: React.ReactNode }) {
FILE: packages/react/src/index.tsx
function useCanAnimate (line 29) | function useCanAnimate({ respectMotionPreference = true } = {}) {
FILE: packages/react/test/apps/react-18/app/can-animate/page.tsx
function Page (line 6) | function Page() {
FILE: packages/react/test/apps/react-18/app/group-1-unchanged/page.tsx
function Page (line 7) | function Page() {
FILE: packages/react/test/apps/react-18/app/hashes/page.tsx
function Page (line 3) | function Page() {
FILE: packages/react/test/apps/react-18/app/layout.tsx
function RootLayout (line 11) | function RootLayout({
FILE: packages/react/test/apps/react-18/app/nonce/page.tsx
function Page (line 3) | function Page() {
FILE: packages/react/test/apps/react-18/app/page.tsx
function Page (line 7) | function Page() {
FILE: packages/react/test/apps/react-18/next.config.mjs
method headers (line 9) | async headers() {
method webpack (line 31) | webpack(config, context) {
FILE: packages/react/test/apps/react-19/app/can-animate/page.tsx
function Page (line 6) | function Page() {
FILE: packages/react/test/apps/react-19/app/group-1-unchanged/page.tsx
function Page (line 7) | function Page() {
FILE: packages/react/test/apps/react-19/app/hashes/page.tsx
function Page (line 3) | function Page() {
FILE: packages/react/test/apps/react-19/app/layout.tsx
function RootLayout (line 11) | function RootLayout({
FILE: packages/react/test/apps/react-19/app/nonce/page.tsx
function Page (line 3) | function Page() {
FILE: packages/react/test/apps/react-19/app/page.tsx
function Page (line 7) | function Page() {
FILE: packages/react/test/apps/react-19/app/sc/page.tsx
function SC (line 3) | function SC() {
FILE: packages/react/test/apps/react-19/next.config.ts
method headers (line 11) | async headers() {
method webpack (line 33) | webpack(config, context) {
FILE: packages/svelte/src/lib/group.ts
type RegisterWithGroup (line 7) | type RegisterWithGroup = (el: Readable<NumberFlowLite | undefined>) => void
type GroupContext (line 9) | type GroupContext = { register: RegisterWithGroup }
function setGroupContext (line 11) | function setGroupContext(ctx: GroupContext) {
function getGroupContext (line 15) | function getGroupContext() {
FILE: packages/vue/src/group.ts
type RegisterWithGroup (line 5) | type RegisterWithGroup = (
FILE: packages/vue/src/index.ts
function useCanAnimate (line 22) | function useCanAnimate({
FILE: site/astro.config.mjs
method transform (line 23) | transform(code, id) {
method 'astro:config:setup' (line 58) | 'astro:config:setup'({ addWatchFile, config }) {
FILE: site/src/components/AnimateHeightFragment.tsx
type AnimateHeightProps (line 7) | type AnimateHeightProps = Pick<SnapshotterProps, 'dependencies'> & {
function AnimateHeightFragment (line 11) | function AnimateHeightFragment(props: AnimateHeightProps) {
function AnimateHeightImpl (line 23) | function AnimateHeightImpl({ children, dependencies }: AnimateHeightProp...
FILE: site/src/components/Demo.tsx
type TabValue (line 22) | type TabValue = 'preview' | 'code'
type DemoProps (line 24) | type DemoProps = {
type Props (line 34) | type Props = DemoProps & {
function Demo (line 40) | function Demo({
function DemoTitle (line 169) | function DemoTitle({
function DemoMenu (line 181) | function DemoMenu({ children }: { children: React.ReactNode }) {
function DemoMenuButton (line 185) | function DemoMenuButton({
function DemoMenuItems (line 208) | function DemoMenuItems({
function DemoMenuItem (line 233) | function DemoMenuItem({
function DemoSwitch (line 263) | function DemoSwitch({
FILE: site/src/components/FrameworkMenu.tsx
function FrameworkMenu (line 17) | function FrameworkMenu({
FILE: site/src/components/Freeze.tsx
type FreezeProps (line 3) | type FreezeProps = {
type FragmentObserver (line 8) | type FragmentObserver = {
type ObservableFragmentInstance (line 14) | type ObservableFragmentInstance = React.FragmentInstance & {
class ElementsObserver (line 19) | class ElementsObserver implements FragmentObserver {
method constructor (line 20) | constructor(private readonly elementsRef: React.RefObject<Set<HTMLElem...
method observe (line 22) | observe(element: HTMLElement) {
method unobserve (line 26) | unobserve(element: HTMLElement) {
method disconnect (line 30) | disconnect() {
function Suspend (line 37) | function Suspend() {
function Freeze (line 42) | function Freeze({ frozen, children }: FreezeProps) {
FILE: site/src/components/Link.tsx
type Props (line 10) | type Props = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
function Link (line 16) | function Link({
FILE: site/src/components/Nav.tsx
type Props (line 7) | type Props = React.ComponentPropsWithoutRef<'nav'> & {
function Nav (line 14) | function Nav({ stargazers, className, repo, ...props }: Props) {
function NavLinkActive (line 153) | function NavLinkActive() {
FILE: site/src/components/Snapshotter.tsx
type SnapshotterProps (line 3) | type SnapshotterProps = {
class Snapshotter (line 8) | class Snapshotter extends React.Component<SnapshotterProps> {
method getSnapshotBeforeUpdate (line 9) | override getSnapshotBeforeUpdate(prevProps: SnapshotterProps) {
method componentDidUpdate (line 22) | override componentDidUpdate() {
method render (line 25) | override render() {
FILE: site/src/components/Supported.tsx
function Supported (line 5) | function Supported() {
FILE: site/src/components/TOC.tsx
type Props (line 4) | type Props = React.ComponentPropsWithoutRef<'nav'> & {
function getTop (line 10) | function getTop(id: Heading['id']) {
function TOC (line 15) | function TOC({ headings, ...props }: Props) {
FILE: site/src/components/Tweet/api/get-oembed.ts
function getOEmbed (line 1) | async function getOEmbed(url: string): Promise<any> {
FILE: site/src/components/Tweet/api/get-tweet.ts
constant SYNDICATION_URL (line 3) | const SYNDICATION_URL = 'https://cdn.syndication.twimg.com'
class TwitterApiError (line 5) | class TwitterApiError extends Error {
method constructor (line 9) | constructor({ message, status, data }: { message: string; status: numb...
constant TWEET_ID (line 17) | const TWEET_ID = /^[0-9]+$/
function getToken (line 19) | function getToken(id: string) {
function getTweet (line 26) | async function getTweet(id: string, fetchOptions?: RequestInit): Promise...
FILE: site/src/components/Tweet/api/types/edit.ts
type TweetEditControl (line 1) | interface TweetEditControl {
FILE: site/src/components/Tweet/api/types/entities.ts
type Indices (line 1) | type Indices = [number, number]
type HashtagEntity (line 3) | interface HashtagEntity {
type UserMentionEntity (line 8) | interface UserMentionEntity {
type MediaEntity (line 15) | interface MediaEntity {
type UrlEntity (line 22) | interface UrlEntity {
type SymbolEntity (line 29) | interface SymbolEntity {
type TweetEntities (line 34) | interface TweetEntities {
FILE: site/src/components/Tweet/api/types/media.ts
type RGB (line 3) | type RGB = {
type Rect (line 9) | type Rect = {
type Size (line 16) | type Size = {
type VideoInfo (line 22) | interface VideoInfo {
type MediaBase (line 31) | interface MediaBase {
type MediaPhoto (line 59) | interface MediaPhoto extends MediaBase {
type MediaAnimatedGif (line 64) | interface MediaAnimatedGif extends MediaBase {
type MediaVideo (line 69) | interface MediaVideo extends MediaBase {
type MediaDetails (line 74) | type MediaDetails = MediaPhoto | MediaAnimatedGif | MediaVideo
FILE: site/src/components/Tweet/api/types/photo.ts
type TweetPhoto (line 3) | interface TweetPhoto {
FILE: site/src/components/Tweet/api/types/tweet.ts
type TweetBase (line 11) | interface TweetBase {
type Tweet (line 51) | interface Tweet extends TweetBase {
type TweetParent (line 70) | interface TweetParent extends TweetBase {
type QuotedTweet (line 79) | interface QuotedTweet extends TweetBase {
FILE: site/src/components/Tweet/api/types/user.ts
type TweetUser (line 1) | interface TweetUser {
FILE: site/src/components/Tweet/api/types/video.ts
type TweetVideo (line 1) | interface TweetVideo {
FILE: site/src/components/Tweet/twitter-theme/TweetMediaVideo.tsx
function TweetMediaVideo (line 5) | function TweetMediaVideo(props: React.ComponentPropsWithoutRef<'video'>) {
FILE: site/src/components/Tweet/twitter-theme/types.ts
type TwitterComponents (line 8) | type TwitterComponents = {
type TweetComponents (line 17) | type TweetComponents = TwitterComponents
FILE: site/src/components/Tweet/utils.ts
type TweetCoreProps (line 16) | type TweetCoreProps = {
type TextEntity (line 78) | type TextEntity = {
type TweetEntity (line 83) | type TweetEntity = HashtagEntity | UserMentionEntity | UrlEntity | Media...
type EntityWithType (line 85) | type EntityWithType =
type Entity (line 93) | type Entity = {
function getEntities (line 104) | function getEntities(tweet: TweetBase): Entity[] {
function addEntities (line 141) | function addEntities(
function fixRange (line 177) | function fixRange(tweet: TweetBase, entities: EntityWithType[]) {
type EnrichedTweet (line 191) | type EnrichedTweet = Omit<Tweet, 'entities' | 'quoted_tweet'> & {
type EnrichedQuotedTweet (line 204) | type EnrichedQuotedTweet = Omit<QuotedTweet, 'entities'> & {
FILE: site/src/components/icons/frameworks/react.tsx
function React (line 1) | function React(props: React.SVGProps<SVGSVGElement>) {
FILE: site/src/components/icons/frameworks/svelte.tsx
function Web (line 1) | function Web(props: React.SVGProps<SVGSVGElement>) {
FILE: site/src/components/icons/frameworks/vanilla.tsx
function Web (line 1) | function Web(props: React.SVGProps<SVGSVGElement>) {
FILE: site/src/components/icons/frameworks/vue.tsx
function Vue (line 1) | function Vue(props: React.SVGProps<SVGSVGElement>) {
FILE: site/src/context/toc.ts
type Heading (line 6) | type Heading = { title: string; id: string }
FILE: site/src/hooks/useCycle.ts
function useCycle (line 3) | function useCycle<T>(options: Array<T>, defaultValue?: T) {
FILE: site/src/lib/framework.ts
type FrameworkData (line 7) | type FrameworkData = {
constant FRAMEWORKS (line 17) | const FRAMEWORKS = {
type Framework (line 56) | type Framework = keyof typeof FRAMEWORKS
constant DEFAULT_FRAMEWORK (line 58) | const DEFAULT_FRAMEWORK: Framework = 'react'
FILE: site/src/lib/spring.ts
method toString (line 9) | toString() {
FILE: site/src/lib/stores.ts
type CyclableAtom (line 11) | type CyclableAtom<T> = ReadableAtom<T> & {
function cyclable (line 17) | function cyclable<T>(...options: Array<T>): CyclableAtom<T> {
function hydratable (line 27) | function hydratable<T, A extends CyclableAtom<T> | PreinitializedWritabl...
FILE: site/src/lib/types.ts
type Rename (line 1) | type Rename<T, K extends keyof T, N extends string> = {
FILE: site/src/pages/[...framework]/_Hero.tsx
constant NUMBERS (line 7) | const NUMBERS = [431.1, -3243.6, 42, 398.43, -3243.5, 1435237.2, 12348.4...
constant LOCALES (line 8) | const LOCALES = ['en-US', 'en-US', 'fr-FR', 'en-US', 'en-US', 'zh-CN', '...
constant FORMATS (line 9) | const FORMATS = [
function Hero (line 44) | function Hero({ sandbox }: { sandbox: string }) {
FILE: site/src/pages/[...framework]/_demos/Continuous.tsx
constant NUMBERS (line 7) | const NUMBERS = [120, 140]
function DemoHOC (line 9) | function DemoHOC({
FILE: site/src/pages/[...framework]/_demos/Isolate.tsx
function DemoHOC (line 5) | function DemoHOC({ ...rest }: Omit<DemoProps, 'children' | 'code'>) {
FILE: site/src/pages/[...framework]/_demos/Styling.tsx
constant NUMBERS (line 6) | const NUMBERS: Value[] = [3, 15, 50]
function DemoHOC (line 8) | function DemoHOC({
FILE: site/src/pages/[...framework]/_demos/Suffix.tsx
constant NUMBERS (line 6) | const NUMBERS: Value[] = [3, 15, 50]
function DemoHOC (line 8) | function DemoHOC({
FILE: site/src/pages/[...framework]/_demos/TabularNums.tsx
function DemoHOC (line 5) | function DemoHOC({ ...rest }: Omit<DemoProps, 'children' | 'code'>) {
FILE: site/src/pages/[...framework]/_demos/Timings.tsx
constant NUMBERS (line 14) | const NUMBERS = [124.23, 41.75, 2125.95]
function DemoHOC (line 16) | function DemoHOC({
FILE: site/src/pages/[...framework]/_demos/Trend.tsx
constant NUMBERS (line 12) | const NUMBERS = [20, 19]
constant TRENDS (line 14) | const TRENDS: Record<string, Trend | undefined> = {
function DemoHOC (line 21) | function DemoHOC({ ...rest }: Omit<DemoProps, 'children' | 'code'>) {
FILE: site/src/pages/[...framework]/examples/_Activity/index.tsx
function Activity (line 4) | function Activity(props: DemoProps) {
FILE: site/src/pages/[...framework]/examples/_Activity/react/Component.tsx
type Props (line 12) | type Props = ComponentPropsWithoutRef<'div'> & {
function Activity (line 25) | function Activity({
FILE: site/src/pages/[...framework]/examples/_Activity/stores.ts
type CounterState (line 6) | interface CounterState {
function countable (line 11) | function countable(
function randomBetween (line 54) | function randomBetween(min: number, max: number) {
FILE: site/src/pages/[...framework]/examples/_ColoredTrends/Example.tsx
type Props (line 4) | type Props = {
function PriceWithColoredTrend (line 8) | function PriceWithColoredTrend({ value }: Props) {
FILE: site/src/pages/[...framework]/examples/_ColoredTrends/index.tsx
constant NUMBERS (line 6) | const NUMBERS = [12398.432, -3243.6, 543.2]
function DemoHOC (line 8) | function DemoHOC({
FILE: site/src/pages/[...framework]/examples/_Countdown/index.tsx
function Activity (line 4) | function Activity(props: DemoProps) {
FILE: site/src/pages/[...framework]/examples/_Countdown/react/Component.tsx
type Props (line 3) | type Props = {
function Countdown (line 7) | function Countdown({ seconds }: Props) {
FILE: site/src/pages/[...framework]/examples/_Countdown/stores.ts
function countdownable (line 6) | function countdownable(
FILE: site/src/pages/[...framework]/examples/_Group/index.tsx
function Group (line 4) | function Group(props: DemoProps) {
FILE: site/src/pages/[...framework]/examples/_Group/react/Component.tsx
type Props (line 4) | type Props = {
function PriceWithDiff (line 9) | function PriceWithDiff({ value, diff }: Props) {
FILE: site/src/pages/[...framework]/examples/_Input/react/Component.tsx
type Props (line 6) | type Props = {
function Input (line 13) | function Input({ value = 0, min = -Infinity, max = Infinity, onChange }:...
FILE: site/src/pages/[...framework]/examples/_Input/react/index.tsx
function Input (line 4) | function Input() {
FILE: site/src/pages/[...framework]/examples/_Motion/index.tsx
function Group (line 4) | function Group(props: DemoProps) {
FILE: site/src/pages/[...framework]/examples/_Motion/react/Component.tsx
type Props (line 10) | type Props = {
function MotionExample (line 14) | function MotionExample({ value }: Props) {
FILE: site/src/pages/[...framework]/examples/_Slider/react/Component.tsx
function Slider (line 5) | function Slider({ value, className, ...props }: RadixSlider.SliderProps) {
FILE: site/src/pages/[...framework]/examples/_Slider/react/index.tsx
function Slider (line 4) | function Slider() {
FILE: site/src/react.d.ts
type CSSProperties (line 4) | interface CSSProperties {
type IntrinsicAttributes (line 9) | interface IntrinsicAttributes {
Condensed preview — 340 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (450K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 349,
"preview": "{\n\t\"$schema\": \"https://unpkg.com/@changesets/config@3.0.3/schema.json\",\n\t\"changelog\": [\"@svitejs/changesets-changelog-gi"
},
{
"path": ".gitattributes",
"chars": 66,
"preview": "# Auto detect text files and perform LF normalization\n* text=auto\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 853,
"preview": "# These are supported funding model platforms\n\ngithub: [barvian]\npatreon: # Replace with a single Patreon username\nopen_"
},
{
"path": ".github/ISSUE_TEMPLATE/0-bug.yml",
"chars": 3328,
"preview": "name: '🐛 Report a bug'\ndescription: Report a reproducible bug or regression.\nbody:\n - type: markdown\n attributes:\n "
},
{
"path": ".github/ISSUE_TEMPLATE/1-docs.yml",
"chars": 629,
"preview": "name: \"📖 Docs issue\"\ndescription: \"Report a typo or other issue on the docs.\"\ntitle: \"[Docs]: \"\n# labels: [\"type: docume"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 792,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: '💬 Get help'\n url: https://github.com/barvian/number-flow/discus"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1017,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n pull_request:\n\nenv:\n # we call `pnpm playwright install` instead\n P"
},
{
"path": ".github/workflows/release.yml",
"chars": 999,
"preview": "name: Release\n\non:\n push:\n branches:\n - main\n\njobs:\n release:\n # prevents this action from running on forks"
},
{
"path": ".gitignore",
"chars": 139,
"preview": ".DS_Store\nnode_modules\n.turbo\ndist/\n.env\nbuild/\n.astro/\n.env.*\n!.env.example\nvite.config.js.timestamp-*\nvite.config.ts.t"
},
{
"path": ".npmrc",
"chars": 30,
"preview": "link-workspace-packages = true"
},
{
"path": ".prettierignore",
"chars": 24,
"preview": "pnpm-lock.yaml\n\n**/*.mdx"
},
{
"path": ".vscode/settings.json",
"chars": 54,
"preview": "{\n\t\"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
},
{
"path": "LICENSE.md",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2024 Maxwell Barvian\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 636,
"preview": "https://github.com/user-attachments/assets/fb49ac50-039e-41e6-a19b-64e74ebb5930\n\n# NumberFlow\n\nAn animated number compon"
},
{
"path": "lib/playwright.ts",
"chars": 2317,
"preview": "import { defineConfig, devices } from '@playwright/test'\n\n/**\n * Read environment variables from file.\n * https://github"
},
{
"path": "package.json",
"chars": 895,
"preview": "{\n\t\"private\": true,\n\t\"type\": \"module\",\n\t\"pnpm\": {\n\t\t\"overrides\": {\n\t\t\t\"@types/react\": \"^19.2.14\",\n\t\t\t\"@types/react-dom\":"
},
{
"path": "packages/number-flow/CHANGELOG.md",
"chars": 5172,
"preview": "# number-flow\n\n## 0.6.0\n\n### Minor Changes\n\n- Remove `--number-flow-char-height` CSS property in favor of `line-height` "
},
{
"path": "packages/number-flow/README.md",
"chars": 625,
"preview": "[](https://number-flow.barvian.me/vanilla)\n\n# NumberFlow\n\nAn a"
},
{
"path": "packages/number-flow/package.json",
"chars": 2021,
"preview": "{\n\t\"name\": \"number-flow\",\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"version\": \"0.6.0\",\n\t\"author\": {\n\t\t\"name\": \"Maxw"
},
{
"path": "packages/number-flow/src/csp.ts",
"chars": 233,
"preview": "import runtimeStyles from './styles'\nimport { styles as ssrStyles, renderFallbackStyles } from './ssr'\n\nexport const bui"
},
{
"path": "packages/number-flow/src/env.d.ts",
"chars": 162,
"preview": "// Fix types for Intl.NumberFormat\ndeclare namespace Intl {\n\tinterface NumberFormat {\n\t\tformatToParts(number?: number | "
},
{
"path": "packages/number-flow/src/formatter.ts",
"chars": 3752,
"preview": "// Merge the plus and minus sign types\nexport type NumberPartType =\n\t| Exclude<Intl.NumberFormatPartTypes, 'minusSign' |"
},
{
"path": "packages/number-flow/src/group.ts",
"chars": 2150,
"preview": "import { define } from './util/dom'\nimport { ServerSafeHTMLElement } from './ssr'\nimport { CONNECT_EVENT, UPDATE_EVENT }"
},
{
"path": "packages/number-flow/src/index.ts",
"chars": 2264,
"preview": "import NumberFlowLite from './lite'\nimport { define } from './util/dom'\nimport { renderInnerHTML as defaultRenderInnerHT"
},
{
"path": "packages/number-flow/src/lite.ts",
"chars": 21369,
"preview": "import { createElement, offset, visible, type HTMLProps, type Justify } from './util/dom'\nimport { forEach } from './uti"
},
{
"path": "packages/number-flow/src/plugins/continuous.ts",
"chars": 1396,
"preview": "import { max } from '../util/math'\nimport type { Plugin } from '.'\nimport type NumberFlowLite from '../lite'\n\nconst star"
},
{
"path": "packages/number-flow/src/plugins/index.ts",
"chars": 326,
"preview": "import type NumberFlowLite from '../lite'\nimport type { Digit } from '../lite'\nimport type { Data } from '../formatter'\n"
},
{
"path": "packages/number-flow/src/ssr.ts",
"chars": 2074,
"preview": "import type { Data, KeyedNumberPart } from './formatter'\nimport { css, html } from './util/string'\nimport { halfMaskHeig"
},
{
"path": "packages/number-flow/src/styles.ts",
"chars": 6447,
"preview": "import { BROWSER } from 'esm-env'\nimport { css } from './util/string'\n\nexport const supportsLinear =\n\tBROWSER &&\n\t(() =>"
},
{
"path": "packages/number-flow/src/util/dom.ts",
"chars": 1513,
"preview": "import { BROWSER } from 'esm-env'\n\ntype ExcludeReadonly<T> = {\n\t-readonly [K in keyof T as T[K] extends Readonly<any> ? "
},
{
"path": "packages/number-flow/src/util/iterable.ts",
"chars": 242,
"preview": "export function forEach<T>(\n\tarr: T[],\n\tfn: (item: T, index: number) => void,\n\t{ reverse = false } = {}\n) {\n\tconst len ="
},
{
"path": "packages/number-flow/src/util/math.ts",
"chars": 172,
"preview": "// Math.max that handles nullish numbers\nexport const max = (n1?: number, n2?: number) => {\n\tif (n1 == null) return n2\n\t"
},
{
"path": "packages/number-flow/src/util/string.ts",
"chars": 61,
"preview": "export const html = String.raw\nexport const css = String.raw\n"
},
{
"path": "packages/number-flow/src/util/types.ts",
"chars": 61,
"preview": "export type Mutable<T> = {\n\t-readonly [P in keyof T]: T[P]\n}\n"
},
{
"path": "packages/number-flow/test/apps/astro/.gitignore",
"chars": 344,
"preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# testing\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/"
},
{
"path": "packages/number-flow/test/apps/astro/README.md",
"chars": 2039,
"preview": "# Astro Starter Kit: Basics\n\n```sh\npnpm create astro@latest -- --template basics\n```\n\n[](https://number-flow.barvian.me)\n\n# NumberFlow for"
},
{
"path": "packages/react/package.json",
"chars": 1535,
"preview": "{\n\t\"name\": \"@number-flow/react\",\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"version\": \"0.6.0\",\n\t\"author\": {\n\t\t\"name\""
},
{
"path": "packages/react/src/NumberFlow.tsx",
"chars": 7611,
"preview": "'use client'\n\n// This has to be in a separate file for #95.\n// Make sure tsup outputs both files.\n\nimport * as React fr"
},
{
"path": "packages/react/src/index.tsx",
"chars": 1124,
"preview": "import * as React from 'react'\nimport {\n\tprefersReducedMotion as _prefersReducedMotion,\n\tcanAnimate as _canAnimate\n} fro"
},
{
"path": "packages/react/test/apps/react-18/.gitignore",
"chars": 450,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "packages/react/test/apps/react-18/app/can-animate/page.tsx",
"chars": 427,
"preview": "'use client'\n\nimport * as React from 'react'\nimport { useCanAnimate } from '@number-flow/react'\n\nexport default function"
},
{
"path": "packages/react/test/apps/react-18/app/globals.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "packages/react/test/apps/react-18/app/group-1-unchanged/page.tsx",
"chars": 1112,
"preview": "'use client'\n\nimport * as React from 'react'\nimport NumberFlow, { NumberFlowElement, NumberFlowGroup } from '@number-flo"
},
{
"path": "packages/react/test/apps/react-18/app/hashes/page.tsx",
"chars": 114,
"preview": "import NumberFlow from '@number-flow/react'\n\nexport default function Page() {\n\treturn <NumberFlow value={42} />\n}\n"
},
{
"path": "packages/react/test/apps/react-18/app/layout.tsx",
"chars": 464,
"preview": "import type { Metadata } from 'next'\n// import { Inter } from 'next/font/google'\nimport './globals.css'\n\n// const inter "
},
{
"path": "packages/react/test/apps/react-18/app/nonce/page.tsx",
"chars": 200,
"preview": "import NumberFlow from '@number-flow/react'\n\nexport default function Page() {\n\treturn (\n\t\t<>\n\t\t\t<NumberFlow nonce=\"test-"
},
{
"path": "packages/react/test/apps/react-18/app/page.tsx",
"chars": 1953,
"preview": "'use client'\n\nimport * as React from 'react'\nimport NumberFlow, { NumberFlowElement, NumberFlowGroup, continuous } from "
},
{
"path": "packages/react/test/apps/react-18/next.config.mjs",
"chars": 853,
"preview": "import webpack from 'webpack'\nimport { createHash } from 'node:crypto'\nimport { styles } from '@number-flow/react'\n\ncons"
},
{
"path": "packages/react/test/apps/react-18/package.json",
"chars": 863,
"preview": "{\n\t\"name\": \"test\",\n\t\"version\": \"0.1.0\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"build\": \"next build\",\n\t\t\"start\": \"next start "
},
{
"path": "packages/react/test/apps/react-18/playwright.config.ts",
"chars": 66,
"preview": "export { config as default } from '../../../../../lib/playwright'\n"
},
{
"path": "packages/react/test/apps/react-18/postcss.config.mjs",
"chars": 127,
"preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n\tplugins: {\n\t\ttailwindcss: {}\n\t}\n}\n\nexport default "
},
{
"path": "packages/react/test/apps/react-18/tailwind.config.ts",
"chars": 406,
"preview": "import type { Config } from 'tailwindcss'\nimport reset from 'tw-reset'\n\nconst config: Config = {\n\tpresets: [reset],\n\tcon"
},
{
"path": "packages/react/test/apps/react-18/tsconfig.json",
"chars": 525,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n\t\t\"allowJs\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"strict"
},
{
"path": "packages/react/test/apps/react-19/.gitignore",
"chars": 521,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "packages/react/test/apps/react-19/app/can-animate/page.tsx",
"chars": 427,
"preview": "'use client'\n\nimport * as React from 'react'\nimport { useCanAnimate } from '@number-flow/react'\n\nexport default function"
},
{
"path": "packages/react/test/apps/react-19/app/globals.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "packages/react/test/apps/react-19/app/group-1-unchanged/page.tsx",
"chars": 1112,
"preview": "'use client'\n\nimport * as React from 'react'\nimport NumberFlow, { NumberFlowElement, NumberFlowGroup } from '@number-flo"
},
{
"path": "packages/react/test/apps/react-19/app/hashes/page.tsx",
"chars": 114,
"preview": "import NumberFlow from '@number-flow/react'\n\nexport default function Page() {\n\treturn <NumberFlow value={42} />\n}\n"
},
{
"path": "packages/react/test/apps/react-19/app/layout.tsx",
"chars": 464,
"preview": "import type { Metadata } from 'next'\n// import { Inter } from 'next/font/google'\nimport './globals.css'\n\n// const inter "
},
{
"path": "packages/react/test/apps/react-19/app/nonce/page.tsx",
"chars": 200,
"preview": "import NumberFlow from '@number-flow/react'\n\nexport default function Page() {\n\treturn (\n\t\t<>\n\t\t\t<NumberFlow nonce=\"test-"
},
{
"path": "packages/react/test/apps/react-19/app/page.tsx",
"chars": 1953,
"preview": "'use client'\n\nimport * as React from 'react'\nimport NumberFlow, { NumberFlowElement, NumberFlowGroup, continuous } from "
},
{
"path": "packages/react/test/apps/react-19/app/sc/page.tsx",
"chars": 113,
"preview": "import NumberFlow from '@number-flow/react'\n\nexport default function SC() {\n\treturn <NumberFlow value={123} />\n}\n"
},
{
"path": "packages/react/test/apps/react-19/next.config.ts",
"chars": 934,
"preview": "import type { NextConfig } from 'next'\nimport webpack from 'webpack'\nimport { createHash } from 'node:crypto'\nimport { s"
},
{
"path": "packages/react/test/apps/react-19/package.json",
"chars": 674,
"preview": "{\n\t\"name\": \"react-19\",\n\t\"version\": \"0.1.0\",\n\t\"private\": true,\n\t\"scripts\": {\n\t\t\"dev\": \"next dev --port 3039\",\n\t\t\"build\": "
},
{
"path": "packages/react/test/apps/react-19/playwright.config.ts",
"chars": 66,
"preview": "export { config as default } from '../../../../../lib/playwright'\n"
},
{
"path": "packages/react/test/apps/react-19/postcss.config.mjs",
"chars": 127,
"preview": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n\tplugins: {\n\t\ttailwindcss: {}\n\t}\n}\n\nexport default "
},
{
"path": "packages/react/test/apps/react-19/tailwind.config.ts",
"chars": 358,
"preview": "import type { Config } from 'tailwindcss'\n\nconst config: Config = {\n\tcontent: [\n\t\t'./pages/**/*.{js,ts,jsx,tsx,mdx}',\n\t\t"
},
{
"path": "packages/react/test/apps/react-19/tsconfig.json",
"chars": 547,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"ES2017\",\n\t\t\"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n\t\t\"allowJs\": true,\n\t\t\"skipLibC"
},
{
"path": "packages/react/tsconfig.build.json",
"chars": 65,
"preview": "{\n\t\"extends\": [\"./tsconfig.json\", \"../../tsconfig.build.json\"]\n}\n"
},
{
"path": "packages/react/tsconfig.json",
"chars": 99,
"preview": "{\n\t\"extends\": \"../../tsconfig.json\",\n\t\"include\": [\"src\"],\n\t\"compilerOptions\": { \"jsx\": \"react\" }\n}\n"
},
{
"path": "packages/svelte/.gitignore",
"chars": 290,
"preview": "test-results\nnode_modules\n\n# Output\n.output\n.vercel\n/.svelte-kit\n/build\n/dist\n\n# testing\n/test-results/\n/playwright-repo"
},
{
"path": "packages/svelte/.npmrc",
"chars": 19,
"preview": "engine-strict=true\n"
},
{
"path": "packages/svelte/CHANGELOG.md",
"chars": 8011,
"preview": "# @number-flow/svelte\n\n## 0.4.0\n\n### Minor Changes\n\n- Remove `--number-flow-char-height` CSS property in favor of `line-"
},
{
"path": "packages/svelte/README.md",
"chars": 676,
"preview": "[](https://number-flow.barvian.me/svelte)\n\n# Number"
},
{
"path": "packages/svelte/package.json",
"chars": 1880,
"preview": "{\n\t\"name\": \"@number-flow/svelte\",\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"version\": \"0.4.0\",\n\t\"description\": \"A c"
},
{
"path": "packages/svelte/playwright.config.ts",
"chars": 310,
"preview": "import { defineConfig } from '@playwright/test'\nimport { config } from '../../lib/playwright'\n\n// Use prod build cause i"
},
{
"path": "packages/svelte/postcss.config.js",
"chars": 72,
"preview": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {}\n\t}\n}\n"
},
{
"path": "packages/svelte/src/app.css",
"chars": 95,
"preview": "@import 'tailwindcss/base';\n@import 'tailwindcss/components';\n@import 'tailwindcss/utilities';\n"
},
{
"path": "packages/svelte/src/app.d.ts",
"chars": 273,
"preview": "// See https://svelte.dev/docs/kit/types#app.d.ts\n// for information about these interfaces\ndeclare global {\n\tnamespace "
},
{
"path": "packages/svelte/src/app.html",
"chars": 260,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width,"
},
{
"path": "packages/svelte/src/lib/NumberFlow.svelte",
"chars": 3604,
"preview": "<script lang=\"ts\" context=\"module\">\n\timport NumberFlowLite, { define, type Data } from 'number-flow/lite'\n\t// Svelte onl"
},
{
"path": "packages/svelte/src/lib/NumberFlowGroup.svelte",
"chars": 877,
"preview": "<script lang=\"ts\">\n\timport type NumberFlowLite from 'number-flow/lite'\n\timport { type Readable, get } from 'svelte/store"
},
{
"path": "packages/svelte/src/lib/group.ts",
"chars": 499,
"preview": "import type NumberFlowLite from 'number-flow/lite'\nimport { getContext, setContext } from 'svelte'\nimport type { Readabl"
},
{
"path": "packages/svelte/src/lib/index.ts",
"chars": 1261,
"preview": "import {\n\tcanAnimate as _canAnimate,\n\tprefersReducedMotion as _prefersReducedMotion\n} from 'number-flow/lite'\nimport { o"
},
{
"path": "packages/svelte/src/routes/+layout.svelte",
"chars": 65,
"preview": "<script lang=\"ts\">\n\timport '../app.css'\n</script>\n\n<slot></slot>\n"
},
{
"path": "packages/svelte/src/routes/+page.svelte",
"chars": 1683,
"preview": "<script lang=\"ts\">\n\timport NumberFlow, { NumberFlowGroup, NumberFlowElement, continuous } from '$lib/index.js'\n\timport {"
},
{
"path": "packages/svelte/src/routes/can-animate/+page.svelte",
"chars": 394,
"preview": "<script lang=\"ts\">\n\timport { getCanAnimate } from '$lib/index.js'\n\tconst canAnimate = getCanAnimate()\n\tconst disrespectM"
},
{
"path": "packages/svelte/src/routes/group-1-unchanged/+page.svelte",
"chars": 932,
"preview": "<script lang=\"ts\">\n\timport NumberFlow, { NumberFlowGroup, NumberFlowElement } from '$lib/index.js'\n\timport { afterUpdate"
},
{
"path": "packages/svelte/src/routes/hashes/+page.server.ts",
"chars": 387,
"preview": "import type { PageServerLoad } from './$types'\nimport { createHash } from 'node:crypto'\nimport { styles } from '$lib/ind"
},
{
"path": "packages/svelte/src/routes/hashes/+page.svelte",
"chars": 96,
"preview": "<script lang=\"ts\">\n\timport NumberFlow from '$lib/index.js'\n</script>\n\n<NumberFlow value={42} />\n"
},
{
"path": "packages/svelte/src/routes/nonce/+page.server.ts",
"chars": 197,
"preview": "import type { PageServerLoad } from './$types'\n\nexport const load: PageServerLoad = ({ setHeaders }) => {\n\tsetHeaders({\n"
},
{
"path": "packages/svelte/src/routes/nonce/+page.svelte",
"chars": 159,
"preview": "<script lang=\"ts\">\n\timport NumberFlow from '$lib/index.js'\n</script>\n\n<NumberFlow value={42} nonce=\"test-nonce\" /><Numbe"
},
{
"path": "packages/svelte/svelte.config.js",
"chars": 658,
"preview": "import adapter from '@sveltejs/adapter-auto'\nimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte'\n\n/** @type {i"
},
{
"path": "packages/svelte/tailwind.config.ts",
"chars": 160,
"preview": "import type { Config } from 'tailwindcss'\n\nexport default {\n\tcontent: ['./src/**/*.{html,js,svelte,ts}'],\n\n\ttheme: {\n\t\te"
},
{
"path": "packages/svelte/tsconfig.json",
"chars": 92,
"preview": "{\n\t\"extends\": [\"./.svelte-kit/tsconfig.json\", \"../../tsconfig.json\"],\n\t\"include\": [\"src\"]\n}\n"
},
{
"path": "packages/svelte/vite.config.ts",
"chars": 189,
"preview": "import { sveltekit } from '@sveltejs/kit/vite'\nimport { defineConfig } from 'vite'\n\nexport default defineConfig({\n\tplugi"
},
{
"path": "packages/vue/CHANGELOG.md",
"chars": 8324,
"preview": "# @number-flow/vue\n\n## 0.5.0\n\n### Minor Changes\n\n- Remove `--number-flow-char-height` CSS property in favor of `line-hei"
},
{
"path": "packages/vue/README.md",
"chars": 649,
"preview": "[](https://number-flow.barvian.me/vue)\n\n# NumberFlow f"
},
{
"path": "packages/vue/package.json",
"chars": 1455,
"preview": "{\n\t\"name\": \"@number-flow/vue\",\n\t\"type\": \"module\",\n\t\"publishConfig\": {\n\t\t\"access\": \"public\"\n\t},\n\t\"version\": \"0.5.0\",\n\t\"au"
},
{
"path": "packages/vue/src/NumberFlowGroup.vue",
"chars": 866,
"preview": "<script lang=\"ts\" setup>\nimport type NumberFlowLite from 'number-flow/lite'\nimport { key, type RegisterWithGroup } from "
},
{
"path": "packages/vue/src/group.ts",
"chars": 374,
"preview": "import type NumberFlowLite from 'number-flow/lite'\nimport type { formatToData } from 'number-flow/lite'\nimport type { In"
},
{
"path": "packages/vue/src/index.ts",
"chars": 1422,
"preview": "import { computed, onMounted, ref, toValue, watchEffect, type MaybeRefOrGetter } from 'vue'\nimport NumberFlowElement, {\n"
},
{
"path": "packages/vue/src/index.vue",
"chars": 2633,
"preview": "<script lang=\"ts\" setup>\nimport NumberFlowLite, {\n\ttype Value,\n\ttype Format,\n\trenderInnerHTML,\n\tformatToData,\n\ttype Prop"
},
{
"path": "packages/vue/test/apps/nuxt3/.gitignore",
"chars": 273,
"preview": "# Nuxt dev/build outputs\n.output\n.data\n.nuxt\n.nitro\n.cache\ndist\n\n# Node dependencies\nnode_modules\n\n# testing\n/test-resul"
},
{
"path": "packages/vue/test/apps/nuxt3/README.md",
"chars": 822,
"preview": "# Nuxt Minimal Starter\n\nLook at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn mo"
},
{
"path": "packages/vue/test/apps/nuxt3/nuxt.config.ts",
"chars": 1088,
"preview": "import tailwindcss from '@tailwindcss/vite'\nimport { createHash } from 'node:crypto'\nimport { styles } from '@number-flo"
},
{
"path": "packages/vue/test/apps/nuxt3/package.json",
"chars": 754,
"preview": "{\n\t\"name\": \"nuxt-app\",\n\t\"private\": true,\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"nuxt dev\",\n\t\t\"build\": \"nuxt build\",\n"
},
{
"path": "packages/vue/test/apps/nuxt3/playwright.config.ts",
"chars": 66,
"preview": "export { config as default } from '../../../../../lib/playwright'\n"
},
{
"path": "packages/vue/test/apps/nuxt3/src/assets/css/main.css",
"chars": 23,
"preview": "@import 'tailwindcss';\n"
},
{
"path": "packages/vue/test/apps/nuxt3/src/pages/can-animate.vue",
"chars": 392,
"preview": "<script lang=\"ts\" setup>\nimport { useCanAnimate } from '@number-flow/vue'\n\nconst canAnimate = useCanAnimate()\nconst disr"
},
{
"path": "packages/vue/test/apps/nuxt3/src/pages/group-1-unchanged.vue",
"chars": 963,
"preview": "<script setup lang=\"ts\">\nimport NumberFlow, { NumberFlowGroup } from '@number-flow/vue'\nimport { nextTick, ref, useTempl"
},
{
"path": "packages/vue/test/apps/nuxt3/src/pages/hashes.vue",
"chars": 128,
"preview": "<script setup lang=\"ts\">\nimport NumberFlow from '@number-flow/vue'\n</script>\n<template>\n\t<NumberFlow :value=\"42\" />\n</te"
},
{
"path": "packages/vue/test/apps/nuxt3/src/pages/index.vue",
"chars": 1813,
"preview": "<script setup lang=\"ts\">\nimport NumberFlow, { NumberFlowGroup, continuous } from '@number-flow/vue'\nimport { nextTick, r"
},
{
"path": "packages/vue/test/apps/nuxt3/src/pages/nonce.vue",
"chars": 194,
"preview": "<script setup lang=\"ts\">\nimport NumberFlow from '@number-flow/vue'\n</script>\n<template>\n\t<NumberFlow :value=\"42\" nonce=\""
},
{
"path": "packages/vue/test/apps/nuxt3/src/server/tsconfig.json",
"chars": 51,
"preview": "{\n\t\"extends\": \"../../.nuxt/tsconfig.server.json\"\n}\n"
},
{
"path": "packages/vue/test/apps/nuxt3/tsconfig.json",
"chars": 92,
"preview": "{\n\t// https://nuxt.com/docs/guide/concepts/typescript\n\t\"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
},
{
"path": "packages/vue/tsconfig.build.json",
"chars": 65,
"preview": "{\n\t\"extends\": [\"./tsconfig.json\", \"../../tsconfig.build.json\"]\n}\n"
},
{
"path": "packages/vue/tsconfig.json",
"chars": 129,
"preview": "{\n\t\"extends\": \"../../tsconfig.json\",\n\t\"include\": [\"src\"],\n\t\"compilerOptions\": {\n\t\t\"declaration\": true,\n\t\t\"outDir\": \"./di"
},
{
"path": "packages/vue/vite.config.mjs",
"chars": 1031,
"preview": "import { resolve } from 'path'\nimport { defineConfig } from 'vite'\nimport dts from 'vite-plugin-dts'\nimport vue from '@v"
},
{
"path": "pnpm-workspace.yaml",
"chars": 141,
"preview": "packages:\n - 'packages/*'\n - 'packages/number-flow/test/apps/*'\n - 'packages/react/test/apps/*'\n - 'packages/vue/tes"
},
{
"path": "prettier.config.js",
"chars": 402,
"preview": "/** @type {import('prettier').Config} */\nexport default {\n\tuseTabs: true,\n\tsingleQuote: true,\n\tsemi: false,\n\ttrailingCom"
},
{
"path": "site/.gitignore",
"chars": 272,
"preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
},
{
"path": "site/.vscode/extensions.json",
"chars": 85,
"preview": "{\n\t\"recommendations\": [\"astro-build.astro-vscode\"],\n\t\"unwantedRecommendations\": []\n}\n"
},
{
"path": "site/.vscode/launch.json",
"chars": 188,
"preview": "{\n\t\"version\": \"0.2.0\",\n\t\"configurations\": [\n\t\t{\n\t\t\t\"command\": \"./node_modules/.bin/astro dev\",\n\t\t\t\"name\": \"Development s"
},
{
"path": "site/astro.config.mjs",
"chars": 1674,
"preview": "import { defineConfig, envField } from 'astro/config'\nimport pkg from '/../packages/number-flow/package.json'\nimport mdx"
},
{
"path": "site/highlighter-theme.json",
"chars": 8169,
"preview": "{\n\t\"name\": \"Lambda Studio — Blackout\",\n\t\"semanticHighlighting\": true,\n\t\"colors\": {\n\t\t\"editorLink.activeForeground\": \"#ca"
},
{
"path": "site/package.json",
"chars": 1788,
"preview": "{\n\t\"name\": \"site\",\n\t\"private\": true,\n\t\"version\": \"0.0.1\",\n\t\"scripts\": {\n\t\t\"dev\": \"astro dev\",\n\t\t\"start\": \"astro dev\",\n\t\t"
},
{
"path": "site/postcss.config.cjs",
"chars": 131,
"preview": "module.exports = {\n\tplugins: [\n\t\trequire('tailwindcss'),\n\t\t// @ts-expect-error no types\n\t\trequire('postcss-easing-gradie"
},
{
"path": "site/src/assets/main.css",
"chars": 2793,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@font-face {\n\tfont-weight: 100 900;\n\tfont-style: normal;\n\tfo"
},
{
"path": "site/src/components/Alert.astro",
"chars": 215,
"preview": "<div\n\trole=\"alert\"\n\tclass:list={[\n\t\t'text-primary border-faint relative rounded-lg border px-4 py-3 text-sm' /*, 'border"
},
{
"path": "site/src/components/AnimateHeightFragment.tsx",
"chars": 1748,
"preview": "import { spring } from '@/lib/spring'\nimport { usePrefersReducedMotion } from '@number-flow/react'\nimport { cloneElement"
},
{
"path": "site/src/components/AnimationsOnTheWeb.astro",
"chars": 974,
"preview": "---\nimport { Image } from 'astro:assets'\nimport headshot from '@/assets/images/headshot.jpeg'\nimport { ArrowUpRight } fr"
},
{
"path": "site/src/components/Code.astro",
"chars": 666,
"preview": "---\nimport styles from './code.module.css'\nimport { Code as AstroCode } from 'astro:components'\nimport theme from '/high"
},
{
"path": "site/src/components/Comp.mdx",
"chars": 115,
"preview": "import Match from './Match.astro'\n\n<Match><Fragment slot=\"vanilla\">`<number-flow>`</Fragment>`<NumberFlow>`</Match>"
},
{
"path": "site/src/components/Demo.tsx",
"chars": 8006,
"preview": "import * as React from 'react'\n// import { atom, useAtom } from 'jotai'\nimport { clsx } from 'clsx/lite'\nimport { inView"
},
{
"path": "site/src/components/FrameworkMenu.tsx",
"chars": 2479,
"preview": "import { MenuTrigger, Button, Popover, Menu, MenuItem } from 'react-aria-components'\nimport { FRAMEWORKS, toFrameworkPat"
},
{
"path": "site/src/components/Freeze.tsx",
"chars": 1757,
"preview": "import * as React from 'react'\n\ntype FreezeProps = {\n\tfrozen: boolean\n\tchildren: React.ReactNode\n}\n\ntype FragmentObserve"
},
{
"path": "site/src/components/GroupComp.mdx",
"chars": 126,
"preview": "import Match from './Match.astro'\n\n<Match><Fragment slot=\"vanilla\">`<number-flow-group>`</Fragment>`<NumberFlowGroup>`</"
},
{
"path": "site/src/components/Heading.astro",
"chars": 412,
"preview": "---\nimport { getTOCContext } from '@/context/toc.ts'\nimport type { HTMLAttributes } from 'astro/types'\nimport { slug } f"
},
{
"path": "site/src/components/Link.astro",
"chars": 137,
"preview": "---\nimport ReactLink from './Link'\nexport type { Props } from './Link'\n---\n\n<ReactLink {...Astro.props} client:load><slo"
},
{
"path": "site/src/components/Link.tsx",
"chars": 2194,
"preview": "import * as React from 'react'\nimport { useStore } from '@nanostores/react'\nimport { $url, $pageFramework } from '@/stor"
},
{
"path": "site/src/components/LogoWall/LogoWall.astro",
"chars": 585,
"preview": "---\nimport Wall from './Wall.astro'\nimport type { HTMLAttributes } from 'astro/types'\n\ntype Props = HTMLAttributes<'div'"
},
{
"path": "site/src/components/LogoWall/Wall.astro",
"chars": 2212,
"preview": "---\nimport Intercom from '@/assets/images/logos/intercom.svg'\nimport Polymarket from '@/assets/images/logos/polymarket.s"
},
{
"path": "site/src/components/Match.astro",
"chars": 1553,
"preview": "---\nimport { getFramework, type Framework } from '@/lib/framework'\nimport type { HTMLTag, Polymorphic } from 'astro/type"
},
{
"path": "site/src/components/Meta.astro",
"chars": 88,
"preview": "<span class=\"caption text-muted mt-[.375rem] block text-xs font-normal\"><slot /></span>\n"
},
{
"path": "site/src/components/Nav.astro",
"chars": 1123,
"preview": "---\nimport { GITHUB_TOKEN } from 'astro:env/server'\nimport ReactNav from './Nav'\nimport pkg from '/../packages/number-fl"
},
{
"path": "site/src/components/Nav.tsx",
"chars": 7039,
"preview": "import Link from '@/components/Link'\nimport { BookOpen, Shapes, GalleryVerticalEnd } from 'lucide-react'\nimport { Animat"
},
{
"path": "site/src/components/Note.astro",
"chars": 587,
"preview": "---\nimport { Info } from 'lucide-react'\nimport type { HTMLAttributes } from 'astro/types'\n\nexport type Props = HTMLAttri"
},
{
"path": "site/src/components/Pre.astro",
"chars": 160,
"preview": "---\nimport styles from './code.module.css'\nconst { class: _, style: __, ...props } = Astro.props\n---\n\n<pre {...props} cl"
},
{
"path": "site/src/components/Snapshotter.tsx",
"chars": 795,
"preview": "import React from 'react'\n\nexport type SnapshotterProps = {\n\tdependencies?: React.DependencyList\n\tonSnapshot: () => void"
},
{
"path": "site/src/components/Supported.tsx",
"chars": 1252,
"preview": "import { useIsSupported, usePrefersReducedMotion } from '@number-flow/react'\nimport clsx from 'clsx/lite'\nimport Link fr"
},
{
"path": "site/src/components/TOC.astro",
"chars": 226,
"preview": "---\nimport TOCReact from './TOC'\nimport { getTOCContext } from '@/context/toc.ts'\n\nexport type { Props } from './TOC'\n\nc"
},
{
"path": "site/src/components/TOC.tsx",
"chars": 1943,
"preview": "import type { Heading } from '@/context/toc'\nimport * as React from 'react'\n\nexport type Props = React.ComponentPropsWit"
},
{
"path": "site/src/components/Tweet/Tweet.astro",
"chars": 217,
"preview": "---\nimport TweetContent from './TweetContent.astro'\n\ninterface Props {\n\tid: string\n\tautoplay?: boolean\n\tfetchOptions?: R"
},
{
"path": "site/src/components/Tweet/TweetContent.astro",
"chars": 373,
"preview": "---\nimport { getTweet } from './api'\nimport EmbeddedTweet from './twitter-theme/EmbeddedTweet.astro'\ninterface Props {\n\t"
},
{
"path": "site/src/components/Tweet/api/get-oembed.ts",
"chars": 247,
"preview": "export async function getOEmbed(url: string): Promise<any> {\n\tconst res = await fetch(`https://publish.twitter.com/oembe"
},
{
"path": "site/src/components/Tweet/api/get-tweet.ts",
"chars": 2046,
"preview": "import type { Tweet } from './types/index.js'\n\nconst SYNDICATION_URL = 'https://cdn.syndication.twimg.com'\n\nexport class"
},
{
"path": "site/src/components/Tweet/api/index.ts",
"chars": 96,
"preview": "export * from './types/index.js'\nexport * from './get-tweet.js'\nexport * from './get-oembed.js'\n"
},
{
"path": "site/src/components/Tweet/api/types/edit.ts",
"chars": 146,
"preview": "export interface TweetEditControl {\n\tedit_tweet_ids: string[]\n\teditable_until_msecs: string\n\tis_edit_eligible: boolean\n\t"
},
{
"path": "site/src/components/Tweet/api/types/entities.ts",
"chars": 663,
"preview": "export type Indices = [number, number]\n\nexport interface HashtagEntity {\n\tindices: Indices\n\ttext: string\n}\n\nexport inter"
},
{
"path": "site/src/components/Tweet/api/types/index.ts",
"chars": 190,
"preview": "export * from './edit.js'\nexport * from './entities.js'\nexport * from './media.js'\nexport * from './photo.js'\nexport * f"
},
{
"path": "site/src/components/Tweet/api/types/media.ts",
"chars": 1153,
"preview": "import type { Indices } from './entities.js'\n\nexport type RGB = {\n\tred: number\n\tgreen: number\n\tblue: number\n}\n\nexport ty"
},
{
"path": "site/src/components/Tweet/api/types/photo.ts",
"chars": 188,
"preview": "import type { Rect, RGB } from './media.js'\n\nexport interface TweetPhoto {\n\tbackgroundColor: RGB\n\tcropCandidates: Rect[]"
},
{
"path": "site/src/components/Tweet/api/types/tweet.ts",
"chars": 1988,
"preview": "import type { TweetEditControl } from './edit.js'\nimport type { Indices, TweetEntities } from './entities.js'\nimport typ"
},
{
"path": "site/src/components/Tweet/api/types/user.ts",
"chars": 246,
"preview": "export interface TweetUser {\n\tid_str: string\n\tname: string\n\tprofile_image_url_https: string\n\tprofile_image_shape: 'Circl"
},
{
"path": "site/src/components/Tweet/api/types/video.ts",
"chars": 271,
"preview": "export interface TweetVideo {\n\taspectRatio: [number, number]\n\tcontentType: string\n\tdurationMs: number\n\tmediaAvailability"
},
{
"path": "site/src/components/Tweet/twitter-theme/AvatarImg.astro",
"chars": 111,
"preview": "---\ninterface Props {\n\tsrc: string\n\talt: string\n\twidth: number\n\theight: number\n}\n---\n\n<img {...Astro.props} />\n"
},
{
"path": "site/src/components/Tweet/twitter-theme/EmbeddedTweet.astro",
"chars": 1102,
"preview": "---\nimport type { Tweet } from '../api/index.js'\nimport TweetContainer from './TweetContainer.astro'\nimport TweetHeader "
},
{
"path": "site/src/components/Tweet/twitter-theme/MediaImg.astro",
"chars": 117,
"preview": "---\ninterface Props {\n\tsrc: string\n\talt: string\n\tclass?: string\n\tdraggable?: boolean\n}\n---\n\n<img {...Astro.props} />\n"
},
{
"path": "site/src/components/Tweet/twitter-theme/Skeleton.astro",
"chars": 116,
"preview": "---\nimport styles from './skeleton.module.css'\n---\n\n<span class={styles.skeleton} style={Astro.props.style}></span>\n"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetActions.astro",
"chars": 1670,
"preview": "---\nimport { type EnrichedTweet, formatNumber } from '../utils.js'\n// import { TweetActionsCopy } from './tweet-actions-"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetBody.astro",
"chars": 849,
"preview": "---\nimport type { EnrichedTweet } from '../utils.js'\nimport TweetLink from './TweetLink.astro'\nimport styles from './twe"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetContainer.astro",
"chars": 308,
"preview": "---\nimport clsx from 'clsx'\nimport styles from './tweet-container.module.css'\nimport './theme.css'\n\ninterface Props {\n\tc"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetHeader.astro",
"chars": 1970,
"preview": "---\nimport clsx from 'clsx'\nimport type { EnrichedTweet } from '../utils.js'\nimport AvatarImg from './AvatarImg.astro'\ni"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetInReplyTo.astro",
"chars": 318,
"preview": "---\nimport type { EnrichedTweet } from '../utils.js'\nimport s from './tweet-in-reply-to.module.css'\ninterface Props {\n\tt"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetInfo.astro",
"chars": 948,
"preview": "---\nimport type { EnrichedTweet } from '../utils.js'\nimport TweetInfoCreatedAt from './TweetInfoCreatedAt.astro'\nimport "
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetInfoCreatedAt.astro",
"chars": 522,
"preview": "---\nimport format from 'date-fns/format/index.js'\nimport type { EnrichedTweet } from '../utils.js'\nimport styles from '."
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetLink.astro",
"chars": 180,
"preview": "---\nimport s from './tweet-link.module.css'\ninterface Props {\n\thref: string\n}\n---\n\n<a href={Astro.props.href} class={s.r"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetMedia.astro",
"chars": 2047,
"preview": "---\nimport clsx from 'clsx'\nimport { type EnrichedTweet, type EnrichedQuotedTweet, getMediaUrl } from '../utils.js'\nimpo"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetMediaVideo.astro",
"chars": 647,
"preview": "---\nimport type { MediaAnimatedGif, MediaVideo } from '../api/index.js'\nimport { type EnrichedQuotedTweet, type Enriched"
},
{
"path": "site/src/components/Tweet/twitter-theme/TweetMediaVideo.tsx",
"chars": 933,
"preview": "import * as React from 'react'\n\nconst all = new Map<string, HTMLVideoElement>()\n\nexport default function TweetMediaVideo"
}
]
// ... and 140 more files (download for full content)
About this extraction
This page contains the full source code of the barvian/number-flow GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 340 files (376.9 KB), approximately 129.4k tokens, and a symbol index with 274 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.