diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index 30c1379b..79249ab1 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -33,3 +33,4 @@ - [Always rebase before PR](feedback_pr_rebase.md) — rebase onto origin/main before creating PRs - [New branch per PR](feedback_pr_new_branch.md) — always create a fresh branch from main for each new PR/issue - [Upgrade gstack locally](feedback_gstack_upgrade.md) — always upgrade from the project's .claude/skills/gstack, not global +- [No lookbehind regex](feedback_no_lookbehind_regex.md) — never use `(?<=)` or `(? { }); }); + describe('hardcoded pixel width clamping', () => { + it('adds max-width and border-box when width exceeds viewport', () => { + const css = '.calibre8 { display: block; width: 1200px; padding: 2em 0 0 1em; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).toContain('max-width: calc(var(--available-width) * 1px)'); + expect(result).toContain('box-sizing: border-box'); + }); + + it('does not clamp when width is smaller than viewport', () => { + const css = '.box { width: 450px; padding: 2em; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).not.toContain('max-width: calc(var(--available-width)'); + }); + + it('does not add max-width when one already exists', () => { + const css = '.box { width: 1200px; max-width: 100%; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + const matches = result.match(/max-width/g); + expect(matches).toHaveLength(1); + }); + + it('does not affect max-width or min-width properties', () => { + const css = '.box { max-width: 1200px; min-width: 200px; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).not.toContain('max-width: calc(var(--available-width)'); + }); + + it('does not add max-width for non-pixel width values', () => { + const css = '.box { width: 50%; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).not.toContain('max-width: calc(var(--available-width)'); + }); + + it('does not add max-width for em width values', () => { + const css = '.box { width: 20em; }'; + const result = transformStylesheet(css, VW, VH, VERTICAL); + expect(result).not.toContain('max-width: calc(var(--available-width)'); + }); + }); + describe('preserves unrelated CSS', () => { it('passes through CSS without any matching patterns unchanged', () => { const css = '.custom { display: flex; padding: 10px; margin: 5px; }'; diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index 0e697d6e..526e6ff2 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -764,6 +764,20 @@ export const transformStylesheet = (css: string, vw: number, vh: number, vertica return selector + block; }); + // clip hardcoded pixel widths to available width when they exceed viewport + css = css.replace(ruleRegex, (match, selector, block) => { + const widthMatch = /(?:^|[^a-z-])width\s*:\s*(\d+(?:\.\d+)?)px/.exec(block); + const pxWidth = widthMatch ? parseFloat(widthMatch[1] ?? '0') : 0; + if (pxWidth > vw && !/max-width\s*:/.test(block)) { + block = block.replace( + /}$/, + ' max-width: calc(var(--available-width) * 1px); box-sizing: border-box; }', + ); + return selector + block; + } + return match; + }); + // replace absolute font sizes with rem units // replace vw and vh as they cause problems with layout // replace hardcoded colors diff --git a/apps/readest-app/vitest.browser.config.mts b/apps/readest-app/vitest.browser.config.mts index 1f411378..5f8388a4 100644 --- a/apps/readest-app/vitest.browser.config.mts +++ b/apps/readest-app/vitest.browser.config.mts @@ -17,20 +17,22 @@ export default defineConfig({ }, optimizeDeps: { include: [ - 'js-md5', + '@supabase/supabase-js', '@tauri-apps/plugin-fs', '@tauri-apps/plugin-http', '@tauri-apps/api/path', '@tauri-apps/api/core', + '@testing-library/react', '@zip.js/zip.js', 'franc-min', 'iso-639-2', 'iso-639-3', - 'uuid', + 'js-md5', 'jwt-decode', - '@supabase/supabase-js', + 'uuid', ], exclude: [ + '@pdfjs/pdf.min.mjs', '@tursodatabase/database-wasm', '@tursodatabase/database-wasm-common', '@tursodatabase/database-common', diff --git a/packages/foliate-js b/packages/foliate-js index 9a0c1c6f..fe33bb51 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 9a0c1c6f5bcb3a16b659d4ee4c4ceb437170fda9 +Subproject commit fe33bb510871bd0489493c49638a5b82c1de5df5