// Studwright hub page: module catalogue, test list and an in-browser Luau playground for the free modules. // © 2026 CyberMax. const $ = (s) => document.querySelector(s); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const toast = (msg) => { const t = $('#toast'); t.textContent = msg; t.classList.add('on'); setTimeout(() => t.classList.remove('on'), 2200); }; const EXAMPLES = { format: `local Format = require(Studwright.Format) print(Format.abbreviate(1530000)) -- coins on a simulator HUD print(Format.abbreviate(999999)) -- never rounds up into the wrong suffix print(Format.abbreviate(2.5e15)) print(Format.commas(1234567)) print(Format.clock(3725), Format.duration(3725)) print("You placed " .. Format.ordinal(22)) print("Legendary drop chance: " .. Format.percent(0.0125))`, signal: `local Signal = require(Studwright.Signal) local coinsChanged = Signal.new() local hud = coinsChanged:Connect(function(new, delta) print(("HUD: %d coins (%+d)"):format(new, delta)) end) coinsChanged:Once(function() print("first coins ever: show a tip") end) coinsChanged:Fire(10, 10) coinsChanged:Fire(25, 15) hud:Disconnect() coinsChanged:Fire(30, 5) -- nothing listening any more print("still connected?", hud.Connected)`, cleanup: `local Cleanup = require(Studwright.Cleanup) local Signal = require(Studwright.Signal) local round = Cleanup.new() local tick = Signal.new() round:Connect(tick, function(n) print("round tick", n) end) round:Add(function() print("map reset") end) round:Add({ Destroy = function() print("boss despawned") end }) tick:Fire(1) print(round:Count(), "things to clean") round:Clean() -- reverse order: boss, map, then the connection tick:Fire(2) -- prints nothing: the connection is gone`, }; // ---- module catalogue + tests --------------------------------------------------------------- async function loadCatalogue() { const data = await (await fetch('modules.json')).json(); $('#mods').innerHTML = data.modules.map((m) => `
${esc(m.name)}${esc(m.side)}${m.free ? 'FREE' : ''} ${esc(m.summary)}
${esc(m.usage || '')}

${m.lines} lines${m.free ? ` · view the full source` : ''}

`).join(''); } async function loadTests() { const r = await fetch('tests.json'); if (!r.ok) return; const t = await r.json(); $('#test-count').textContent = t.pass; const items = t.tests.map((x) => `
  • ${x.ok ? '✓' : '✗'} ${esc(x.name)}
  • `); const list = $('#tests'); list.innerHTML = items.slice(0, 12).join(''); $('#more-tests').addEventListener('click', (e) => { list.innerHTML = items.join(''); e.target.remove(); }); } // ---- playground -------------------------------------------------------------------------------- let LuauState = null; const sources = {}; const PRELUDE = ` local __out = {} local function print(...) local n = select("#", ...) local parts = table.create(n) for i = 1, n do parts[i] = tostring((select(i, ...))) end table.insert(__out, table.concat(parts, " ")) end local function warn(...) print("warning:", ...) end local task = {} function task.spawn(f, ...) local th = type(f) == "thread" and f or coroutine.create(f) local ok, err = coroutine.resume(th, ...) if not ok then print("error: " .. tostring(err)) end return th end task.defer = task.spawn function task.cancel(th) if coroutine.status(th) == "suspended" then coroutine.close(th) end end function task.wait() return 0 end local function typeof(v) return type(v) end `; async function loadVM() { try { ({ LuauState } = await import('./vendor/luau-web/index.js')); for (const n of ['Signal', 'Cleanup', 'Format']) sources[n] = await (await fetch(`free/${n}.luau`)).text(); $('#run').disabled = false; $('#vm').textContent = 'Luau VM ready.'; } catch (e) { $('#vm').textContent = 'Could not load the Luau VM in this browser. You can still download the free modules below.'; } } function bundle(user) { const defs = Object.entries(sources).map(([n, src]) => `__defs[${JSON.stringify(n)}] = function(script)\n${src}\nend`).join('\n'); return `${PRELUDE} local __defs, __cache = {}, {} local __folder = setmetatable({}, { __index = function(_, k) return { __module = k } end }) local function require(ref) local name = ref.__module assert(__defs[name], "only the free modules (Signal, Cleanup, Format) run here; the kit has all 16") if __cache[name] == nil then __cache[name] = __defs[name]({ Parent = __folder, Name = name }) end return __cache[name] end ${defs} local Studwright = __folder local __ok, __err = pcall(function() ${user} end) if not __ok then print("error: " .. tostring(__err)) end return table.concat(__out, "\\n")`; } async function run() { const btn = $('#run'); btn.disabled = true; const out = $('#out'); try { const st = await LuauState.createAsync({}); const fn = st.loadstring(bundle($('#code').value), 'playground', false); if (typeof fn !== 'function') { out.textContent = 'Syntax error: ' + String(fn).replace(/\[string "playground"\]:\d+: /, ''); } else { const res = await fn(); out.textContent = (Array.isArray(res) ? res[0] : res) || '(no output)'; } } catch (e) { out.textContent = 'error: ' + (e && e.message ? e.message : e); } btn.disabled = false; } function pick(name) { document.querySelectorAll('#examples .chip').forEach((c) => c.classList.toggle('on', c.dataset.ex === name)); $('#code').value = EXAMPLES[name]; $('#out').textContent = 'Press Run.'; } $('#examples').addEventListener('click', (e) => { const b = e.target.closest('[data-ex]'); if (b) pick(b.dataset.ex); }); $('#run').addEventListener('click', run); $('#code').addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!$('#run').disabled) run(); } if (e.key === 'Tab') { e.preventDefault(); const t = e.target; const s = t.selectionStart; t.setRangeText(' ', s, t.selectionEnd, 'end'); } }); $('#btn-free').addEventListener('click', () => toast('Free modules downloading: drag Studwright.rbxmx into Studio.')); pick('format'); loadCatalogue(); loadTests(); loadVM();