/** * Pure helpers — no DOM, no SDK, no IO. Safe to import from anywhere. * * Ported from `reachy_mini_marionette/lib/util.js`; the shape and * behaviour are 1:1 so on-disk move IDs / labels stay stable when a * user migrates from the v1 app. */ export const EMOJIS = [ "đŸ•ē", "💃", "🤖", "✨", "đŸŽĩ", "🎉", "🌈", "đŸ”Ĩ", "🧠", "đŸĒŠ", "👀", "đŸĻž", "🐙", "🚀", "🌟", "🎭", "🎨", "đŸŽĒ", ] as const; export function newId(): string { return Math.random().toString(36).slice(2, 10); } export function pickRandomEmoji(): string { return EMOJIS[Math.floor(Math.random() * EMOJIS.length)]!; } export function fmtTime(s: number): string { if (!Number.isFinite(s) || s < 0) return "0:00"; const m = Math.floor(s / 60); const r = Math.floor(s % 60); return `${m}:${String(r).padStart(2, "0")}`; } export function slugify(s: string): string { return ( String(s) .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 48) || "move" ); } /** * Next "Move N" label that doesn't collide with anything in the * supplied list (case-insensitive). Used both in the wizard (default * before the user types) and after-save (auto-name when the input is * blank). */ export function nextDefaultMoveName(existingLabels: Iterable): string { const used = new Set( Array.from(existingLabels, (m) => String(m || "") .trim() .toLowerCase(), ), ); let n = used.size + 1; while (used.has(`move ${n}`)) n++; return `Move ${n}`; } export const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms));