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.

at trunk 3.6 kB View raw
1import { useLayoutEffect, useRef } from 'react'; 2import type { BlockInstance } from '@wordpress/blocks'; 3import SkyEditor from './SkyEditor'; 4import CoverImagePicker from './CoverImagePicker'; 5import type { MediaUploadHandler } from '../lib/media/mediaUpload'; 6import type { CoverUpload } from '../lib/media/cover'; 7import type { BlockNode } from '../lib/blocks/render'; 8 9interface Props { 10 title: string; 11 onTitleChange: ( value: string ) => void; 12 lede: string; 13 onLedeChange: ( value: string ) => void; 14 /** Live block instances on every editor change — the parent normalises/stores as it needs. */ 15 onBlocksChange: ( blocks: BlockInstance[] ) => void; 16 mediaUpload?: MediaUploadHandler; 17 initialBlocks?: BlockNode[]; 18 cover: CoverUpload | null; 19 /** When provided, the cover picker renders and uploads through this handler (eager or deferred). */ 20 onCoverUpload?: ( file: File ) => Promise< CoverUpload >; 21 onCoverChange: ( cover: CoverUpload | null ) => void; 22} 23 24/** 25 * The shared writing surface for both the editor (`/editor`) and the writing-first page 26 * (`/write`): the borderless title + lede headings above the framed block canvas, plus the 27 * optional per-article cover picker. Presentational — it owns no document state, only the 28 * textareas' auto-grow. Each island wires the content state, the media-upload handler, and 29 * (for the cover) the upload path that fits its flow: eager PDS upload in the editor, deferred 30 * `data:`-URL hold in the writing-first flow. Lives only in `client:only` islands — it pulls in 31 * `SkyEditor`, which is browser-only (Decision 0003). 32 */ 33export default function EditorCanvas( { 34 title, 35 onTitleChange, 36 lede, 37 onLedeChange, 38 onBlocksChange, 39 mediaUpload, 40 initialBlocks, 41 cover, 42 onCoverUpload, 43 onCoverChange, 44}: Props ) { 45 const titleRef = useRef< HTMLTextAreaElement >( null ); 46 const ledeRef = useRef< HTMLTextAreaElement >( null ); 47 48 // Grow the title textarea to fit its content so long titles wrap into view instead of 49 // clipping on one line. Layout effect so it sizes before paint. 50 useLayoutEffect( () => { 51 const el = titleRef.current; 52 if ( ! el ) { 53 return; 54 } 55 el.style.height = 'auto'; 56 el.style.height = `${ el.scrollHeight }px`; 57 }, [ title ] ); 58 59 // Same auto-grow for the lede (and on hydrate from an edit-load / restored draft). 60 useLayoutEffect( () => { 61 const el = ledeRef.current; 62 if ( ! el ) { 63 return; 64 } 65 el.style.height = 'auto'; 66 el.style.height = `${ el.scrollHeight }px`; 67 }, [ lede ] ); 68 69 return ( 70 <> 71 <textarea 72 ref={ titleRef } 73 className="studio__title" 74 rows={ 1 } 75 placeholder="Add title" 76 aria-label="Article title" 77 value={ title } 78 // Single-line semantically: let it wrap visually, but don't let Enter insert a 79 // literal newline into the stored value. 80 onKeyDown={ ( event ) => { 81 if ( event.key === 'Enter' ) { 82 event.preventDefault(); 83 } 84 } } 85 onChange={ ( event ) => onTitleChange( event.target.value ) } 86 /> 87 <textarea 88 ref={ ledeRef } 89 className="studio__lede" 90 rows={ 1 } 91 maxLength={ 3000 } 92 placeholder="Add a subtitle…" 93 aria-label="Subtitle" 94 value={ lede } 95 onChange={ ( event ) => onLedeChange( event.target.value ) } 96 /> 97 { lede.length > 200 && ( 98 <p className="studio__lede-hint"> 99 Long subtitles get truncated on the Bluesky card. 100 </p> 101 ) } 102 <SkyEditor 103 onChange={ onBlocksChange } 104 mediaUpload={ mediaUpload } 105 initialBlocks={ initialBlocks } 106 /> 107 { onCoverUpload && ( 108 <CoverImagePicker cover={ cover } onUpload={ onCoverUpload } onChange={ onCoverChange } /> 109 ) } 110 </> 111 ); 112}