diff --git a/apps/docs/content/docs/components/typography/prose.mdx b/apps/docs/content/docs/components/typography/prose.mdx index c33c8f6d..aca3e56c 100644 --- a/apps/docs/content/docs/components/typography/prose.mdx +++ b/apps/docs/content/docs/components/typography/prose.mdx @@ -17,6 +17,13 @@ Lists inside `Prose` include markers and indentation, including nested lists. +## Media, code, and tables + +Images and videos shrink to fit the content width. Long preformatted lines scroll within their code +block. Tables fill the available width and separate body rows and footers. + + + ## API diff --git a/apps/docs/src/examples/prose/content.tsx b/apps/docs/src/examples/prose/content.tsx new file mode 100644 index 00000000..a21ad963 --- /dev/null +++ b/apps/docs/src/examples/prose/content.tsx @@ -0,0 +1,42 @@ +import { Code } from '@luke-ui/react/code'; +import { Prose } from '@luke-ui/react/prose'; +import { Text } from '@luke-ui/react/text'; + +const swatch = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='64'%3E%3Crect width='320' height='64' fill='%23888'/%3E%3C/svg%3E"; + +export default () => { + return ( + + Media fits the column, while long code lines scroll within it. + Gray sample swatch + + {'padding-inline: var(--luke-space-sp16);'} + + + + + + + + + + + + + + + + + + + + + + + + +
StepSpace
Small8px
Medium24px
Total steps2
+
+ ); +}; diff --git a/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx b/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx index 02c71157..d7164bfb 100644 --- a/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx +++ b/packages/@luke-ui/react/src/core/prose/prose.browser.test.tsx @@ -109,6 +109,74 @@ test('preserves native ordered-list type markers under proseRecipe alone', () => ]); }); +test('fits media inside a narrow prose column', async () => { + const { locator } = render( + + + + + + , + ); + const root = query(locator.element(), 'div'); + const rootBounds = root.getBoundingClientRect(); + await Promise.all([...root.querySelectorAll('img')].map((img) => img.decode())); + + for (const media of root.querySelectorAll('img, picture, video')) { + const bounds = media.getBoundingClientRect(); + expect(bounds.right).toBeLessThanOrEqual(rootBounds.right + 1); + } + + for (const media of root.querySelectorAll('img, video')) { + const bounds = media.getBoundingClientRect(); + expect(bounds.width / bounds.height).toBeCloseTo(5, 0); + } +}); + +test('scrolls long preformatted lines without widening a narrow grid column', () => { + const code = 'padding-inline: var(--luke-space-sp16);'.repeat(5); + const { locator } = render( + +
+				{code}
+			
+ + {code} + +
, + ); + const root = query(locator.element(), 'div'); + + for (const pre of root.querySelectorAll('pre')) { + expect(pre.getBoundingClientRect().right).toBeLessThanOrEqual( + root.getBoundingClientRect().right + 1, + ); + expect(pre.scrollWidth).toBeGreaterThan(pre.clientWidth); + pre.scrollLeft = 100; + expect(pre.scrollLeft).toBe(100); + } +}); + +test('keeps multiline Code inside pre at the same size as plain code', () => { + const code = 'first line\nsecond line'; + const { locator } = render( + + {code} + + {code} + + , + ); + const plain = query(locator.element(), 'pre:first-of-type'); + const composed = query(locator.element(), 'pre:last-of-type'); + + expect(composed.getBoundingClientRect().height).toBeCloseTo( + plain.getBoundingClientRect().height, + 0, + ); +}); + const swatch = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='64'%3E%3Crect width='320' height='64' fill='%23888'/%3E%3C/svg%3E"; @@ -169,8 +237,11 @@ const document = ( Reading a token +
+			{'padding-inline: var(--luke-space-sp16);\nmargin-block: 0;'}
+		
- {'padding-inline: var(--luke-space-sp16);'} + {'padding-inline: var(--luke-space-sp16);\nmargin-block: 0;'} @@ -199,6 +270,12 @@ const document = ( 24px + + + Total steps + 2 + +
@@ -236,7 +313,7 @@ const document = ( test('kitchen sink', { tags: ['visual'] }, async () => { for (const appearance of visualAppearances) { - const { locator } = render({document}, { appearance }); + const { locator } = render({document}, { appearance }); await captureVisualAppearance(locator, 'prose/kitchen-sink', appearance); } diff --git a/packages/@luke-ui/react/src/core/prose/recipe.css.ts b/packages/@luke-ui/react/src/core/prose/recipe.css.ts index fc3d2d80..d2935c1a 100644 --- a/packages/@luke-ui/react/src/core/prose/recipe.css.ts +++ b/packages/@luke-ui/react/src/core/prose/recipe.css.ts @@ -41,10 +41,26 @@ proseStyle('h4 + *, h5 + *, h6 + *', { marginBlockStart: vars.space.sp12 }); proseStyle('hr + *', { marginBlockStart: vars.space.sp48 }); proseStyle('img, picture, video', { display: 'block' }); +proseStyle('img, video', { blockSize: 'auto', maxInlineSize: '100%' }); +proseStyle('picture', { maxInlineSize: '100%' }); proseStyle('figure > img, figure > picture, figure > video, picture > img', { marginBlockStart: 0, }); +proseStyle('pre', { + fontFamily: vars.font.family.code, + maxInlineSize: '100%', + minInlineSize: 0, + overflowX: 'auto', +}); +proseStyle('pre code', { + backgroundColor: 'transparent', + borderRadius: 0, + fontSize: 'inherit', + padding: 0, + whiteSpace: 'inherit', +}); + proseStyle('ul', { listStyleType: 'disc', paddingInlineStart: vars.space.sp24 }); // Untyped ols restore decimal. Typed ols omit list-style-type so HTML presentational hints apply // inside Prose; the reset leaves those ols alone via `proseScopeClassName` from `./scope.css.js`. @@ -56,7 +72,12 @@ proseStyle('hr', { border: 'none', borderBlockStart: `1px solid ${vars.color.border.decorative}`, }); +proseStyle('table', { inlineSize: '100%' }); // Table cells need padding after the reset removes it. proseStyle('th, td', { paddingBlock: vars.space.sp8, paddingInline: vars.space.sp12 }); -proseStyle('th', { textAlign: 'start' }); +proseStyle('th', { textAlign: 'start', verticalAlign: 'bottom' }); +proseStyle('td', { verticalAlign: 'baseline' }); proseStyle('thead th', { borderBlockEnd: `1px solid ${vars.color.border.decorative}` }); +proseStyle('tbody tr + tr, tfoot', { + borderBlockStart: `1px solid ${vars.color.border.decorative}`, +});