char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
403dcb9
main
1import type { Signal } from "@char/aftercare" ; 2import type { JSX } from "@char/aftercare/jsx-runtime" ; 3 4/** 5* run `render` now and (coalesced per microtask) whenever any signal changes, for as long as 6* `el` stays in the document 7*/ 8// deno-lint-ignore no-explicit-any 9export function watch ( el :Node , signals :readonly Signal < any >[], render :() => void ) :void { 10let mounted = false ; 11let queued = false ; 12const run = () => { 13queued = false ; 14if ( mounted && ! el . isConnected ) return subs . forEach ( s => s . unsubscribe ()); 15mounted ||= el . isConnected ; 16render (); 17}; 18const subs = signals . map ( s => 19s . subscribe (() => { 20if ( queued ) return ; 21queued = true ; 22queueMicrotask ( run ); 23}), 24); 25render (); 26} 27 28/** follow a pointer drag at the window level, so it survives the target being re-rendered */ 29export function drag ( 30start :PointerEvent , 31move :( e :PointerEvent ) => void , 32end ?:( e :PointerEvent ) => void , 33) :void { 34const onMove = ( e :PointerEvent ) => { 35if ( e . pointerId === start . pointerId ) move ( e ); 36}; 37const onUp = ( e :PointerEvent ) => { 38if ( e . pointerId !== start . pointerId ) return ; 39removeEventListener ( "pointermove" , onMove ); 40removeEventListener ( "pointerup" , onUp ); 41removeEventListener ( "pointercancel" , onUp ); 42end ?.( e ); 43}; 44addEventListener ( "pointermove" , onMove ); 45addEventListener ( "pointerup" , onUp ); 46addEventListener ( "pointercancel" , onUp ); 47} 48 49/** 50* make `parent`'s children exactly `nodes`, leaving nodes that stay in place untouched: moving a 51* node out and back in would blur it if focused, or drop a click that's halfway through 52*/ 53export function setChildren ( parent :Node , nodes :readonly Node []) :void { 54const keep = new Set ( nodes ); 55for ( const child of [ ...parent . childNodes ]) if ( ! keep . has ( child )) child . remove (); 56let at = parent . firstChild ; 57for ( const node of nodes ) { 58if ( node === at ) at = at . nextSibling ; 59else parent . insertBefore ( node , at ); 60} 61} 62 63export const px = ( n :number ) :string => ` ${ n } px` ; 64 65/** a toggle for a collapsible panel, with a chevron for which way it is */ 66export const disclosure = ( open :Signal < boolean >, label :string ) :JSX . Element => ( 67< button 68type = "button" 69class = "disclosure" 70aria - expanded = { open . derive ( String )} 71_onclick = {() => open . mut ( o => ! o )} 72> 73{ label} 74< / button> 75); 76 77/** a (?) that explains something when hovered */ 78export const help = ( text : string ) : JSX . Element => ( 79< span class = "help" title = { text } aria - label = { text } role = "note" > 80? 81</ span > 82); 83 84/** ask for a file to open; nothing comes of it if none is chosen */ 85export function chooseFile ( accept: string , then: ( file: File ) => void ) : void { 86const input = (< input type = "file" accept = { accept } />) as HTMLInputElement ; 87input . addEventListener ( "change" , () => input . files ?.[ 0 ] && then ( input . files [ 0 ])); 88input . click (); 89} 90 91/** download something as a file, named for `name` (which may be anything a project's called) */ 92export function saveFile ( name: string , extension: string , data: BlobPart , type: string ) : void { 93const url = URL . createObjectURL ( new Blob ([ data ], { type })); 94const link = document . createElement ( "a" ); 95link . href = url ; 96link . download = ` ${ name . replace ( / [/\\:*?"<>|] / g , "-" ) || "untitled" } ${ extension } ` ; 97link . click (); 98setTimeout (() => URL . revokeObjectURL ( url ), 1000 ); 99}