A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Portal the record JSON modal out of the eyebrow paragraph

Rendering the modal inside the eyebrow <p> nested a block-level backdrop in a
paragraph (invalid) and, worse, inherited the eyebrow's text-transform: uppercase,
corrupting the displayed JSON casing. Portal to <body> fixes both and lets the
fixed overlay escape any ancestor overflow/transform. Also pin the value-only
(no uri/cid envelope) decision in the page source test.

+39 -24
+32 -24
src/components/RecordJsonViewer.tsx
··· 1 1 import { useEffect, useRef, useState } from 'react'; 2 + import { createPortal } from 'react-dom'; 2 3 3 4 export interface RecordJsonViewerProps { 4 5 /** Sanitised, syntax-highlighted JSON markup — a `<code class="hljs language-json">…</code>` string. */ ··· 63 64 <path d="m8 6-6 6 6 6" /> 64 65 </svg> 65 66 </button> 66 - { open && ( 67 - <div className="record-json__backdrop" onClick={ close }> 68 - <div 69 - className="record-json__dialog" 70 - role="dialog" 71 - aria-modal="true" 72 - aria-label="Record JSON" 73 - onClick={ ( event ) => event.stopPropagation() } 74 - > 75 - <button 76 - ref={ closeRef } 77 - type="button" 78 - className="record-json__close" 79 - aria-label="Close" 80 - onClick={ close } 67 + { /* Portalled to <body>: the trigger lives inside the eyebrow <p>, where a 68 + block-level backdrop would be invalid, and the fixed overlay must escape any 69 + ancestor overflow/transform. Focus is moved in on open and restored to the 70 + trigger on close; we intentionally don't trap Tab — the dialog is view-only 71 + (one focusable control), and Escape/backdrop both dismiss it. */ } 72 + { open && 73 + createPortal( 74 + <div className="record-json__backdrop" onClick={ close }> 75 + <div 76 + className="record-json__dialog" 77 + role="dialog" 78 + aria-modal="true" 79 + aria-label="Record JSON" 80 + onClick={ ( event ) => event.stopPropagation() } 81 81 > 82 - × 83 - </button> 84 - <pre 85 - className="record-json__code" 86 - dangerouslySetInnerHTML={ { __html: highlightedHtml } } 87 - /> 88 - </div> 89 - </div> 90 - ) } 82 + <button 83 + ref={ closeRef } 84 + type="button" 85 + className="record-json__close" 86 + aria-label="Close" 87 + onClick={ close } 88 + > 89 + × 90 + </button> 91 + <pre 92 + className="record-json__code" 93 + dangerouslySetInnerHTML={ { __html: highlightedHtml } } 94 + /> 95 + </div> 96 + </div>, 97 + document.body 98 + ) } 91 99 </> 92 100 ); 93 101 }
+7
src/pages/[author]/[slug]/_[rkey].meta.test.ts
··· 129 129 /<RecordJsonViewer\s+client:only="react"\s+highlightedHtml=\{\s*recordJson\s*\}/ 130 130 ); 131 131 } ); 132 + 133 + it( 'renders the record value only, not the { uri, cid } envelope', () => { 134 + // `doc` is `document.value`; passing the whole `document` would leak the AT-URI/cid 135 + // wrapper into the viewer. Lock the "value only" decision (design 2026-06-11). 136 + expect( page ).toMatch( /const\s+doc\s*=\s*document\.value/ ); 137 + expect( page ).not.toMatch( /renderRecordJson\(\s*document\s*\)/ ); 138 + } ); 132 139 } );