Skip to content

Commit a050597

Browse files
Streamline playground GitHub and export actions (#114)
1 parent b4ef69c commit a050597

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
@@ -63,6 +63,8 @@ const floorGuideTravel = $<HTMLSpanElement>("floor-guide-travel");
6363
const floorGuideReset = $<HTMLSpanElement>("floor-guide-reset");
6464
const copyBtn = $<HTMLButtonElement>("copy-prompt");
6565
const shareBtn = $<HTMLButtonElement>("share");
66+
const exportMenuButton = $<HTMLButtonElement>("export-menu-button");
67+
const exportMenu = $<HTMLDivElement>("export-menu");
6668
const downloadBvhBtn = $<HTMLButtonElement>("download-bvh");
6769
const downloadGltfBtn = $<HTMLButtonElement>("download-gltf");
6870
const 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 ---
759793
const howto = $<HTMLElement>("howto");
@@ -788,7 +822,13 @@ $<HTMLButtonElement>("howto-copy").addEventListener("click", (e) =>
788822
copyPrompt(e.currentTarget as HTMLButtonElement),
789823
);
790824
document.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 ---

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)