Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
module.exports = {
preset: 'ts-jest/presets/default-esm', // or other ESM presets
globals: {
'ts-jest': {
useESM: true,
},
},
preset: 'ts-jest/presets/default-esm',
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testEnvironment: 'node',
transform: {},
};
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
// The repo's tsconfig compiles to CommonJS for the published builds.
// Jest, however, loads test modules as ESM (see the default-esm
// preset), so ts-jest must emit ESM too — otherwise the compiled
// `exports`/`require` references throw "exports is not defined".
tsconfig: { module: 'esnext' },
},
],
},
};
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"packages": [
"packages/*"
],
"version": "2.1.5"
"version": "2.2.0-alpha.1"
}
76 changes: 76 additions & 0 deletions packages/react-ui/UPGRADING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Upgrading datocms-react-ui

## v3.0.0 — Semantic color tokens and dark-mode support

This release reworks the color system around the host's semantic color
tokens. The CMS now computes a full color palette for the active theme
(including dark mode), and `datocms-react-ui` consumes it directly. All
built-in components automatically adapt to whichever theme the user has
selected.

### Action required

**Test your plugin in dark mode before publishing.** Upgrading to v3 opts
your plugin into the host's active theme. If the user has dark mode
enabled, your plugin renders with dark colors.

Common things to audit:

- **Hardcoded colors** in your CSS (e.g. `color: #333`, `background: white`).
They won't follow the theme; users in dark mode will see them as-is and
the contrast may break.
- **Hardcoded SVG fills** in custom icons. Use `fill="currentColor"` so they
inherit the surrounding `color`.
- **Custom CSS that mixes library components with your own colors.** Verify
the combinations look right in both themes.

### What's new

A new set of CSS custom properties is available inside `<Canvas>`. They
follow the host's active theme:

- `--color--surface`, `--color--surface-hover`, `--color--surface-muted`, …
- `--color--ink`, `--color--ink-subtle`, `--color--ink-placeholder`,
`--color--ink-accent`, …
- `--color--border`, `--color--border-hover`
- Per-context variants: `--color--primary--surface`, `--color--primary--ink`,
`--color--tinted--surface`, `--color--accent--ink`,
`--color--selected--surface`, `--color--disabled--surface`,
`--color--danger--surface`, …
- Feedback: `--color--feedback-fail--ink`,
`--color--feedback-warning--surface`, `--color--feedback-success--ink`, …
- Plus diff, status, overlay, stacked, progress, tooltip, code and shadow
groups.

See the `Canvas` JSDoc for the full reference.

All built-in components (`Button`, `Dropdown`, `Section`, `TextInput`,
`SwitchInput`, `Toolbar`, `Tooltip`, …) now use these tokens. Built-in
icons render with `fill="currentColor"` and inherit the surrounding
`color`.

### Deprecated CSS variables

Two groups of legacy color variables remain available for backward
compatibility, but **both are deprecated and will be removed in a future
major version**. Migrate everything to the `--color--*` semantic tokens.

**Structural legacy vars** — defined inside `<Canvas>` (e.g.
`--border-color`, `--base-body-color`, `--light-bg-color`, `--alert-color`,
`--add-color`, `--remove-color`, …). Each one now resolves to the closest
semantic token first, falling back to its original light-mode value if the
token is unavailable. So a v3 plugin that still uses them will follow the
active theme.

**Theme-derived legacy vars** — `--accent-color`, `--primary-color`,
`--light-color`, `--dark-color`, `--semi-transparent-accent-color` (plus
their `*-rgb-components` counterparts). These are emitted from the
deprecated `ctx.theme` field, which the host now pins to **light values
only**, regardless of the active theme. Mixing these with the new
`--color--*` tokens in dark mode will produce a light accent on a dark
surface — visibly mismatched. Replace them with the corresponding
semantic tokens (`--color--accent--surface`, `--color--accent--ink`,
`--color--primary--surface`, etc.).

