Skip to content

Sync from monorepo: introduce terria design system based on react-aria - #7981

Merged
zoran995 merged 222 commits into
mainfrom
copybara/from-monorepo
Nov 9, 2026
Merged

zoran995 merged 222 commits into
mainfrom
copybara/from-monorepo

Conversation

@zoran995

@zoran995 zoran995 commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds the Terria design system: React components, icons and design tokens, with Storybook and visual tests. TerriaJS doesn't use it yet; we'll migrate gradually behind a feature flag.

Automated Copybara sync. Merge with a normal merge commit.

Packages

Package Where What it is Published?
@terria/design-system packages/design-system/components 67 React components built on React Aria, with their CSS and tokens No, private (see below)
@terria/design-system-icons packages/design-system/icons Icons as React components and an SVG sprite, generated from Figma No, private
@terria/design-system-storybook packages/design-system/storybook Storybook with every component's stories, in light and dark mode No, private

The packages are private for now. TerriaJS will carry them inside its own npm package (bundleDependencies) instead of publishing them separately.

Design tokens

The tokens (packages/design-system/components/src/css/tokens.css) are generated automatically from the Figma variables. Don't edit tokens.css by hand: token changes come from Figma.

Dependencies

  • react-aria, react-aria-components, react-stately, @internationalized/date and @react-aria/* are peer dependencies with ranges, so an app has one copy of each. The design system pins them in devDependencies for its own builds and tests.
  • React 18 and 19 are supported.
  • The Inter font (@fontsource-variable/inter) is a normal dependency.

Using it in an app

import "@terria/design-system/layers.css";
import "@terria/design-system/tokens.css";
import "@terria/design-system/reset.css";
import "@terria/design-system/base.css";
import "@terria/design-system/fonts.css";
import { Button } from "@terria/design-system";

All CSS is in cascade layers (terria-ds.*). Components are styled through data-tds attributes, so an app's own className never clashes. Components' own strings (like "Close") are in English, and an app can translate them with LocalizedStringsProvider.

Running it

From the repo root:

What Command
Storybook (localhost:6006) pnpm storybook
Build Storybook pnpm build-storybook
Build the packages pnpm --filter "@terria/design-system*" -r build
Rebuild while you work pnpm --filter @terria/design-system dev
Tests (Vitest, in Chromium) pnpm --filter "@terria/design-system*" -r test
Type-check and lint pnpm --filter "@terria/design-system*" -r typecheck / -r lint

The first test run needs Playwright's browser: pnpm --filter @terria/design-system exec playwright install chromium.

Visual tests

Every component has screenshot tests (*.visual.tsx) for each state, in both themes.

  • Locally: pnpm --filter @terria/design-system test:visual -u takes the references; pnpm --filter @terria/design-system test:visual compares.
  • On a PR: a maintainer comments /terria-bot visual-test. It compares the PR's components with the base branch's and posts the differences on the PR. Comment again after new commits.

CI

  • design-system-ci.yml: formats, lints, type-checks, runs the tests and builds Storybook. It runs on PRs that touch the design system.
  • design-system-visual*.yml: the /terria-bot visual-test command, the comparison and the clean-up of its images.
  • design-system-chromatic.yml is off until we have a Chromatic account; it can only be started by hand.

Test me

  1. pnpm install
  2. pnpm storybook and open http://localhost:6006. Check a few components in light and dark (toolbar toggle).
  3. pnpm --filter "@terria/design-system*" -r build && pnpm --filter "@terria/design-system*" -r test

Checklist

  • There are unit tests to verify my changes are correct or unit tests aren't applicable (if so, write quick reason why unit tests don't exist)
  • I've updated relevant documentation in doc/.
  • I've updated CHANGES.md with what I changed.
  • I've provided instructions in the PR description on how to test this PR.

MONOREPO_REV_ID: 0f6d11348ce1b905507cbd5bbda873c507f84fed
MONOREPO_REV_ID: 372efb26d5522b2475a432f35e41180e7193a571
MONOREPO_REV_ID: 94557ef49640dc7a5d39c2b313691778cad86ff0
MONOREPO_REV_ID: 7a235a49f7ee04950f57d3fa720f36cbef8983f3
MONOREPO_REV_ID: 7c1df6ccb5db56c7d1dd9d6f95e26995f58bb8fd
MONOREPO_REV_ID: 08d7222c1d6de5f039eba8fa8d5fee7321cdac86
MONOREPO_REV_ID: 1544486ceb7d0057bf38ac54a8b3d18425e51e79
MONOREPO_REV_ID: b90cfe189f05f9f52ea3fc9fe6cd55fcd88811ed
Build CSS custom properties from the Figma variables export with
style-dictionary: light theme on :root, dark theme on
[data-theme='dark'], colors converted to oklch and Figma number
scopes converted to rem/px.
MONOREPO_REV_ID: ecc44705f99e63b52cbc0a0dac7e8eb481ce5914
MONOREPO_REV_ID: f5eaf54f6ec0938cd472355c81c3b9c97b36a195
MONOREPO_REV_ID: 5ccc1c5e271363187ad43e198b88a0b222c5001a
- manager and docs use the light Storybook theme
- the backgrounds toolbar sets data-theme on the preview, so Dark shows
  the design system's dark tokens
- both backgrounds use the surface-bg token instead of fixed neutrals

MONOREPO_REV_ID: 24478a0a4a10e65248c82c364d8e4e4d6a8bbf90
MONOREPO_REV_ID: 07495d5bc6b6e338fd545e6946dea31883f958d9
Compound component (Track, TrackCircle, FillCircle) on react-aria's
ProgressBar, drawn with plain svg/circle parts.
Colors accent, default, success, warning, danger and current (follows
the text color); sizes sm, md and lg.

MONOREPO_REV_ID: 17aa2d7dac2b72eb8d71d227e2380b7204c56526
MONOREPO_REV_ID: 432623899d23fe6a731bb8b91f5d36519ca783f2
MONOREPO_REV_ID: 13a70a7e98f25c899cf53ad22f1830a6a361809c
MONOREPO_REV_ID: 293f0cf81ff750361ca56640d88d770a2c6e6bd0
MONOREPO_REV_ID: f14cad92d435742839d98a72be91405f0a3dead4
MONOREPO_REV_ID: 42782d655697253ec41d9883e99399ce23d82f92
MONOREPO_REV_ID: 68792fe8b1f721bc6d912ac569b2b1a4127ab8ee
MONOREPO_REV_ID: ececc1c10e1aa5b4fc2919a3083bed8e870d6a76
…gs visible

MONOREPO_REV_ID: 23101ebe5535eaf01cc41787be4ea0ca6a3aa9ee
MONOREPO_REV_ID: 0efcd87ca69b58bf782f16be644b05fc7f4e25f3
MONOREPO_REV_ID: 3157bfa9691cf8d8d20ca5751c64c2479e4075c9
MONOREPO_REV_ID: 64f8347d975569b0882c8a714ee6ff309b5a7288
MONOREPO_REV_ID: a0ba9c9cb19478f8db2d8866ad82df9c4b5c64a1
MONOREPO_REV_ID: 13b34ea221fbf12d82de478b69d38499d6805f18
MONOREPO_REV_ID: d74e7cca51afa8274fca4e57c3c162bf28538146
MONOREPO_REV_ID: f8a1753e9a61267b9042d30e2f86e8d4610fb51a
MONOREPO_REV_ID: f852cbfb72e5bd6505621190fae5c78cc2bbf745
zoran995 and others added 25 commits November 7, 2026 23:24
React Aria's year picker goes to its item's date, and over a range
ending at maxValue every item is that day of its year: 31 December for
the default 1900 to 2099. Picking 2030 from 16 June focused 31 December
2030. The year picker now keeps the month and day it was on.

MONOREPO_REV_ID: 632129c329e98e22a578e8c30864014f388dffe4
isCurrentYear compared a Gregorian year from new Date() with years of
the calendar's own system, so under a Buddhist or Persian calendar no
year was current, and under the Japanese one year 8 of any era was.
Today is now read in the calendar's system, era and time zone.

MONOREPO_REV_ID: 8d6bb116faa114922d58f5b4ccc307fc43a1465d
A year added to a date in 9999, the Gregorian calendar's last, stays in
it, so stepping through the years to a maxValue there never ended. That
froze the page as soon as a Calendar with a year picker rendered, with
maxValue set to 31 December 9999 as an open end. getYearRange now stops
when a year doesn't advance. React Aria's useCalendarYearPicker loops
the same way, so the year picker hands it a maxValue a year short of
the calendar's last year.

MONOREPO_REV_ID: 140cb18c3b858ef157f8e0406a567102bf384a98
…hey run

The year picker's right arrow always went to the next year, though in a
right-to-left locale its rows run right to left: under ar-EG, the right
arrow from 2024 went to 2025, the year on its left. The left and right
arrows now follow the locale's direction.

MONOREPO_REV_ID: a5c8d54566c684e96021bb0e61abe2f2432302eb
Saturation and brightness were the axes whatever the colour space, so
<ColorArea colorSpace="hsl" /> or "rgb" threw on render: neither has
both channels. They stay the default in hsb; another space's axes are
its own, as React Aria picks them.

MONOREPO_REV_ID: 5d720e6566b60d69894a1bfb16611491b2616b23
ListBox.Item took its text only from a single string child, so an item
written as {name} ({count}), which JSX splits into several, had none and
couldn't be reached by typing. It now joins them as Dropdown.Item does,
with the helpers moved to utils for both.

MONOREPO_REV_ID: 8bbbba48e25836b33452db79913485838d82aa91
Turning isEnabled off with visibility="auto" stopped detection but left
the data attributes it last set, so the fade stayed painted. The effect
that sets controlled visibility now clears them in that case too.

MONOREPO_REV_ID: f952cb39cfac54fe5e045d159bf570aebb24f8fb
A collapse reported the keys it ends on only when its rows had finished
animating, keys taken when it started. Given other expandedKeys in the
meantime, a controlled tree was then told to undo them: starting to
collapse A, then expanding A and B from outside, ended with neither.
The collapse is now cancelled when the tree's keys change under it.

MONOREPO_REV_ID: d494d3f1310854a1e1f5487cc8aab83dba798ce7
Its clear button isn't a tab stop, as a Select's isn't, so its choice
could only be cleared with a pointer. Backspace or Delete on its button
now clears a field that has a clear button, as a Select's trigger does,
through one useClear shared with the clear button.

MONOREPO_REV_ID: e643611ea6eaf457df1f32774b00c4e02b2f58e6
The items were spread into a list on every render, though an iterable
such as a generator can only be read once: its rows went as soon as
the list drew again, as it does when a drag starts. Each iterable given
is now read once.

MONOREPO_REV_ID: 6b9d1e5c4b1449cefece8bbedd83e838efa95a72
React Aria's progressbar drops aria-hidden, so <Spinner aria-hidden />,
as the loading toast draws it, was still an unnamed progressbar. Hidden,
it's now a plain span, as AIActivityIndicator already is. Both take a
plain className and style rather than React Aria's functions of the
progressbar's state, which never change for an indeterminate one, so
the hidden span needs only the DOM props.

MONOREPO_REV_ID: 2db624f48c0204fbc8ba67b482362d9f721a3f11
Splitting a tag's children from its remove button filtered them for
truthiness, dropping a 0: "Results: {0}" read "Results: ", and a tag
of only {0} was blank. Children.map already leaves out the button taken
out, so the filter is gone.

MONOREPO_REV_ID: 1dfd46f04ed8c1445100d63652771a800aaaeef1
A thumb's tooltip showed whenever the thumb was hovered, dragged or
focused, with nothing to dismiss it, so it stayed up after Escape.
Escape now hides it, a hovered thumb's too, until the thumb is next
focused, hovered, moved or dragged. Escape never shows the focused
thumb's tooltip, and goes on to what holds the slider unless it closed
a tooltip that showed.

MONOREPO_REV_ID: c76bf68e88da64d08d6255414b9c2f7423eaead1
A string built from values had only the values, so plurals were left to
each string: the English one picked between two forms itself, and a
language with more, such as Serbian's 2 to 4, needed its own rules. A
string function is now also given a formatter for its locale, whose
plural picks the form by Intl.PluralRules and number writes numbers the
locale's way, as React Aria's own strings are. The strings are still
looked up a string at a time, falling back to the language and then
English.

MONOREPO_REV_ID: 5ea1db04191bfe5cd0dba79850ce0969886134e9
Without one, a pending button's spinner is a progress bar with no name.
The Pending story now says so, and what the label is for.

MONOREPO_REV_ID: e3e78ce3f39f06534fe9a0d07ebd41b8ba6e0907
The report found its comment by its marker alone. On a public repo
anyone can comment, so a comment starting with the marker would be
taken for the report: the results edited into a comment its author
could change, and its text repeated by every re-run. Only a comment by
github-actions[bot] is now taken.

MONOREPO_REV_ID: 6d4f2b0c709fa6e2a97243e252d1f7e956889b1e
MONOREPO_REV_ID: e96f11e2453d673c733e991fe87ca566be762e90
MONOREPO_REV_ID: 08041fddd8c62bbf6eec395b3aaeea201d8653c1
MONOREPO_REV_ID: 963bceff17ccee6fcd7cd82507be68dc62a7233c
…layout

MONOREPO_REV_ID: 7b594a476e9ad8701e1998c2e377bc4e908fd69a
…encies

react-aria, react-aria-components, react-stately, @internationalized/date and
the @react-aria packages must exist once in an app, so they're peers with
ranges, pinned in devDependencies for the package's own builds. The font has
no dependencies and nothing shares it, so it stays a dependency.

Rollup now takes its externals from package.json, and an import of an
undeclared package fails the build instead of being left as an import.

MONOREPO_REV_ID: 70ec06982ecdb60e092fd38da04221515106a6a1
MONOREPO_REV_ID: 91201056fb376ba4b2d08ef655dc5a7cf772e4d7
MONOREPO_REV_ID: 9bde07d02532881eadb599bab083dbd495ddb9c0
MONOREPO_REV_ID: 9bde07d02532881eadb599bab083dbd495ddb9c0
MONOREPO_REV_ID: 9bde07d02532881eadb599bab083dbd495ddb9c0
@zoran995
zoran995 requested a review from soyarsauce November 7, 2026 22:41
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ zoran995
❌ Terria Bot


Terria Bot seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@zoran995
zoran995 merged commit 49f9b22 into main Nov 9, 2026
16 of 19 checks passed
@zoran995
zoran995 deleted the copybara/from-monorepo branch November 9, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants