@@ -63,6 +63,8 @@ const floorGuideTravel = $<HTMLSpanElement>("floor-guide-travel");
6363const floorGuideReset = $ < HTMLSpanElement > ( "floor-guide-reset" ) ;
6464const copyBtn = $ < HTMLButtonElement > ( "copy-prompt" ) ;
6565const shareBtn = $ < HTMLButtonElement > ( "share" ) ;
66+ const exportMenuButton = $ < HTMLButtonElement > ( "export-menu-button" ) ;
67+ const exportMenu = $ < HTMLDivElement > ( "export-menu" ) ;
6668const downloadBvhBtn = $ < HTMLButtonElement > ( "download-bvh" ) ;
6769const downloadGltfBtn = $ < HTMLButtonElement > ( "download-gltf" ) ;
6870const tabEditor = $ < HTMLButtonElement > ( "tab-editor" ) ;
@@ -702,10 +704,10 @@ async function downloadBvh(): Promise<void> {
702704 const source = editorApi . getValue ( ) ;
703705 const { ir, errors } = parse ( source ) ;
704706 if ( ! ir || errors . length > 0 ) {
705- flash ( downloadBvhBtn , "Fix errors first" , "error" ) ;
707+ flash ( exportMenuButton , "Fix errors first" , "error" ) ;
706708 return ;
707709 }
708- flash ( downloadBvhBtn , "Exporting…" , "pending" , 0 ) ;
710+ flash ( exportMenuButton , "Exporting…" , "pending" , 0 ) ;
709711 try {
710712 const { exportBVH } = await import ( "posecode-render" ) ;
711713 const bvh = exportBVH ( ir ) ;
@@ -718,12 +720,11 @@ async function downloadBvh(): Promise<void> {
718720 a . click ( ) ;
719721 a . remove ( ) ;
720722 URL . revokeObjectURL ( url ) ;
721- flash ( downloadBvhBtn , "Downloaded ✓" , "success" ) ;
723+ flash ( exportMenuButton , "Downloaded ✓" , "success" ) ;
722724 } catch {
723- flash ( downloadBvhBtn , "Export failed" , "error" ) ;
725+ flash ( exportMenuButton , "Export failed" , "error" ) ;
724726 }
725727}
726- downloadBvhBtn . addEventListener ( "click" , downloadBvh ) ;
727728
728729// --- glTF / GLB export ---
729730// Bake the current movement into a GLB (rig + animation clip) and download it.
@@ -732,10 +733,10 @@ async function downloadGltf(): Promise<void> {
732733 const source = editorApi . getValue ( ) ;
733734 const { ir, errors } = parse ( source ) ;
734735 if ( ! ir || errors . length > 0 ) {
735- flash ( downloadGltfBtn , "Fix errors first" , "error" ) ;
736+ flash ( exportMenuButton , "Fix errors first" , "error" ) ;
736737 return ;
737738 }
738- flash ( downloadGltfBtn , "Exporting…" , "pending" , 0 ) ;
739+ flash ( exportMenuButton , "Exporting…" , "pending" , 0 ) ;
739740 try {
740741 const { exportGLTF } = await import ( "posecode-render" ) ;
741742 const glb = ( await exportGLTF ( ir , { binary : true } ) ) as ArrayBuffer ;
@@ -748,12 +749,45 @@ async function downloadGltf(): Promise<void> {
748749 a . click ( ) ;
749750 a . remove ( ) ;
750751 URL . revokeObjectURL ( url ) ;
751- flash ( downloadGltfBtn , "Downloaded ✓" , "success" ) ;
752+ flash ( exportMenuButton , "Downloaded ✓" , "success" ) ;
752753 } catch {
753- flash ( downloadGltfBtn , "Export failed" , "error" ) ;
754+ flash ( exportMenuButton , "Export failed" , "error" ) ;
754755 }
755756}
756- downloadGltfBtn . addEventListener ( "click" , downloadGltf ) ;
757+
758+ // --- Export menu ---
759+ function setExportMenu ( open : boolean ) : void {
760+ exportMenu . hidden = ! open ;
761+ exportMenuButton . setAttribute ( "aria-expanded" , String ( open ) ) ;
762+ if ( open ) downloadBvhBtn . focus ( ) ;
763+ }
764+ exportMenuButton . addEventListener ( "click" , ( ) => {
765+ setExportMenu ( exportMenu . hasAttribute ( "hidden" ) ) ;
766+ } ) ;
767+ downloadBvhBtn . addEventListener ( "click" , ( ) => {
768+ setExportMenu ( false ) ;
769+ exportMenuButton . focus ( ) ;
770+ void downloadBvh ( ) ;
771+ } ) ;
772+ downloadGltfBtn . addEventListener ( "click" , ( ) => {
773+ setExportMenu ( false ) ;
774+ exportMenuButton . focus ( ) ;
775+ void downloadGltf ( ) ;
776+ } ) ;
777+ document . addEventListener ( "click" , ( event ) => {
778+ if ( ! ( event . target instanceof Element ) || ! event . target . closest ( ".tb-downloads" ) ) {
779+ setExportMenu ( false ) ;
780+ }
781+ } ) ;
782+ exportMenu . addEventListener ( "keydown" , ( event ) => {
783+ const options = [ downloadBvhBtn , downloadGltfBtn ] ;
784+ const index = options . indexOf ( document . activeElement as HTMLButtonElement ) ;
785+ if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
786+ event . preventDefault ( ) ;
787+ const direction = event . key === "ArrowDown" ? 1 : - 1 ;
788+ options [ ( index + direction + options . length ) % options . length ] ?. focus ( ) ;
789+ }
790+ } ) ;
757791
758792// --- Slide-over panels (how-to, movement library) sharing one scrim ---
759793const howto = $ < HTMLElement > ( "howto" ) ;
@@ -788,7 +822,13 @@ $<HTMLButtonElement>("howto-copy").addEventListener("click", (e) =>
788822 copyPrompt ( e . currentTarget as HTMLButtonElement ) ,
789823) ;
790824document . addEventListener ( "keydown" , ( e ) => {
791- if ( e . key === "Escape" ) closePanels ( ) ;
825+ if ( e . key === "Escape" ) {
826+ closePanels ( ) ;
827+ if ( ! exportMenu . hidden ) {
828+ setExportMenu ( false ) ;
829+ exportMenuButton . focus ( ) ;
830+ }
831+ }
792832} ) ;
793833
794834// --- Intro strip ---
0 commit comments