Priority: MANDATORY Follow these patterns when working on packages/docs.
IMPORTANT: Modals should be defined once at the app root level and managed via global state using Preact Signals.
import { signal } from "@preact/signals-react";
// Modal state signal
export const authModalSignal = signal({
isOpen: false,
});
// Helper functions
export const openAuthModal = () => {
authModalSignal.value = { isOpen: true };
};
export const closeAuthModal = () => {
authModalSignal.value = { isOpen: false };
};import { AuthModal } from "./components/AuthModal";
import { authModalSignal, closeAuthModal } from "./lib/signals";
export default function App() {
return (
<>
{/* Single modal instance at root */}
<AuthModal
isOpen={authModalSignal.value.isOpen}
onClose={closeAuthModal}
/>
{/* Rest of your app */}
</>
);
}import { openAuthModal } from '../lib/signals';
// In component
<button onClick={openAuthModal}>
Sign In
</button>When a feature has sub-pages (e.g., Subscription > Upgrade/Downgrade, Alternative Marketplace > Onside), use a directory structure instead of hash anchors or flat file naming.
src/pages/docs/features/
├── subscription/
│ ├── index.tsx # Main subscription page
│ └── upgrade-downgrade.tsx # Sub-page
├── alternative-marketplace/
│ ├── index.tsx # Main overview page
│ └── onside.tsx # Sub-page
├── purchase.tsx # No sub-pages → flat file
└── discount.tsx # No sub-pages → flat file
// Imports
import SubscriptionFeature from './features/subscription/index';
import SubscriptionUpgradeDowngrade from './features/subscription/upgrade-downgrade';
// Routes
<Route path="features/subscription" element={<SubscriptionFeature />} />
<Route path="features/subscription/upgrade-downgrade" element={<SubscriptionUpgradeDowngrade />} />Use MenuDropdown for collapsible parent-child navigation:
<MenuDropdown
title="Subscription"
titleTo="/docs/features/subscription"
items={[
{
to: "/docs/features/subscription/upgrade-downgrade",
label: "Upgrade/Downgrade",
},
]}
onItemClick={closeSidebar}
/>- Never use hash anchors (
#section) for sub-section navigation in the sidebar — always use separate routes/pages - Parent page (
index.tsx) should contain the overview; sub-pages contain detailed content - Import paths from sub-directories use
../../../../components/(one level deeper) - Update all internal
<Link to="...">references when moving files
- Place reusable components that are used across multiple pages/features
- If a component is only used in one place, it should be co-located with its parent
When a component has sub-components that are only used within it:
// For a component with internal sub-components
src/components/AuthModal/
├── index.tsx // Main AuthModal component
└── Modal.tsx // Modal used only within AuthModal
// If Modal is used elsewhere too
src/components/
├── AuthModal.tsx // Main component
└── Modal.tsx // Shared modal component
CRITICAL: All components must respect parent boundaries. Children must NEVER overflow outside parent containers.
- ALL components must fit within parent boundaries
- Use
overflow-hiddenon parent containers when necessary - Apply
break-wordsfor text content that might be long - Use
whitespace-nowrapfor navigation items to prevent wrapping
- Delete unused components, functions, and imports immediately
- Don't keep commented-out code
- Remove unused variables and parameters
Framework implementation listings must be derived from
packages/docs/src/lib/images.ts:
LIBRARIESis the canonical order and membership for framework libraries (Expo, React Native, Flutter, KMP, MAUI, Godot).- Pages that show framework lists, setup links, sponsor links, or home-page
icons must map over
LIBRARIESinstead of hand-writing their own arrays. - When adding, removing, renaming, or reordering a framework, update
LIBRARIESfirst and let pages derive labels, images, setup paths, install commands, and documentation links from that metadata. - If a page needs new per-framework copy, add a typed field to
LibraryInfoinstead of creating another local list with duplicated order.
Release notes are located at packages/docs/src/pages/docs/updates/releases.tsx.
- Add new entry at the top of the
allNotesarray - Follow the existing pattern with
id,date, andelement - Use semantic IDs like
gql-1-3-16-apple-1-3-14 - Verify every package version against its source of truth before writing it (see "Release package version verification" below)
const allNotes: Note[] = [
// GQL 1.3.16 / Apple 1.3.14 - Jan 26, 2026
{
id: "gql-1-3-16-apple-1-3-14",
date: new Date("2026-01-26"),
element: (
<div key="gql-1-3-16-apple-1-3-14" style={noteCardStyle}>
<AnchorLink id="gql-1-3-16-apple-1-3-14" level="h4">
📅 openiap-gql v1.3.16 / openiap-apple v1.3.14 - Feature Description
</AnchorLink>
{/* Content here */}
</div>
),
},
// ... older notes
];- AnchorLink: For deep linking to specific release
- Version info: Package names and versions in title
- Date: In format
new Date('YYYY-MM-DD') - References: Links to Apple/Google documentation when applicable
- Issue links: Reference GitHub issues when fixing bugs
Release note package lists must never be guessed from memory or inferred from a previous block. Verify each version from the package's real source of truth:
| Package | Source of Truth |
|---|---|
| openiap-apple | openiap-versions.json field apple, or GitHub release tag {version} |
| openiap-google | openiap-versions.json field google, or GitHub release tag google-{version} |
| react-native-iap | libraries/react-native-iap/package.json |
| expo-iap | libraries/expo-iap/package.json |
| flutter_inapp_purchase | libraries/flutter_inapp_purchase/pubspec.yaml |
| godot-iap | libraries/godot-iap/addons/godot-iap/plugin.cfg |
| kmp-iap | libraries/kmp-iap/gradle.properties field libraryVersion |
| maui-iap | libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj field PackageVersion |
Before adding or editing a Package Releases list:
git fetch origin main --tags(orgit fetch --no-tags origin mainif local stale tags would fail).- Read the current package metadata from
origin/main, not from memory. - For planned patch releases, add exactly one patch version to each affected
framework package and label the block
Planned Package Releases. - For published release links, confirm each tag exists with
gh release view <tag> --repo hyodotdev/openiapbefore adding an<a href>. - If a release workflow is still running, keep the entry as plain text with planned wording. Add links only after the GitHub Release exists.
- Run
bun run audit:docs; the audit fails when a publishedPackage Releasesblock contains a package/version item without a GitHub Release link.
Do not use openiap-versions.json to derive React Native, Expo, Flutter,
Godot, KMP, or MAUI versions; that manifest tracks only spec, google, and
apple.