chore: clean up repo for open source release
Browse files- Replace CRA boilerplate README with proper project documentation
- Add MIT LICENSE
- Clean package.json (name, description, author, license, repository)
- Remove CRA boilerplate files (logo.svg, App.test.js, setupTests.js, reportWebVitals.js)
- Remove dev artifacts (font-names-results/, scripts/, yarn.lock)
- Clean up index.html title and meta description
- Simplify .gitignore
Co-authored-by: Cursor <cursoragent@cursor.com>
- .gitignore +11 -14
- LICENSE +21 -0
- README.md +129 -0
- package.json +9 -3
- public/index.html +2 -29
- src/App.test.js +0 -8
- src/index.js +0 -6
- src/logo.svg +0 -1
- src/reportWebVitals.js +0 -13
- src/setupTests.js +0 -5
.gitignore
CHANGED
|
@@ -1,34 +1,31 @@
|
|
| 1 |
-
#
|
| 2 |
/node_modules
|
| 3 |
src/typography/new-pipe/node_modules
|
| 4 |
-
src/typography/generated/
|
| 5 |
-
/.pnp
|
| 6 |
-
.pnp.js
|
| 7 |
-
|
| 8 |
-
# testing
|
| 9 |
-
/coverage
|
| 10 |
|
| 11 |
-
#
|
| 12 |
/build
|
|
|
|
| 13 |
|
| 14 |
-
#
|
| 15 |
-
.DS_Store
|
| 16 |
.env.local
|
| 17 |
.env.development.local
|
| 18 |
.env.test.local
|
| 19 |
.env.production.local
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
npm-debug.log*
|
| 22 |
yarn-debug.log*
|
| 23 |
yarn-error.log*
|
| 24 |
|
|
|
|
| 25 |
src/typography/new-pipe/input
|
| 26 |
src/typography/new-pipe/output
|
| 27 |
src/typography/new-pipe/public
|
|
|
|
| 28 |
|
| 29 |
-
# Large generated data (
|
| 30 |
public/data/embeddings.json
|
| 31 |
public/data/font-map.json
|
| 32 |
-
|
| 33 |
-
# Temp docs
|
| 34 |
-
TEST_LIVE_UMAP.md
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
/node_modules
|
| 3 |
src/typography/new-pipe/node_modules
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
# Build output
|
| 6 |
/build
|
| 7 |
+
/coverage
|
| 8 |
|
| 9 |
+
# Environment
|
|
|
|
| 10 |
.env.local
|
| 11 |
.env.development.local
|
| 12 |
.env.test.local
|
| 13 |
.env.production.local
|
| 14 |
|
| 15 |
+
# OS
|
| 16 |
+
.DS_Store
|
| 17 |
+
|
| 18 |
+
# Logs
|
| 19 |
npm-debug.log*
|
| 20 |
yarn-debug.log*
|
| 21 |
yarn-error.log*
|
| 22 |
|
| 23 |
+
# Pipeline working directories
|
| 24 |
src/typography/new-pipe/input
|
| 25 |
src/typography/new-pipe/output
|
| 26 |
src/typography/new-pipe/public
|
| 27 |
+
src/typography/generated/
|
| 28 |
|
| 29 |
+
# Large generated data (regenerate with pipeline)
|
| 30 |
public/data/embeddings.json
|
| 31 |
public/data/font-map.json
|
|
|
|
|
|
|
|
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Thibaud Frere
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -9,3 +9,132 @@ pinned: false
|
|
| 9 |
app_build_command: "CI=false npm run build"
|
| 10 |
app_file: "build/index.html"
|
| 11 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
app_build_command: "CI=false npm run build"
|
| 10 |
app_file: "build/index.html"
|
| 11 |
---
|
| 12 |
+
|
| 13 |
+
# FontMap
|
| 14 |
+
|
| 15 |
+
An interactive map of 1,100+ Google Fonts organized by visual similarity, using [FontCLIP](https://github.com/kinit-sk/FontCLIP) embeddings and UMAP dimensionality reduction.
|
| 16 |
+
|
| 17 |
+
Inspired by [IDEO's Font Map](https://medium.com/ideo-stories/organizing-the-world-of-fonts-with-ai-7d9e49ff2b25) (2017, Kevin Ho) — rebuilt from scratch with modern ML, fully open source, and documented.
|
| 18 |
+
|
| 19 |
+
**[Live demo](https://huggingface.co/spaces/tfrere/fontmap)**
|
| 20 |
+
|
| 21 |
+

|
| 22 |
+
|
| 23 |
+
## Features
|
| 24 |
+
|
| 25 |
+
- **Visual similarity map** — fonts that look alike are physically close together
|
| 26 |
+
- **1,192 Google Fonts** — full catalog, family variants merged
|
| 27 |
+
- **Click any font** — see details + 5 nearest visual neighbors
|
| 28 |
+
- **Filter by category** — serif, sans-serif, display, handwriting, monospace
|
| 29 |
+
- **Category colors** — toggle color coding to see how clusters map to official categories
|
| 30 |
+
- **Search** — find any font by name
|
| 31 |
+
- **Zoom & pan** — explore the full map with smooth D3.js navigation
|
| 32 |
+
|
| 33 |
+
## How it works
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
Font images ──→ FontCLIP (512D) ──→ PCA (50D) ──→ UMAP (2D) ──→ Interactive map
|
| 37 |
+
↓
|
| 38 |
+
k-NN (similar fonts)
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
1. **Render** — each font is rendered as a 224×224 composite glyph image
|
| 42 |
+
2. **Embed** — [FontCLIP](https://github.com/kinit-sk/FontCLIP) (CLIP ViT-B/32 fine-tuned for typography) encodes each image into a 512-dimensional vector
|
| 43 |
+
3. **Reduce** — PCA compresses to 50D, then UMAP with spectral initialization projects to 2D (n_neighbors=12, min_dist=1.0)
|
| 44 |
+
4. **Merge** — font family variants (Regular, Bold, Italic…) are fused into a single representative point
|
| 45 |
+
5. **Neighbors** — k-NN is computed in the original 512D space for "similar fonts" recommendations
|
| 46 |
+
6. **Visualize** — React + D3.js renders all glyphs from a single SVG sprite (zero individual network requests)
|
| 47 |
+
|
| 48 |
+
## Getting started
|
| 49 |
+
|
| 50 |
+
### Run locally
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
git clone https://github.com/tfrere/fontmap.git
|
| 54 |
+
cd fontmap
|
| 55 |
+
npm install
|
| 56 |
+
npm start
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Open [http://localhost:3000](http://localhost:3000).
|
| 60 |
+
|
| 61 |
+
### Build for production
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
npm run build
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Deploy to Hugging Face Spaces
|
| 68 |
+
|
| 69 |
+
The project is configured for [HF Spaces static SDK](https://huggingface.co/docs/hub/spaces-sdks-static) with a build step:
|
| 70 |
+
|
| 71 |
+
```yaml
|
| 72 |
+
sdk: static
|
| 73 |
+
app_build_command: "CI=false npm run build"
|
| 74 |
+
app_file: "build/index.html"
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Regenerating the map
|
| 78 |
+
|
| 79 |
+
The embedding pipeline lives in `src/typography/new-pipe/`. See its [README](src/typography/new-pipe/README.md) for full instructions.
|
| 80 |
+
|
| 81 |
+
**Quick summary:**
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
# 1. Generate FontCLIP embeddings (requires Python + GPU, ~1h one-time)
|
| 85 |
+
cd src/typography/new-pipe/python-pipeline
|
| 86 |
+
python run_fontclip.py
|
| 87 |
+
|
| 88 |
+
# 2. Test different UMAP configurations
|
| 89 |
+
cd ..
|
| 90 |
+
npm run batch-umap
|
| 91 |
+
npm run copy-to-debug
|
| 92 |
+
# Compare visually at http://localhost:3000/#/debug-umap
|
| 93 |
+
|
| 94 |
+
# 3. Deploy chosen config to production
|
| 95 |
+
npm run deploy fontclip-spectral
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## Project structure
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
fontmap/
|
| 102 |
+
├── public/
|
| 103 |
+
│ ├── data/
|
| 104 |
+
│ │ ├── char/ # Individual glyph SVGs (~1,200 files)
|
| 105 |
+
│ │ ├── sentences/ # Sentence preview SVGs
|
| 106 |
+
│ │ ├── font-sprite.svg # All glyphs in a single sprite (3 MB)
|
| 107 |
+
│ │ └── typography_data.json # Font positions + metadata
|
| 108 |
+
│ └── debug-umap/ # Pre-computed UMAP configs for comparison
|
| 109 |
+
├── src/
|
| 110 |
+
│ ├── components/
|
| 111 |
+
│ │ ├── FontMap/ # Main map component (production)
|
| 112 |
+
│ │ ├── DebugUMAP/ # UMAP comparison tool (development)
|
| 113 |
+
│ │ └── FontMapV2/ # Experimental canvas renderer
|
| 114 |
+
│ ├── hooks/ # Shared hooks (data loading, dark mode)
|
| 115 |
+
│ ├── store/ # Zustand state management
|
| 116 |
+
│ └── typography/new-pipe/ # Embedding + UMAP generation pipeline
|
| 117 |
+
│ └── python-pipeline/ # FontCLIP + PCA + UMAP (Python)
|
| 118 |
+
└── package.json
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Tech stack
|
| 122 |
+
|
| 123 |
+
| Layer | Tech |
|
| 124 |
+
|-------|------|
|
| 125 |
+
| Frontend | React 19, D3.js v7, Zustand |
|
| 126 |
+
| Rendering | SVG sprite + D3 zoom/pan |
|
| 127 |
+
| Embeddings | FontCLIP (CLIP ViT-B/32 fine-tuned for typography) |
|
| 128 |
+
| Dim. reduction | PCA + UMAP (spectral init) via `umap-learn` |
|
| 129 |
+
| Hosting | Hugging Face Spaces (static SDK) |
|
| 130 |
+
|
| 131 |
+
## Credits
|
| 132 |
+
|
| 133 |
+
- **[FontCLIP](https://github.com/kinit-sk/FontCLIP)** — Typography-aware CLIP model by KInIT
|
| 134 |
+
- **[IDEO Font Map](https://medium.com/ideo-stories/organizing-the-world-of-fonts-with-ai-7d9e49ff2b25)** — Original inspiration by Kevin Ho (2017)
|
| 135 |
+
- **[Google Fonts](https://fonts.google.com)** — Font catalog
|
| 136 |
+
- **[UMAP](https://umap-learn.readthedocs.io/)** — Dimensionality reduction by Leland McInnes
|
| 137 |
+
|
| 138 |
+
## License
|
| 139 |
+
|
| 140 |
+
MIT
|
package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
-
"version": "
|
| 4 |
-
"
|
|
|
|
|
|
|
| 5 |
"homepage": ".",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"dependencies": {
|
| 7 |
"@testing-library/dom": "^10.4.0",
|
| 8 |
"@testing-library/jest-dom": "^6.6.3",
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "fontmap",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Interactive map of 1,100+ Google Fonts organized by visual similarity using FontCLIP + UMAP",
|
| 5 |
+
"author": "Thibaud Frere",
|
| 6 |
+
"license": "MIT",
|
| 7 |
"homepage": ".",
|
| 8 |
+
"repository": {
|
| 9 |
+
"type": "git",
|
| 10 |
+
"url": "https://github.com/tfrere/fontmap"
|
| 11 |
+
},
|
| 12 |
"dependencies": {
|
| 13 |
"@testing-library/dom": "^10.4.0",
|
| 14 |
"@testing-library/jest-dom": "^6.6.3",
|
public/index.html
CHANGED
|
@@ -5,43 +5,16 @@
|
|
| 5 |
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
-
<meta
|
| 9 |
-
name="description"
|
| 10 |
-
content="Web site created using create-react-app"
|
| 11 |
-
/>
|
| 12 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
-
<!--
|
| 14 |
-
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
-
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
-
-->
|
| 17 |
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
| 18 |
-
<!-- Google Fonts -->
|
| 19 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 20 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 21 |
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet">
|
| 22 |
-
<
|
| 23 |
-
Notice the use of %PUBLIC_URL% in the tags above.
|
| 24 |
-
It will be replaced with the URL of the `public` folder during the build.
|
| 25 |
-
Only files inside the `public` folder can be referenced from the HTML.
|
| 26 |
-
|
| 27 |
-
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
| 28 |
-
work correctly both with client-side routing and a non-root public URL.
|
| 29 |
-
Learn how to configure a non-root public URL by running `npm run build`.
|
| 30 |
-
-->
|
| 31 |
-
<title>React App</title>
|
| 32 |
</head>
|
| 33 |
<body>
|
| 34 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 35 |
<div id="root"></div>
|
| 36 |
-
<!--
|
| 37 |
-
This HTML file is a template.
|
| 38 |
-
If you open it directly in the browser, you will see an empty page.
|
| 39 |
-
|
| 40 |
-
You can add webfonts, meta tags, or analytics to this file.
|
| 41 |
-
The build step will place the bundled scripts into the <body> tag.
|
| 42 |
-
|
| 43 |
-
To begin the development, run `npm start` or `yarn start`.
|
| 44 |
-
To create a production bundle, use `npm run build` or `yarn build`.
|
| 45 |
-
-->
|
| 46 |
</body>
|
| 47 |
</html>
|
|
|
|
| 5 |
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
+
<meta name="description" content="Interactive map of 1,100+ Google Fonts organized by visual similarity using FontCLIP + UMAP" />
|
|
|
|
|
|
|
|
|
|
| 9 |
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
|
|
| 11 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 12 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 13 |
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" rel="stylesheet">
|
| 14 |
+
<title>FontMap</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</head>
|
| 16 |
<body>
|
| 17 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 18 |
<div id="root"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</body>
|
| 20 |
</html>
|
src/App.test.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
import { render, screen } from '@testing-library/react';
|
| 2 |
-
import App from './App';
|
| 3 |
-
|
| 4 |
-
test('renders learn react link', () => {
|
| 5 |
-
render(<App />);
|
| 6 |
-
const linkElement = screen.getByText(/learn react/i);
|
| 7 |
-
expect(linkElement).toBeInTheDocument();
|
| 8 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import React from 'react';
|
|
| 2 |
import ReactDOM from 'react-dom/client';
|
| 3 |
import './index.css';
|
| 4 |
import App from './App';
|
| 5 |
-
import reportWebVitals from './reportWebVitals';
|
| 6 |
|
| 7 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 8 |
root.render(
|
|
@@ -10,8 +9,3 @@ root.render(
|
|
| 10 |
<App />
|
| 11 |
</React.StrictMode>
|
| 12 |
);
|
| 13 |
-
|
| 14 |
-
// If you want to start measuring performance in your app, pass a function
|
| 15 |
-
// to log results (for example: reportWebVitals(console.log))
|
| 16 |
-
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
| 17 |
-
reportWebVitals();
|
|
|
|
| 2 |
import ReactDOM from 'react-dom/client';
|
| 3 |
import './index.css';
|
| 4 |
import App from './App';
|
|
|
|
| 5 |
|
| 6 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 7 |
root.render(
|
|
|
|
| 9 |
<App />
|
| 10 |
</React.StrictMode>
|
| 11 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/logo.svg
DELETED
src/reportWebVitals.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
const reportWebVitals = onPerfEntry => {
|
| 2 |
-
if (onPerfEntry && onPerfEntry instanceof Function) {
|
| 3 |
-
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
| 4 |
-
getCLS(onPerfEntry);
|
| 5 |
-
getFID(onPerfEntry);
|
| 6 |
-
getFCP(onPerfEntry);
|
| 7 |
-
getLCP(onPerfEntry);
|
| 8 |
-
getTTFB(onPerfEntry);
|
| 9 |
-
});
|
| 10 |
-
}
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export default reportWebVitals;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/setupTests.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
| 2 |
-
// allows you to do things like:
|
| 3 |
-
// expect(element).toHaveTextContent(/react/i)
|
| 4 |
-
// learn more: https://github.com/testing-library/jest-dom
|
| 5 |
-
import '@testing-library/jest-dom';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|