diff --git a/.github/workflows/app-tests.yaml b/.github/workflows/app-tests.yaml index f90b1838..1b8a009d 100644 --- a/.github/workflows/app-tests.yaml +++ b/.github/workflows/app-tests.yaml @@ -146,6 +146,11 @@ jobs: sudo apt-get update sudo apt-get install -y ffmpeg ffmpeg -version + # Keep video assertions enabled while avoiding agent-browser 0.21.2's + # undersized JPEG probe on Ubuntu FFmpeg 6 (see the compatibility shim). + echo "CUJ_FFMPEG_REAL=$(command -v ffmpeg)" >> "$GITHUB_ENV" + chmod +x app/test/browser/compat-bin/ffmpeg + echo "$GITHUB_WORKSPACE/app/test/browser/compat-bin" >> "$GITHUB_PATH" - name: Checkout Runme (direct Jupyter bridge) uses: actions/checkout@v4 diff --git a/app/src/components/Actions/ActionOutputItems.tsx b/app/src/components/Actions/ActionOutputItems.tsx index 869fad91..4dd59ccb 100644 --- a/app/src/components/Actions/ActionOutputItems.tsx +++ b/app/src/components/Actions/ActionOutputItems.tsx @@ -108,7 +108,7 @@ function ActionOutputItemView({ ) } else { content = ( -
+      
         {text}
       
) diff --git a/app/src/components/Actions/Actions.test.tsx b/app/src/components/Actions/Actions.test.tsx index bd5257ef..129ae4b8 100644 --- a/app/src/components/Actions/Actions.test.tsx +++ b/app/src/components/Actions/Actions.test.tsx @@ -1333,7 +1333,7 @@ describe('Actions tabs', () => { }) }) - it('enables horizontal scrolling for wide notebook content', () => { + it('bounds the notebook scroll area so wide blocks scroll within cells', () => { const uri = 'local://file/wide-table.runme.md' contextMocks.currentDoc = uri contextMocks.workspaceDocuments = [ @@ -1361,9 +1361,9 @@ describe('Actions tabs', () => { expect(scrollViewport).toBeTruthy() expect(scrollRoot).toBeTruthy() - expect(scrollRoot?.className).not.toContain('overflow-x-hidden') + expect(scrollRoot?.className).toContain('notebook-scroll-area') expect((scrollViewport as HTMLElement | undefined)?.style.overflowX).toBe( - 'scroll' + 'hidden' ) }) diff --git a/app/src/components/Actions/Actions.tsx b/app/src/components/Actions/Actions.tsx index cab8b27f..6945b4a3 100644 --- a/app/src/components/Actions/Actions.tsx +++ b/app/src/components/Actions/Actions.tsx @@ -3730,8 +3730,8 @@ function NotebookTabContent({ {/* Full-width notebook column with horizontal padding for breathing room. diff --git a/app/src/components/Actions/MarkdownCell.test.tsx b/app/src/components/Actions/MarkdownCell.test.tsx index 61a815e5..462a63a1 100644 --- a/app/src/components/Actions/MarkdownCell.test.tsx +++ b/app/src/components/Actions/MarkdownCell.test.tsx @@ -83,6 +83,41 @@ function renderMarkdownCell( } describe("MarkdownCell", () => { + it.each(["Scrollable code block", "Scrollable table"])( + "preserves focus on %s when its cell becomes active", + async (label) => { + const stub = new StubCellData( + create(parser_pb.CellSchema, { + refId: "md-nested-scroll-focus", + kind: parser_pb.CellKind.MARKUP, + languageId: "markdown", + value: "```sh\necho hello\n```\n\n| Column |\n| --- |\n| value |", + }), + ); + /** Model the parent activation caused by a descendant's focus event. */ + function ActivatingCell() { + const [active, setActive] = React.useState(false); + return ( + {}} + isActiveCell={active} + activeFocusRole="rendered" + isWindowFocused + onFocusRoleChange={() => setActive(true)} + /> + ); + } + render(); + const scroller = screen.getByLabelText(label); + await act(async () => scroller.focus()); + expect(document.activeElement).toBe(scroller); + }, + ); + it("starts markdown cells in editor mode when persisted focus target is editor", () => { const cell = create(parser_pb.CellSchema, { refId: "md-active-editor", diff --git a/app/src/components/Actions/MarkdownCell.tsx b/app/src/components/Actions/MarkdownCell.tsx index 41c30c16..9bd8db73 100644 --- a/app/src/components/Actions/MarkdownCell.tsx +++ b/app/src/components/Actions/MarkdownCell.tsx @@ -108,7 +108,7 @@ const markdownComponents: Components = { } return ( {children} @@ -118,6 +118,8 @@ const markdownComponents: Components = { pre: ({ children, ...props }) => (
       {children}
@@ -132,7 +134,12 @@ const markdownComponents: Components = {
     
   ),
   table: ({ children, ...props }) => (
-    
+
{children}
@@ -283,6 +290,18 @@ const MarkdownCell = memo( const shouldOwnFocus = isActiveCell && isWindowFocused const tracksActiveCell = Boolean(onFocusRoleChange) + /** + * Activating a cell also runs focus restoration. Preserve focus already + * inside it, so a newly focused table, code scroller, or link receives + * the user's next key instead of losing focus to the rendered wrapper. + */ + const restoreRenderedFocus = useCallback(() => { + const root = renderedRef.current + if (root && !root.contains(root.ownerDocument.activeElement)) { + root.focus({ preventScroll: true }) + } + }, []) + // Enforce invariant: empty cells must be in edit mode. // This handles external changes (undo/redo, sync) that clear the value. useEffect(() => { @@ -327,15 +346,22 @@ const MarkdownCell = memo( return } setRendered(true) - renderedRef.current?.focus({ preventScroll: true }) - }, [activeFocusRole, canOpenSource, readOnly, shouldOwnFocus, value]) + restoreRenderedFocus() + }, [ + activeFocusRole, + canOpenSource, + readOnly, + restoreRenderedFocus, + shouldOwnFocus, + value, + ]) useEffect(() => { if (!shouldOwnFocus || activeFocusRole !== 'rendered' || !rendered) { return } - renderedRef.current?.focus({ preventScroll: true }) - }, [activeFocusRole, rendered, shouldOwnFocus]) + restoreRenderedFocus() + }, [activeFocusRole, rendered, restoreRenderedFocus, shouldOwnFocus]) useEffect(() => { if (!tracksActiveCell || isActiveCell || rendered || !value.trim()) { @@ -562,7 +588,7 @@ const MarkdownCell = memo( // Rendered markdown view - double-click or keyboard to edit
.rt-ScrollAreaViewport > div { + width: 100%; + max-width: 100%; +} + +/* Unlike break-word, anywhere also reduces intrinsic minimum width. This is + for prose/inline code/links; fenced code retains whitespace and scrolls. */ +.notebook-markdown { + overflow-wrap: anywhere; +} + +.notebook-markdown pre, +.notebook-markdown table { + overflow-wrap: normal; +} + /* ═══════════════════════════════════════════════════════════════════ 1. Breakpoints (build-time only — no CSS vars emitted) ═══════════════════════════════════════════════════════════════════ */ diff --git a/app/test/browser/compat-bin/ffmpeg b/app/test/browser/compat-bin/ffmpeg new file mode 100755 index 00000000..625e670c --- /dev/null +++ b/app/test/browser/compat-bin/ffmpeg @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# agent-browser 0.21.2 disables buffering and probes only 32 bytes of its +# streamed JPEG input. Ubuntu's FFmpeg 6 can then exit before discovering the +# image dimensions. Use FFmpeg's normal input buffering/probing for this exact +# recorder signature. Preserve every other argument, output, and exit status. +# Remove this shim when the pinned recorder no longer emits these flags. +args=("$@") +if [[ " ${*} " == *" -f image2pipe -c:v mjpeg "* ]]; then + args=() + while (($#)); do + case "$1:${2-}" in + -avioflags:direct|-fpsprobesize:0|-probesize:32|-analyzeduration:0) + shift 2 ;; + *) args+=("$1"); shift ;; + esac + done +fi +exec "${CUJ_FFMPEG_REAL:?Set CUJ_FFMPEG_REAL to the system ffmpeg}" "${args[@]}" diff --git a/app/test/browser/run-cuj-scenarios.ts b/app/test/browser/run-cuj-scenarios.ts index 062f314a..95f1f09c 100644 --- a/app/test/browser/run-cuj-scenarios.ts +++ b/app/test/browser/run-cuj-scenarios.ts @@ -80,6 +80,7 @@ function fetchWithTimeout( const SCENARIO_DRIVERS = [ join(SCRIPT_DIR, "test-scenario-colab-export-recovery.ts"), + join(SCRIPT_DIR, "test-scenario-horizontal-rendering.ts"), join(SCRIPT_DIR, "test-scenario-drive-revision-recovery.ts"), join(SCRIPT_DIR, "test-scenario-html-cell.ts"), join(SCRIPT_DIR, "test-scenario-hello-world.ts"), diff --git a/app/test/browser/test-scenario-horizontal-rendering.ts b/app/test/browser/test-scenario-horizontal-rendering.ts new file mode 100644 index 00000000..6fea948c --- /dev/null +++ b/app/test/browser/test-scenario-horizontal-rendering.ts @@ -0,0 +1,221 @@ +/** CUJ: docs-dev/cujs/horizontal-rendering.md. + * Use stored output and synthetic Markdown: no runner or authenticated service + * is needed. Layout must be checked in Chromium; jsdom has no layout engine. + */ +import { execFileSync } from 'node:child_process' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +const dir = here.endsWith('/.generated') ? dirname(here) : here +const output = join(dir, 'test-output') +const url = process.env.CUJ_FRONTEND_URL ?? 'http://localhost:5173' +const session = + process.env.AGENT_BROWSER_SESSION?.trim() || + `horizontal-rendering-${Date.now()}` +const profile = process.env.AGENT_BROWSER_PROFILE?.trim() +const headed = process.env.AGENT_BROWSER_HEADED?.toLowerCase() === 'true' +const keepOpen = process.env.AGENT_BROWSER_KEEP_OPEN?.toLowerCase() === 'true' +const movie = join(output, 'scenario-horizontal-rendering.webm') +const uri = 'local://file/horizontal-rendering-regression' +const fixture = JSON.parse( + readFileSync( + resolve(dir, '../fixtures/notebooks/horizontal-rendering.json'), + 'utf8' + ) +) +mkdirSync(output, { recursive: true }) +let passed = 0 +let failed = 0 + +/** Invoke the existing CUJ browser tool with argument boundaries preserved. */ +function browser(...args: string[]): string { + const options = ['--session', session] + if (profile) options.push('--profile', profile) + if (headed) options.push('--headed') + return execFileSync('agent-browser', [...options, ...args], { + encoding: 'utf8', + timeout: 30000, + maxBuffer: 4 * 1024 * 1024, + }).trim() +} + +/** Measure visible elements only: inactive tabs deliberately remain mounted. */ +function inspectLayout() { + const column = document.querySelector( + '[role="tabpanel"][data-state="active"] #notebook-column' + )! + const viewport = column.closest('.rt-ScrollAreaViewport')! + const markdown = column.querySelector('.notebook-markdown')! + const paragraph = markdown.querySelector('p')! + const tables = [...markdown.querySelectorAll('table')] + const pre = markdown.querySelector('pre')! + const output = column.querySelector( + '[data-testid="cell-output-item"] pre' + )! + const rect = paragraph.getBoundingClientRect() + const pane = viewport.getBoundingClientRect() + return { + viewport: viewport.clientWidth, + column: column.clientWidth, + scroll: viewport.scrollWidth, + proseFits: rect.left >= pane.left && rect.right <= pane.right + 1, + proseWraps: + rect.height > parseFloat(getComputedStyle(paragraph).lineHeight) * 1.5, + smallTableFits: + tables[0].scrollWidth <= tables[0].parentElement!.clientWidth + 1, + wideTableScrolls: + tables[1].parentElement!.scrollWidth > + tables[1].parentElement!.clientWidth && + getComputedStyle(tables[1].parentElement!).overflowX === 'auto', + codeScrolls: + pre.scrollWidth > pre.clientWidth && + getComputedStyle(pre).overflowX === 'auto', + scrollRegionsFocusable: + pre.tabIndex === 0 && tables[1].parentElement!.tabIndex === 0, + outputFits: output.scrollWidth <= output.clientWidth + 1, + outputIntact: output.textContent!.includes( + 'unbroken_output_identifier_'.repeat(500) + ), + siblingFits: + column.querySelectorAll('.notebook-markdown').length === 2 && + [...column.querySelectorAll('.notebook-markdown')].every( + (cell) => { + const bounds = cell.getBoundingClientRect() + return bounds.left >= pane.left && bounds.right <= pane.right + 1 + } + ), + } +} + +/** Fail on missing elements rather than treating empty measurements as success. */ +function check(name: string, ok: boolean) { + if (ok) passed++ + else failed++ + console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`) +} + +try { + browser('open', url) + browser('record', 'start', movie) + // Recording may replace the context. Seed only after recording has started. + browser('wait', '--fn', 'Boolean(window.app?.localNotebooks)') + browser( + 'eval', + `(async () => { + const store = window.app?.localNotebooks; + if (!store) throw new Error('Local notebook store is not ready'); + await store.files.put({ + id:${JSON.stringify(uri)}, uri:${JSON.stringify(uri)}, + name:'horizontal-rendering.json', remoteId:'', + doc:${JSON.stringify(JSON.stringify(fixture))}, + parent:'local://folder/local', updatedAt:new Date().toISOString(), + lastSynced:'', lastRemoteChecksum:'' + }); + sessionStorage.setItem('runme/openNotebooks',JSON.stringify([{uri:${JSON.stringify(uri)},name:'horizontal-rendering.json',type:'file',children:[],parents:['local://folder/local']} ])); + sessionStorage.setItem('runme/currentDoc',${JSON.stringify(uri)}); + })()` + ) + browser('reload') + browser('wait', '#markdown-rendered-markup_horizontal_rendering') + browser('wait', '[data-testid="cell-output-item"] pre') + browser('wait', '--fn', 'document.fonts.status === "loaded"') + // Exercise a normal and a narrow viewport; the sidebar may consume width too. + for (const width of [1280, 900]) { + browser('set', 'viewport', String(width), '900') + browser('wait', '300') + const raw = browser('eval', `(${inspectLayout.toString()})()`) + let result = JSON.parse(raw) + if (typeof result === 'string') result = JSON.parse(result) + writeFileSync( + join(output, `scenario-horizontal-rendering-${width}.json`), + JSON.stringify(result, null, 2) + ) + check( + `${width}: notebook width is bounded`, + result.column <= result.viewport + 1 && + result.scroll <= result.viewport + 1 + ) + for (const key of [ + 'proseFits', + 'proseWraps', + 'smallTableFits', + 'wideTableScrolls', + 'codeScrolls', + 'scrollRegionsFocusable', + 'outputFits', + 'outputIntact', + 'siblingFits', + ]) + check(`${width}: ${key}`, result[key] === true) + browser( + 'screenshot', + join(output, `scenario-horizontal-rendering-${width}.png`) + ) + // Capture the blocks the geometry checks cover, not just the first screen. + // Pressing Right must move each local scroll region; focusability alone + // would miss keyboard handlers that accidentally swallow navigation. + for (const [name, selector] of [ + ['code', '#markdown-rendered-markup_horizontal_rendering pre'], + [ + 'table', + '#markdown-rendered-markup_horizontal_rendering [role="region"]:has(th:nth-child(12))', + ], + ]) { + browser('scrollintoview', selector) + browser('focus', selector) + const before = JSON.parse( + browser( + 'eval', + `document.querySelector(${JSON.stringify(selector)}).scrollLeft` + ) + ) + browser('press', 'ArrowRight') + browser( + 'wait', + '--fn', + `document.querySelector(${JSON.stringify(selector)}).scrollLeft > ${Number(before)}` + ) + check(`${width}: ${name} scrolls with the keyboard`, true) + browser( + 'screenshot', + join(output, `scenario-horizontal-rendering-${width}-${name}.png`) + ) + } + browser('scrollintoview', '[data-testid="cell-output-item"]') + browser( + 'screenshot', + join(output, `scenario-horizontal-rendering-${width}-output.png`) + ) + browser('scrollintoview', '#markdown-rendered-markup_horizontal_sibling') + browser( + 'screenshot', + join(output, `scenario-horizontal-rendering-${width}-sibling.png`) + ) + browser( + 'scrollintoview', + '#markdown-rendered-markup_horizontal_rendering h1' + ) + } +} catch (error) { + check(String(error), false) +} finally { + try { + browser('record', 'stop') + } catch (error) { + check(`Could not finalize browser recording: ${error}`, false) + } + if (!keepOpen) { + try { + browser('close') + } catch { + /* Preserve test result during cleanup. */ + } + } +} +console.log(`Movie: ${movie}`) +console.log( + `Assertions: ${passed + failed}, Passed: ${passed}, Failed: ${failed}` +) +process.exitCode = failed ? 1 : 0 diff --git a/app/test/fixtures/notebooks/horizontal-rendering.json b/app/test/fixtures/notebooks/horizontal-rendering.json new file mode 100644 index 00000000..64e47042 --- /dev/null +++ b/app/test/fixtures/notebooks/horizontal-rendering.json @@ -0,0 +1,36 @@ +{ + "metadata": {}, + "cells": [ + { + "kind": 1, + "refId": "markup_horizontal_rendering", + "languageId": "markdown", + "value": "# Horizontal rendering regression\n\n## Outcome\n\n**Readable prose stays within the notebook pane.** This paragraph must wrap naturally even when a different cell contains a very long URL, code line or output. The notebook should never require horizontal scrolling to read ordinary text.\n\n## Timeline\n\n| Time | Evidence |\n| --- | --- |\n| 10:00 | A two-column table should wrap its descriptions and remain readable at narrow widths. |\n| 10:05 | Wider content below must not increase the width of this paragraph or its siblings. |\n\n## Long inline identifier\n\n`unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_unbroken_identifier_`\n\n## Wide code block\n\n```sh\necho long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_long_command_argument_\n```\n\n## Wide table\n\n| Column 1 | Column 2 | Column 3 | Column 4 | Column 5 | Column 6 | Column 7 | Column 8 | Column 9 | Column 10 | Column 11 | Column 12 |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n| value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ | value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_value_without_spaces_ |\n\n## After the wide content\n\nThis final paragraph should also fit within the notebook pane. All code and table content remains accessible through its own local scrolling.\n", + "metadata": {} + }, + { + "kind": 2, + "refId": "code_horizontal_output", + "languageId": "javascript", + "value": "// Stored output fixture; no backend execution is required.", + "metadata": {}, + "outputs": [ + { + "items": [ + { + "mime": "text/plain", + "data": "TG9uZyBvdXRwdXQgVVJMOiBodHRwczovL2V4YW1wbGUudGVzdC91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl91bmJyb2tlbl9vdXRwdXRfaWRlbnRpZmllcl8=" + } + ] + } + ] + }, + { + "kind": 1, + "refId": "markup_horizontal_sibling", + "languageId": "markdown", + "metadata": {}, + "value": "## Independent sibling cell\n\nThis cell must stay within the viewport even when earlier cells have very wide content. Unicode stays intact: caf\u00e9 \ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66 \u65e5\u672c\u8a9e." + } + ] +} diff --git a/docs-dev/cujs/README.md b/docs-dev/cujs/README.md index 2708d9f2..f93733fa 100644 --- a/docs-dev/cujs/README.md +++ b/docs-dev/cujs/README.md @@ -8,6 +8,9 @@ from `docs-dev/cujs/`. ## Current CUJs +- [horizontal-rendering.md](horizontal-rendering.md) — viewport-bounded prose with locally scrolling wide + tables/code and wrapping outputs; verified at normal and narrow widths. + - `hello-world-local-notebook.md` — baseline notebook flow: - configure local runner, - open local notebook, diff --git a/docs-dev/cujs/horizontal-rendering.md b/docs-dev/cujs/horizontal-rendering.md new file mode 100644 index 00000000..354b1372 --- /dev/null +++ b/docs-dev/cujs/horizontal-rendering.md @@ -0,0 +1,46 @@ +# Read notebooks containing wide Markdown and outputs + +Regression: a wide table, code block, or output expands Radix's fit-content +wrapper and makes every paragraph wrap beyond the visible pane. + +## Design decisions + +- The viewport owns notebook width. Constrain Radix's generated inner wrapper + to 100% width and max-width, scoped to `.notebook-scroll-area`. `w-full` on + the notebook alone refers to the already expanded wrapper and cannot fix it. +- The notebook scrolls vertically. Horizontal scrolling belongs to individual + tables and fenced code blocks, with keyboard-focusable scroll regions. +- Prose, links, inline identifiers and plain-text outputs use `overflow-wrap: + anywhere`. Unlike `break-word`, it also reduces intrinsic minimum width. +- Tables use normal wrapping so wide columns stay readable and scroll locally. + Fenced code preserves whitespace; the `pre`, not a second nested `code` + scroller, owns overflow. Do not clip content to conceal a sizing regression. +- Scope the unlayered CSS override to notebook panes; other Radix scroll areas, + including the tab rail and review panels, keep their existing behavior. +- Restoring active-cell focus must preserve a focused descendant. Otherwise + activating a cell steals focus from its table/code scroller and prevents + the first keyboard scroll. Test this with a previously inactive cell. + +Full design and before/after evidence: +[20260910_horizontal_rendering.runme](https://runme.gateway.unified-0.internal.api.openai.org/?doc=https%3A%2F%2Fdrive.google.com%2Ffile%2Fd%2F1m4Zx1EPXjrrXKYdzgXBhTatI5nYLj-ES%2Fview). + +## Fixture and acceptance criteria + +`app/test/fixtures/notebooks/horizontal-rendering.json` includes ordinary prose, +a two-column table, long inline code, a long fenced command, a 12-column table, +a long stored output URL, and a sibling Markdown cell. It contains no real +incident data or service credentials and needs no backend execution. + +Run `test-scenario-horizontal-rendering.ts` through the canonical CUJ runner. +At 1280px and 900px browser widths, require: + +1. Notebook column and scroll width do not exceed the viewport (1px tolerance). +2. Paragraphs fit and wrap; the small table fits. +3. Wide table and code block overflow locally, with keyboard focus available. +4. Long output remains intact and wraps within its output panel. +5. The sibling cell is present; earlier wide content does not expand the column. + +Record measurements, screenshots and video under `app/test/browser/test-output/`. +Also inspect the wide blocks after scrolling locally and test resizing with +Explorer/Comments panels open. A CSS-class test in jsdom cannot establish these +layout properties; use browser geometry and inspect screenshots.