diff --git a/.changeset/prime-message-origin.md b/.changeset/prime-message-origin.md
new file mode 100644
index 0000000..4f7ba9a
--- /dev/null
+++ b/.changeset/prime-message-origin.md
@@ -0,0 +1,5 @@
+---
+"datocms-plugin-sdk": patch
+---
+
+Faster host → plugin messaging in Chromium. Chrome deliberately slows the deserialization of cross-origin messages of 16 KB or more when `event.data` is read before `event.origin` (it re-deserializes the payload 4-8 times to mask timing), and Penpal 4 reads `data` first. The SDK now registers a capture-phase `message` listener that reads `event.origin` before Penpal runs, which makes every host message take the fast path: a 266 KB ctx went from ~7 ms to ~1 ms per message in Chrome. Firefox and Safari deserialize eagerly and are unaffected.
diff --git a/.changeset/raised-form-controls.md b/.changeset/raised-form-controls.md
new file mode 100644
index 0000000..8d5d8e8
--- /dev/null
+++ b/.changeset/raised-form-controls.md
@@ -0,0 +1,5 @@
+---
+"datocms-react-ui": patch
+---
+
+`TextInput`, `TextareaInput` and `SelectInput` now paint on `--color--surface-raised`, the same token native DatoCMS form controls use. In dark mode they were one step darker than the controls around them.
diff --git a/.changeset/root-font-size.md b/.changeset/root-font-size.md
new file mode 100644
index 0000000..11ebebb
--- /dev/null
+++ b/.changeset/root-font-size.md
@@ -0,0 +1,5 @@
+---
+"datocms-plugin-sdk": minor
+---
+
+New `ctx.rootFontSize`: the font size of the host's `` element, in CSS pixels. The host steps it with the viewport width (14.4, 15 or 16 px), and the SDK runtime now mirrors it onto the plugin's ``, so `rem` units and every rem-based token (`--font-size-*`, `--spacing-*`) measure the same inside the frame as in the surrounding UI.
diff --git a/packages/react-ui/src/Canvas/index.tsx b/packages/react-ui/src/Canvas/index.tsx
index e58b601..3849afe 100644
--- a/packages/react-ui/src/Canvas/index.tsx
+++ b/packages/react-ui/src/Canvas/index.tsx
@@ -104,14 +104,14 @@ export type CanvasProps = {
* collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
* >
*
- * One-level tokens that work on any neutral page. The surface, ink and border families cover the page background, body text and dividers; the surface-raised variants belong to the elevated layer used by modals, dropdowns and popovers. The tone-on-neutral inks (ink-danger, ink-warning, ink-success) color text and icons on a neutral surface; inside a toned panel use that context's own ink instead.
+ * One-level tokens that work on any neutral page. The surface, ink and border families cover the page background, body text and dividers; the surface-raised variants belong to the elevated layer used by form controls, modals, dropdowns and popovers. The tone-on-neutral inks (ink-danger, ink-warning, ink-success) color text and icons on a neutral surface; inside a toned panel use that context's own ink instead.
*
* {
: 'var(--color--focus--border)',
backgroundColor: isDisabled
? 'var(--color--disabled--surface)'
- : 'var(--color--surface)',
+ : 'var(--color--surface-raised)',
boxShadow: `0 0 0 4px ${
error
? 'var(--color--danger-soft--outline)'
@@ -81,7 +81,7 @@ const useStyles = (isDisabled?: boolean, error?: boolean) => {
: 'var(--color--border)',
backgroundColor: isDisabled
? 'var(--color--disabled--surface)'
- : 'var(--color--surface)',
+ : 'var(--color--surface-raised)',
'&:hover': {
borderColor: error
? 'var(--color--danger-soft--border)'
diff --git a/packages/react-ui/src/TextInput/styles.module.css b/packages/react-ui/src/TextInput/styles.module.css
index dde70b4..4c90a2b 100644
--- a/packages/react-ui/src/TextInput/styles.module.css
+++ b/packages/react-ui/src/TextInput/styles.module.css
@@ -6,7 +6,7 @@
border: 1px solid var(--color--border);
appearance: none;
border-radius: 0;
- background-color: var(--color--surface);
+ background-color: var(--color--surface-raised);
background-image: none;
transition: border 0.2s var(--material-ease);
font-size: var(--font-size-m);
diff --git a/packages/react-ui/src/TextareaInput/styles.module.css b/packages/react-ui/src/TextareaInput/styles.module.css
index 6f01057..ca66fe0 100644
--- a/packages/react-ui/src/TextareaInput/styles.module.css
+++ b/packages/react-ui/src/TextareaInput/styles.module.css
@@ -6,7 +6,7 @@
border: 1px solid var(--color--border);
appearance: none;
border-radius: 0;
- background-color: var(--color--surface);
+ background-color: var(--color--surface-raised);
background-image: none;
transition: border 0.2s var(--material-ease);
font-size: var(--font-size-m);
diff --git a/packages/sdk/manifest.json b/packages/sdk/manifest.json
index bcc70d3..5407a5c 100644
--- a/packages/sdk/manifest.json
+++ b/packages/sdk/manifest.json
@@ -3157,6 +3157,16 @@
"lineNumber": 134
},
"type": "'light' | 'dark'"
+ },
+ "rootFontSize": {
+ "comment": {
+ "markdownText": "Font size of the host's `` element, in CSS pixels. The host steps\nit with the viewport width, so the SDK runtime mirrors it onto the\nplugin's ``: `rem` units, and with them every `--font-size-*` and\n`--spacing-*` token, then measure the same inside the frame as outside.\n\nTyped as required for parity with the other appearance fields (e.g.\n`colorScheme`), but only hosts new enough to send it populate it: a plugin\nrunning inside an older host reads `undefined` at runtime. Guard for that\nif you consume the value directly."
+ },
+ "location": {
+ "filePath": "src/ctx/base.ts",
+ "lineNumber": 147
+ },
+ "type": "number"
}
}
},
@@ -3172,7 +3182,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 144
+ "lineNumber": 157
},
"type": "Partial>"
},
@@ -3182,7 +3192,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 151
+ "lineNumber": 164
},
"type": "Partial>"
},
@@ -3192,7 +3202,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 158
+ "lineNumber": 171
},
"type": "Partial>"
},
@@ -3202,7 +3212,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 165
+ "lineNumber": 178
},
"type": "Partial>"
},
@@ -3212,7 +3222,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 172
+ "lineNumber": 185
},
"type": "Partial>"
}
@@ -3233,7 +3243,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 235
+ "lineNumber": 248
},
"type": "(itemTypeId: string) => Promise"
},
@@ -3244,7 +3254,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 254
+ "lineNumber": 267
},
"type": "(itemTypeId: string) => Promise"
},
@@ -3255,7 +3265,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 271
+ "lineNumber": 284
},
"type": "() => Promise"
},
@@ -3266,7 +3276,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 284
+ "lineNumber": 297
},
"type": "() => Promise"
},
@@ -3277,7 +3287,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 297
+ "lineNumber": 310
},
"type": "() => Promise"
}
@@ -3296,7 +3306,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 319
+ "lineNumber": 332
},
"type": "(params: Record) => Promise"
},
@@ -3307,7 +3317,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 370
+ "lineNumber": 383
},
"type": "(\n fieldId: string,\n changes: FieldAppearanceChange[],\n ) => Promise"
}
@@ -3326,7 +3336,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 475
+ "lineNumber": 488
},
"type": "(message: string) => Promise"
},
@@ -3337,7 +3347,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 490
+ "lineNumber": 503
},
"type": "(message: string) => Promise"
},
@@ -3348,7 +3358,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 514
+ "lineNumber": 527
},
"type": "(\n toast: Toast,\n ) => Promise"
}
@@ -3367,7 +3377,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 400
+ "lineNumber": 413
},
"type": "(itemTypeId: string) => Promise- "
},
@@ -3378,7 +3388,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 421
+ "lineNumber": 434
},
"type": "{\n (\n itemTypeId: string,\n options: { multiple: true; initialLocationQuery?: ItemListLocationQuery },\n ): Promise
- ;\n (\n itemTypeId: string,\n options?: {\n multiple: false;\n initialLocationQuery?: ItemListLocationQuery;\n },\n ): Promise
- ;\n }"
},
@@ -3389,7 +3399,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 453
+ "lineNumber": 466
},
"type": "(itemId: string) => Promise
- "
}
@@ -3408,7 +3418,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 541
+ "lineNumber": 554
},
"type": "{\n (options: { multiple: true }): Promise
;\n (options?: { multiple: false }): Promise;\n }"
},
@@ -3419,7 +3429,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 569
+ "lineNumber": 582
},
"type": "(\n uploadId: string,\n ) => Promise<(Upload & { deleted?: true }) | null>"
},
@@ -3430,7 +3440,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 598
+ "lineNumber": 611
},
"type": "(\n /** The \"single asset\" field structure */\n fileFieldValue: FileFieldValue,\n /** Shows metadata information for a specific locale */\n locale?: string,\n ) => Promise"
}
@@ -3449,7 +3459,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 629
+ "lineNumber": 642
},
"type": "(modal: Modal) => Promise"
},
@@ -3460,7 +3470,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 666
+ "lineNumber": 679
},
"type": "(options: ConfirmOptions) => Promise"
}
@@ -3479,7 +3489,7 @@
},
"location": {
"filePath": "src/ctx/base.ts",
- "lineNumber": 680
+ "lineNumber": 693
},
"type": "(path: string) => Promise"
}
diff --git a/packages/sdk/src/connect.ts b/packages/sdk/src/connect.ts
index aa39cb7..9f19884 100644
--- a/packages/sdk/src/connect.ts
+++ b/packages/sdk/src/connect.ts
@@ -136,10 +136,41 @@ export type FullConnectParameters = AssetSourcesHook &
UploadSidebarsHook &
ValidateManualFieldExtensionParametersHook;
-function applyColorScheme(properties: unknown): void {
+type HostAppearance = {
+ colorScheme?: 'light' | 'dark';
+ rootFontSize?: number;
+};
+
+/**
+ * Mirrors the host's appearance onto the plugin's ``: color scheme and
+ * root font size. Runs on the first settings read and on every `onChange`.
+ */
+function applyHostAppearance(properties: unknown): void {
if (typeof document === 'undefined') return;
- const next = (properties as { colorScheme?: 'light' | 'dark' } | null)
- ?.colorScheme;
+ const { colorScheme, rootFontSize } =
+ (properties as HostAppearance | null) ?? {};
+ applyColorScheme(colorScheme);
+ applyRootFontSize(rootFontSize);
+}
+
+function applyRootFontSize(next: number | undefined): void {
+ // Hosts that predate the property leave the plugin's own default in place.
+ // Reject non-positive/non-finite values too: a host bug or a transient 0
+ // during boot would otherwise set `font-size: 0px` and collapse every
+ // rem-based size in the plugin to nothing.
+ if (typeof next !== 'number' || !Number.isFinite(next) || next <= 0) return;
+ // The host's font size steps with the viewport width; mirroring it
+ // makes rem-based sizes (every --font-size-* and --spacing-* token) measure
+ // the same on both sides of the iframe boundary.
+ const px = `${next}px`;
+ // `applyHostAppearance` runs on every ctx update (i.e. every keystroke in a
+ // sibling field). Writing `fontSize` unconditionally would reflow the whole
+ // rem-based frame each time, so only touch the DOM when the value changed.
+ if (document.documentElement.style.fontSize === px) return;
+ document.documentElement.style.fontSize = px;
+}
+
+function applyColorScheme(next: 'light' | 'dark' | undefined): void {
if (next !== 'light' && next !== 'dark') return;
if (document.documentElement.dataset.colorScheme === next) return;
document.documentElement.dataset.colorScheme = next;
@@ -180,6 +211,18 @@ export async function connect(
),
};
+ // Chromium deliberately slows the deserialization of cross-origin messages
+ // of 16 KB or more when `event.data` is read before `event.origin`: it
+ // re-deserializes the payload 4-8 times in a throwaway isolate to mask
+ // timing (MaskDeserializationTimings in message_event.cc). Penpal 4 reads
+ // `data` first. `message` events target `window` directly, so this listener
+ // runs before Penpal's simply because it is registered first (added before
+ // connectToParent below); reading `origin` here clears the flag for the
+ // event, so every message takes the fast path. The value is returned, not
+ // discarded, so that minifiers with `pure_getters` enabled keep the read.
+ // No-op where deserialization is eager (Firefox, Safari).
+ window.addEventListener('message', (event) => event.origin, true);
+
const penpalConnection = connectToParent({
methods: {
// Protocol revision, not the npm version: it's bumped by hand whenever
@@ -204,7 +247,7 @@ export async function connect(
),
),
onChange(newSettings: unknown) {
- applyColorScheme(newSettings);
+ applyHostAppearance(newSettings);
if (onChangeListener) {
onChangeListener(newSettings);
}
@@ -231,8 +274,9 @@ export async function connect(
});
const methods = await penpalConnection.promise;
+
const initialProperties = await methods.getSettings();
- applyColorScheme(initialProperties);
+ applyHostAppearance(initialProperties);
if (initialProperties.mode === 'onBoot') {
let currentProperties = initialProperties;
diff --git a/packages/sdk/src/ctx/base.ts b/packages/sdk/src/ctx/base.ts
index 3c569e9..0dfe1f5 100644
--- a/packages/sdk/src/ctx/base.ts
+++ b/packages/sdk/src/ctx/base.ts
@@ -132,6 +132,19 @@ type ProjectProperties = {
* directly.
*/
colorScheme: 'light' | 'dark';
+
+ /**
+ * Font size of the host's `` element, in CSS pixels. The host steps
+ * it with the viewport width, so the SDK runtime mirrors it onto the
+ * plugin's ``: `rem` units, and with them every `--font-size-*` and
+ * `--spacing-*` token, then measure the same inside the frame as outside.
+ *
+ * Typed as required for parity with the other appearance fields (e.g.
+ * `colorScheme`), but only hosts new enough to send it populate it: a plugin
+ * running inside an older host reads `undefined` at runtime. Guard for that
+ * if you consume the value directly.
+ */
+ rootFontSize: number;
};
/**
diff --git a/packages/sdk/src/manifest.ts b/packages/sdk/src/manifest.ts
index 64cc9d1..6cc9ce3 100644
--- a/packages/sdk/src/manifest.ts
+++ b/packages/sdk/src/manifest.ts
@@ -3393,6 +3393,17 @@ export const manifest: Manifest = {
},
type: "'light' | 'dark'",
},
+ rootFontSize: {
+ comment: {
+ markdownText:
+ "Font size of the host's `` element, in CSS pixels. The host steps\nit with the viewport width, so the SDK runtime mirrors it onto the\nplugin's ``: `rem` units, and with them every `--font-size-*` and\n`--spacing-*` token, then measure the same inside the frame as outside.\n\nTyped as required for parity with the other appearance fields (e.g.\n`colorScheme`), but only hosts new enough to send it populate it: a plugin\nrunning inside an older host reads `undefined` at runtime. Guard for that\nif you consume the value directly.",
+ },
+ location: {
+ filePath: 'src/ctx/base.ts',
+ lineNumber: 147,
+ },
+ type: 'number',
+ },
},
},
{
@@ -3409,7 +3420,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 144,
+ lineNumber: 157,
},
type: 'Partial>',
},
@@ -3420,7 +3431,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 151,
+ lineNumber: 164,
},
type: 'Partial>',
},
@@ -3431,7 +3442,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 158,
+ lineNumber: 171,
},
type: 'Partial>',
},
@@ -3442,7 +3453,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 165,
+ lineNumber: 178,
},
type: 'Partial>',
},
@@ -3453,7 +3464,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 172,
+ lineNumber: 185,
},
type: 'Partial>',
},
@@ -3477,7 +3488,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 235,
+ lineNumber: 248,
},
type: '(itemTypeId: string) => Promise',
},
@@ -3490,7 +3501,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 254,
+ lineNumber: 267,
},
type: '(itemTypeId: string) => Promise',
},
@@ -3503,7 +3514,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 271,
+ lineNumber: 284,
},
type: '() => Promise',
},
@@ -3516,7 +3527,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 284,
+ lineNumber: 297,
},
type: '() => Promise',
},
@@ -3529,7 +3540,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 297,
+ lineNumber: 310,
},
type: '() => Promise',
},
@@ -3551,7 +3562,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 319,
+ lineNumber: 332,
},
type: '(params: Record) => Promise',
},
@@ -3564,7 +3575,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 370,
+ lineNumber: 383,
},
type: '(\n fieldId: string,\n changes: FieldAppearanceChange[],\n ) => Promise',
},
@@ -3586,7 +3597,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 475,
+ lineNumber: 488,
},
type: '(message: string) => Promise',
},
@@ -3599,7 +3610,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 490,
+ lineNumber: 503,
},
type: '(message: string) => Promise',
},
@@ -3612,7 +3623,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 514,
+ lineNumber: 527,
},
type: '(\n toast: Toast,\n ) => Promise',
},
@@ -3634,7 +3645,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 400,
+ lineNumber: 413,
},
type: '(itemTypeId: string) => Promise- ',
},
@@ -3647,7 +3658,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 421,
+ lineNumber: 434,
},
type: '{\n (\n itemTypeId: string,\n options: { multiple: true; initialLocationQuery?: ItemListLocationQuery },\n ): Promise
- ;\n (\n itemTypeId: string,\n options?: {\n multiple: false;\n initialLocationQuery?: ItemListLocationQuery;\n },\n ): Promise
- ;\n }',
},
@@ -3660,7 +3671,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 453,
+ lineNumber: 466,
},
type: '(itemId: string) => Promise
- ',
},
@@ -3682,7 +3693,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 541,
+ lineNumber: 554,
},
type: '{\n (options: { multiple: true }): Promise
;\n (options?: { multiple: false }): Promise;\n }',
},
@@ -3695,7 +3706,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 569,
+ lineNumber: 582,
},
type: '(\n uploadId: string,\n ) => Promise<(Upload & { deleted?: true }) | null>',
},
@@ -3708,7 +3719,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 598,
+ lineNumber: 611,
},
type: '(\n /** The "single asset" field structure */\n fileFieldValue: FileFieldValue,\n /** Shows metadata information for a specific locale */\n locale?: string,\n ) => Promise',
},
@@ -3730,7 +3741,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 629,
+ lineNumber: 642,
},
type: '(modal: Modal) => Promise',
},
@@ -3743,7 +3754,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 666,
+ lineNumber: 679,
},
type: '(options: ConfirmOptions) => Promise',
},
@@ -3764,7 +3775,7 @@ export const manifest: Manifest = {
},
location: {
filePath: 'src/ctx/base.ts',
- lineNumber: 680,
+ lineNumber: 693,
},
type: '(path: string) => Promise',
},