@@ -72,6 +72,8 @@ const jointSelection = $<HTMLDivElement>("joint-selection");
7272const jointSelectionName = $ < HTMLElement > ( "joint-selection-name" ) ;
7373const copyBtn = $ < HTMLButtonElement > ( "copy-prompt" ) ;
7474const shareBtn = $ < HTMLButtonElement > ( "share" ) ;
75+ const exportMenuButton = $ < HTMLButtonElement > ( "export-menu-button" ) ;
76+ const exportMenu = $ < HTMLDivElement > ( "export-menu" ) ;
7577const downloadBvhBtn = $ < HTMLButtonElement > ( "download-bvh" ) ;
7678const downloadGltfBtn = $ < HTMLButtonElement > ( "download-gltf" ) ;
7779const tabEditor = $ < HTMLButtonElement > ( "tab-editor" ) ;
@@ -729,10 +731,10 @@ async function downloadBvh(): Promise<void> {
729731 const source = editorApi . getValue ( ) ;
730732 const { ir, errors } = parse ( source ) ;
731733 if ( ! ir || errors . length > 0 ) {
732- flash ( downloadBvhBtn , "Fix errors first" , "error" ) ;
734+ flash ( exportMenuButton , "Fix errors first" , "error" ) ;
733735 return ;
734736 }
735- flash ( downloadBvhBtn , "Exporting…" , "pending" , 0 ) ;
737+ flash ( exportMenuButton , "Exporting…" , "pending" , 0 ) ;
736738 try {
737739 const { exportBVH } = await import ( "posecode-render" ) ;
738740 const bvh = exportBVH ( ir ) ;
@@ -745,12 +747,11 @@ async function downloadBvh(): Promise<void> {
745747 a . click ( ) ;
746748 a . remove ( ) ;
747749 URL . revokeObjectURL ( url ) ;
748- flash ( downloadBvhBtn , "Downloaded ✓" , "success" ) ;
750+ flash ( exportMenuButton , "Downloaded ✓" , "success" ) ;
749751 } catch {
750- flash ( downloadBvhBtn , "Export failed" , "error" ) ;
752+ flash ( exportMenuButton , "Export failed" , "error" ) ;
751753 }
752754}
753- downloadBvhBtn . addEventListener ( "click" , downloadBvh ) ;
754755
755756// --- glTF / GLB export ---
756757// Bake the current movement into a GLB (rig + animation clip) and download it.
@@ -759,10 +760,10 @@ async function downloadGltf(): Promise<void> {
759760 const source = editorApi . getValue ( ) ;
760761 const { ir, errors } = parse ( source ) ;
761762 if ( ! ir || errors . length > 0 ) {
762- flash ( downloadGltfBtn , "Fix errors first" , "error" ) ;
763+ flash ( exportMenuButton , "Fix errors first" , "error" ) ;
763764 return ;
764765 }
765- flash ( downloadGltfBtn , "Exporting…" , "pending" , 0 ) ;
766+ flash ( exportMenuButton , "Exporting…" , "pending" , 0 ) ;
766767 try {
767768 const { exportGLTF } = await import ( "posecode-render" ) ;
768769 const glb = ( await exportGLTF ( ir , { binary : true } ) ) as ArrayBuffer ;
@@ -775,12 +776,45 @@ async function downloadGltf(): Promise<void> {
775776 a . click ( ) ;
776777 a . remove ( ) ;
777778 URL . revokeObjectURL ( url ) ;
778- flash ( downloadGltfBtn , "Downloaded ✓" , "success" ) ;
779+ flash ( exportMenuButton , "Downloaded ✓" , "success" ) ;
779780 } catch {
780- flash ( downloadGltfBtn , "Export failed" , "error" ) ;
781+ flash ( exportMenuButton , "Export failed" , "error" ) ;
781782 }
782783}
783- downloadGltfBtn . addEventListener ( "click" , downloadGltf ) ;
784+
785+ // --- Export menu ---
786+ function setExportMenu ( open : boolean ) : void {
787+ exportMenu . hidden = ! open ;
788+ exportMenuButton . setAttribute ( "aria-expanded" , String ( open ) ) ;
789+ if ( open ) downloadBvhBtn . focus ( ) ;
790+ }
791+ exportMenuButton . addEventListener ( "click" , ( ) => {
792+ setExportMenu ( exportMenu . hasAttribute ( "hidden" ) ) ;
793+ } ) ;
794+ downloadBvhBtn . addEventListener ( "click" , ( ) => {
795+ setExportMenu ( false ) ;
796+ exportMenuButton . focus ( ) ;
797+ void downloadBvh ( ) ;
798+ } ) ;
799+ downloadGltfBtn . addEventListener ( "click" , ( ) => {
800+ setExportMenu ( false ) ;
801+ exportMenuButton . focus ( ) ;
802+ void downloadGltf ( ) ;
803+ } ) ;
804+ document . addEventListener ( "click" , ( event ) => {
805+ if ( ! ( event . target instanceof Element ) || ! event . target . closest ( ".tb-downloads" ) ) {
806+ setExportMenu ( false ) ;
807+ }
808+ } ) ;
809+ exportMenu . addEventListener ( "keydown" , ( event ) => {
810+ const options = [ downloadBvhBtn , downloadGltfBtn ] ;
811+ const index = options . indexOf ( document . activeElement as HTMLButtonElement ) ;
812+ if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
813+ event . preventDefault ( ) ;
814+ const direction = event . key === "ArrowDown" ? 1 : - 1 ;
815+ options [ ( index + direction + options . length ) % options . length ] ?. focus ( ) ;
816+ }
817+ } ) ;
784818
785819// --- Slide-over panels (how-to, movement library) sharing one scrim ---
786820const howto = $ < HTMLElement > ( "howto" ) ;
@@ -815,7 +849,13 @@ $<HTMLButtonElement>("howto-copy").addEventListener("click", (e) =>
815849 copyPrompt ( e . currentTarget as HTMLButtonElement ) ,
816850) ;
817851document . addEventListener ( "keydown" , ( e ) => {
818- if ( e . key === "Escape" ) closePanels ( ) ;
852+ if ( e . key === "Escape" ) {
853+ closePanels ( ) ;
854+ if ( ! exportMenu . hidden ) {
855+ setExportMenu ( false ) ;
856+ exportMenuButton . focus ( ) ;
857+ }
858+ }
819859} ) ;
820860
821861// --- Intro strip ---
0 commit comments