diff --git a/knowledge/_claude-context/context.md b/knowledge/_claude-context/context.md
index 9f537c7a8..edfc7b97a 100644
--- a/knowledge/_claude-context/context.md
+++ b/knowledge/_claude-context/context.md
@@ -1,7 +1,7 @@
# OpenIAP Project Context
> **Auto-generated for Claude Code**
-> Last updated: 2026-08-11T08:49:51.526Z
+> Last updated: 2026-08-12T06:58:01.676Z
>
> Usage: `claude --context knowledge/_claude-context/context.md`
@@ -2545,8 +2545,10 @@ from OpenIAP 3.0, `react-native-iap` 16.0.0, `expo-iap` 5.0.0,
redirects may name removed surfaces when they clearly describe history or
route readers to the replacement.
- The public migration catalog remains
- `/docs/updates/deprecations`. It records the removed-to-replacement mapping
- and the exact package major boundary.
+ `/docs/updates/migration` (published at `/docs/updates/deprecations` until
+ 2026-08; that path still redirects). It is organized as one section per
+ coordinated major train, and each train records the removed-to-replacement
+ mapping and the exact package major boundary.
- A future deprecation must be introduced through the canonical GraphQL
deprecation directives or an explicit package-local notice, name one future
major boundary, include a canonical replacement, and update the migration
diff --git a/knowledge/internal/07-docs-consistency.md b/knowledge/internal/07-docs-consistency.md
index b0ee17038..9cc62936f 100644
--- a/knowledge/internal/07-docs-consistency.md
+++ b/knowledge/internal/07-docs-consistency.md
@@ -287,8 +287,10 @@ from OpenIAP 3.0, `react-native-iap` 16.0.0, `expo-iap` 5.0.0,
redirects may name removed surfaces when they clearly describe history or
route readers to the replacement.
- The public migration catalog remains
- `/docs/updates/deprecations`. It records the removed-to-replacement mapping
- and the exact package major boundary.
+ `/docs/updates/migration` (published at `/docs/updates/deprecations` until
+ 2026-08; that path still redirects). It is organized as one section per
+ coordinated major train, and each train records the removed-to-replacement
+ mapping and the exact package major boundary.
- A future deprecation must be introduced through the canonical GraphQL
deprecation directives or an explicit package-local notice, name one future
major boundary, include a canonical replacement, and update the migration
diff --git a/libraries/expo-iap/README.md b/libraries/expo-iap/README.md
index b0996c59f..facc3cae1 100644
--- a/libraries/expo-iap/README.md
+++ b/libraries/expo-iap/README.md
@@ -9,7 +9,7 @@ Expo IAP is a powerful in-app purchase solution for Expo and React Native applic
If you're shipping an app with expo-iap, we’d love to hear about it—please share your product and feedback in [expo-iap Q&A Discussions](https://github.com/hyodotdev/openiap/discussions/categories/expo-iap). Community stories help us keep improving the ecosystem.
-
+
@@ -64,7 +64,7 @@ For detailed usage examples and error handling, see the [documentation](https://
## Powered by OpenIAP
-
+
Expo IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
diff --git a/libraries/expo-iap/example/amazon.sdktester.json b/libraries/expo-iap/example/amazon.sdktester.json
index 43cd5e901..cd536dfc8 100644
--- a/libraries/expo-iap/example/amazon.sdktester.json
+++ b/libraries/expo-iap/example/amazon.sdktester.json
@@ -4,28 +4,28 @@
"price": 0.99,
"title": "10 Bulbs",
"description": "A small pack of bulbs for testing consumable purchases",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.30bulbs": {
"itemType": "CONSUMABLE",
"price": 1.99,
"title": "30 Bulbs",
"description": "A larger pack of bulbs for testing consumable purchases",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.certified": {
"itemType": "ENTITLED",
"price": 4.99,
"title": "Certified",
"description": "A non-consumable entitlement for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.premium": {
"itemType": "SUBSCRIPTION",
"price": 4.99,
"title": "Premium Monthly",
"description": "Monthly premium access for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png",
+ "smallIconUrl": "https://openiap.dev/logo.webp",
"subscriptionBase": "dev.hyo.martie.premium.base",
"subscriptionParent": "dev.hyo.martie.premium.parent",
"term": "Monthly"
@@ -35,7 +35,7 @@
"price": 49.99,
"title": "Premium Yearly",
"description": "Yearly premium access for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png",
+ "smallIconUrl": "https://openiap.dev/logo.webp",
"subscriptionBase": "dev.hyo.martie.premium.base",
"subscriptionParent": "dev.hyo.martie.premium.parent",
"term": "Yearly"
diff --git a/libraries/flutter_inapp_purchase/README.md b/libraries/flutter_inapp_purchase/README.md
index 9756fd395..e5a0ead59 100644
--- a/libraries/flutter_inapp_purchase/README.md
+++ b/libraries/flutter_inapp_purchase/README.md
@@ -7,7 +7,7 @@
A comprehensive Flutter plugin for implementing in-app purchases that conforms to the [Open IAP specification](https://openiap.dev)
-
+
@@ -103,7 +103,7 @@ final sameIap = FlutterInappPurchase.instance; // Same instance
## Powered by OpenIAP
-
+
flutter_inapp_purchase conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
diff --git a/libraries/godot-iap/README.md b/libraries/godot-iap/README.md
index 051ea982b..a25f35738 100644
--- a/libraries/godot-iap/README.md
+++ b/libraries/godot-iap/README.md
@@ -13,7 +13,7 @@ A comprehensive in-app purchase plugin for Godot 4.x that conforms to the
+
@@ -80,7 +80,7 @@ See the [Quick Start Guide](https://openiap.dev/docs/setup/godot) for complete c
## Powered by OpenIAP
-
+
godot-iap conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
diff --git a/libraries/kmp-iap/README.md b/libraries/kmp-iap/README.md
index faf3a7e89..b82e6beeb 100644
--- a/libraries/kmp-iap/README.md
+++ b/libraries/kmp-iap/README.md
@@ -1,7 +1,7 @@
# kmp-iap
-
+
@@ -10,7 +10,7 @@
A comprehensive Kotlin Multiplatform library for in-app purchases on Android and iOS platforms that conforms to the Open IAP specification
-
+
## 📚 Documentation
@@ -127,7 +127,7 @@ kmpIAP.finishTransaction(
## Powered by OpenIAP
-
+
kmp-iap conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
diff --git a/libraries/kmp-iap/images/create_release_and_tag.png b/libraries/kmp-iap/images/create_release_and_tag.png
deleted file mode 100644
index 737d45e73..000000000
Binary files a/libraries/kmp-iap/images/create_release_and_tag.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/create_release_and_tag.webp b/libraries/kmp-iap/images/create_release_and_tag.webp
new file mode 100644
index 000000000..1b8eab1c5
Binary files /dev/null and b/libraries/kmp-iap/images/create_release_and_tag.webp differ
diff --git a/libraries/kmp-iap/images/draft_release.png b/libraries/kmp-iap/images/draft_release.png
deleted file mode 100644
index 1687c9782..000000000
Binary files a/libraries/kmp-iap/images/draft_release.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/draft_release.webp b/libraries/kmp-iap/images/draft_release.webp
new file mode 100644
index 000000000..58cdd29fd
Binary files /dev/null and b/libraries/kmp-iap/images/draft_release.webp differ
diff --git a/libraries/kmp-iap/images/github_releases.png b/libraries/kmp-iap/images/github_releases.png
deleted file mode 100644
index 64a917593..000000000
Binary files a/libraries/kmp-iap/images/github_releases.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/github_releases.webp b/libraries/kmp-iap/images/github_releases.webp
new file mode 100644
index 000000000..4bcc14c82
Binary files /dev/null and b/libraries/kmp-iap/images/github_releases.webp differ
diff --git a/libraries/kmp-iap/images/github_secrets.png b/libraries/kmp-iap/images/github_secrets.png
deleted file mode 100644
index 0118ec8f1..000000000
Binary files a/libraries/kmp-iap/images/github_secrets.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/github_secrets.webp b/libraries/kmp-iap/images/github_secrets.webp
new file mode 100644
index 000000000..1af6e744a
Binary files /dev/null and b/libraries/kmp-iap/images/github_secrets.webp differ
diff --git a/libraries/kmp-iap/images/published_on_maven_central.png b/libraries/kmp-iap/images/published_on_maven_central.png
deleted file mode 100644
index 088ac0a0b..000000000
Binary files a/libraries/kmp-iap/images/published_on_maven_central.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/published_on_maven_central.webp b/libraries/kmp-iap/images/published_on_maven_central.webp
new file mode 100644
index 000000000..9579cd0b5
Binary files /dev/null and b/libraries/kmp-iap/images/published_on_maven_central.webp differ
diff --git a/libraries/kmp-iap/images/release_settings.png b/libraries/kmp-iap/images/release_settings.png
deleted file mode 100644
index 83976074b..000000000
Binary files a/libraries/kmp-iap/images/release_settings.png and /dev/null differ
diff --git a/libraries/kmp-iap/images/release_settings.webp b/libraries/kmp-iap/images/release_settings.webp
new file mode 100644
index 000000000..42a212228
Binary files /dev/null and b/libraries/kmp-iap/images/release_settings.webp differ
diff --git a/libraries/react-native-iap/README.md b/libraries/react-native-iap/README.md
index 66fd22f13..607b9b609 100644
--- a/libraries/react-native-iap/README.md
+++ b/libraries/react-native-iap/README.md
@@ -7,7 +7,7 @@
**React Native IAP** is a high-performance in-app purchase library using Nitro Modules that **conforms to the [Open IAP specification](https://openiap.dev)**. It provides a unified API for handling in-app purchases across iOS and Android platforms with comprehensive error handling and modern TypeScript support.
-
+
## 📚 Documentation
@@ -137,7 +137,7 @@ Quick links:
## Powered by OpenIAP
-
+
React Native IAP conforms to the **[OpenIAP specification](https://openiap.dev)** — an open, vendor-neutral interoperability standard for in-app purchases. OpenIAP provides:
diff --git a/libraries/react-native-iap/example/amazon.sdktester.json b/libraries/react-native-iap/example/amazon.sdktester.json
index 43cd5e901..cd536dfc8 100644
--- a/libraries/react-native-iap/example/amazon.sdktester.json
+++ b/libraries/react-native-iap/example/amazon.sdktester.json
@@ -4,28 +4,28 @@
"price": 0.99,
"title": "10 Bulbs",
"description": "A small pack of bulbs for testing consumable purchases",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.30bulbs": {
"itemType": "CONSUMABLE",
"price": 1.99,
"title": "30 Bulbs",
"description": "A larger pack of bulbs for testing consumable purchases",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.certified": {
"itemType": "ENTITLED",
"price": 4.99,
"title": "Certified",
"description": "A non-consumable entitlement for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png"
+ "smallIconUrl": "https://openiap.dev/logo.webp"
},
"dev.hyo.martie.premium": {
"itemType": "SUBSCRIPTION",
"price": 4.99,
"title": "Premium Monthly",
"description": "Monthly premium access for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png",
+ "smallIconUrl": "https://openiap.dev/logo.webp",
"subscriptionBase": "dev.hyo.martie.premium.base",
"subscriptionParent": "dev.hyo.martie.premium.parent",
"term": "Monthly"
@@ -35,7 +35,7 @@
"price": 49.99,
"title": "Premium Yearly",
"description": "Yearly premium access for OpenIAP example testing",
- "smallIconUrl": "https://openiap.dev/img/logo.png",
+ "smallIconUrl": "https://openiap.dev/logo.webp",
"subscriptionBase": "dev.hyo.martie.premium.base",
"subscriptionParent": "dev.hyo.martie.premium.parent",
"term": "Yearly"
diff --git a/logo.png b/logo.png
index 6dd62592b..da177bd60 100644
Binary files a/logo.png and b/logo.png differ
diff --git a/logo.webp b/logo.webp
new file mode 100644
index 000000000..10d21cc33
Binary files /dev/null and b/logo.webp differ
diff --git a/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.png b/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.png
deleted file mode 100644
index 5a81a5f40..000000000
Binary files a/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.png and /dev/null differ
diff --git a/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.webp b/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.webp
new file mode 100644
index 000000000..dae6a5fa8
Binary files /dev/null and b/packages/docs/public/docs/images/openiap-mcp-iphone-purchase.webp differ
diff --git a/packages/docs/public/llms-full.txt b/packages/docs/public/llms-full.txt
index 15bfa1f54..40f517ab7 100644
--- a/packages/docs/public/llms-full.txt
+++ b/packages/docs/public/llms-full.txt
@@ -2245,7 +2245,7 @@ assuming DAU implies safe request volume.
replacement, generated warnings where supported, migration documentation,
and executable absence checks at the removal boundary. Patch and minor
releases must not remove them early.
-- See https://openiap.dev/docs/updates/deprecations for the complete mapping.
+- See https://openiap.dev/docs/updates/migration for the complete mapping.
---
diff --git a/packages/docs/public/llms.txt b/packages/docs/public/llms.txt
index eb56fa2db..3f8c6fc63 100644
--- a/packages/docs/public/llms.txt
+++ b/packages/docs/public/llms.txt
@@ -84,7 +84,7 @@ Current NuGet package version: 2.3.0
replacement, generated warnings where supported, migration documentation,
and executable absence checks at the removal boundary. Patch and minor
releases must not remove them early.
-- See https://openiap.dev/docs/updates/deprecations for the complete mapping.
+- See https://openiap.dev/docs/updates/migration for the complete mapping.
## Core APIs
diff --git a/packages/docs/public/logos/android.webp b/packages/docs/public/logos/android.webp
new file mode 100644
index 000000000..e2bde900c
Binary files /dev/null and b/packages/docs/public/logos/android.webp differ
diff --git a/packages/docs/public/logos/apple.webp b/packages/docs/public/logos/apple.webp
new file mode 100644
index 000000000..30fa54d6f
Binary files /dev/null and b/packages/docs/public/logos/apple.webp differ
diff --git a/packages/docs/public/logos/expo-iap.webp b/packages/docs/public/logos/expo-iap.webp
new file mode 100644
index 000000000..ca092afcb
Binary files /dev/null and b/packages/docs/public/logos/expo-iap.webp differ
diff --git a/packages/docs/public/logos/expo.webp b/packages/docs/public/logos/expo.webp
new file mode 100644
index 000000000..e1f7014be
Binary files /dev/null and b/packages/docs/public/logos/expo.webp differ
diff --git a/packages/docs/public/logos/flutter.webp b/packages/docs/public/logos/flutter.webp
new file mode 100644
index 000000000..8fc9151c9
Binary files /dev/null and b/packages/docs/public/logos/flutter.webp differ
diff --git a/packages/docs/public/logos/flutter_inapp_purchase.webp b/packages/docs/public/logos/flutter_inapp_purchase.webp
new file mode 100644
index 000000000..eb949644e
Binary files /dev/null and b/packages/docs/public/logos/flutter_inapp_purchase.webp differ
diff --git a/packages/docs/public/logos/godot-iap.webp b/packages/docs/public/logos/godot-iap.webp
new file mode 100644
index 000000000..456917b52
Binary files /dev/null and b/packages/docs/public/logos/godot-iap.webp differ
diff --git a/packages/docs/public/logos/godot.webp b/packages/docs/public/logos/godot.webp
new file mode 100644
index 000000000..88c30da77
Binary files /dev/null and b/packages/docs/public/logos/godot.webp differ
diff --git a/packages/docs/public/logos/horizonos.webp b/packages/docs/public/logos/horizonos.webp
new file mode 100644
index 000000000..a59001a73
Binary files /dev/null and b/packages/docs/public/logos/horizonos.webp differ
diff --git a/packages/docs/public/logos/kmp-iap.webp b/packages/docs/public/logos/kmp-iap.webp
new file mode 100644
index 000000000..e7ba73c49
Binary files /dev/null and b/packages/docs/public/logos/kmp-iap.webp differ
diff --git a/packages/docs/public/logos/kmp.webp b/packages/docs/public/logos/kmp.webp
new file mode 100644
index 000000000..89142a305
Binary files /dev/null and b/packages/docs/public/logos/kmp.webp differ
diff --git a/packages/docs/public/logos/maui-iap.webp b/packages/docs/public/logos/maui-iap.webp
new file mode 100644
index 000000000..40fb31e7f
Binary files /dev/null and b/packages/docs/public/logos/maui-iap.webp differ
diff --git a/packages/docs/public/logos/openiap-apple.webp b/packages/docs/public/logos/openiap-apple.webp
new file mode 100644
index 000000000..020f6be73
Binary files /dev/null and b/packages/docs/public/logos/openiap-apple.webp differ
diff --git a/packages/docs/public/logos/openiap-google.webp b/packages/docs/public/logos/openiap-google.webp
new file mode 100644
index 000000000..49f0df6f6
Binary files /dev/null and b/packages/docs/public/logos/openiap-google.webp differ
diff --git a/packages/docs/public/logos/openiap-gql.webp b/packages/docs/public/logos/openiap-gql.webp
new file mode 100644
index 000000000..1530de356
Binary files /dev/null and b/packages/docs/public/logos/openiap-gql.webp differ
diff --git a/packages/docs/public/logos/openiap.webp b/packages/docs/public/logos/openiap.webp
new file mode 100644
index 000000000..8cb27a248
Binary files /dev/null and b/packages/docs/public/logos/openiap.webp differ
diff --git a/packages/docs/public/logos/react-native-iap.webp b/packages/docs/public/logos/react-native-iap.webp
new file mode 100644
index 000000000..2d5ab731a
Binary files /dev/null and b/packages/docs/public/logos/react-native-iap.webp differ
diff --git a/packages/docs/public/logos/react.webp b/packages/docs/public/logos/react.webp
new file mode 100644
index 000000000..726873c82
Binary files /dev/null and b/packages/docs/public/logos/react.webp differ
diff --git a/packages/docs/public/sitemap.xml b/packages/docs/public/sitemap.xml
index eee34ad34..e7360ff30 100644
--- a/packages/docs/public/sitemap.xml
+++ b/packages/docs/public/sitemap.xml
@@ -158,7 +158,7 @@
0.7
- https://openiap.dev/docs/updates/deprecations
+ https://openiap.dev/docs/updates/migration2026-07-24monthly0.8
diff --git a/packages/docs/scripts/render-mcp-demo-video.mjs b/packages/docs/scripts/render-mcp-demo-video.mjs
index eec1e9482..de089dbb5 100644
--- a/packages/docs/scripts/render-mcp-demo-video.mjs
+++ b/packages/docs/scripts/render-mcp-demo-video.mjs
@@ -31,9 +31,9 @@ const outputPath = resolve(
);
const purchaseCapturePath = resolve(
scriptDir,
- '../public/docs/images/openiap-mcp-iphone-purchase.png'
+ '../public/docs/images/openiap-mcp-iphone-purchase.webp'
);
-const purchaseCaptureDataUri = `data:image/png;base64,${readFileSync(
+const purchaseCaptureDataUri = `data:image/webp;base64,${readFileSync(
purchaseCapturePath
).toString('base64')}`;
diff --git a/packages/docs/src/components/EcosystemDiagram.tsx b/packages/docs/src/components/EcosystemDiagram.tsx
new file mode 100644
index 000000000..5a566ae04
--- /dev/null
+++ b/packages/docs/src/components/EcosystemDiagram.tsx
@@ -0,0 +1,289 @@
+import { Link } from 'react-router-dom';
+import { LIBRARIES, type FrameworkLibraryName } from '../lib/images';
+import { OPENIAP_VERSIONS } from '../lib/versioning';
+import '../styles/ecosystem-diagram.css';
+
+// =============================================================================
+// Ecosystem Diagram Data
+// =============================================================================
+// Framework libraries are deliberately NOT listed in this file. Membership,
+// order, display name, version, setup path and the fallback framework mark all
+// come from LIBRARIES in src/lib/images.ts (SSOT, see CONVENTION.md "Framework
+// Listings"). Adding a library there makes it show up here with no edit below.
+//
+// Artwork lives in public/logos as <= 256px .webp files. To swap an image, drop
+// the new file in and repoint the matching entry.
+// =============================================================================
+
+const GITHUB = 'https://github.com/hyodotdev/openiap';
+const GITHUB_TREE = `${GITHUB}/tree/main`;
+
+/** Rendered when a library ships no OpenIAP package artwork of its own. */
+const FALLBACK_PACKAGE_ICON = '/logos/openiap.webp';
+
+/** Package artwork per framework library. Missing key -> FALLBACK_PACKAGE_ICON. */
+const PACKAGE_ICONS: Partial> = {
+ 'expo-iap': '/logos/expo-iap.webp',
+ 'react-native-iap': '/logos/react-native-iap.webp',
+ flutter_inapp_purchase: '/logos/flutter_inapp_purchase.webp',
+ 'kmp-iap': '/logos/kmp-iap.webp',
+ 'maui-iap': '/logos/maui-iap.webp',
+ 'godot-iap': '/logos/godot-iap.webp',
+};
+
+/** Target framework mark. Missing key -> lib.image from LIBRARY_IMAGES. */
+const FRAMEWORK_MARKS: Partial> = {
+ 'expo-iap': '/logos/expo.webp',
+ 'react-native-iap': '/logos/react.webp',
+ flutter_inapp_purchase: '/logos/flutter.webp',
+ 'kmp-iap': '/logos/kmp.webp',
+ 'godot-iap': '/logos/godot.webp',
+ // maui-iap falls back to /frameworks/maui.webp.
+};
+
+/** Flat black artwork - has to flip to white in dark mode. */
+const BLACK_INK_ART = new Set(['/logos/apple.webp', '/logos/expo.webp']);
+
+/** Flat white artwork - has to flip to black in light mode. */
+const WHITE_INK_ART = new Set(['/logos/horizonos.webp']);
+
+/**
+ * Marks whose glyph is narrower than the square box it is fitted into, so they
+ * read smaller than a full-bleed neighbour even at the same height. Apple's
+ * mark renders 18.5px wide next to Horizon OS's 22px ring; this scales the box
+ * so the drawn widths match.
+ */
+const COMPACT_MARKS = new Set(['/logos/apple.webp']);
+
+/** Artwork that already carries its own opaque square background. */
+const SQUARE_ART = new Set([
+ '/logos/openiap-gql.webp',
+ '/logos/expo-iap.webp',
+ '/logos/react-native-iap.webp',
+ '/logos/flutter_inapp_purchase.webp',
+ '/logos/kmp-iap.webp',
+ '/frameworks/maui.webp',
+]);
+
+interface DiagramMark {
+ src: string;
+ label: string;
+}
+
+interface DiagramNode {
+ id: string;
+ name: string;
+ note: string;
+ icon: string;
+ href: string;
+ marks?: DiagramMark[];
+}
+
+const SPEC_NODES: DiagramNode[] = [
+ {
+ id: 'openiap',
+ name: 'openiap',
+ note: `The specification · ${OPENIAP_VERSIONS.spec}`,
+ icon: '/logos/openiap.webp',
+ href: GITHUB,
+ },
+ {
+ id: 'openiap-gql',
+ name: 'openiap-gql',
+ note: 'GraphQL schema · type SSOT',
+ icon: '/logos/openiap-gql.webp',
+ href: `${GITHUB_TREE}/packages/gql`,
+ },
+];
+
+const CORE_NODES: DiagramNode[] = [
+ {
+ id: 'openiap-google',
+ name: 'openiap-google',
+ note: `Kotlin · Play Billing · ${OPENIAP_VERSIONS.google}`,
+ icon: '/logos/openiap-google.webp',
+ href: `${GITHUB_TREE}/packages/google`,
+ marks: [
+ { src: '/logos/android.webp', label: 'Android' },
+ { src: '/logos/horizonos.webp', label: 'Horizon OS' },
+ ],
+ },
+ {
+ id: 'openiap-apple',
+ name: 'openiap-apple',
+ note: `Swift · StoreKit 2 · ${OPENIAP_VERSIONS.apple}`,
+ icon: '/logos/openiap-apple.webp',
+ href: `${GITHUB_TREE}/packages/apple`,
+ marks: [{ src: '/logos/apple.webp', label: 'iOS, macOS and tvOS' }],
+ },
+];
+
+function artClass(src: string, base: string): string {
+ const classes = [base];
+
+ if (SQUARE_ART.has(src)) {
+ classes.push('eco-art--square');
+ }
+
+ if (BLACK_INK_ART.has(src)) {
+ classes.push('eco-art--black-ink');
+ }
+
+ if (WHITE_INK_ART.has(src)) {
+ classes.push('eco-art--white-ink');
+ }
+
+ if (COMPACT_MARKS.has(src)) {
+ classes.push('eco-art--compact');
+ }
+
+ return classes.join(' ');
+}
+
+function NodeCard({
+ node,
+ className,
+}: {
+ node: DiagramNode;
+ className?: string;
+}) {
+ return (
+
+
+
+ {node.name}
+ {node.note}
+
+ {node.marks ? (
+
+ {node.marks.map((mark) => (
+
+ ))}
+
+ ) : null}
+
+ );
+}
+
+function Rail({
+ variant,
+ label,
+}: {
+ variant: 'a' | 'b' | 'bypass';
+ label: string;
+}) {
+ return (
+
diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx
index bf24c4475..3b7a88295 100644
--- a/packages/docs/src/pages/docs/index.tsx
+++ b/packages/docs/src/pages/docs/index.tsx
@@ -5,6 +5,7 @@ import type {
PointerEvent as ReactPointerEvent,
} from 'react';
import { createPortal } from 'react-dom';
+import { Bookmark } from 'lucide-react';
import {
Route,
Routes,
@@ -122,7 +123,7 @@ import KmpSetup from './setup/kmp';
import MauiSetup from './setup/maui';
import Example from './example';
import Announcements from './updates/announcements';
-import Deprecations from './updates/deprecations';
+import Migration from './updates/migration';
import Releases from './updates/releases';
import Versions from './updates/versions';
import AIAssistants from './guides/ai-assistants';
@@ -147,10 +148,17 @@ function NavigatePreservingHash({ to }: { to: string }) {
}
const SIDEBAR_WIDTH_STORAGE_KEY = 'openiap-docs-sidebar-width-v2';
+const SIDEBAR_COLLAPSED_STORAGE_KEY = 'openiap-docs-sidebar-collapsed-v1';
const SIDEBAR_DEFAULT_WIDTH = 340;
const SIDEBAR_MIN_WIDTH = 300;
const SIDEBAR_MAX_WIDTH = 480;
const SIDEBAR_KEYBOARD_STEP = 16;
+// Drag this far past the minimum and the sidebar snaps shut instead of
+// refusing to move: below ~300px the nav labels start truncating, so hiding it
+// outright is more useful than an unreadably narrow column.
+const SIDEBAR_COLLAPSE_SLACK = 60;
+// Pointer travel that separates "clicked the handle" from "dragged the handle".
+const SIDEBAR_DRAG_THRESHOLD = 4;
function clampSidebarWidth(width: number) {
return Math.min(
@@ -172,14 +180,31 @@ function readSavedSidebarWidth() {
: SIDEBAR_DEFAULT_WIDTH;
}
+function readSavedSidebarCollapsed() {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ return window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === 'true';
+}
+
function Docs() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [sidebarWidth, setSidebarWidth] = useState(readSavedSidebarWidth);
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(
+ readSavedSidebarCollapsed
+ );
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
const [isSidebarScrolling, setIsSidebarScrolling] = useState(false);
const sidebarRef = useRef(null);
const sidebarScrollTimeoutRef = useRef(null);
+ const dragRef = useRef<{
+ startX: number;
+ moved: boolean;
+ startedCollapsed: boolean;
+ reopened: boolean;
+ } | null>(null);
const closeSidebar = () => setIsSidebarOpen(false);
@@ -199,6 +224,13 @@ function Docs() {
);
}, [sidebarWidth]);
+ useEffect(() => {
+ window.localStorage.setItem(
+ SIDEBAR_COLLAPSED_STORAGE_KEY,
+ String(isSidebarCollapsed)
+ );
+ }, [isSidebarCollapsed]);
+
useEffect(() => {
return () => {
if (sidebarScrollTimeoutRef.current !== null) {
@@ -218,11 +250,62 @@ function Docs() {
document.body.style.userSelect = 'none';
const handlePointerMove = (event: PointerEvent) => {
+ const drag = dragRef.current;
+
+ if (!drag) {
+ return;
+ }
+
+ if (
+ !drag.moved &&
+ Math.abs(event.clientX - drag.startX) < SIDEBAR_DRAG_THRESHOLD
+ ) {
+ return;
+ }
+
+ drag.moved = true;
+
const sidebarLeft = sidebarRef.current?.getBoundingClientRect().left ?? 0;
- setSidebarWidth(clampSidebarWidth(event.clientX - sidebarLeft));
+ const nextWidth = event.clientX - sidebarLeft;
+
+ if (drag.startedCollapsed) {
+ // Collapsed, the sidebar is 0 wide and the handle sits on its edge, so
+ // nextWidth is only ~25px however the pointer moves. Falling through to
+ // the collapse branch below would make any small jitter a no-op and
+ // strand the user - above 768px this handle is the only way back. Only
+ // a deliberate drag out past the minimum reopens; anything shorter is
+ // left to stopResizing, which treats the release as a toggle.
+ if (nextWidth >= SIDEBAR_MIN_WIDTH - SIDEBAR_COLLAPSE_SLACK) {
+ drag.reopened = true;
+ setIsSidebarCollapsed(false);
+ setSidebarWidth(clampSidebarWidth(nextWidth));
+ }
+
+ return;
+ }
+
+ if (nextWidth < SIDEBAR_MIN_WIDTH - SIDEBAR_COLLAPSE_SLACK) {
+ // Dragged past the minimum: snap shut and end the drag. Clear the drag
+ // so a trailing pointerup cannot toggle it straight back open.
+ dragRef.current = null;
+ setIsSidebarCollapsed(true);
+ setIsResizingSidebar(false);
+ return;
+ }
+
+ setSidebarWidth(clampSidebarWidth(nextWidth));
};
const stopResizing = () => {
+ const drag = dragRef.current;
+
+ // Toggle on a plain click, and also when a gesture that started collapsed
+ // never travelled far enough to count as a reopen drag.
+ if (drag && !drag.reopened && (!drag.moved || drag.startedCollapsed)) {
+ setIsSidebarCollapsed((collapsed) => !collapsed);
+ }
+
+ dragRef.current = null;
setIsResizingSidebar(false);
};
@@ -239,12 +322,18 @@ function Docs() {
};
}, [isResizingSidebar]);
- const startSidebarResize = (event: ReactPointerEvent) => {
+ const startSidebarResize = (event: ReactPointerEvent) => {
if (window.innerWidth <= 768) {
return;
}
event.preventDefault();
+ dragRef.current = {
+ startX: event.clientX,
+ moved: false,
+ startedCollapsed: isSidebarCollapsed,
+ reopened: false,
+ };
setIsResizingSidebar(true);
};
@@ -262,8 +351,32 @@ function Docs() {
};
const handleSidebarResizerKeyDown = (
- event: KeyboardEvent
+ event: KeyboardEvent
) => {
+ // Enter/Space are the handle's primary action, matching a plain click.
+ // preventDefault stops the browser also synthesizing a click from them.
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ setIsSidebarCollapsed((collapsed) => !collapsed);
+ return;
+ }
+
+ if (isSidebarCollapsed) {
+ // Collapsed, so the only meaningful direction is back open.
+ if (event.key === 'ArrowRight') {
+ event.preventDefault();
+ setIsSidebarCollapsed(false);
+ }
+ return;
+ }
+
+ // At the minimum width the only place left to go is closed.
+ if (event.key === 'ArrowLeft' && sidebarWidth === SIDEBAR_MIN_WIDTH) {
+ event.preventDefault();
+ setIsSidebarCollapsed(true);
+ return;
+ }
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
setSidebarWidth((width) =>
@@ -285,11 +398,6 @@ function Docs() {
event.preventDefault();
setSidebarWidth(SIDEBAR_MAX_WIDTH);
}
-
- if (event.key === 'Enter') {
- event.preventDefault();
- setSidebarWidth(SIDEBAR_DEFAULT_WIDTH);
- }
};
const sidebarStyle = {
@@ -337,15 +445,24 @@ function Docs() {
);
return (
-
+
{sidebarToggle}
- {isSidebarOpen && (
-
- )}
+ {/* Kept mounted so it can fade OUT with the drawer; unmounting it on
+ close made the backdrop vanish instantly while the drawer was still
+ sliding. Hidden state is visibility + pointer-events, not unmount. */}
+
setSidebarWidth(SIDEBAR_DEFAULT_WIDTH)}
- onKeyDown={handleSidebarResizerKeyDown}
- />
+ } ${isSidebarCollapsed ? 'is-collapsed' : ''}`}
+ >
+ {/* One bookmark-style handle does both jobs: click toggles the
+ sidebar, drag resizes it. A drag is only recognized past
+ SIDEBAR_DRAG_THRESHOLD px, so a plain click never nudges the
+ width. Keyboard users get arrow keys instead of the drag. */}
+
+
}
/>
} />
- } />
+ } />
+ {/* the page was published at updates/deprecations until 2026-08;
+ keep the old path working for existing links and search results */}
+ }
+ />
} />
} />
diff --git a/packages/docs/src/pages/docs/lifecycle/subscription.tsx b/packages/docs/src/pages/docs/lifecycle/subscription.tsx
index 8cbea1544..419323b7e 100644
--- a/packages/docs/src/pages/docs/lifecycle/subscription.tsx
+++ b/packages/docs/src/pages/docs/lifecycle/subscription.tsx
@@ -104,7 +104,9 @@ function Subscription() {
✅ pendingUpgradeProductId
-
❌
+
+ ✅ pendingPurchaseUpdateAndroid
+
✅
@@ -128,7 +130,10 @@ function Subscription() {
✅ isInBillingRetry
-
❌
+
+ ⚠️ isSuspendedAndroid (state only, no retry
+ details)
+
✅
@@ -158,9 +163,14 @@ function Subscription() {
, but server validation is still recommended for production apps.
- Android: Only isAutoRenewing available
- client-side. Server-side validation is required for
- complete subscription management.
+ Android: client-side{' '}
+ subscription lifecycle data is limited to{' '}
+ isAutoRenewing, isSuspendedAndroid, and{' '}
+ pendingPurchaseUpdateAndroid — the purchase itself
+ still carries productId, purchaseToken,{' '}
+ transactionDate, and purchaseState. Expiry
+ and renewal dates and detailed subscription state require{' '}
+ server-side validation.
Both platforms: Use{' '}
@@ -177,11 +187,12 @@ function Subscription() {
Recommendation: Use{' '}
- iapkit
+ IAPKit
{' '}
for server-side verification to get unified subscription data across
both platforms—including all the iOS-only fields for Android
@@ -220,8 +231,21 @@ function Subscription() {
getAvailablePurchases
- : Returns all purchases including expired subscriptions. Useful for
- showing purchase history.
+ : Returns the purchases the store still holds — owned
+ non-consumables, active subscriptions, and unfinished transactions.
+ It is not a purchase-history source on either
+ platform. The framework SDKs default{' '}
+ onlyIncludeActiveItemsIOS to true, so iOS
+ reads Transaction.currentEntitlements; pass{' '}
+ {'{ onlyIncludeActiveItemsIOS: false }'} to read{' '}
+ Transaction.all including expired and revoked entries.
+ Android's queryPurchasesAsync returns only
+ currently-owned purchases and has no equivalent option. For full iOS
+ history use{' '}
+
+ getAllTransactionsIOS
+
+ .
@@ -253,8 +277,8 @@ function Subscription() {
defaultOpen
>
- Setting up server-side verification can be complex. OpenIAP's
- partner{' '}
+ Setting up server-side verification can be complex. OpenIAP's
+ hosted backend{' '}
+ A delivered PurchaseIOS always carries{' '}
+ purchaseState: 'purchased' — StoreKit
+ only hands the listener completed transactions, so the{' '}
+ pending and unknown members of the
+ shared enum never appear on iOS. Anything that is not a
+ completed purchase arrives through{' '}
+ purchaseErrorListener instead: Ask to Buy and
+ other deferred payments as{' '}
+ ErrorCode.DeferredPayment (
+ 'deferred-payment'), cancellations as{' '}
+ ErrorCode.UserCancelled.
),
android: (
-
1. requestPurchase(sku) → Google Play payment sheet
This type is available on{' '}
-
+
PurchaseIOS
{' '}
and{' '}
@@ -817,12 +849,11 @@ function Subscription() {
in milliseconds).
- expirationReason: Why the subscription
- expired — "VOLUNTARY",{' '}
- "BILLING_ERROR",{' '}
- "DID_NOT_AGREE_TO_PRICE_INCREASE",{' '}
- "PRODUCT_NOT_AVAILABLE", or{' '}
- "UNKNOWN".
+ expirationReason: StoreKit's raw
+ integer expiration-reason value represented as a string (
+ "1", "2", …),
+ not a symbolic name. Preserve unknown future values rather
+ than mapping them to a fallback.
gracePeriodExpirationDate: Grace period end
@@ -833,8 +864,12 @@ function Subscription() {
currently retrying a failed payment.
- offerType / offerIdentifier: Information
- about any active promotional offer.
+ renewalOfferId / renewalOfferType: The
+ offer applied to the next renewal.{' '}
+ renewalOfferType carries values such as{' '}
+ "PROMOTIONAL",{' '}
+ "SUBSCRIPTION_OFFER_CODE", and{' '}
+ "WIN_BACK".
@@ -1041,13 +1076,24 @@ function Subscription() {
isAutoRenewing: Whether the subscription
- will auto-renew (the only renewal-related field)
+ will auto-renew
- purchaseTime: Original purchase timestamp
+ isSuspendedAndroid: Whether the
+ subscription is suspended by a billing issue
- purchaseState: PURCHASED, PENDING, etc.
+ pendingPurchaseUpdateAndroid: The product
+ IDs and purchase token of an uncommitted plan change (Play
+ Billing 5.0+)
+
+
+ transactionDate: Original purchase
+ timestamp (Unix milliseconds)
+
+
+ purchaseState: purchased,{' '}
+ pending, or unknown
@@ -1064,13 +1110,29 @@ function Subscription() {
Next renewal date / Expiration date
-
Pending upgrade/downgrade information
+
+ The effective date of a pending upgrade/downgrade — the
+ pending change itself is on the client as{' '}
+ pendingPurchaseUpdateAndroid
+
Expiration reason
Grace period status
-
Billing retry status
- Detailed subscription state (ACTIVE, CANCELED, PAUSED,
- ON_HOLD, IN_GRACE_PERIOD, EXPIRED)
+ Billing retry details (attempt counts, next retry
+ time) — the billing-issue state itself is on the client as{' '}
+ isSuspendedAndroid
+
- subscriptionState: Current state (ACTIVE,
- CANCELED, IN_GRACE_PERIOD, ON_HOLD, PAUSED, EXPIRED)
+ subscriptionState: Current state (
+ SUBSCRIPTION_STATE_ACTIVE,{' '}
+ SUBSCRIPTION_STATE_CANCELED,{' '}
+ SUBSCRIPTION_STATE_IN_GRACE_PERIOD,{' '}
+ SUBSCRIPTION_STATE_ON_HOLD,{' '}
+ SUBSCRIPTION_STATE_PAUSED,{' '}
+ SUBSCRIPTION_STATE_EXPIRED,{' '}
+ SUBSCRIPTION_STATE_PENDING,{' '}
+ SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED,{' '}
+ SUBSCRIPTION_STATE_UNSPECIFIED).{' '}
+ SUBSCRIPTION_STATE_PENDING means the initial
+ purchase has not completed;{' '}
+ SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED is
+ a pending upgrade or downgrade that was cancelled — resolve
+ the still-active subscription it belongs to through{' '}
+ linkedPurchaseToken.
expiryTime: When the subscription expires
@@ -1144,8 +1220,9 @@ function Subscription() {
failed, grace period started
- SUBSCRIPTION_ON_HOLD (5): Paused due to
- billing issues
+ SUBSCRIPTION_ON_HOLD (5): Account hold
+ after the grace period ended — entitlement suspended
+ (distinct from the user-initiated PAUSED state)
- Since Android doesn't expose pending tier changes client-side,
- track them via server:
+ The client exposes the pending change itself via{' '}
+ pendingPurchaseUpdateAndroid (the new product IDs
+ and purchase token), but its effective date and linkage still
+ come from the server:
@@ -1367,7 +1446,9 @@ function Subscription() {
Android
- Only isAutoRenewing available client-side
+ Client-side subscription lifecycle data limited to{' '}
+ isAutoRenewing, isSuspendedAndroid,
+ and pendingPurchaseUpdateAndroid
Server-side required for detailed subscription info
Use Google Play Developer API for authoritative data
diff --git a/packages/docs/src/pages/docs/setup/flutter.tsx b/packages/docs/src/pages/docs/setup/flutter.tsx
index 9e6daad2a..214d5e300 100644
--- a/packages/docs/src/pages/docs/setup/flutter.tsx
+++ b/packages/docs/src/pages/docs/setup/flutter.tsx
@@ -508,7 +508,7 @@ final allPurchases = await iap.getAvailablePurchases(
dataAndroid. Flutter 10 does not accept the former
custom-channel alias. Native adapters, MethodChannel fixtures, and
mocks must emit dataAndroid. See{' '}
-
+
Deprecations & 3.0 Migration
.
diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx
index 8010af0fe..b8c931314 100644
--- a/packages/docs/src/pages/docs/types/active-subscription.tsx
+++ b/packages/docs/src/pages/docs/types/active-subscription.tsx
@@ -219,7 +219,7 @@ function ActiveSubscription() {
android: (
<>
- ActiveSubscriptionAndroid
+ Android Fields
diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx
index a1ce57665..103afa52e 100644
--- a/packages/docs/src/pages/docs/types/purchase.tsx
+++ b/packages/docs/src/pages/docs/types/purchase.tsx
@@ -744,7 +744,7 @@ function Purchase() {
Only dataAndroid is accepted. Custom adapters and
fixtures must use the canonical field. See{' '}
-
+
the migration schedule
.
diff --git a/packages/docs/src/pages/docs/updates/announcements.tsx b/packages/docs/src/pages/docs/updates/announcements.tsx
index b7a1e8e92..fe1bdd590 100644
--- a/packages/docs/src/pages/docs/updates/announcements.tsx
+++ b/packages/docs/src/pages/docs/updates/announcements.tsx
@@ -159,7 +159,7 @@ function Announcements() {
{' '}
for exact package versions, per-SDK changes, platform availability,
and release links. Before upgrading, follow the{' '}
-
+
Deprecations & 3.0 Migration catalog
.
@@ -230,7 +230,7 @@ function Announcements() {
See the complete{' '}
-
+
deprecation schedule and migration catalog
. Each package reaches its major independently, so this notice does
diff --git a/packages/docs/src/pages/docs/updates/deprecations.tsx b/packages/docs/src/pages/docs/updates/migration.tsx
similarity index 92%
rename from packages/docs/src/pages/docs/updates/deprecations.tsx
rename to packages/docs/src/pages/docs/updates/migration.tsx
index bb9a0a4cf..970e1afc9 100644
--- a/packages/docs/src/pages/docs/updates/deprecations.tsx
+++ b/packages/docs/src/pages/docs/updates/migration.tsx
@@ -478,35 +478,52 @@ const packageCompatibilityMigrations = [
},
] as const;
-function Deprecations() {
+function Migration() {
return (
-
Deprecations & 3.0 Migration
+
Migration
- The coordinated major train removes the previously deprecated,
- OpenIAP-owned compatibility surface. Use this catalog to update calls
- before upgrading.
+ One section per coordinated major train. Each train lists the versions
+ that drop the previously deprecated, OpenIAP-owned compatibility surface
+ and the canonical call to use instead.
-
- The listed versions do not include compatibility wrappers, deprecated
- schema members, or legacy custom-wire aliases. Upgrade coordinated
- native and framework dependencies together.
-
-
+ {/* ---------------------------------------------------------------
+ Migration train: 2.x -> 3.0
+ Add the next train as a sibling ABOVE this one (newest
+ first), with its own AnchorLink id (for example "v3-to-v4") and its
+ own removal table. Keep the heading ids inside a train unique across
+ the page - other docs deep-link to them. The train-independent
+ policy section stays last.
+ --------------------------------------------------------------- */}
-
+
+ 2.x → 3.0
+
+
+ The coordinated major train removes the previously deprecated,
+ OpenIAP-owned compatibility surface. Use this catalog to update calls
+ before upgrading.
+
+
+
+ The listed versions do not include compatibility wrappers, deprecated
+ schema members, or legacy custom-wire aliases. Upgrade coordinated
+ native and framework dependencies together.
+
+
+
Removal boundaries
-
OpenIAP specification and native packages
+
OpenIAP specification and native packages
@@ -530,7 +547,7 @@ function Deprecations() {
-
Framework libraries
+
Framework libraries
@@ -574,10 +591,7 @@ function Deprecations() {
Removed aliases are rejected or ignored; they never override missing
canonical input.
-
-
-
-
+
Flutter purchase payload compatibility
@@ -619,7 +633,7 @@ function Deprecations() {
The canonical id purchase identity is not deprecated.
Flutter 10 requires an explicit transactionId.
-
Issue #248 and Android raw purchase JSON
+
Issue #248 and Android raw purchase JSON
Before Flutter 9.6.1, issue{' '}
@@ -659,10 +673,7 @@ function Deprecations() {
.
-
-
-
-
+
Flutter 10 package-specific migrations
@@ -691,7 +702,7 @@ function Deprecations() {
-
Custom MethodChannel integrations
+
Custom MethodChannel integrations
Applications normally use the Dart API and never call these internal
channel names. Flutter 10 custom integrations must use the
@@ -718,7 +729,7 @@ function Deprecations() {
-
Custom MethodChannel payloads
+
Custom MethodChannel payloads
The official Dart API emits the canonical forms below. Flutter 10 no
longer normalizes the historical custom-channel inputs.
@@ -743,10 +754,7 @@ function Deprecations() {
))}
-
-
-
-
+
Removed schema migration catalog
@@ -756,7 +764,7 @@ function Deprecations() {
{migrationGroups.map((group) => (
Review the complete{' '}
-
+
Deprecations & 3.0 Migration catalog
{' '}
before upgrading. Existing bookmarks for removed API pages redirect
@@ -2389,7 +2389,7 @@ function Releases() {
fields with handwritten bridges and keeps source-first mappings,
alternative-store plan changes, and round-trip regressions
executable across wrappers. See{' '}
-
+
Deprecations & 3.0 Migration
{' '}
for the complete replacement and removal schedule.
diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css
index 08cda162f..82cc3050d 100644
--- a/packages/docs/src/styles/documentation.css
+++ b/packages/docs/src/styles/documentation.css
@@ -333,12 +333,15 @@
color: #666;
}
-.docs-sidebar-resizer {
+/* The rail carries a single bookmark-shaped handle: click it to collapse or
+ restore the sidebar, drag it to resize. Below 769px the drawer toggle in the
+ top nav owns that job instead, so the rail is hidden. */
+.docs-sidebar-rail {
display: none;
}
@media (min-width: 769px) {
- .docs-sidebar-resizer {
+ .docs-sidebar-rail {
position: sticky;
top: 56px;
z-index: 2;
@@ -350,12 +353,11 @@
align-self: flex-start;
margin-left: -9px;
margin-right: 9px;
- cursor: col-resize;
- touch-action: none;
- outline: none;
}
- .docs-sidebar-resizer::after {
+ /* Guide line for the drag only. It spans the full height, so showing it on
+ hover reads as a stray scrollbar; the bookmark already marks the edge. */
+ .docs-sidebar-rail::after {
content: '';
position: absolute;
top: var(--spacing-md);
@@ -366,13 +368,131 @@
background: var(--primary-color);
opacity: 0;
transition: opacity 0.15s ease;
+ pointer-events: none;
}
- .docs-sidebar-resizer:hover::after,
- .docs-sidebar-resizer:focus-visible::after,
- .docs-sidebar-resizer.is-resizing::after {
+ .docs-sidebar-rail.is-resizing::after {
opacity: 0.8;
}
+
+ .docs-sidebar-rail.is-collapsed::after {
+ opacity: 0;
+ }
+
+ .docs-sidebar-handle {
+ --docs-bookmark: #e0a94f;
+ --docs-bookmark-hover: #cf9438;
+
+ position: absolute;
+ top: 10px;
+ /* Rotated a quarter turn, the bookmark's notch points back at the sidebar,
+ so straddling the border reads as "clipped over the edge". The rail is
+ 18px wide with a -9px margin, i.e. its midpoint IS the border. */
+ left: 1px;
+ display: flex;
+ width: 26px;
+ height: 18px;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--docs-bookmark);
+ /* Click is the primary action, so a pointer - not col-resize, which made
+ it look like a scrollbar grip. The drag cursor is set on
for the
+ duration of an actual drag instead. */
+ cursor: pointer;
+ touch-action: none;
+ transition:
+ color 0.15s ease,
+ background 0.15s ease;
+ }
+
+ .docs-sidebar-handle:hover,
+ .docs-sidebar-rail.is-resizing .docs-sidebar-handle {
+ background: rgba(224, 169, 79, 0.16);
+ color: var(--docs-bookmark-hover);
+ }
+
+ .docs-sidebar-handle:focus-visible {
+ outline: 2px solid var(--docs-bookmark-hover);
+ outline-offset: 2px;
+ }
+
+ /* Collapsed: the sidebar is out of the flow entirely and the handle becomes
+ a standalone tab, so it reads as "open me" rather than a resize grip. */
+ /* Collapse by animating the column shut rather than display:none, which
+ cannot transition. visibility flips only after the width finishes, so the
+ hidden links leave the tab order without cutting the animation short. */
+ .docs-sidebar {
+ transition:
+ flex-basis 0.24s ease,
+ width 0.24s ease,
+ opacity 0.16s ease,
+ visibility 0s linear;
+ }
+
+ /* A drag sets a new width on every pointermove; animating each of those makes
+ the column lag the cursor by the transition duration. Snap while dragging,
+ animate only the collapse/restore. */
+ .docs-sidebar.is-resizing,
+ .docs-container.is-sidebar-collapsed .docs-sidebar.is-resizing {
+ transition: none;
+ }
+
+ .docs-container.is-sidebar-collapsed .docs-sidebar {
+ flex-basis: 0;
+ width: 0;
+ min-width: 0;
+ overflow: hidden;
+ opacity: 0;
+ visibility: hidden;
+ /* width:0 with box-sizing:border-box still leaves the 1px border behind */
+ border-right-width: 0;
+ pointer-events: none;
+ transition:
+ flex-basis 0.24s ease,
+ width 0.24s ease,
+ opacity 0.16s ease,
+ visibility 0s linear 0.24s;
+ }
+
+ /* .docs-content stays capped at the prose measure, so without the sidebar it
+ would hug the left edge and leave the freed space dead on the right. An
+ `auto` margin cannot be interpolated, so pad the container instead - that
+ animates, and keeps the content sliding rather than jumping. */
+ .docs-container {
+ transition: padding-left 0.24s ease;
+ }
+
+ /* the 18px rail stays in flow, so discount it to land visually centred */
+ .docs-container.is-sidebar-collapsed {
+ padding-left: max(0px, calc((100% - 900px) / 2 - 18px));
+ }
+
+ .docs-sidebar-rail.is-collapsed .docs-sidebar-handle {
+ left: 9px;
+ }
+}
+
+/* Selectors here must match or beat the rules they suppress: the collapsed and
+ open states re-declare `transition` at (0,3,0) and (0,2,0), so a bare
+ `.docs-sidebar` at (0,1,0) would lose and only the restore direction would
+ stop animating. */
+@media (prefers-reduced-motion: reduce) {
+ .docs-sidebar-rail::after,
+ .docs-sidebar-handle,
+ .docs-sidebar,
+ .docs-sidebar.open,
+ .docs-sidebar.is-resizing,
+ .docs-container,
+ .docs-container.is-sidebar-collapsed .docs-sidebar,
+ .docs-container.is-sidebar-collapsed .docs-sidebar.is-resizing,
+ .sidebar-overlay,
+ .sidebar-overlay.is-visible {
+ transition: none;
+ }
}
/* Nested sub-menu (used inside an outer MenuDropdown's items) */
@@ -729,19 +849,27 @@
min-width: 0;
}
+ /* Headers are nowrap site-wide, which under table-layout:fixed makes
+ "Last compatible major" overflow its track and collide with the next
+ header. Inside this fixed-width table they wrap instead. */
+ .doc-page table.deprecation-schedule-table th {
+ white-space: normal;
+ overflow-wrap: break-word;
+ }
+
.doc-page table.deprecation-schedule-table th:first-child,
.doc-page table.deprecation-schedule-table td:first-child {
- width: 30%;
+ width: 40%;
}
.doc-page table.deprecation-schedule-table th:nth-child(2),
.doc-page table.deprecation-schedule-table td:nth-child(2) {
- width: 26%;
+ width: 30%;
}
.doc-page table.deprecation-schedule-table th:nth-child(3),
.doc-page table.deprecation-schedule-table td:nth-child(3) {
- width: 44%;
+ width: 30%;
}
}
@@ -1068,6 +1196,21 @@
display: block;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition:
+ opacity 0.26s ease,
+ visibility 0s linear 0.26s;
+ }
+
+ .sidebar-overlay.is-visible {
+ opacity: 1;
+ visibility: visible;
+ pointer-events: auto;
+ transition:
+ opacity 0.26s ease,
+ visibility 0s linear;
}
.docs-sidebar {
@@ -1089,10 +1232,14 @@
max-height: calc(100vh - 56px) !important;
background: var(--bg-secondary);
z-index: 9000;
+ /* Slide only. Fading it out at the same time made the drawer read as
+ "vanishing" instead of "sliding away", and the 0.2s opacity/visibility
+ against a 0.25s transform meant it went invisible mid-slide. visibility
+ now flips only after the slide finishes, so the exit stays on screen for
+ its whole travel while still leaving the tab order afterwards. */
transition:
- transform 0.25s ease,
- opacity 0.2s ease,
- visibility 0.2s ease;
+ transform 0.26s cubic-bezier(0.4, 0, 0.2, 1),
+ visibility 0s linear 0.26s;
border-right: 1px solid var(--border-color);
padding: var(--spacing-xl) 0;
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
@@ -1103,7 +1250,6 @@
hit-test area, and pointer-events: none turns off hit-testing for
this entire subtree even if the transform isn't honored. */
transform: translateX(-100%);
- opacity: 0;
visibility: hidden;
pointer-events: none;
overflow-y: auto;
@@ -1118,9 +1264,11 @@
.docs-sidebar.open {
transform: translateX(0);
- opacity: 1;
visibility: visible;
pointer-events: auto;
+ transition:
+ transform 0.26s cubic-bezier(0.4, 0, 0.2, 1),
+ visibility 0s linear;
}
.docs-content {
diff --git a/packages/docs/src/styles/ecosystem-diagram.css b/packages/docs/src/styles/ecosystem-diagram.css
new file mode 100644
index 000000000..4ae4c77ca
--- /dev/null
+++ b/packages/docs/src/styles/ecosystem-diagram.css
@@ -0,0 +1,560 @@
+/* =============================================================================
+ Ecosystem architecture diagram (src/components/EcosystemDiagram.tsx)
+
+ Layout switches on the CONTAINER width, not the viewport. The docs sidebar is
+ user-resizable (300-480px), so .doc-page can be ~350px wide at a 769px
+ viewport - a viewport media query would be wrong by construction here.
+ ============================================================================= */
+
+.doc-page .eco-diagram {
+ container-type: inline-size;
+ container-name: eco;
+ margin: var(--spacing-xl) 0 var(--spacing-2xl);
+
+ /* light theme */
+ --eco-line: rgba(0, 0, 0, 0.24);
+ --eco-border: rgba(0, 0, 0, 0.11);
+ --eco-band-bg: var(--bg-secondary);
+ --eco-card-bg: var(--bg-primary);
+ --eco-shadow: rgba(0, 0, 0, 0.09);
+
+ /* Package names and versions share one stack; only the multi-word families
+ need quoting. */
+ --eco-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace;
+
+ /* geometry */
+ --eco-radius: 16px;
+ --eco-gutter: 0px;
+ --eco-icon: 38px;
+ --eco-mark: 22px;
+}
+
+:root.dark .doc-page .eco-diagram {
+ --eco-line: rgba(255, 255, 255, 0.3);
+ --eco-border: rgba(255, 255, 255, 0.14);
+ --eco-band-bg: rgba(255, 255, 255, 0.035);
+ --eco-card-bg: rgba(255, 255, 255, 0.07);
+ --eco-shadow: rgba(0, 0, 0, 0.45);
+}
+
+/* ---------- grid ---------------------------------------------------------- */
+
+/* Size-dependent tokens live here, not on .eco-diagram: an @container rule only
+ ever matches descendants of the container it queries, never the container
+ element itself. */
+/* Stacked by default: Spec -> Core -> Libraries in one column. The two-column
+ form is defined in the wide-container block at the bottom. */
+.eco-grid {
+ display: grid;
+ /* the band labels ride the top border, so leave room for the first one */
+ padding-top: 0.6rem;
+ grid-template-columns: minmax(0, 1fr);
+}
+
+/* ---------- bands --------------------------------------------------------- */
+
+.doc-page .eco-diagram .eco-band {
+ position: relative;
+ margin: 0;
+ padding: 1rem 0.85rem 0.85rem;
+ border: 1px solid var(--eco-border);
+ border-radius: var(--eco-radius);
+ background: var(--eco-band-bg);
+}
+
+/* Label chips ride the top border, the way the original diagram framed its
+ Core / Libraries groups. */
+.doc-page .eco-diagram .eco-band-title {
+ position: absolute;
+ top: -0.6rem;
+ left: 0.9rem;
+ margin: 0;
+ padding: 0 0.45rem;
+ border: 0;
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ line-height: 1.2;
+ text-transform: uppercase;
+}
+
+/* Stand-in for the bypass connector while the container is too narrow to draw
+ it. Hidden again once .eco-bypass appears. */
+.doc-page .eco-diagram .eco-grid a.eco-band-chip,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-band-chip {
+ position: absolute;
+ top: -0.65rem;
+ right: 0.9rem;
+ padding: 0.1rem 0.5rem;
+ border: 1px solid var(--eco-border);
+ border-radius: 999px;
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ font-size: 0.625rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ line-height: 1.5;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.doc-page .eco-diagram .eco-grid a.eco-band-chip:hover,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-band-chip:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+/* The Spec group is semantic only - its children are laid out by .eco-grid
+ directly, so a connector can align to openiap-gql rather than to a box. */
+.eco-spec {
+ display: contents;
+}
+
+.eco-hidden-title {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
+.eco-band-body {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+}
+
+.eco-band-body--pair,
+.eco-band-body--grid {
+ display: grid;
+ gap: 0.55rem;
+}
+
+.eco-band-body--pair {
+ /* min() so the track can shrink under its preferred width; a bare 240px
+ floor overflows the band on a ~320px viewport. */
+ grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
+}
+
+.eco-band-body--grid {
+ grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr));
+}
+
+/* ---------- nodes ---------------------------------------------------------
+ documentation.css themes every .doc-page anchor: a link color, a bottom
+ border, and display:inline-flex for `a[target='_blank']`. Its dark-mode
+ overrides reach (0,5,1) specificity, so every anchor rule below is written
+ twice - once plain, once :root.dark-prefixed - to sit one class above that
+ in both themes regardless of stylesheet order. */
+
+.doc-page .eco-diagram .eco-grid a.eco-node,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-node {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 0.7rem;
+ padding: 0.6rem 0.7rem;
+ border: 1px solid var(--eco-border);
+ border-radius: 12px;
+ background: var(--eco-card-bg);
+ color: var(--text-primary);
+ text-decoration: none;
+ transition:
+ transform 0.15s ease,
+ border-color 0.15s ease,
+ box-shadow 0.15s ease;
+}
+
+.doc-page .eco-diagram .eco-grid a.eco-node:hover,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-node:hover {
+ border-color: var(--primary-color);
+ box-shadow: 0 6px 16px var(--eco-shadow);
+ transform: translateY(-2px);
+}
+
+.doc-page .eco-diagram .eco-grid a.eco-node:focus-visible,
+.doc-page .eco-diagram a.eco-band-chip:focus-visible,
+.doc-page .eco-diagram .eco-grid a.eco-more:focus-visible {
+ outline: 2px solid var(--primary-color);
+ outline-offset: 2px;
+}
+
+.eco-node-text {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 0.12rem;
+}
+
+.eco-node-name {
+ overflow: hidden;
+ font-family: var(--eco-mono);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ color: var(--text-primary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.eco-node-note {
+ font-size: 0.6875rem;
+ line-height: 1.35;
+ color: var(--text-secondary);
+}
+
+.eco-node-marks {
+ display: flex;
+ flex: none;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+/* Core cards carry platform marks rather than a version chip; sitting them on
+ the card's baseline reads as "this package targets these" instead of
+ floating mid-card. */
+.eco-band--core .eco-node-marks {
+ align-self: end;
+}
+
+.eco-node-version {
+ font-family: var(--eco-mono);
+ font-size: 0.625rem;
+ font-variant-numeric: tabular-nums;
+ color: var(--text-secondary);
+ opacity: 0.85;
+}
+
+.doc-page .eco-diagram .eco-grid a.eco-more,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-more {
+ grid-column: 1 / -1;
+ padding: 0.55rem 0.7rem;
+ border: 1px dashed var(--eco-border);
+ border-radius: 12px;
+ background: none;
+ color: var(--text-secondary);
+ font-size: var(--font-size-xs);
+ text-align: center;
+ text-decoration: none;
+}
+
+.doc-page .eco-diagram .eco-grid a.eco-more:hover,
+:root.dark .doc-page .eco-diagram .eco-grid a.eco-more:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+/* ---------- artwork -------------------------------------------------------
+ Most marks are transparent full-color logos and need no treatment. The two
+ exceptions are declared per asset in EcosystemDiagram.tsx: artwork with its
+ own opaque square background gets cropped to a rounded tile, and flat
+ single-ink artwork is inverted for whichever theme it would vanish into. */
+
+.eco-icon {
+ width: var(--eco-icon);
+ height: var(--eco-icon);
+ object-fit: contain;
+}
+
+.eco-mark {
+ width: var(--eco-mark);
+ height: var(--eco-mark);
+ object-fit: contain;
+}
+
+.eco-icon.eco-art--square {
+ border-radius: 9px;
+ object-fit: cover;
+}
+
+.eco-mark.eco-art--square {
+ border-radius: 5px;
+ object-fit: cover;
+}
+
+/* Optical size: a tall, narrow glyph fitted into a square box draws narrower
+ than a full-bleed neighbour. Scaling the box matches the drawn widths. */
+.eco-mark.eco-art--compact {
+ width: calc(var(--eco-mark) * 1.2);
+ height: calc(var(--eco-mark) * 1.2);
+}
+
+.eco-art--white-ink {
+ filter: invert(1);
+}
+
+:root.dark .eco-art--black-ink {
+ filter: invert(1);
+}
+
+:root.dark .eco-art--white-ink {
+ filter: none;
+}
+
+/* ---------- connectors ---------------------------------------------------- */
+
+.eco-rail {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 58px;
+}
+
+.eco-rail::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 9px;
+ left: 50%;
+ width: 2px;
+ margin-left: -1px;
+ background: var(--eco-line);
+}
+
+.eco-rail::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ width: 0;
+ height: 0;
+ margin-left: -5px;
+ border-right: 5px solid transparent;
+ border-left: 5px solid transparent;
+ border-top: 9px solid var(--eco-line);
+}
+
+/* Only the two-column form has a gutter to route this through. Stacked, the
+ Libraries band carries the same meaning via its .eco-band-chip. Declared
+ after .eco-rail so it wins the same-specificity tie on `display`. */
+.eco-rail--bypass {
+ display: none;
+}
+
+.eco-rail-label {
+ position: relative;
+ z-index: 1;
+ padding: 0 0.55rem;
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ font-size: 0.625rem;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+/* openiap -> openiap-gql */
+.eco-flow-arrow {
+ position: relative;
+ justify-self: center;
+ width: 2px;
+ height: 22px;
+ margin: 7px 0 15px;
+ background: var(--eco-line);
+}
+
+.eco-flow-arrow::after {
+ content: '';
+ position: absolute;
+ bottom: -8px;
+ left: 50%;
+ width: 0;
+ height: 0;
+ margin-left: -5px;
+ border-right: 5px solid transparent;
+ border-left: 5px solid transparent;
+ border-top: 8px solid var(--eco-line);
+}
+
+/* Reclaim part of the .docs-content side padding (2rem each side above 768px)
+ so the figure is wider than the prose measure, while leaving a full 1rem
+ gutter on both sides - taking all of it puts the Libraries border flush
+ against the content edge. This one rule is viewport-based on purpose:
+ `.docs-content` padding is itself viewport-based - `--spacing-2xl` at
+ >=769px, `--spacing-lg` inside documentation.css's max-width: 768px block. */
+@media (min-width: 769px) {
+ .doc-page .eco-diagram {
+ width: calc(100% + 2 * var(--spacing-lg));
+ margin-right: calc(-1 * var(--spacing-lg));
+ margin-left: calc(-1 * var(--spacing-lg));
+ }
+}
+
+/* ---------- caption ------------------------------------------------------- */
+
+.doc-page .eco-diagram .eco-caption {
+ margin: 0.9rem 0 0;
+ color: var(--text-secondary);
+ font-size: var(--font-size-xs);
+ line-height: 1.6;
+ text-align: center;
+}
+
+/* =============================================================================
+ Wide container
+ ============================================================================= */
+
+/* =============================================================================
+ Two-column form. Spec and Core stack down the left, Libraries fills the right
+ in a single tall column, and both gutter arrows point into it - the same
+ left-to-right reading order as the original diagram. Keeping the six library
+ rows beside the left column instead of below it is what makes the figure wide
+ rather than tall.
+ ============================================================================= */
+
+@container eco (min-width: 700px) {
+ .eco-grid {
+ --eco-gutter: 78px;
+
+ grid-template-columns:
+ minmax(0, 0.92fr) var(--eco-gutter)
+ minmax(0, 1.13fr);
+ /* openiap sits alone on row 1; openiap-gql gets row 3 so both "generate
+ types" arrows can leave from it. Row 6 soaks up whatever the taller
+ Libraries column adds, keeping the connectors at their natural size. */
+ grid-template-rows: auto auto auto auto auto minmax(0, 1fr);
+ column-gap: 0;
+ }
+
+ .eco-node--root {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .eco-flow-arrow {
+ grid-column: 1;
+ grid-row: 2;
+ }
+
+ .eco-node--gql {
+ grid-column: 1;
+ grid-row: 3;
+ }
+
+ .eco-rail--a {
+ grid-column: 1;
+ grid-row: 4;
+ }
+
+ .eco-band--core {
+ grid-column: 1;
+ grid-row: 5;
+ align-self: start;
+ }
+
+ /* openiap-gql -> Libraries, level with openiap-gql itself */
+ .eco-rail--bypass {
+ display: flex;
+ grid-column: 2;
+ grid-row: 3;
+ }
+
+ /* Core -> Libraries, level with the Core band */
+ .eco-rail--b {
+ grid-column: 2;
+ grid-row: 5;
+ }
+
+ /* Centred, not stretched: whichever column is shorter keeps its natural
+ height instead of growing a block of empty space at the bottom. */
+ .eco-band--libs {
+ grid-column: 3;
+ grid-row: 1 / -1;
+ align-self: center;
+ }
+
+ /* Gutter rails turn horizontal: line left-to-right, label above it. */
+ .eco-rail--bypass,
+ .eco-rail--b {
+ height: 44px;
+ align-self: center;
+ }
+
+ .eco-rail--bypass::before,
+ .eco-rail--b::before {
+ top: 50%;
+ right: 10px;
+ bottom: auto;
+ left: 0;
+ width: auto;
+ height: 2px;
+ margin-top: -1px;
+ margin-left: 0;
+ }
+
+ .eco-rail--bypass::after,
+ .eco-rail--b::after {
+ top: 50%;
+ right: 0;
+ bottom: auto;
+ left: auto;
+ margin: -5px 0 0;
+ border-top: 5px solid transparent;
+ border-right: 0;
+ border-bottom: 5px solid transparent;
+ border-left: 10px solid var(--eco-line);
+ }
+
+ .eco-rail--bypass .eco-rail-label,
+ .eco-rail--b .eco-rail-label {
+ position: absolute;
+ right: 0;
+ bottom: calc(50% + 7px);
+ left: 0;
+ padding: 0;
+ background: none;
+ line-height: 1.25;
+ text-align: center;
+ white-space: normal;
+ }
+
+ /* the gutter arrows now carry this meaning graphically */
+ .doc-page .eco-diagram .eco-grid a.eco-band-chip {
+ display: none;
+ }
+}
+
+/* ---------- narrow container ---------------------------------------------- */
+
+/* Title and chip both ride the band's top border - one left, one right, both
+ nowrap. Under ~340px they overlap and the chip's opaque pill paints over the
+ heading, so drop the chip; the figcaption still states the relationship. */
+@container eco (max-width: 340px) {
+ .doc-page .eco-diagram .eco-grid a.eco-band-chip {
+ display: none;
+ }
+}
+
+@container eco (max-width: 419px) {
+ .eco-grid {
+ --eco-icon: 32px;
+ --eco-mark: 20px;
+ }
+
+ .eco-node-version {
+ display: none;
+ }
+}
+
+/* ---------- motion / forced colors ---------------------------------------- */
+
+@media (prefers-reduced-motion: reduce) {
+ .doc-page .eco-diagram .eco-grid a.eco-node {
+ transition: none;
+ }
+
+ .doc-page .eco-diagram .eco-grid a.eco-node:hover {
+ transform: none;
+ }
+}
+
+@media (forced-colors: active) {
+ .doc-page .eco-diagram .eco-band,
+ .doc-page .eco-diagram .eco-grid a.eco-node {
+ border-color: CanvasText;
+ }
+}
diff --git a/packages/google/ALTERNATIVE_BILLING.md b/packages/google/ALTERNATIVE_BILLING.md
index e4fff7d2e..7e27f3882 100644
--- a/packages/google/ALTERNATIVE_BILLING.md
+++ b/packages/google/ALTERNATIVE_BILLING.md
@@ -161,6 +161,6 @@ evidence.
## Resources
- [OpenIAP external purchase guide](https://openiap.dev/docs/features/external-purchase)
-- [OpenIAP deprecation schedule](https://openiap.dev/docs/updates/deprecations)
+- [OpenIAP deprecation schedule](https://openiap.dev/docs/updates/migration)
- [Google Play alternative billing documentation](https://developer.android.com/google/play/billing/alternative)
- [Google Play external transaction reporting](https://developer.android.com/google/play/billing/alternative/reporting)
diff --git a/scripts/agent/compile-context.ts b/scripts/agent/compile-context.ts
index 313c89926..073a0cea5 100644
--- a/scripts/agent/compile-context.ts
+++ b/scripts/agent/compile-context.ts
@@ -231,7 +231,7 @@ async function generateLlmsTxt(): Promise<{ quick: number; full: number }> {
replacement, generated warnings where supported, migration documentation,
and executable absence checks at the removal boundary. Patch and minor
releases must not remove them early.
-- See https://openiap.dev/docs/updates/deprecations for the complete mapping.
+- See https://openiap.dev/docs/updates/migration for the complete mapping.
`;
// Read all external API docs
diff --git a/scripts/audit-deprecation-schedule.mjs b/scripts/audit-deprecation-schedule.mjs
index a78b2a458..2bb2f4be5 100644
--- a/scripts/audit-deprecation-schedule.mjs
+++ b/scripts/audit-deprecation-schedule.mjs
@@ -295,7 +295,7 @@ const activeDocsExcluded = [
"packages/docs/src/pages/docs/index.tsx",
"packages/docs/src/pages/docs/apis/index.tsx",
"packages/docs/src/pages/docs/updates/announcements.tsx",
- "packages/docs/src/pages/docs/updates/deprecations.tsx",
+ "packages/docs/src/pages/docs/updates/migration.tsx",
"packages/docs/src/pages/docs/updates/releases.tsx",
];
@@ -320,13 +320,13 @@ const requiredTexts = [
"`godot-iap` 3.0.0",
"`kmp-iap` 3.0.0",
"`OpenIap.Maui` 2.0.0",
- "/docs/updates/deprecations",
+ "/docs/updates/migration",
"Historical release notes, migration catalogs, archives, and URL",
"internal React Native, Expo, KMP, or Godot native-response",
],
},
{
- file: "packages/docs/src/pages/docs/updates/deprecations.tsx",
+ file: "packages/docs/src/pages/docs/updates/migration.tsx",
values: [
'title="Breaking major release"',
"Last compatible major",