Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions playground/play.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<span class="ico-help" aria-hidden="true"></span><span class="lbl">How to use</span>
</button>
<a
id="feedback"
href="mailto:hello@posecode.org?subject=Posecode%20Feedback"
class="btn ghost"
aria-label="Feedback"
Expand All @@ -93,10 +94,11 @@
<button
id="share"
class="btn ghost"
aria-label="Share"
aria-label="Copy link"
title="Copy a shareable link that renders this exact movement"
>
<span class="ico-link" aria-hidden="true"></span><span class="lbl">Share</span>
<span class="ico-link" aria-hidden="true"></span
><span class="lbl" aria-live="polite">Share</span>
</button>
<button
id="copy-prompt"
Expand Down
41 changes: 34 additions & 7 deletions playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,20 +426,46 @@ speed.addEventListener("change", () => viewer?.setSpeed(Number(speed.value)));
// --- Button label feedback ---
// Swap a button's label (the inner `.lbl` span when present, else the button
// text) to a transient message, then restore it.
function flash(btn: HTMLElement, message: string): void {
const flashTimers = new WeakMap<HTMLElement, number>();

function flash(
btn: HTMLElement,
message: string,
status: "pending" | "success" | "error",
duration = 4000,
): void {
const el = btn.querySelector<HTMLElement>(".lbl") ?? btn;
const prev = el.textContent;
const previousTimer = flashTimers.get(btn);
if (previousTimer !== undefined) window.clearTimeout(previousTimer);

const defaultLabel = el.dataset.defaultLabel ?? el.textContent ?? "";
const defaultAriaLabel =
btn.dataset.defaultAriaLabel ?? btn.getAttribute("aria-label") ?? "";
el.dataset.defaultLabel = defaultLabel;
btn.dataset.defaultAriaLabel = defaultAriaLabel;
el.textContent = message;
window.setTimeout(() => (el.textContent = prev), 1500);
btn.dataset.status = status;
btn.setAttribute("aria-label", message);
if (duration === 0) return;

const timer = window.setTimeout(() => {
el.textContent = defaultLabel;
delete btn.dataset.status;
if (defaultAriaLabel === "") btn.removeAttribute("aria-label");
else btn.setAttribute("aria-label", defaultAriaLabel);
flashTimers.delete(btn);
}, duration);
flashTimers.set(btn, timer);
}

// --- Copy LLM prompt (topbar) ---
async function copyPrompt(btn: HTMLButtonElement): Promise<void> {
flash(btn, "Copying…", "pending", 0);
try {
await navigator.clipboard.writeText(llmPrompt);
flash(btn, "Copied ✓");
flash(btn, "Copied ✓", "success");
} catch {
flash(btn, "Copy failed");
flash(btn, "Copy failed", "error");
}
}
copyBtn.addEventListener("click", () => copyPrompt(copyBtn));
Expand All @@ -449,22 +475,23 @@ copyBtn.addEventListener("click", () => copyPrompt(copyBtn));
// (so it's bookmarkable), and copy the full link to the clipboard.
async function shareLink(): Promise<void> {
if (!editorApi) return; // editor still loading; nothing to snapshot yet
flash(shareBtn, "Copying…", "pending", 0);
try {
const source = editorApi.getValue();
const path = buildNicePlayPath(source);
const hash = path === "/play" ? buildNiceShareHash(source) : "";
const url = `${location.origin}${path}${hash}`;
history.replaceState(null, "", `${path}${hash}`);
await navigator.clipboard.writeText(url);
flash(shareBtn, "Link copied ✓");
flash(shareBtn, "Link copied ✓", "success");
} catch (err) {
const message =
err instanceof TypeError
? "Nothing to share"
: err instanceof RangeError
? "Too long to link"
: "Copy failed";
flash(shareBtn, message);
flash(shareBtn, message, "error");
}
}
shareBtn.addEventListener("click", shareLink);
Expand Down
42 changes: 41 additions & 1 deletion playground/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1421,12 +1421,52 @@ select:hover {

@media (max-width: 860px) {
.topbar { padding: 12px; }
.topbar-actions { display: grid; grid-template-columns: 1fr auto auto; gap: 6px; }
.topbar-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: stretch;
gap: 6px;
}
.lib-btn { min-height: 42px; }
#new-doc, #how-to, #feedback, #share {
min-height: 42px;
}
#feedback, #share {
justify-content: center;
}
#share {
grid-column: 2 / -1;
}
#share .lbl {
display: inline;
}
#share:not([data-status]) .lbl {
font-size: 0;
}
#share:not([data-status]) .lbl::after {
content: "Copy link";
font-size: 13px;
}
#copy-prompt { grid-column: 1 / -1; min-height: 42px; }
.intro { padding-left: 12px; padding-right: 12px; }
.layout { grid-template-columns: 1fr; }
.transport { gap: 8px; padding-left: 12px; padding-right: 12px; }
.speed > span, .loop > span { display: none; }
.clock { padding-left: 8px; min-width: 40px; }
}

#share[data-status="success"] {
color: var(--bg);
background: var(--accent);
border-color: var(--accent);
}

#share[data-status="pending"] {
color: var(--accent);
border-color: var(--accent);
}

#share[data-status="error"] {
color: #ffb4a9;
border-color: #a94b41;
}
Loading