Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/ype-5197-bible-card-max-width.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@youversion/platform-react-ui': minor
---

`BibleCard` now caps its painted shell at 700px by default, or at a pixel `maxWidth`. Scripture fills that shell. Pass `maxWidth="100%"` for a full-bleed shell; that path keeps the 600px inner column, and scripture fills the column. Full-bleed hosts must pass `"100%"`. `VerseOfTheDay` and `BibleReader` are unchanged.
4 changes: 0 additions & 4 deletions .lintstagedrc.json

This file was deleted.

22 changes: 22 additions & 0 deletions .lintstagedrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const oxlintSkipped = (file) => {
const rel = file.replaceAll('\\', '/')
return (
rel.includes('/.storybook/') ||
rel.includes('/scripts/') ||
rel.includes('/tools/oxlint/anti-slop/') ||
/\.config\.(js|cjs|mjs|ts)$/.test(rel)
)
}

const quoted = (files) => files.map((file) => JSON.stringify(file)).join(' ')

export default {
'*.{js,jsx,ts,tsx}': (files) => {
const lintable = files.filter((file) => !oxlintSkipped(file))
return [
...(lintable.length > 0 ? [`oxlint --fix ${quoted(lintable)}`] : []),
`prettier --write ${quoted(files)}`,
]
},
'*.{json,md}': 'prettier --write',
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"analyze:select": "node scripts/analyze-select.mjs",
"generate:i18n": "pnpm --filter @youversion/platform-react-ui generate:i18n",
"check:i18n": "node scripts/check-i18n-parity.mjs",
"test:ci-scripts": "bash .github/scripts/check-locale-ownership.test.sh"
"test:ci-scripts": "bash .github/scripts/check-locale-ownership.test.sh",
"storybook": "pnpm --filter @youversion/platform-react-ui storybook"
},
"devDependencies": {
"@changesets/cli": "2.29.7",
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const config: StorybookConfig = {
{ name: '@storybook/addon-coverage', options: { istanbul: { include: ['**/stories/**'] } } },
],
framework: '@storybook/react-vite',
// Official Storybook default. Storybook 10.4+ lazy-loads
// react-docgen-typescript, so this option is enough to keep the
// TypeScript 7 compiler-API crash off the React preset.
typescript: {
reactDocgen: 'react-docgen',
},
staticDirs: ['../public'], // This is for Storybook mock service worker
viteFinal: (config) => {
config.define = { ...config.define, __YV_STYLES__: yvStyles };
Expand Down
10 changes: 5 additions & 5 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@
"@internal/tsconfig": "workspace:*",
"@mdx-js/react": "^3.1.1",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "10.3.5",
"@storybook/addon-onboarding": "10.3.5",
"@storybook/addon-vitest": "10.3.5",
"@storybook/react-vite": "10.3.5",
"@storybook/addon-docs": "10.5.10",
"@storybook/addon-onboarding": "10.5.10",
"@storybook/addon-vitest": "10.5.10",
"@storybook/react-vite": "10.5.10",
"@tailwindcss/cli": "4.1.15",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0",
Expand All @@ -95,7 +95,7 @@
"msw": "2.13.4",
"msw-storybook-addon": "2.0.7",
"playwright": "1.56.1",
"storybook": "10.3.5",
"storybook": "10.5.10",
"tailwindcss": "4.1.15",
"tsup": "8.5.0",
"typescript": "7.0.2",
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/components/bible-card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ const meta = {
control: 'boolean',
description: 'toggle version picker',
},
maxWidth: {
description:
'Painted section max-width. A number is CSS px (default 700). Pass "100%" for full-bleed; that path keeps the 600px inner column. Scripture fills the inner column (the card lifts the 65ch renderer measure).',
},
},
} satisfies Meta<typeof BibleCard>;

