feat: implement three-width layout system (reading, wide, full-bleed)
Replace the narrow 720px column layout with a three-width breakout system: - Reading width (--width-prose, 720px): prose paragraphs, lists, blockquotes - Wide width (--width-code, 840px): code blocks, inline SVGs, figures, .wide/.breakout - Full width (--width-container, 1100px): hero diagrams, .full elements The outer container is wide; individual content blocks self-constrain via max-width + margin: auto. This creates the narrow-wide-narrow rhythm. Changes: - prose.css: Add .prose > * breakout pattern for three widths, SVG styling - BaseLayout.astro: Remove max-width from main (children handle their own widths) - [...slug].astro: Remove --width-prose constraint from .prose-wrapper - prose.css: Remove old left:50%/translateX(-50%) code block hack
This commit is contained in:
@@ -87,9 +87,6 @@ const base = import.meta.env.BASE_URL;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: var(--width-container);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: var(--gutter);
|
||||
padding-right: var(--gutter);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ const readingTime = post.body ? getReadingTime(post.body) : undefined;
|
||||
|
||||
<style>
|
||||
.prose-wrapper {
|
||||
max-width: var(--width-prose);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-bottom: var(--space-12);
|
||||
|
||||
+40
-6
@@ -1,5 +1,34 @@
|
||||
/* Prose — typographic treatment for rendered markdown */
|
||||
|
||||
/* --- Three-width layout system --- */
|
||||
/* Default: all direct children constrained to reading width */
|
||||
|
||||
.prose > * {
|
||||
max-width: var(--width-prose);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Wide: code blocks, SVGs, figures, and .wide/.breakout elements */
|
||||
|
||||
.prose > pre,
|
||||
.prose > svg,
|
||||
.prose > figure,
|
||||
.prose > .wide,
|
||||
.prose > .breakout {
|
||||
max-width: var(--width-code);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Full-bleed: hero diagrams, comparison layouts */
|
||||
|
||||
.prose > .full {
|
||||
max-width: var(--width-container);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* --- Headings (Structure voice: Inter) --- */
|
||||
|
||||
.prose h2 {
|
||||
@@ -107,14 +136,9 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* --- Code blocks (breakout width) --- */
|
||||
/* --- Code blocks (wide breakout via three-width system) --- */
|
||||
|
||||
.prose pre {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(var(--width-code), calc(100vw - var(--gutter) * 2));
|
||||
max-width: var(--width-code);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-left: 3px solid var(--accent-primary);
|
||||
border-radius: var(--rounded-md);
|
||||
@@ -152,6 +176,16 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* --- Inline SVG diagrams --- */
|
||||
|
||||
.prose svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: var(--rounded-md);
|
||||
margin-top: var(--space-5);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
/* --- Horizontal rules --- */
|
||||
|
||||
.prose hr {
|
||||
|
||||
@@ -231,10 +231,15 @@ describe('Responsive behavior', () => {
|
||||
'Title should use smaller type scale at breakpoints');
|
||||
});
|
||||
|
||||
test('code blocks respect gutters on mobile', () => {
|
||||
test('code blocks respect gutters via three-width layout system', () => {
|
||||
const prose = readFileSync('src/styles/prose.css', 'utf-8');
|
||||
assert.ok(prose.includes('100vw') && prose.includes('--gutter'),
|
||||
'Code blocks should constrain to viewport minus gutters');
|
||||
const layout = readFileSync('src/layouts/BaseLayout.astro', 'utf-8');
|
||||
// Code blocks self-constrain via max-width in the breakout pattern
|
||||
assert.ok(prose.includes('--width-code'),
|
||||
'Code blocks should use --width-code for wide breakout');
|
||||
// Gutters are provided by main element padding
|
||||
assert.ok(layout.includes('--gutter'),
|
||||
'Main element should provide gutter padding for responsive safety');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -126,10 +126,9 @@ describe('Post page template', () => {
|
||||
});
|
||||
|
||||
describe('Style', () => {
|
||||
it('should have .prose-wrapper with prose max-width, auto margins, and space-12 bottom padding', () => {
|
||||
it('should have .prose-wrapper with auto margins and space-12 bottom padding (no max-width — children self-constrain)', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /\.prose-wrapper/, 'should style .prose-wrapper');
|
||||
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should have prose max-width');
|
||||
assert.match(content, /margin-left\s*:\s*auto/, 'should have auto left margin');
|
||||
assert.match(content, /margin-right\s*:\s*auto/, 'should have auto right margin');
|
||||
assert.match(content, /padding-bottom\s*:\s*var\(--space-12\)/, 'should have space-12 bottom padding');
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const prosePath = 'src/styles/prose.css';
|
||||
const layoutPath = 'src/layouts/BaseLayout.astro';
|
||||
const postTemplatePath = 'src/pages/posts/[...slug].astro';
|
||||
|
||||
function readProse() {
|
||||
return readFileSync(prosePath, 'utf-8');
|
||||
}
|
||||
|
||||
function readLayout() {
|
||||
return readFileSync(layoutPath, 'utf-8');
|
||||
}
|
||||
|
||||
function readPostTemplate() {
|
||||
return readFileSync(postTemplatePath, 'utf-8');
|
||||
}
|
||||
|
||||
describe('Three-width layout system', () => {
|
||||
describe('Reading width (default)', () => {
|
||||
it('should constrain all direct children of .prose to reading width by default', () => {
|
||||
const css = readProse();
|
||||
// .prose > * should have max-width: var(--width-prose)
|
||||
assert.match(css, /\.prose\s*>\s*\*\s*\{[^}]*max-width:\s*var\(--width-prose\)/s,
|
||||
'.prose > * should set max-width to --width-prose');
|
||||
});
|
||||
|
||||
it('should center prose children with auto margins', () => {
|
||||
const css = readProse();
|
||||
// .prose > * should have margin-left: auto and margin-right: auto
|
||||
assert.match(css, /\.prose\s*>\s*\*\s*\{[^}]*margin-left:\s*auto/s,
|
||||
'.prose > * should have margin-left: auto');
|
||||
assert.match(css, /\.prose\s*>\s*\*\s*\{[^}]*margin-right:\s*auto/s,
|
||||
'.prose > * should have margin-right: auto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wide width (code blocks, SVGs, figures)', () => {
|
||||
it('should break code blocks to wide width', () => {
|
||||
const css = readProse();
|
||||
// .prose > pre should have max-width: var(--width-code)
|
||||
assert.match(css, /\.prose\s*>\s*pre[^{]*\{[^}]*max-width:\s*var\(--width-code\)/s,
|
||||
'.prose > pre should set max-width to --width-code');
|
||||
});
|
||||
|
||||
it('should break SVGs to wide width', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s*>\s*svg[^{]*\{[^}]*max-width:\s*var\(--width-code\)/s,
|
||||
'.prose > svg should set max-width to --width-code');
|
||||
});
|
||||
|
||||
it('should break .breakout and .wide elements to wide width', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s*>\s*\.wide[^{]*\{[^}]*max-width:\s*var\(--width-code\)/s,
|
||||
'.prose > .wide should set max-width to --width-code');
|
||||
assert.match(css, /\.prose\s*>\s*\.breakout[^{]*\{[^}]*max-width:\s*var\(--width-code\)/s,
|
||||
'.prose > .breakout should set max-width to --width-code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full width (full-bleed)', () => {
|
||||
it('should support full-bleed elements with .full class', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s*>\s*\.full[^{]*\{[^}]*max-width:\s*var\(--width-container\)/s,
|
||||
'.prose > .full should set max-width to --width-container');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SVG styling', () => {
|
||||
it('should make SVGs responsive with width 100% and auto height', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s+svg[^{]*\{[^}]*width:\s*100%/s,
|
||||
'SVGs should have width: 100%');
|
||||
assert.match(css, /\.prose\s+svg[^{]*\{[^}]*height:\s*auto/s,
|
||||
'SVGs should have height: auto');
|
||||
});
|
||||
|
||||
it('should give SVGs proper vertical spacing', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s+svg[^{]*\{[^}]*margin-top:\s*var\(--space-5\)/s,
|
||||
'SVGs should have --space-5 top margin');
|
||||
assert.match(css, /\.prose\s+svg[^{]*\{[^}]*margin-bottom:\s*var\(--space-5\)/s,
|
||||
'SVGs should have --space-5 bottom margin');
|
||||
});
|
||||
|
||||
it('should give SVGs rounded corners', () => {
|
||||
const css = readProse();
|
||||
assert.match(css, /\.prose\s+svg[^{]*\{[^}]*border-radius/s,
|
||||
'SVGs should have border-radius');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BaseLayout main element', () => {
|
||||
it('should not constrain main to --width-container', () => {
|
||||
const content = readLayout();
|
||||
// The main style block should NOT have max-width: var(--width-container)
|
||||
const mainBlock = content.match(/main\s*\{[^}]*\}/s);
|
||||
assert.ok(mainBlock, 'main style block should exist');
|
||||
assert.ok(
|
||||
!mainBlock[0].includes('max-width'),
|
||||
'main should not have max-width — child elements handle their own widths'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Post template prose-wrapper', () => {
|
||||
it('should not constrain prose-wrapper to --width-prose', () => {
|
||||
const content = readPostTemplate();
|
||||
const styleBlock = content.match(/<style>([\s\S]*?)<\/style>/);
|
||||
assert.ok(styleBlock, 'style block should exist');
|
||||
assert.ok(
|
||||
!styleBlock[1].includes('max-width: var(--width-prose)'),
|
||||
'.prose-wrapper should not be constrained to --width-prose (children self-constrain)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user