From 9446be6ce1c728a41db4c387f93a9af33d153918 Mon Sep 17 00:00:00 2001 From: eleith Date: Sat, 7 Nov 2026 10:55:34 -0800 Subject: [PATCH 1/9] [web] restore responsive browser font sizing --- .changeset/responsive-browser-font-sizing.md | 6 ++++++ apps/web/e2e/typography.e2e.ts | 15 +++++++++++++++ packages/web/theme/src/theme.ts | 1 - packages/web/worker/src/pieces/render.test.ts | 9 +++++++-- packages/web/worker/src/pieces/render.ts | 7 ++++++- 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .changeset/responsive-browser-font-sizing.md create mode 100644 apps/web/e2e/typography.e2e.ts diff --git a/.changeset/responsive-browser-font-sizing.md b/.changeset/responsive-browser-font-sizing.md new file mode 100644 index 00000000..8d373032 --- /dev/null +++ b/.changeset/responsive-browser-font-sizing.md @@ -0,0 +1,6 @@ +--- +"@luzzle/web": patch +"@luzzle/web.worker": patch +--- + +Restore configured responsive browser font sizing while keeping Open Graph typography fixed. diff --git a/apps/web/e2e/typography.e2e.ts b/apps/web/e2e/typography.e2e.ts new file mode 100644 index 00000000..d2cfa963 --- /dev/null +++ b/apps/web/e2e/typography.e2e.ts @@ -0,0 +1,15 @@ +import { expect, test } from './support/test.js' +import { notebook } from './support/fixture.js' + +test('root typography scales from the mobile minimum to the desktop size', async ({ page }) => { + await page.goto(notebook.publicUrl) + + for (const [width, fontSize] of [ + [390, '17.6px'], + [704, '19.8px'], + [1440, '22px'] + ] as const) { + await page.setViewportSize({ width, height: 900 }) + await expect(page.locator('html')).toHaveCSS('font-size', fontSize) + } +}) diff --git a/packages/web/theme/src/theme.ts b/packages/web/theme/src/theme.ts index 5efff147..975d057b 100644 --- a/packages/web/theme/src/theme.ts +++ b/packages/web/theme/src/theme.ts @@ -63,7 +63,6 @@ ${darkBlock} :root { font-family: var(--font-sans-name), 'Adjusted Sans'; - font-size: calc(var(--font-size-root) * 1px); } @media (prefers-color-scheme: dark) { diff --git a/packages/web/worker/src/pieces/render.test.ts b/packages/web/worker/src/pieces/render.test.ts index 44c22cdc..4db895e4 100644 --- a/packages/web/worker/src/pieces/render.test.ts +++ b/packages/web/worker/src/pieces/render.test.ts @@ -85,7 +85,7 @@ afterAll(() => { }) describe('pieces/render', () => { - test('passes configured WOFF2 bytes into the actual PNG renderer instead of the static URL', async () => { + test('keeps configured fonts and fixed root sizing in the PNG renderer', async () => { const config = makeConfig() const fontPath = path.resolve(import.meta.dirname, '../../assets/fonts/noto-sans.woff2') const fontBytes = readFileSync(fontPath) @@ -100,7 +100,12 @@ describe('pieces/render', () => { expectPng(await renderOpengraphPng(makePiece(), [], config)) expect(vi.mocked(ImageResponse).mock.calls.at(-1)![1]).toEqual( - expect.objectContaining({ fonts: [{ name: 'Noto Sans', data: fontBytes }] }) + expect.objectContaining({ + fonts: [{ name: 'Noto Sans', data: fontBytes }], + stylesheets: expect.arrayContaining([ + ':root { font-size: calc(var(--font-size-root) * 1px); }', + ]), + }) ) }) diff --git a/packages/web/worker/src/pieces/render.ts b/packages/web/worker/src/pieces/render.ts index b9bb11ac..ab449c02 100644 --- a/packages/web/worker/src/pieces/render.ts +++ b/packages/web/worker/src/pieces/render.ts @@ -108,7 +108,12 @@ export async function renderOpengraphPng( outDir?: string ): Promise { const { staticStylesheets, fontData } = await loadStaticAssets(config) - const stylesheets = [...staticStylesheets, generateThemeCss(config)] + // Share images use fixed canvas typography, not responsive browser sizing. + const stylesheets = [ + ...staticStylesheets, + generateThemeCss(config), + ':root { font-size: calc(var(--font-size-root) * 1px); }', + ] const markup = prepareTakumiMarkup(await renderOpengraphHtml(piece, assets, config)) const fetchedResources = loadLocalImageResources(markup.body, getAssetsDirectory(config, outDir)) const globals = config.theme?.globals || {} -- 2.54.0 From 72647d8ddbfaf7aff8223c513ef5e1ea14429e77 Mon Sep 17 00:00:00 2001 From: eleith Date: Sat, 7 Nov 2026 11:28:55 -0800 Subject: [PATCH 2/9] [web] refine prose typography and reading layout --- .changeset/shared-reading-typography.md | 5 + apps/web/e2e/support/fixture.ts | 94 +++++++++++++++---- apps/web/e2e/typography.e2e.ts | 68 +++++++++++++- .../templates/src/pages/note/template.svelte | 25 ++--- packages/web/theme/assets/styles/markdown.css | 45 ++++++++- 5 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 .changeset/shared-reading-typography.md diff --git a/.changeset/shared-reading-typography.md b/.changeset/shared-reading-typography.md new file mode 100644 index 00000000..8ccac216 --- /dev/null +++ b/.changeset/shared-reading-typography.md @@ -0,0 +1,5 @@ +--- +"@luzzle/web": patch +--- + +Give rendered Markdown shared prose typography and simplify the note page's headline scale and reading-column gutters. diff --git a/apps/web/e2e/support/fixture.ts b/apps/web/e2e/support/fixture.ts index 1da8c22e..fcb7328e 100644 --- a/apps/web/e2e/support/fixture.ts +++ b/apps/web/e2e/support/fixture.ts @@ -13,6 +13,36 @@ export const notebook = { title: 'Meteor Notebook' } +export const readingBook = { + file: 'quiet.books.md', + publicUrl: '/pieces/books/quiet-book', + type: 'books', + slug: 'quiet-book', + title: 'Quiet Book' +} + +const note = `Original observation. + +## A quiet section + +A small thought deserves room to breathe. + +- First thought +- Second thought + +### A detail + +A final paragraph. +` +const noteHtml = `
+

Original observation.

+

A quiet section

+

A small thought deserves room to breathe.

+
  • First thought
  • Second thought
+

A detail

+

A final paragraph.

+
` + const pieces = [ notebook, { @@ -20,7 +50,8 @@ const pieces = [ type: 'notes', slug: 'forest-notebook', title: 'Forest Notebook' - } + }, + readingBook ] export async function createFixture(directory: string, origin: string) { @@ -30,23 +61,25 @@ export async function createFixture(directory: string, origin: string) { await mkdir(path.join(archive, '.luzzle/schemas'), { recursive: true }) await mkdir(path.join(archive, 'reading'), { recursive: true }) await mkdir(path.join(directory, 'data'), { recursive: true }) - await writeFile( - path.join(archive, '.luzzle/schemas/notes.json'), - JSON.stringify({ - title: 'notes', - type: 'object', - properties: { - title: { type: 'string' }, - date: { type: 'string', format: 'date' } - }, - required: ['title', 'date'], - additionalProperties: false - }) - ) + for (const type of new Set(pieces.map((piece) => piece.type))) { + await writeFile( + path.join(archive, `.luzzle/schemas/${type}.json`), + JSON.stringify({ + title: type, + type: 'object', + properties: { + title: { type: 'string' }, + date: { type: 'string', format: 'date' } + }, + required: ['title', 'date'], + additionalProperties: false + }) + ) + } for (const piece of pieces) { await writeFile( path.join(archive, piece.file), - `---\ntitle: ${piece.title}\ndate: ${dateConsumed}\n---\n\nOriginal observation.\n` + `---\ntitle: ${piece.title}\ndate: ${dateConsumed}\n---\n\n${note}` ) } const database = path.join(directory, 'data/luzzle.sqlite') @@ -57,8 +90,21 @@ export async function createFixture(directory: string, origin: string) { storage: { root: archive }, paths: { database }, content: { text: { title: 'E2E Garden' } }, + // Book prose should inherit its smaller body size, unlike the reading page. + theme: { globals: { 'font-size-small': '0.95rem' } }, auth: { secret: 'e2e-only-session-secret-at-least-32-characters', credentials: ADMIN }, - pieces: [{ type: 'notes', fields: { title: 'title', date_consumed: 'date' } }] + pieces: [ + { + type: 'notes', + fields: { title: 'title', date_consumed: 'date' }, + templates: { page: { name: 'note' } } + }, + { + type: 'books', + fields: { title: 'title', date_consumed: 'date' }, + templates: { page: { name: 'book' } } + } + ] }) ) const db = createAppDb(database) @@ -82,13 +128,27 @@ export async function createFixture(directory: string, origin: string) { type: piece.type, file_path: piece.file, title: piece.title, - note: 'Original observation.', + note, json_metadata: JSON.stringify({ title: piece.title, date: dateConsumed }), date_added: consumedAt, date_consumed: consumedAt })) ) .execute() + await db + .insertInto('web_pieces_assets') + .values( + pieces.map((piece) => ({ + piece_file_path: piece.file, + piece_key: piece.slug, + asset_key: piece.slug, + transformation: 'markdown', + mime_type: 'text/html', + is_embedded: 1, + content: noteHtml + })) + ) + .execute() } finally { await db.destroy() } diff --git a/apps/web/e2e/typography.e2e.ts b/apps/web/e2e/typography.e2e.ts index d2cfa963..d310e78e 100644 --- a/apps/web/e2e/typography.e2e.ts +++ b/apps/web/e2e/typography.e2e.ts @@ -1,5 +1,5 @@ import { expect, test } from './support/test.js' -import { notebook } from './support/fixture.js' +import { notebook, readingBook } from './support/fixture.js' test('root typography scales from the mobile minimum to the desktop size', async ({ page }) => { await page.goto(notebook.publicUrl) @@ -13,3 +13,69 @@ test('root typography scales from the mobile minimum to the desktop size', async await expect(page.locator('html')).toHaveCSS('font-size', fontSize) } }) + +test('shared prose follows its template body size without restyling metadata labels', async ({ + page +}) => { + for (const piece of [notebook, readingBook]) { + await page.goto(piece.publicUrl) + const prose = page.locator('main .markdown') + await expect(prose.getByRole('heading', { name: 'A quiet section' })).toBeVisible() + const typography = await prose.evaluate((element) => { + const body = getComputedStyle(element) + const heading = getComputedStyle(element.querySelector('h2')!) + return { + bodySize: parseFloat(body.fontSize), + parentSize: parseFloat(getComputedStyle(element.parentElement!).fontSize), + lineHeight: parseFloat(body.lineHeight), + headingSize: parseFloat(heading.fontSize), + headingSpaceBefore: parseFloat(heading.marginTop), + headingSpaceAfter: parseFloat(heading.marginBottom) + } + }) + expect(typography.bodySize).toBe(typography.parentSize) + expect(typography.lineHeight / typography.bodySize).toBeCloseTo(1.6) + expect(typography.headingSize / typography.bodySize).toBeCloseTo(1.35) + expect(typography.headingSpaceBefore).toBeGreaterThan(typography.headingSpaceAfter) + + if (piece === readingBook) { + const labelSize = await page + .getByRole('heading', { name: 'Note', exact: true }) + .evaluate((element) => parseFloat(getComputedStyle(element).fontSize)) + expect(labelSize).toBeLessThan(typography.bodySize) + } + } +}) + +test('the reading page keeps a quiet headline and centered, aligned prose', async ({ page }) => { + await page.goto(notebook.publicUrl) + const headline = page.getByRole('heading', { name: notebook.title, exact: true }) + await expect(headline).toBeVisible() + + for (const width of [390, 1440]) { + await page.setViewportSize({ width, height: 900 }) + const metrics = await page.locator('main').evaluate((element) => { + const heading = element.querySelector('h1')! + const prose = element.querySelector('.markdown')! + const titleBox = heading.getBoundingClientRect() + const proseBox = prose.getBoundingClientRect() + return { + titleSize: parseFloat(getComputedStyle(heading).fontSize), + bodySize: parseFloat(getComputedStyle(prose).fontSize), + titleLeft: titleBox.left, + proseLeft: proseBox.left, + proseRight: innerWidth - proseBox.right, + proseWidth: proseBox.width + } + }) + expect(metrics.titleSize / metrics.bodySize).toBeCloseTo(2) + expect(metrics.proseRight).toBeCloseTo(metrics.proseLeft) + expect(metrics.titleLeft).toBeCloseTo(metrics.proseLeft) + if (width === 390) { + expect(metrics.proseLeft).toBeGreaterThanOrEqual(20) + expect(metrics.proseLeft).toBeLessThanOrEqual(24) + } else { + expect(metrics.proseWidth).toBeLessThanOrEqual(800) + } + } +}) diff --git a/packages/web/templates/src/pages/note/template.svelte b/packages/web/templates/src/pages/note/template.svelte index 197bbec6..f1d30c8d 100644 --- a/packages/web/templates/src/pages/note/template.svelte +++ b/packages/web/templates/src/pages/note/template.svelte @@ -57,20 +57,19 @@