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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/replace-inject-styles-with-style-precedence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@youversion/platform-core': minor
'@youversion/platform-react-hooks': minor
'@youversion/platform-react-ui': minor
---

Replace module-level injectStyles() side effect with React 19 style precedence hoisting via YouVersionProvider. Add static CSS export at @youversion/platform-react-ui/styles.css for non-React consumers.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pnpm --filter @youversion/platform-react-ui build

**React 19.1.2 exact pinning**: pnpm overrides lock all React packages to exact version

**Tailwind CSS injection**: Auto-injected as JS constant via tsup define (no build step needed by consumers)
**Tailwind CSS injection**: Built CSS embedded as JS constant via tsup define, rendered by `YouVersionProvider` using React 19 `<style precedence>` (no build step needed by consumers)

**Changeset workflow**: pnpm changeset → pnpm version-packages → pnpm release

Expand Down
4 changes: 4 additions & 0 deletions packages/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const config: StorybookConfig = {
framework: '@storybook/react-vite',
staticDirs: ['../public'], // This is for Storybook mock service worker
viteFinal: (config) => {
// __YV_STYLES__ is normally injected by tsup at build time (see tsup.config.ts).
// Storybook uses its own Vite server, so we define it as empty here — CSS is
// already loaded via `import '../dist/tailwind.css'` in preview.tsx.
config.define = { ...config.define, __YV_STYLES__: '""' };
config.resolve = {
...config.resolve,
alias: {
Expand Down
15 changes: 8 additions & 7 deletions packages/ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Complete UI layer with many Bible components: BibleTextView, VerseOfTheDay, Bibl
```
components/ # Public Bible components (exported)
components/ui/ # Internal Radix primitives (not exported)
lib/ # Utilities (injectStyles, utils)
src/index.ts # Entry point with style injection side effect
lib/ # Utilities (yv-styles, utils)
src/index.ts # Entry point (re-exports components, types, hooks)
```

## PUBLIC API
Expand All @@ -32,7 +32,7 @@ src/index.ts # Entry point with style injection side effect

❌ Don't: Make raw network requests from UI components
❌ Don't: Import from `@youversion/platform-core` directly (except re-exports in index.ts)
❌ Don't: Add global CSS files; all styling goes through Tailwind build and `injectStyles`
❌ Don't: Add global CSS files; all styling goes through Tailwind build and `<YvStyles />`
❌ Don't: Use unprefixed Tailwind classes (causes collisions in consumer apps)

## CONVENTIONS
Expand All @@ -42,9 +42,10 @@ src/index.ts # Entry point with style injection side effect
- tsup for bundling, tsc for type declarations

## STYLING
**Auto-injected on import**: `src/index.ts` calls `injectStyles()` on module load
- CSS embedded as `__YV_STYLES__` constant via tsup define (no separate CSS file)
- Built Tailwind CSS: `dist/tailwind.css` → injected as JS string at build time
**React 19 `<style precedence>`**: The `YouVersionProvider` wrapper (in `src/components/YouVersionProvider.tsx`) renders `<YvStyles />` once, which outputs a `<style href="yv-sdk-styles" precedence="yv-sdk">` element. React handles hoisting to `<head>`, deduplication, SSR streaming, and Suspense integration. Individual components do NOT render `<YvStyles />` — it's centralized in the provider.
- CSS embedded as `__YV_STYLES__` constant via tsup define
- Built Tailwind CSS: `dist/tailwind.css` → embedded as JS string at build time
- Static CSS also available via `import '@youversion/platform-react-ui/styles.css'` for non-React consumers
- Each component includes a `data-yv-sdk` attribute on its root element for style scoping (consumers don't need to add this)
- Tailwind CSS classes must be prefixed with `yv:` to prevent class naming collision when someone uses our components in their app. For example, `mt-4` becomes `yv:mt-4`
- Light/dark mode via CSS variables (`[data-yv-sdk]`)
Expand Down Expand Up @@ -168,6 +169,6 @@ From repo root, `pnpm build` runs Turbo which builds in order:
3. `@youversion/platform-react-ui` (build:css → build:js → build:types)

## CRITICAL
- **Side effect**: importing package injects styles automatically
- **No module side effects**: styles are rendered via React 19 `<style precedence>` in the `YouVersionProvider` wrapper
- Never skip build:css step (styles required for __YV_STYLES__ constant)
- Always rebuild after CSS changes
12 changes: 12 additions & 0 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ function App() {
}
```

## Styling

All component CSS is automatically injected when you wrap your app with `YouVersionProvider` — no extra imports or build steps needed. Under the hood, it uses React 19's [`<style precedence>`](https://react.dev/reference/react-dom/components/style) to hoist styles into `<head>` with built-in deduplication and SSR/Suspense support.

**Non-React or manual CSS import:**

```tsx
import '@youversion/platform-react-ui/styles.css';
```

All component classes are prefixed with `yv:` to avoid collisions with your app's styles. Override design tokens with CSS variables on `[data-yv-sdk]` (see [Custom CSS variables](#custom-css-variables)).

## Theming

Set the theme via the `YouVersionProvider`'s `theme` prop. Defaults to `'light'`.
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"./styles.css": "./dist/tailwind.css"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/components/YouVersionProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { type ComponentProps } from 'react';
import { YouVersionProvider as BaseYouVersionProvider } from '@youversion/platform-react-hooks';
import { YvStyles } from '@/lib/yv-styles';

export function YouVersionProvider(
props: ComponentProps<typeof BaseYouVersionProvider>,
): React.ReactElement {
return (
<BaseYouVersionProvider {...props}>
<YvStyles />
{props.children}
</BaseYouVersionProvider>
);
}
1 change: 1 addition & 0 deletions packages/ui/src/components/verse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { getBibleTextErrorMessage } from '@/lib/bible-text-error';
import { cn } from '@/lib/utils';
import { type FontFamily } from '@/lib/verse-html-utils';

import { transformBibleHtml } from '@youversion/platform-core/browser';

const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
Expand Down
9 changes: 3 additions & 6 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// React SDK main entry point

import { injectStyles } from './lib/inject-styles';

// Inject styles on import
injectStyles();

export * from './components';
export * from './types';

Expand All @@ -20,7 +15,9 @@ export {
} from '@youversion/platform-core';

export {
YouVersionProvider,
YouVersionProvider as BaseYouVersionProvider,
useYVAuth,
type UseYVAuthReturn,
} from '@youversion/platform-react-hooks';

export { YouVersionProvider } from './components/YouVersionProvider';
15 changes: 0 additions & 15 deletions packages/ui/src/lib/inject-styles.ts

This file was deleted.

9 changes: 9 additions & 0 deletions packages/ui/src/lib/yv-styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare const __YV_STYLES__: string;

export function YvStyles() {
return (
<style href="yv-sdk-styles" precedence="yv-sdk">
{__YV_STYLES__}
</style>
);
}
12 changes: 7 additions & 5 deletions packages/ui/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
* Tailwind v4 CSS Layer Strategy
* ==============================
* All SDK styles are wrapped in custom @layer directives (yv-sdk-*) so that
* when injected into a consumer's app, they sit below unlayered author styles
* in the CSS cascade. This prevents our SDK from overriding consumer CSS.
* they sit below unlayered author styles in the CSS cascade. This prevents
* our SDK from overriding consumer CSS.
*
* LAYER ORDER IS CRITICAL. We declare Tailwind v4's standard layer names
* (theme, base, components, utilities) BEFORE our yv-sdk-* layers. Because
* injectStyles() appends our <style> tag first, we establish the document's
* layer order before the consumer's Tailwind can. This ensures:
* (theme, base, components, utilities) BEFORE our yv-sdk-* layers. Each
* public component renders a React 19 <style href="yv-sdk-styles"
* precedence="yv-sdk"> element which React hoists to <head>, deduplicates,
* and includes in SSR-streamed HTML. This establishes the document's layer
* order before the consumer's Tailwind can. This ensures:
*
* 1. Consumer Tailwind layers (theme/base/utilities) = LOW priority
* 2. SDK layers (yv-sdk-*) = MEDIUM priority
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export default defineConfig({
projects: [
// Unit tests project
defineProject({
// __YV_STYLES__ is normally injected by tsup at build time (see tsup.config.ts).
// Tests run against source files directly, so we define it as empty here
// to prevent ReferenceError when <YvStyles /> renders during tests.
define: {
__YV_STYLES__: '""',
},
resolve: {
alias: {
'@': path.join(dirname, 'src'),
Expand Down
Loading