Expand Down Expand Up @@ -61,6 +65,7 @@ export const WideContainer: Story = {
args: {
reference: 'LUK.1.39-45',
versionId: 111,
maxWidth: '100%',
},
tags: ['integration'],
parameters: {
Expand Down
86 changes: 72 additions & 14 deletions packages/ui/src/components/bible-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, act, within, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { requireHtmlButton } from '@/test/dom-stubs';
import { requireHtmlButton, requireHtmlElement } from '@/test/dom-stubs';
import { HookOverrideProvider } from '@/test/hook-overrides';
import { BibleCard } from './bible-card';
import type { FootnoteData } from './verse';
Expand Down Expand Up @@ -64,6 +64,8 @@ function renderCard(
versionId?: number;
highlights?: Highlight[];
onVersionChange?: (id: number) => void;
maxWidth?: number | '100%';
hostWidth?: number;
} = {},
) {
const {
Expand All @@ -72,8 +74,10 @@ function renderCard(
versionId = 3034,
highlights,
onVersionChange,
maxWidth,
hostWidth,
} = extra;
return render(
const card = (
<HookOverrideProvider
overrides={{
useVersion: () => idleVersion(),
Expand All @@ -86,9 +90,18 @@ function renderCard(
onFootnotePress={onFootnotePress}
highlights={highlights}
onVersionChange={onVersionChange}
maxWidth={maxWidth}
/>
</HookOverrideProvider>,
</HookOverrideProvider>
);
return render(hostWidth === undefined ? card : <div style={{ width: hostWidth }}>{card}</div>);
}

function cardShell(container: HTMLElement) {
return {
section: requireHtmlElement(container.querySelector('section')),
inner: requireHtmlElement(container.querySelector('section > div')),
};
}

const YELLOW = 'fffe00';
Expand Down Expand Up @@ -161,24 +174,69 @@ describe('BibleCard - Delayed spinner', () => {
);
});

it('should let the card fill its container while centering the content group', () => {
it('should hide inline verse numbers in the bible renderer', () => {
const { container } = renderCard(passageResult({ passage: mockPassage, loading: false }));
const card = container.querySelector('section');
const contentGroup = container.querySelector('section > div');
const bibleRenderer = container.querySelector('[data-slot="yv-bible-renderer"]');

expect(bibleRenderer).toHaveAttribute('data-show-verse-numbers', 'false');
});
});

describe('BibleCard - maxWidth', () => {
const loaded = passageResult({ passage: mockPassage, loading: false });

it('omitting maxWidth caps the painted section at 700px and lets the inner column fill', () => {
const { container } = renderCard(loaded);
const { section, inner } = cardShell(container);
const bibleTextView = container.querySelector('[data-slot="yv-bible-renderer"]')?.parentElement;

expect(card).toHaveClass('yv:w-full');
expect(card).not.toHaveClass('yv:max-w-md');
expect(card).toHaveClass('yv:box-border');
expect(contentGroup).toHaveClass('yv:card-content');
expect(section).toHaveClass('yv:w-full');
expect(section).toHaveClass('yv:p-6');
expect(section).toHaveClass('yv:box-border');
expect(section).not.toHaveClass('yv:max-w-md');
expect(section).toHaveStyle({ maxWidth: '700px' });
expect(inner).toHaveClass('yv:w-full');
expect(inner).not.toHaveClass('yv:card-content');
expect(inner).not.toHaveStyle({ maxWidth: '600px' });
expect(bibleTextView).not.toHaveClass('yv:max-w-[600px]');
expect(section.style.getPropertyValue('--yv-reader-max-width')).toBe('none');
});

it('should hide inline verse numbers in the bible renderer', () => {
const { container } = renderCard(passageResult({ passage: mockPassage, loading: false }));
const bibleRenderer = container.querySelector('[data-slot="yv-bible-renderer"]');
it('uses a number maxWidth as the section cap and lets the inner column fill', () => {
const { container } = renderCard(loaded, { maxWidth: 480 });
const { section, inner } = cardShell(container);

expect(bibleRenderer).toHaveAttribute('data-show-verse-numbers', 'false');
expect(section).toHaveClass('yv:w-full');
expect(section).toHaveStyle({ maxWidth: '480px' });
expect(inner).toHaveClass('yv:w-full');
expect(inner).not.toHaveClass('yv:card-content');
expect(inner).not.toHaveStyle({ maxWidth: '600px' });
expect(section.style.getPropertyValue('--yv-reader-max-width')).toBe('none');
});

it('keeps the 600px inner column when maxWidth is 100%', () => {
const { container } = renderCard(loaded, { maxWidth: '100%' });
const { section, inner } = cardShell(container);

expect(section).toHaveClass('yv:w-full');
expect(section).toHaveStyle({ maxWidth: '100%' });
expect(inner).toHaveClass('yv:card-content');
expect(inner).not.toHaveStyle({ maxWidth: 'none' });
expect(section.style.getPropertyValue('--yv-reader-max-width')).toBe('none');
});

it('still fills a parent that is narrower than the section cap', () => {
const { container } = renderCard(loaded, { hostWidth: 400 });
const host = container.firstElementChild;
const { section, inner } = cardShell(container);

expect(host).toHaveStyle({ width: '400px' });
expect(section.parentElement).toBe(host);
expect(section).toHaveClass('yv:w-full');
expect(section).toHaveStyle({ maxWidth: '700px' });
expect(inner).toHaveClass('yv:w-full');
expect(inner).not.toHaveClass('yv:card-content');
expect(section.style.getPropertyValue('--yv-reader-max-width')).toBe('none');
});
});

Expand Down
25 changes: 24 additions & 1 deletion packages/ui/src/components/bible-card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react';
import { usePassage, useVersion, useTheme } from '@youversion/platform-react-hooks';
import { DEFAULT_LICENSE_FREE_BIBLE_VERSION, type Highlight } from '@youversion/platform-core';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -41,6 +42,18 @@ export type BibleCardProps = {
* references paint the whole chapter.
*/
highlights?: Highlight[];
/**
* Caps the painted `<section>` shell. A number is CSS pixels. `'100%'` fills
* the parent (Come and See / full-bleed) and keeps the 600px inner column.
* Omit for 700. Full-bleed hosts must pass `'100%'`.
* The section lifts the Bible renderer `65ch` measure so scripture fills
* that inner column.
*/
maxWidth?: number | '100%';
};

type BibleCardSectionStyle = CSSProperties & {
'--yv-reader-max-width': 'none';
};

/**
Expand Down Expand Up @@ -129,6 +142,8 @@ function BibleCardFooter({ copyright }: { copyright?: string | null }): React.Re
);
}

const BIBLE_CARD_DEFAULT_MAX_WIDTH_PX = 700;

export function BibleCard({
reference,
versionId: controlledVersionId,
Expand All @@ -139,6 +154,7 @@ export function BibleCard({
onVersionPickerPress,
onFootnotePress,
highlights,
maxWidth = BIBLE_CARD_DEFAULT_MAX_WIDTH_PX,
}: BibleCardProps): React.ReactNode {
// Controlled only when both versionId + onVersionChange are provided.
// versionId alone seeds uncontrolled state, preserving backwards compatibility
Expand Down Expand Up @@ -167,14 +183,21 @@ export function BibleCard({

const isRefetching = passageLoading && passage !== null;
const showSpinner = useDelayedLoading(isRefetching);
const sectionStyle: BibleCardSectionStyle = {
maxWidth: maxWidth === '100%' ? '100%' : `${maxWidth}px`,
marginInline: 'auto',
'--yv-reader-max-width': 'none',
};

return (
<section
data-yv-sdk
data-yv-theme={theme}
className="yv:w-full yv:flex yv:flex-col yv:grow yv:bg-card yv:p-6 yv:rounded-2xl yv:box-border"
style={sectionStyle}
>
<div className="yv:card-content">
{/* Default/number: fill the shell. Keep shared card-content (600px) only for full-bleed. */}
<div className={maxWidth === '100%' ? 'yv:card-content' : 'yv:w-full'}>
<div className="yv:flex yv:w-full yv:justify-between yv:items-center yv:mb-4">
{/*
The error branch stays separate rather than folding into the loading
Expand Down
Loading
Loading