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.
+
+
+ {'padding-inline: var(--luke-space-sp16);'}
+
+
+
+
+ | Step |
+ Space |
+
+
+
+
+ | Small |
+ 8px |
+
+
+ | Medium |
+ 24px |
+
+
+
+
+ | Total steps |
+ 2 |
+
+
+
+
+ );
+};
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}`,
+});