Non-color tokens (`--spacing-*`, `--font-size-*`, `--font-weight-bold`,
`--material-ease`, font families) are stable and remain available.
94 changes: 91 additions & 3 deletions packages/react-ui/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,93 @@
describe('connect()', () => {
it('works', () => {
expect(true).toBeTruthy();
import { generateStyleFromCtx } from '../src/generateStyleFromCtx';

const baseCtx = {
bodyPadding: [10, 20, 10, 20] as [number, number, number, number],
theme: {
primaryColor: 'rgb(0, 76, 209)',
accentColor: 'rgb(0, 76, 209)',
semiTransparentAccentColor: 'rgb(0, 76, 209)',
lightColor: 'rgb(219, 234, 254)',
darkColor: 'rgb(0, 33, 90)',
},
semanticColorTokensTheme: {},
};

describe('generateStyleFromCtx', () => {
it('generates existing theme CSS variables with RGB components', () => {
const style = generateStyleFromCtx(baseCtx as any) as any;

expect(style['--primary-color']).toBe('rgb(0, 76, 209)');
expect(style['--primary-color-rgb-components']).toBe('0, 76, 209');
expect(style['--accent-color']).toBe('rgb(0, 76, 209)');
expect(style['--semi-transparent-accent-color']).toBe('rgb(0, 76, 209)');
expect(style.padding).toBe('10px 20px 10px 20px');
});

it('respects noBodyPadding flag', () => {
const style = generateStyleFromCtx(baseCtx as any, true) as any;

expect(style.padding).toBeUndefined();
expect(style['--primary-color']).toBe('rgb(0, 76, 209)');
});

it('applies semantic color tokens verbatim, keyed by their CSS variable name', () => {
const ctx = {
...baseCtx,
semanticColorTokensTheme: {
'--color--surface': 'rgb(255, 255, 255)',
'--color--ink': 'rgb(52, 54, 58)',
'--color--feedback-fail--ink': 'rgb(255, 94, 73)',
'--shadow--raised': '0 1px 2px rgba(0, 0, 0, 0.1)',
},
};

const style = generateStyleFromCtx(ctx as any) as any;

expect(style['--color--surface']).toBe('rgb(255, 255, 255)');
expect(style['--color--ink']).toBe('rgb(52, 54, 58)');
expect(style['--color--feedback-fail--ink']).toBe('rgb(255, 94, 73)');
expect(style['--shadow--raised']).toBe('0 1px 2px rgba(0, 0, 0, 0.1)');

// Semantic tokens are passed through as-is — no RGB-component derivation.
expect(style['--color--surface-rgb-components']).toBeUndefined();
expect(style['--color--ink-rgb-components']).toBeUndefined();
});

it('works when semanticColorTokensTheme is undefined', () => {
const { semanticColorTokensTheme, ...ctx } = baseCtx;
const style = generateStyleFromCtx(ctx as any) as any;

expect(style['--primary-color']).toBe('rgb(0, 76, 209)');
// Should not throw, no semantic token variables present
expect(style['--color--surface']).toBeUndefined();
});

it('works when semanticColorTokensTheme is an empty object', () => {
const ctx = {
...baseCtx,
semanticColorTokensTheme: {},
};

const style = generateStyleFromCtx(ctx as any) as any;

expect(style['--primary-color']).toBe('rgb(0, 76, 209)');
const semanticKeys = Object.keys(style).filter((k: string) =>
k.startsWith('--color--'),
);
expect(semanticKeys).toHaveLength(0);
});

it('forwards arbitrary host tokens the SDK has never heard of', () => {
const ctx = {
...baseCtx,
semanticColorTokensTheme: {
'--color--brand-new--surface': 'rgb(100, 200, 50)',
},
};

const style = generateStyleFromCtx(ctx as any) as any;

// The SDK keeps no token list: whatever the host sends is applied as-is.
expect(style['--color--brand-new--surface']).toBe('rgb(100, 200, 50)');
});
});
6 changes: 3 additions & 3 deletions packages/react-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/react-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "datocms-react-ui",
"version": "2.1.5",
"version": "2.2.0-alpha.1",
"description": "React components to use inside DatoCMS plugins",
"keywords": [
"datocms",
Expand Down Expand Up @@ -42,7 +42,7 @@
"dependencies": {
"@floating-ui/react": "^0.27.16",
"classnames": "^2.3.1",
"datocms-plugin-sdk": "^2.1.5",
"datocms-plugin-sdk": "^2.2.0-alpha.1",
"react-intersection-observer": "^8.31.0",
"react-select": "^5.2.1",
"scroll-into-view-if-needed": "^2.2.20"
Expand All @@ -58,5 +58,5 @@
"postcss-nested": "^5.0.6",
"typedoc": "^0.26.7"
},
"gitHead": "a523642c7188862c8431027e51ab479945dde5dd"
"gitHead": "6b88a998f0807c7da1e17afa6a8f0336aed7497f"
}
47 changes: 28 additions & 19 deletions packages/react-ui/src/Button/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
cursor: pointer;
line-height: inherit;
background-color: transparent;
color: var(--base-body-color);
color: var(--color--ink);
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 4px;
Expand All @@ -32,56 +32,65 @@
}

.buttonType-muted {
background-color: var(--light-color);
color: var(--accent-color);
background-color: var(--color--tinted--surface);
color: var(--color--tinted--ink);

&.disabled {
background-color: var(--light-bg-color);
color: rgba(0, 0, 0, 0.2);
background-color: var(--color--disabled--surface);
color: var(--color--disabled--ink);

&:hover,
&:focus,
&:active {
color: rgba(0, 0, 0, 0.2);
color: var(--color--disabled--ink);
}
}
}

.buttonType-primary {
background-color: var(--accent-color);
color: white;
background-color: var(--color--primary--surface);
color: var(--color--primary--ink);

&:hover,
&:focus,
&:active {
color: white;
color: var(--color--primary--ink);
}

&:hover {
background-color: var(--color--primary--surface-hover);
}

&:active,
&:focus {
background-color: var(--color--primary--surface-active);
}

&.disabled {
background-color: var(--disabled-bg-color);
color: rgba(0, 0, 0, 0.2);
background-color: var(--color--disabled--surface);
color: var(--color--disabled--ink);
&:hover,
&:focus,
&:active {
color: rgba(0, 0, 0, 0.2);
color: var(--color--disabled--ink);
}
}
}

.buttonType-negative {
background-color: var(--alert-color);
color: white;
background-color: var(--color--danger--surface);
color: var(--color--danger--ink);

&:hover,
&:focus,
&:active {
color: white;
background-color: var(--alert-color);
color: var(--color--danger--ink);
background-color: var(--color--danger--surface);
}

&.disabled {
background-color: var(--disabled-bg-color);
color: rgba(0, 0, 0, 0.2);
background-color: var(--color--disabled--surface);
color: var(--color--disabled--ink);
}
}

Expand Down Expand Up @@ -128,7 +137,7 @@
line-height: 0.6;

svg {
fill: var(--accent-color);
fill: var(--color--ink-accent);
}
}

Expand Down
Loading