Files
twenty/packages/twenty-ui
Félix Malfait 8774bf8604 Self-host every font instead of loading them from Google (#23859)
Google Fonts logs the IP and user agent of everyone who loads a font
from it. Any page of ours that links to `fonts.googleapis.com` hands our
users (and every self-hoster's users) to a third party for nothing in
return, since we can serve the same bytes ourselves.

After this PR there is no reference to `fonts.googleapis.com`,
`fonts.gstatic.com` or `next/font/google` left in the repo.

## What changed

**twenty-front, PDF export.** `exportBlockNoteEditorToPdf` registered
Inter by URL against `fonts.gstatic.com`, so exporting a note made the
browser fetch three TTFs from Google. The registration turned out to be
unnecessary altogether: `@blocknote/xl-pdf-exporter` already registers
an `Inter` family for its PDF schema, shipped inlined in the package as
a base64 TTF with the same 2849-codepoint coverage. Deleting our
`Font.register` means no font request leaves the browser, with 41 fewer
lines and nothing vendored.

Only weights 400 and 700 were ever used, and 700 already resolved to
blocknote's `Inter18pt-Bold` before this branch, so the custom 500/600
registrations were dead. The only rendering change is body text going
from `Inter` to `Inter18pt`, the same typeface at its 18pt optical size.

**twenty-sdk, OAuth callback page.** The local "you can close this tab"
page linked to Google Fonts, which meant running `twenty auth` phoned
Google from the developer's browser. Replaced with a system font stack;
a transient callback page did not justify a webfont round trip in the
first place.

**twenty-ui, Storybook.** `preview-head.html` loaded Inter from Google.
It now imports `@fontsource/inter` in `preview.tsx`, matching what
twenty-front's Storybook already does.

**twenty-website.** Host Grotesk, Aleo, Azeret Mono and VT323 came
through `next/font/google`. Next self-hosts those at runtime, so this
was not a visitor-facing leak, but the build still had to reach Google,
which makes builds non-hermetic and fails in an air-gapped environment.
The latin subsets are now vendored in `src/fonts/`, next to the Inter
files that were already there, and loaded with `next/font/local`. All
four are OFL 1.1; `src/fonts/README.md` records each file's upstream and
license. Total added weight is ~78 KB, and these are the exact files
Next was downloading at build time anyway.

Host Grotesk and Azeret Mono ship as single variable files, so they are
declared once over their full `wght` axis rather than as one face per
weight.

## Also removed

Both Storybooks pulled `iframeResizer.contentWindow.min.js` from
`cdnjs.cloudflare.com`. Storybook has not needed it since v7 and nothing
in either package references `iframeResizer` or `parentIFrame`, so it
was a third-party script executing in the preview iframe for no reason.
Argos does not screenshot through the manager iframe either:
`@argos-ci/storybook` hooks Vitest browser mode and calls
`server.commands.argosScreenshot`, so Playwright drives the page
directly.

## Verification

Not just typecheck. The interesting parts were tested end to end, which
caught two bugs an earlier revision of this PR had introduced.

**PDF export** — production Vite build, served over HTTP, real Chromium,
exporting through the actual `exportBlockNoteEditorToPdf`, then
extracting the PDF's text back out:

```
Latin heading  Cyrillic: Привет мир  Greek: Ελληνικά κείμενο
Latin-ext: Zażółć gęślą jaźń, Český  Vietnamese: Tiếng Việt

PASS Latin / Cyrillic / Greek / Polish / Czech / Vietnamese
```

Embedded fonts are `Inter18pt-Regular` / `Inter18pt-Bold`, no Helvetica
fallback, zero requests off-origin.

**Website** — built it, audited the build output (12 `@font-face` rules,
all `/_next/static/media/`, weights `300 800` / `100 900` / `300` /
`400` / `400,500,600`, `display: swap` preserved), then loaded it in
Chromium: 136 requests, zero to Google. The deployed preview was checked
too: no Google references in the served HTML or across all 21 CSS
chunks, every font file returns `200 font/woff2` and parses to the
expected family, and the asset hashes match a local build byte for byte.

**Two bugs this caught**, both in earlier commits on this branch, both
now fixed:

1. Registering `@fontsource/inter`'s latin file dropped coverage from
2849 codepoints to 230, silently removing Cyrillic, Greek, Vietnamese
and extended-Latin from every export. fontsource splits Inter into seven
per-script files chosen by `unicode-range`, but `Font.register` binds
one file per weight with no equivalent.
2. Any woff2 aborts the export outright with `RangeError: Offset is
outside the bounds of the DataView`. fontkit parses woff2, but
`@react-pdf`'s subsetter chokes on the transformed `glyf` table.
Confirmed format was the only variable by running identical content
through local TTF, WOFF and WOFF2 files.

Both are moot now that the registration is gone, but they are why this
is worth a careful look rather than a rubber stamp.

## Left alone, but worth knowing about

More third-party calls exist. None are font-related and each is a
separate decision:

- `twenty-website` loads `dotlottie-player.wasm` from **unpkg.com** at
runtime on the homepage, via `@lottiefiles/dotlottie-react`. This is a
live third-party CDN request on every visit, the same class of problem
as the fonts, and looks like a small config change to self-host.
- The halftone studio loads the Draco decoder from `www.gstatic.com`
and, in exported scenes, three.js from `unpkg.com`.
- The partners marketplace fixtures hotlink logos from
`cdn.simpleicons.org` and `upload.wikimedia.org`.
- reCAPTCHA and the Front support chat are config-gated and off unless
an admin configures them, which seems right.
- `APP_REGISTRY_CDN_URL` defaults to `https://unpkg.com`.
- `twenty-front/index.html` points its `og:image` at
`raw.githubusercontent.com`. Only social crawlers fetch it, so this is
cosmetic.
2026-08-06 17:58:55 +02:00
..
2026-07-04 07:07:29 +02:00
2026-06-24 13:19:03 +00:00
2026-06-11 11:02:28 +02:00

twenty-ui logo

twenty-ui

Twenty's open-source React UI component library: components, icons, and design tokens built on a zero-runtime, CSS-variable styling layer.

Installation

npm install twenty-ui

react, react-dom, and monaco-editor are peer dependencies (install them in your app). monaco-editor is only required if you use the code editor components.

Usage

Import the base styles once, pick a theme stylesheet, and wrap your app in ThemeProvider:

import { ThemeProvider } from 'twenty-ui/theme-constants';
import { Button } from 'twenty-ui/input';

import 'twenty-ui/style.css';
import 'twenty-ui/theme-light.css';

export const App = () => (
  <ThemeProvider colorScheme="light">
    <Button title="Click me" />
  </ThemeProvider>
);

Components are available from the root entry point or from a specific subpath for better tree-shaking:

import { Button } from 'twenty-ui';
import { Button } from 'twenty-ui/input';

Entry points

Subpath Contents
twenty-ui All components, icons, theme tokens, and utilities
twenty-ui/accessibility Accessibility helpers
twenty-ui/assets Logos and static assets
twenty-ui/data-display Avatars, chips, tags, and other display components
twenty-ui/feedback Progress bars, loaders, and status feedback
twenty-ui/icon Icon components and the icon provider
twenty-ui/input Buttons, toggles, and form inputs
twenty-ui/json-visualizer JSON tree viewer
twenty-ui/layout Layout primitives
twenty-ui/navigation Menus, links, and navigation components
twenty-ui/surfaces Cards, tooltips, and surface components
twenty-ui/testing Storybook and test decorators
twenty-ui/theme Theme types and helpers
twenty-ui/theme-constants Design tokens, ThemeProvider, and useTheme
twenty-ui/typography Text and typography components
twenty-ui/utilities Hooks and shared utilities

Theming

  • twenty-ui/style.css ships the base reset and component styles. Import it once.
  • twenty-ui/theme-light.css and twenty-ui/theme-dark.css define the design-token CSS variables for each color scheme.
  • ThemeProvider exposes the active theme through useTheme() and applies the light / dark class. Pass applyToRoot={false} with overrides to scope a theme to a subtree instead of the document root.

Development

npx nx build twenty-ui                 # Build the library (dual ESM/CJS + types)
npx nx storybook:serve:dev twenty-ui   # Run Storybook
npx nx test twenty-ui                  # Run unit tests

License

twenty-ui is released under the MIT license.