Skip to content

Commit 3eca2d5

Browse files
committed
Streamline playground GitHub and export actions
1 parent 7cc9f80 commit 3eca2d5

3 files changed

Lines changed: 136 additions & 31 deletions

File tree

playground/play.html

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@
101101
</a>
102102
<a
103103
id="feedback"
104-
href="https://github.com/posecode-dev/posecode/issues/new"
104+
href="https://github.com/posecode-dev/posecode"
105105
class="btn ghost"
106106
target="_blank"
107107
rel="noreferrer"
108-
aria-label="Report an issue on GitHub"
109-
title="Report an issue on GitHub"
108+
aria-label="Open the Posecode repository on GitHub"
109+
title="Open the Posecode repository on GitHub"
110110
>
111111
<span class="ico-github" aria-hidden="true"></span><span class="lbl">GitHub</span>
112112
</a>
@@ -121,21 +121,38 @@
121121
</button>
122122
<div class="tb-downloads">
123123
<button
124-
id="download-bvh"
124+
id="export-menu-button"
125125
class="btn ghost"
126-
aria-label="Download BVH"
127-
title="Download the movement as a .bvh motion file for Blender and other tools"
126+
type="button"
127+
aria-haspopup="menu"
128+
aria-expanded="false"
129+
aria-controls="export-menu"
128130
>
129-
<span class="lbl" aria-live="polite">Download BVH</span>
130-
</button>
131-
<button
132-
id="download-gltf"
133-
class="btn ghost"
134-
aria-label="Download glTF"
135-
title="Download the movement as a .glb glTF asset (rig + animation) for Three.js and web pipelines"
136-
>
137-
<span class="lbl" aria-live="polite">Download glTF</span>
131+
<span class="lbl" aria-live="polite">Export</span>
132+
<span class="export-caret" aria-hidden="true"></span>
138133
</button>
134+
<div id="export-menu" class="export-menu" role="menu" hidden>
135+
<button
136+
id="download-bvh"
137+
class="export-option"
138+
type="button"
139+
role="menuitem"
140+
title="Download the movement as a .bvh motion file for Blender and other tools"
141+
>
142+
<span>BVH</span>
143+
<small>For Blender and animation tools</small>
144+
</button>
145+
<button
146+
id="download-gltf"
147+
class="export-option"
148+
type="button"
149+
role="menuitem"
150+
title="Download the movement as a .glb glTF asset (rig + animation) for Three.js and web pipelines"
151+
>
152+
<span>glTF / GLB</span>
153+
<small>For Three.js and web pipelines</small>
154+
</button>
155+
</div>
139156
</div>
140157
<button
141158
id="copy-prompt"

playground/src/main.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const jointSelection = $<HTMLDivElement>("joint-selection");
7272
const jointSelectionName = $<HTMLElement>("joint-selection-name");
7373
const copyBtn = $<HTMLButtonElement>("copy-prompt");
7474
const shareBtn = $<HTMLButtonElement>("share");
75+
const exportMenuButton = $<HTMLButtonElement>("export-menu-button");
76+
const exportMenu = $<HTMLDivElement>("export-menu");
7577
const downloadBvhBtn = $<HTMLButtonElement>("download-bvh");
7678
const downloadGltfBtn = $<HTMLButtonElement>("download-gltf");
7779
const 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 ---
786820
const howto = $<HTMLElement>("howto");
@@ -815,7 +849,13 @@ $<HTMLButtonElement>("howto-copy").addEventListener("click", (e) =>
815849
copyPrompt(e.currentTarget as HTMLButtonElement),
816850
);
817851
document.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 ---

playground/src/style.css

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,60 @@ select:hover {
301301
align-items: center;
302302
}
303303

304-
/* The two motion-export buttons are grouped so mobile can lay them out as one
305-
balanced row. On desktop the wrapper is transparent (`display: contents`),
306-
so the buttons stay direct children of the flex toolbar and the desktop row
307-
is unchanged. */
304+
/* Desktop motion export lives behind one compact menu; mobile hides the whole
305+
desktop-pipeline action below. */
308306
.tb-downloads {
309-
display: contents;
307+
position: relative;
308+
}
309+
.export-caret {
310+
width: 7px;
311+
height: 7px;
312+
margin-left: 2px;
313+
border-right: 1.5px solid currentColor;
314+
border-bottom: 1.5px solid currentColor;
315+
transform: translateY(-2px) rotate(45deg);
316+
transition: transform 0.16s var(--ease);
317+
}
318+
#export-menu-button[aria-expanded="true"] .export-caret {
319+
transform: translateY(2px) rotate(225deg);
320+
}
321+
.export-menu {
322+
position: absolute;
323+
top: calc(100% + 8px);
324+
right: 0;
325+
z-index: 30;
326+
width: 260px;
327+
padding: 6px;
328+
background:
329+
var(--sheen),
330+
var(--panel-2);
331+
border: 1px solid var(--border-2);
332+
border-radius: var(--radius-sm);
333+
box-shadow: var(--shadow-float);
334+
}
335+
.export-option {
336+
display: grid;
337+
width: 100%;
338+
gap: 2px;
339+
padding: 10px 11px;
340+
color: var(--text);
341+
text-align: left;
342+
font: inherit;
343+
background: transparent;
344+
border: 0;
345+
border-radius: 5px;
346+
cursor: pointer;
347+
}
348+
.export-option:hover,
349+
.export-option:focus-visible {
350+
background: var(--panel-3);
351+
}
352+
.export-option > span {
353+
font-weight: 700;
354+
}
355+
.export-option small {
356+
color: var(--text-2);
357+
font-size: 11px;
310358
}
311359

312360
/* Movement-library trigger — the one content control in the topbar. Reads like

0 commit comments

Comments
 (0)