@homie-rocks/studio 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -4
- package/bin/homie-studio.mjs +83 -8
- package/lib/build.mjs +2 -0
- package/lib/cloudflare.mjs +5 -5
- package/lib/codex.mjs +28 -9
- package/lib/demo.mjs +85 -0
- package/lib/files.mjs +154 -0
- package/lib/handoff.mjs +128 -0
- package/lib/jobs.mjs +181 -0
- package/lib/mcp-tools.mjs +1000 -0
- package/lib/mcp.mjs +180 -0
- package/lib/office.mjs +172 -0
- package/lib/scaffold.mjs +98 -25
- package/lib/stats.mjs +1 -1
- package/lib/studio.mjs +4 -1
- package/lib/template-history.json +52 -0
- package/lib/upgrade.mjs +6 -3
- package/mcp/ui/bridge.js +148 -0
- package/mcp/ui/build.js +63 -0
- package/mcp/ui/card.css +73 -0
- package/mcp/ui/codex.js +23 -0
- package/mcp/ui/setup.js +72 -0
- package/mcp/ui/studio.js +41 -0
- package/netplay/NETPLAY.md +101 -15
- package/netplay/netplay.ts +49 -3
- package/package.json +2 -1
- package/worker/index.mjs +172 -33
- package/worker/office-page.mjs +366 -0
- package/worker/office-schema.mjs +39 -0
- package/worker/office.mjs +762 -0
- package/worker/pages.mjs +257 -10
- package/worker/room.mjs +301 -8
- package/worker/site.mjs +24 -2
- package/worker/stats-page.mjs +16 -7
- package/worker/stats.mjs +39 -12
- package/worker/version.mjs +1 -1
|
@@ -247,6 +247,58 @@
|
|
|
247
247
|
"## Beta": "9d02638207a26e73"
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
+
},
|
|
251
|
+
"0.13.0": {
|
|
252
|
+
"files": {
|
|
253
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
254
|
+
"README.md": "684b3cca8001673c",
|
|
255
|
+
"games/README.md": "5e5e942baa7e0372",
|
|
256
|
+
"music/README.md": "b8a828de9311bd28",
|
|
257
|
+
"videos/README.md": "70614388cbb203c7",
|
|
258
|
+
"posts/README.md": "37841aae476bd540",
|
|
259
|
+
"site/README.md": "ca131cd0472464fb",
|
|
260
|
+
"site/src/worker.mjs": "ca3f1630f2420ad3"
|
|
261
|
+
},
|
|
262
|
+
"sections": {
|
|
263
|
+
"AGENTS.md": {
|
|
264
|
+
"(intro)": "214aa46eb3a895d5",
|
|
265
|
+
"## Layout": "f14d641159480e6c",
|
|
266
|
+
"## Commands (all through the pinned CLI in node_modules)": "3a3d3bef7e8d1a50",
|
|
267
|
+
"## The site": "7d3525ba39b7f262",
|
|
268
|
+
"## Making games": "63069b31c87a2aa2",
|
|
269
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
270
|
+
"## Running live games (the back office)": "f0566f07cff04113",
|
|
271
|
+
"## Rules": "969318425c99f5a5",
|
|
272
|
+
"## Beta": "9d02638207a26e73"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"0.14.0": {
|
|
277
|
+
"files": {
|
|
278
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
279
|
+
"README.md": "684b3cca8001673c",
|
|
280
|
+
"HANDOFF.md": "ce7a8fd91c6bbee2",
|
|
281
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
282
|
+
"music/README.md": "b8a828de9311bd28",
|
|
283
|
+
"videos/README.md": "70614388cbb203c7",
|
|
284
|
+
"posts/README.md": "37841aae476bd540",
|
|
285
|
+
"site/README.md": "ca131cd0472464fb",
|
|
286
|
+
"site/src/worker.mjs": "ca3f1630f2420ad3"
|
|
287
|
+
},
|
|
288
|
+
"sections": {
|
|
289
|
+
"AGENTS.md": {
|
|
290
|
+
"(intro)": "214aa46eb3a895d5",
|
|
291
|
+
"## Layout": "f14d641159480e6c",
|
|
292
|
+
"## Commands (all through the pinned CLI in node_modules)": "e579cd6ce3ce342a",
|
|
293
|
+
"## The site": "7c1618431bae6324",
|
|
294
|
+
"## Making games": "63069b31c87a2aa2",
|
|
295
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
296
|
+
"## Running live games (the back office)": "f0566f07cff04113",
|
|
297
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
298
|
+
"## Rules": "969318425c99f5a5",
|
|
299
|
+
"## Beta": "9d02638207a26e73"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
250
302
|
}
|
|
251
303
|
}
|
|
252
304
|
}
|
package/lib/upgrade.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { readStudio } from './studio.mjs';
|
|
|
31
31
|
import { STUDIO_VERSION, packageSpec } from './version.mjs';
|
|
32
32
|
|
|
33
33
|
/** Whole files the template writes and a studio may keep as they are: updated only while the studio never changed them. */
|
|
34
|
-
export const TEMPLATE_FILES = ['AGENTS.md', 'CLAUDE.md', 'README.md', 'games/README.md', 'music/README.md', 'videos/README.md', 'posts/README.md', 'site/README.md', 'site/src/worker.mjs'];
|
|
34
|
+
export const TEMPLATE_FILES = ['AGENTS.md', 'CLAUDE.md', 'README.md', 'HANDOFF.md', 'games/README.md', 'music/README.md', 'videos/README.md', 'posts/README.md', 'site/README.md', 'site/src/worker.mjs'];
|
|
35
35
|
/** Files the template adds when missing and never changes once they exist (a migration is applied; a manifest is the studio's). */
|
|
36
36
|
export const ADD_ONLY = ['music/manifest.json', 'videos/manifest.json', '.claude/skills/.gitkeep'];
|
|
37
37
|
const MIGRATIONS = /^site\/migrations\/[^/]+\.sql$/;
|
|
@@ -170,11 +170,14 @@ export function upgradePlan(root, { history = readHistory() } = {}) {
|
|
|
170
170
|
}
|
|
171
171
|
const have = sections(mine);
|
|
172
172
|
const tsecs = sections(want);
|
|
173
|
+
const adding = new Set();
|
|
173
174
|
for (const [k, t] of tsecs.entries()) {
|
|
174
175
|
const at = have.find((s) => s.heading === t.heading);
|
|
175
176
|
if (!at) {
|
|
176
|
-
// After the nearest template section before it that the studio has
|
|
177
|
-
|
|
177
|
+
// After the nearest template section before it that the studio has (or that this plan adds just before it, so
|
|
178
|
+
// two new sections in a row keep the template's order); else before the first one after it.
|
|
179
|
+
const before = tsecs.slice(0, k).reverse().find((p) => have.some((s) => s.heading === p.heading) || adding.has(p.heading));
|
|
180
|
+
adding.add(t.heading);
|
|
178
181
|
const after = before ? null : tsecs.slice(k + 1).find((p) => have.some((s) => s.heading === p.heading));
|
|
179
182
|
changes.push({ file: rel, kind: 'add-section', section: t.heading, what: `new section ${t.heading === INTRO ? 'intro' : `"${t.heading}"`}`, after: before?.heading ?? null, before: after?.heading ?? null, text: t.text });
|
|
180
183
|
continue;
|
package/mcp/ui/bridge.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Homie's local cards: the MCP Apps bridge (spec 2026-01-26) every card shares. JSON-RPC 2.0 over postMessage
|
|
3
|
+
* with the host: ui/initialize, the tool's input and result, the host's theme, tools/call (the host proxies it to
|
|
4
|
+
* the local Homie server), ui/open-link for every link, ui/update-model-context to tell the model once, and the
|
|
5
|
+
* card's size. No network of its own; every text is set as text, never as HTML.
|
|
6
|
+
*/
|
|
7
|
+
var Card = (function () {
|
|
8
|
+
var seq = 0; var pending = {}; var ctx = {}; var caps = {};
|
|
9
|
+
var S = { result: null, data: null, timer: null, torn: false, visible: true, told: {}, busy: false };
|
|
10
|
+
var spec = null;
|
|
11
|
+
var root = document.getElementById('root');
|
|
12
|
+
|
|
13
|
+
function post(m) { window.parent.postMessage(m, '*'); }
|
|
14
|
+
function rpc(method, params) {
|
|
15
|
+
var id = ++seq;
|
|
16
|
+
post({ jsonrpc: '2.0', id: id, method: method, params: params || {} });
|
|
17
|
+
return new Promise(function (resolve, reject) {
|
|
18
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
19
|
+
setTimeout(function () { if (pending[id]) { delete pending[id]; reject(new Error('timeout')); } }, 65000);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function note(method, params) { post({ jsonrpc: '2.0', method: method, params: params || {} }); }
|
|
23
|
+
function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined && text !== null) e.textContent = String(text); return e; }
|
|
24
|
+
function add(parent) { for (var i = 1; i < arguments.length; i++) { var k = arguments[i]; if (k) parent.appendChild(k); } return parent; }
|
|
25
|
+
function btn(label, cls, onclick) { var b = el('button', cls || '', label); b.type = 'button'; b.onclick = onclick; return b; }
|
|
26
|
+
function pill(text, cls, live) { var p = el('span', 'pill ' + (cls || '')); if (live) p.appendChild(el('span', 'dot')); p.appendChild(el('span', '', text)); return p; }
|
|
27
|
+
function top(label, right) { return add(el('div', 'top'), add(el('div', 'eyebrow'), el('span', 'mark'), el('span', '', 'Homie · ' + label)), right || null); }
|
|
28
|
+
function img(src, alt) {
|
|
29
|
+
if (typeof src !== 'string' || !/^data:image\/(png|jpeg|webp|gif);base64,/.test(src)) return null;
|
|
30
|
+
var i = el('img'); i.alt = alt || ''; i.decoding = 'async'; i.src = src; i.onerror = function () { i.remove(); size(); };
|
|
31
|
+
return i;
|
|
32
|
+
}
|
|
33
|
+
function open(url) { if (typeof url === 'string' && /^https?:\/\//.test(url)) rpc('ui/open-link', { url: url }).catch(function () {}); }
|
|
34
|
+
function call(name, args) { return rpc('tools/call', { name: name, arguments: args || {} }); }
|
|
35
|
+
function tell(key, text) {
|
|
36
|
+
if (S.told[key]) return; S.told[key] = true;
|
|
37
|
+
rpc('ui/update-model-context', { content: [{ type: 'text', text: text }] }).catch(function () {});
|
|
38
|
+
}
|
|
39
|
+
function ago(iso) {
|
|
40
|
+
var t = Date.parse(iso || ''); if (!isFinite(t)) return '';
|
|
41
|
+
var s = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
42
|
+
return s < 5 ? 'just now' : s < 60 ? s + ' s ago' : s < 3600 ? Math.round(s / 60) + ' min ago' : Math.round(s / 3600) + ' h ago';
|
|
43
|
+
}
|
|
44
|
+
function dur(ms) { if (!(ms >= 0)) return ''; var s = Math.round(ms / 1000); return s < 60 ? s + ' s' : Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); }
|
|
45
|
+
// As the ext-apps SDK measures: the content's own height (html at max-content) and the frame's width, once a
|
|
46
|
+
// frame, only when either changed.
|
|
47
|
+
var sized = { w: 0, h: 0, queued: false };
|
|
48
|
+
function size() {
|
|
49
|
+
if (sized.queued) return; sized.queued = true;
|
|
50
|
+
requestAnimationFrame(function () {
|
|
51
|
+
sized.queued = false;
|
|
52
|
+
var html = document.documentElement; var was = html.style.height;
|
|
53
|
+
html.style.height = 'max-content';
|
|
54
|
+
var h = Math.ceil(html.getBoundingClientRect().height);
|
|
55
|
+
html.style.height = was;
|
|
56
|
+
var w = Math.ceil(window.innerWidth);
|
|
57
|
+
if (w === sized.w && h === sized.h) return;
|
|
58
|
+
sized.w = w; sized.h = h;
|
|
59
|
+
note('ui/notifications/size-changed', { width: w, height: h });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function theme(c) {
|
|
63
|
+
if (!c) return;
|
|
64
|
+
if (c.theme === 'light' || c.theme === 'dark') { document.documentElement.setAttribute('data-theme', c.theme); document.documentElement.style.colorScheme = c.theme; }
|
|
65
|
+
var v = c.styles && c.styles.variables;
|
|
66
|
+
if (v) for (var k in v) if (v[k] !== undefined && /^--[a-z0-9-]+$/.test(k)) document.documentElement.style.setProperty(k, v[k]);
|
|
67
|
+
if (c.styles && c.styles.css && c.styles.css.fonts && !document.getElementById('host-fonts')) { var st = el('style'); st.id = 'host-fonts'; st.textContent = c.styles.css.fonts; document.head.appendChild(st); }
|
|
68
|
+
}
|
|
69
|
+
function fullscreenButton() {
|
|
70
|
+
var modes = (ctx && ctx.availableDisplayModes) || [];
|
|
71
|
+
if (modes.indexOf('fullscreen') < 0) return null;
|
|
72
|
+
var full = ctx.displayMode === 'fullscreen';
|
|
73
|
+
return btn(full ? 'Smaller' : 'Full screen', 'ghost small', function () {
|
|
74
|
+
rpc('ui/request-display-mode', { mode: full ? 'inline' : 'fullscreen' }).then(function (r) { ctx.displayMode = r && r.mode; render(); }).catch(function () {});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function textOf(r) { return (r && r.content && r.content[0] && r.content[0].text) || ''; }
|
|
78
|
+
function render() {
|
|
79
|
+
if (!spec) return;
|
|
80
|
+
var card;
|
|
81
|
+
if (!S.data) {
|
|
82
|
+
card = el('main', 'card');
|
|
83
|
+
if (S.result && S.result.isError) add(card, top(spec.label, pill('Not done', 'bad')), el('p', 'note bad', textOf(S.result) || 'That did not work.'));
|
|
84
|
+
else add(card, top(spec.label, pill('Loading', 'live', true)), el('div', 'skel'), el('div', 'skel'));
|
|
85
|
+
} else {
|
|
86
|
+
try { card = spec.render(S.data, S.result); } catch (e) { card = add(el('main', 'card'), top(spec.label), el('p', 'note bad', String(e && e.message || e))); }
|
|
87
|
+
if (S.result && S.result.isError && S.data) card.appendChild(el('p', 'note bad', textOf(S.result)));
|
|
88
|
+
}
|
|
89
|
+
card.id = 'root';
|
|
90
|
+
if (ctx.displayMode === 'fullscreen') card.classList.add('full');
|
|
91
|
+
root.replaceWith(card); root = card; size();
|
|
92
|
+
}
|
|
93
|
+
function take(result) {
|
|
94
|
+
S.result = result;
|
|
95
|
+
if (result && result.structuredContent && result.structuredContent.kind === spec.kind) S.data = result.structuredContent;
|
|
96
|
+
render();
|
|
97
|
+
schedule();
|
|
98
|
+
}
|
|
99
|
+
function schedule() {
|
|
100
|
+
if (S.timer) { clearTimeout(S.timer); S.timer = null; }
|
|
101
|
+
if (S.torn || !spec.poll || !S.data) return;
|
|
102
|
+
var p = spec.poll(S.data);
|
|
103
|
+
if (!p) return;
|
|
104
|
+
S.timer = setTimeout(function () {
|
|
105
|
+
S.timer = null;
|
|
106
|
+
if (!S.visible) { schedule(); return; }
|
|
107
|
+
call(p.tool, p.args).then(function (r) { if (r && r.structuredContent && r.structuredContent.kind === spec.kind) take(r); else schedule(); })
|
|
108
|
+
.catch(function () { schedule(); });
|
|
109
|
+
}, p.every || 2000);
|
|
110
|
+
}
|
|
111
|
+
document.addEventListener('visibilitychange', function () { S.visible = !document.hidden; });
|
|
112
|
+
window.addEventListener('resize', size);
|
|
113
|
+
window.addEventListener('message', function (ev) {
|
|
114
|
+
if (ev.source !== window.parent) return;
|
|
115
|
+
var m = ev.data; if (!m || m.jsonrpc !== '2.0') return;
|
|
116
|
+
if (m.id !== undefined && m.id !== null && !m.method) {
|
|
117
|
+
var p = pending[m.id]; if (!p) return; delete pending[m.id];
|
|
118
|
+
if (m.error) p.reject(m.error); else p.resolve(m.result);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (m.method === 'ui/notifications/tool-input') { if (!S.result) render(); }
|
|
122
|
+
else if (m.method === 'ui/notifications/tool-result') take(m.params);
|
|
123
|
+
else if (m.method === 'ui/notifications/tool-cancelled') { if (!S.result) { S.result = { isError: true, content: [{ type: 'text', text: 'Cancelled.' }] }; render(); } }
|
|
124
|
+
else if (m.method === 'ui/notifications/host-context-changed') { for (var k in m.params || {}) ctx[k] = m.params[k]; theme(m.params); render(); }
|
|
125
|
+
else if (m.method === 'ui/resource-teardown') { S.torn = true; if (S.timer) clearTimeout(S.timer); if (m.id !== undefined) post({ jsonrpc: '2.0', id: m.id, result: {} }); }
|
|
126
|
+
else if (m.method === 'ping' && m.id !== undefined) post({ jsonrpc: '2.0', id: m.id, result: {} });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
el: el, add: add, btn: btn, pill: pill, top: top, img: img, open: open, call: call, tell: tell, ago: ago, dur: dur,
|
|
131
|
+
size: size, render: render, take: take, fullscreenButton: fullscreenButton, textOf: textOf,
|
|
132
|
+
ctx: function () { return ctx; },
|
|
133
|
+
state: S,
|
|
134
|
+
/** spec: { kind, label, render(data, result) -> element, poll(data) -> { tool, args, every } | null } */
|
|
135
|
+
start: function (s) {
|
|
136
|
+
spec = s;
|
|
137
|
+
rpc('ui/initialize', {
|
|
138
|
+
protocolVersion: '2026-01-26',
|
|
139
|
+
appInfo: { name: 'homie-studio-' + s.kind + '-card', version: '1' },
|
|
140
|
+
appCapabilities: { availableDisplayModes: s.fullscreen ? ['inline', 'fullscreen'] : ['inline'] },
|
|
141
|
+
}).then(function (res) {
|
|
142
|
+
caps = (res && res.hostCapabilities) || {}; ctx = (res && res.hostContext) || {};
|
|
143
|
+
theme(ctx); note('ui/notifications/initialized', {}); render();
|
|
144
|
+
}).catch(function () { note('ui/notifications/initialized', {}); render(); });
|
|
145
|
+
void caps;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
})();
|
package/mcp/ui/build.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* The build card: one build's stages, each check going green, a picture and where to play it, money against the
|
|
2
|
+
* budget, and Stop. It follows the build every 2 s while it runs and tells the model once when it ends. */
|
|
3
|
+
(function () {
|
|
4
|
+
var C = Card;
|
|
5
|
+
var ICON = { pass: '✓', done: '✓', fail: '✕', failed: '✕', running: '●', pending: '·', skip: '–', skipped: '–', stopped: '■' };
|
|
6
|
+
var STATE = { running: ['Running', 'live', true], passed: ['Done', 'ok'], failed: ['Failed', 'bad'], stopped: ['Stopped', ''] };
|
|
7
|
+
function money(n, unit) { return unit === 'credits' ? Math.round(n).toLocaleString('en-US') + ' credits' : '$' + Number(n || 0).toFixed(2); }
|
|
8
|
+
function render(sc) {
|
|
9
|
+
var f = sc.feed; var card = C.el('main', 'card');
|
|
10
|
+
var st = STATE[f.state] || ['…', ''];
|
|
11
|
+
C.add(card, C.top('Build', C.add(C.el('div', 'actions'), C.pill(st[0], st[1], st[2]), C.fullscreenButton())), C.el('h1', '', f.title || 'A build'),
|
|
12
|
+
C.el('p', 'sub', [f.studio, f.what === 'game' ? null : f.what, f.state === 'running' ? 'started ' + C.ago(f.startedAt) : f.endedAt ? 'took ' + C.dur(Date.parse(f.endedAt) - Date.parse(f.startedAt)) : null].filter(Boolean).join(' · ')));
|
|
13
|
+
var stages = C.el('div', 'stages');
|
|
14
|
+
(f.stages || []).forEach(function (s) { stages.appendChild(C.add(C.el('div', 'stage ' + s.state), C.el('div', 'bar'), C.el('span', '', (ICON[s.state] && s.state !== 'pending' ? ICON[s.state] + ' ' : '') + s.label))); });
|
|
15
|
+
card.appendChild(stages);
|
|
16
|
+
var cur = (f.stages || []).filter(function (s) { return s.note && s.state !== 'pending'; }).pop();
|
|
17
|
+
if (cur) card.appendChild(C.el('p', 'fine', cur.label + ': ' + cur.note));
|
|
18
|
+
if ((f.checks || []).length) {
|
|
19
|
+
var list = C.el('div');
|
|
20
|
+
f.checks.forEach(function (c) { list.appendChild(C.add(C.el('div', 'check ' + c.state), C.el('span', 'i', ICON[c.state] || '·'), C.el('span', '', c.label + (c.note ? ' — ' + c.note : '')), c.ms ? C.el('span', 'n', C.dur(c.ms)) : null)); });
|
|
21
|
+
card.appendChild(list);
|
|
22
|
+
}
|
|
23
|
+
var p = f.preview;
|
|
24
|
+
if (p && (p.image || p.url)) {
|
|
25
|
+
var box = C.el('div', 'preview');
|
|
26
|
+
var pic = C.img(p.image, p.caption || 'The game');
|
|
27
|
+
if (pic) box.appendChild(pic);
|
|
28
|
+
if (p.caption) box.appendChild(C.el('span', 'fine', p.caption));
|
|
29
|
+
card.appendChild(box);
|
|
30
|
+
}
|
|
31
|
+
if (f.spend && (f.spend.used > 0 || f.spend.budget !== null)) card.appendChild(C.el('p', 'fine', 'Spent ' + money(f.spend.used, f.spend.unit) + (f.spend.budget !== null ? ' of ' + money(f.spend.budget, f.spend.unit) : '')));
|
|
32
|
+
if (f.error) card.appendChild(C.el('p', 'note bad', f.error));
|
|
33
|
+
var failed = (sc.jobs || []).filter(function (j) { return j.state === 'failed'; });
|
|
34
|
+
if (failed.length && !f.error) card.appendChild(C.el('pre', 'log', failed.map(function (j) { return j.label + ': ' + ((j.result && j.result.why) || (j.tail || []).slice(-3).join('\n')); }).join('\n')));
|
|
35
|
+
else if ((f.log || []).length && f.state === 'running') card.appendChild(C.el('pre', 'log', f.log.slice(-3).map(function (l) { return l.text; }).join('\n')));
|
|
36
|
+
var foot = C.el('div', 'foot');
|
|
37
|
+
foot.appendChild(C.el('span', 'when', f.state === 'running' ? 'Updated ' + C.ago(f.updatedAt) : 'Ended ' + C.ago(f.endedAt)));
|
|
38
|
+
var acts = C.el('div', 'actions');
|
|
39
|
+
if (f.state === 'running' && !(f.stop && f.stop.requested)) {
|
|
40
|
+
acts.appendChild(C.btn(C.state.stopping ? 'Stopping…' : 'Stop', 'danger small', function () {
|
|
41
|
+
if (C.state.stopping) return; C.state.stopping = true; C.render();
|
|
42
|
+
C.call('build_stop', { build: sc.build }).then(function (r) { C.state.stopping = false; C.take(r); }).catch(function () { C.state.stopping = false; C.render(); });
|
|
43
|
+
}));
|
|
44
|
+
} else if (f.stop && f.stop.requested && f.state === 'running') acts.appendChild(C.pill('Stopping at the next safe point', 'warn'));
|
|
45
|
+
if (p && p.url && /^https?:/.test(p.url)) acts.appendChild(C.btn('Play it', f.state === 'passed' ? '' : 'ghost small', function () { C.open(p.url); }));
|
|
46
|
+
foot.appendChild(acts);
|
|
47
|
+
card.appendChild(foot);
|
|
48
|
+
var r = sc.run;
|
|
49
|
+
if (r && r.state !== 'running' && f.state === 'running') {
|
|
50
|
+
card.insertBefore(C.el('p', 'note' + (r.state === 'failed' ? ' bad' : ' ok'), (r.state === 'passed' ? '✓ ' : '') + r.title + ': ' + (r.state === 'passed' ? 'passed' : 'failed' + (r.why ? ' — ' + r.why : ''))), foot);
|
|
51
|
+
C.tell('run-' + sc.build + '-' + r.kind, r.title + ' ' + r.state + (r.why ? ': ' + r.why : '') + ' (build ' + sc.build + ' stays open).');
|
|
52
|
+
}
|
|
53
|
+
if (f.state !== 'running') {
|
|
54
|
+
var passed = (f.checks || []).filter(function (c) { return c.state === 'pass'; }).length;
|
|
55
|
+
C.tell('end-' + sc.build, 'Build ' + sc.build + ' (' + f.title + ') ended: ' + f.state + (f.checks && f.checks.length ? ', ' + passed + ' of ' + f.checks.length + ' checks passed' : '') + (f.error ? '. Error: ' + f.error : '') + (p && p.url ? '. Play: ' + p.url : '') + '. Tell the person in a line or two.');
|
|
56
|
+
}
|
|
57
|
+
return card;
|
|
58
|
+
}
|
|
59
|
+
C.start({
|
|
60
|
+
kind: 'build', label: 'Build', fullscreen: true, render: render,
|
|
61
|
+
poll: function (sc) { return sc.feed && sc.feed.state === 'running' ? { tool: 'build_progress', args: { build: sc.build }, every: 2000 } : null; },
|
|
62
|
+
});
|
|
63
|
+
})();
|
package/mcp/ui/card.css
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
:root{color-scheme:light dark;
|
|
2
|
+
--c-bg:var(--color-background-primary,light-dark(#ffffff,#1c1c1e));--c-bg2:var(--color-background-secondary,light-dark(#f6f5f2,#242427));--c-bg3:var(--color-background-tertiary,light-dark(#eeece6,#2d2d31));
|
|
3
|
+
--c-fg:var(--color-text-primary,light-dark(#1d1c1a,#f3f1ec));--c-fg2:var(--color-text-secondary,light-dark(#5e5b54,#b8b4ab));--c-fg3:var(--color-text-tertiary,light-dark(#8b877e,#8a867e));
|
|
4
|
+
--c-line:var(--color-border-tertiary,light-dark(rgba(20,18,12,.10),rgba(255,255,255,.10)));--c-line2:var(--color-border-secondary,light-dark(rgba(20,18,12,.18),rgba(255,255,255,.18)));
|
|
5
|
+
--c-ok:var(--color-text-success,light-dark(#17753b,#5fd08a));--c-okbg:var(--color-background-success,light-dark(#e5f4ea,rgba(95,208,138,.14)));
|
|
6
|
+
--c-bad:var(--color-text-danger,light-dark(#b42318,#ff8a80));--c-badbg:var(--color-background-danger,light-dark(#fdecea,rgba(255,138,128,.14)));
|
|
7
|
+
--c-warn:var(--color-text-warning,light-dark(#8a5a00,#f2c94c));--c-warnbg:var(--color-background-warning,light-dark(#fff4d6,rgba(242,201,76,.14)));
|
|
8
|
+
--accent:#ffcf5a;--accent-ink:#1a1405;
|
|
9
|
+
--r-sm:var(--border-radius-sm,8px);--r-md:var(--border-radius-md,12px);--r-lg:var(--border-radius-lg,16px);--r-full:var(--border-radius-full,999px);
|
|
10
|
+
--f-sans:var(--font-sans,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);--f-mono:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,monospace)}
|
|
11
|
+
[data-theme=light]{color-scheme:light}[data-theme=dark]{color-scheme:dark}
|
|
12
|
+
*{box-sizing:border-box}html,body{margin:0;background:transparent}
|
|
13
|
+
body{font:14px/1.45 var(--f-sans);color:var(--c-fg);-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%}
|
|
14
|
+
.card{background:var(--c-bg);border:1px solid var(--c-line);border-radius:var(--r-lg);padding:16px 16px 14px;overflow:hidden}
|
|
15
|
+
.top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
|
|
16
|
+
.eyebrow{display:flex;align-items:center;gap:8px;font:600 12px/1 var(--f-sans);letter-spacing:.04em;text-transform:uppercase;color:var(--c-fg3)}
|
|
17
|
+
.mark{width:14px;height:14px;border-radius:4px;background:var(--accent);box-shadow:inset 0 -3px 0 rgba(0,0,0,.18)}
|
|
18
|
+
h1{margin:0 0 2px;font:650 20px/1.2 var(--f-sans);letter-spacing:-.01em;overflow-wrap:anywhere}
|
|
19
|
+
h2{margin:16px 0 8px;font:600 13px/1.2 var(--f-sans);color:var(--c-fg2);text-transform:uppercase;letter-spacing:.04em}
|
|
20
|
+
.sub{margin:0 0 12px;color:var(--c-fg2);overflow-wrap:anywhere}
|
|
21
|
+
.note{margin:10px 0 0;padding:10px 12px;border-radius:var(--r-md);background:var(--c-bg2);color:var(--c-fg2)}
|
|
22
|
+
.note.bad{background:var(--c-badbg);color:var(--c-bad)}.note.ok{background:var(--c-okbg);color:var(--c-ok);font-weight:600}
|
|
23
|
+
.fine{margin:10px 0 0;font-size:12px;color:var(--c-fg3)}
|
|
24
|
+
.pill{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--r-full);font:600 12px/1.2 var(--f-sans);background:var(--c-bg2);color:var(--c-fg2);white-space:nowrap}
|
|
25
|
+
.pill.ok{background:var(--c-okbg);color:var(--c-ok)}.pill.bad{background:var(--c-badbg);color:var(--c-bad)}.pill.warn{background:var(--c-warnbg);color:var(--c-warn)}
|
|
26
|
+
.pill.live{background:color-mix(in srgb,var(--accent) 22%,transparent);color:var(--c-fg)}
|
|
27
|
+
.dot{width:7px;height:7px;border-radius:50%;background:currentColor;animation:pulse 1.4s ease-in-out infinite}
|
|
28
|
+
@keyframes pulse{50%{opacity:.25}}
|
|
29
|
+
@media (prefers-reduced-motion:reduce){.dot{animation:none}}
|
|
30
|
+
button{font:600 13px/1 var(--f-sans);border:0;border-radius:var(--r-full);padding:9px 14px;cursor:pointer;background:var(--accent);color:var(--accent-ink);min-height:34px}
|
|
31
|
+
button.ghost{background:var(--c-bg2);color:var(--c-fg);box-shadow:inset 0 0 0 1px var(--c-line2)}
|
|
32
|
+
button.small{padding:7px 12px;min-height:30px;font-size:12px}
|
|
33
|
+
button.danger{background:var(--c-badbg);color:var(--c-bad)}
|
|
34
|
+
button:disabled{opacity:.55;cursor:default}
|
|
35
|
+
button:focus-visible{outline:2px solid var(--c-fg);outline-offset:2px}
|
|
36
|
+
.actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|
37
|
+
.foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;padding-top:12px;border-top:1px solid var(--c-line)}
|
|
38
|
+
.when{color:var(--c-fg3);font-size:12px}
|
|
39
|
+
.skel{height:14px;margin:10px 0;border-radius:6px;background:linear-gradient(90deg,var(--c-bg2),var(--c-bg3),var(--c-bg2));background-size:200% 100%;animation:sk 1.2s linear infinite}
|
|
40
|
+
@keyframes sk{to{background-position:-200% 0}}
|
|
41
|
+
ol.list,ul.list{list-style:none;margin:0;padding:0;display:grid;gap:6px}
|
|
42
|
+
.list li{display:flex;align-items:flex-start;gap:10px;padding:8px 10px;border-radius:var(--r-md);background:var(--c-bg2)}
|
|
43
|
+
.list li.now{box-shadow:inset 0 0 0 1.5px var(--accent);background:color-mix(in srgb,var(--accent) 10%,var(--c-bg2))}
|
|
44
|
+
.list li.done .cn{background:var(--c-okbg);color:var(--c-ok)}
|
|
45
|
+
.list li.todo{opacity:.72}
|
|
46
|
+
.cn{flex:none;display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--c-bg3);color:var(--c-fg2);font:700 12px/1 var(--f-sans)}
|
|
47
|
+
.cb{flex:1;min-width:0;display:grid;gap:2px}.cb b{font-weight:600}.cb span{color:var(--c-fg2);font-size:13px;overflow-wrap:anywhere}
|
|
48
|
+
.cb .fix{color:var(--c-fg);font-size:12.5px}
|
|
49
|
+
.state-ok .cn{background:var(--c-okbg);color:var(--c-ok)}.state-act .cn,.state-missing .cn{background:var(--c-warnbg);color:var(--c-warn)}
|
|
50
|
+
.stages{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:6px;margin:12px 0}
|
|
51
|
+
.stage{display:grid;gap:6px}.stage .bar{height:6px;border-radius:3px;background:var(--c-bg3)}
|
|
52
|
+
.stage.done .bar{background:var(--c-ok)}.stage.running .bar{background:var(--accent);animation:pulse 1.4s ease-in-out infinite}.stage.failed .bar{background:var(--c-bad)}.stage.skipped .bar{background:var(--c-bg3);opacity:.5}
|
|
53
|
+
.stage span{font-size:12px;color:var(--c-fg2)}
|
|
54
|
+
.check{display:flex;gap:8px;align-items:baseline;padding:4px 0;font-size:13px}.check .i{flex:none;width:18px;text-align:center}
|
|
55
|
+
.check.pass .i{color:var(--c-ok)}.check.fail .i{color:var(--c-bad)}.check.running .i{color:var(--c-warn)}
|
|
56
|
+
.check .n{color:var(--c-fg3);font-size:12px;margin-left:auto;white-space:nowrap}
|
|
57
|
+
.preview{margin-top:12px;display:grid;gap:8px}
|
|
58
|
+
.preview img{width:100%;max-height:260px;object-fit:cover;border-radius:var(--r-md);background:var(--c-bg2)}
|
|
59
|
+
.games{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:10px;margin-top:8px}
|
|
60
|
+
.game{display:grid;gap:6px;padding:8px;border-radius:var(--r-md);background:var(--c-bg2)}
|
|
61
|
+
.game .art{aspect-ratio:16/9;border-radius:var(--r-sm);background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 50%,var(--c-bg3)),var(--c-bg3));display:grid;place-items:center;font:800 26px/1 var(--f-sans);color:var(--c-fg2);overflow:hidden}
|
|
62
|
+
.game .art img{width:100%;height:100%;object-fit:cover}
|
|
63
|
+
.game b{font-size:14px}.game span{font-size:12.5px;color:var(--c-fg2)}
|
|
64
|
+
.soon{padding:14px;border-radius:var(--r-md);background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 18%,var(--c-bg2)),var(--c-bg2));display:grid;gap:6px}
|
|
65
|
+
.soon b{font-size:16px}
|
|
66
|
+
.stats{display:flex;flex-wrap:wrap;gap:6px 16px;margin:4px 0 6px;color:var(--c-fg2);font-size:13px}.stats b{color:var(--c-fg)}
|
|
67
|
+
.log{margin:10px 0 0;padding:8px 10px;border-radius:var(--r-md);background:var(--c-bg2);font:12px/1.5 var(--f-mono);color:var(--c-fg2);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
68
|
+
.frame{width:100%;height:620px;border:0;border-radius:var(--r-md);background:var(--c-bg2);display:block}
|
|
69
|
+
.full .frame{height:calc(100vh - 120px)}
|
|
70
|
+
.ready{margin:0 0 8px;font-size:13px;color:var(--c-fg2)}.ready .ok{color:var(--c-ok);font-weight:600}
|
|
71
|
+
.tabs{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0}
|
|
72
|
+
.tabs span{padding:4px 9px;border-radius:var(--r-full);background:var(--c-bg2);font-size:12px;color:var(--c-fg2)}
|
|
73
|
+
@media (max-width:420px){.card{padding:14px 12px 12px;border-radius:var(--r-md)}h1{font-size:18px}.stages{grid-auto-flow:row;grid-auto-columns:auto}.frame{height:520px}}
|
package/mcp/ui/codex.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* The Game Codex card: the codex page itself (drawn by @homie-rocks/studio lib/codex.mjs in the game's own look:
|
|
2
|
+
* a tab per section, cards, controls, milestones, decisions, open questions, the build's status), in a frame. */
|
|
3
|
+
(function () {
|
|
4
|
+
var C = Card;
|
|
5
|
+
function render(sc) {
|
|
6
|
+
var card = C.el('main', 'card');
|
|
7
|
+
var todo = (sc.missing || []).length;
|
|
8
|
+
C.add(card, C.top('Game Codex', C.add(C.el('div', 'actions'), C.pill(todo ? todo + ' to decide' : 'Every section filled', todo ? 'warn' : 'ok'), C.fullscreenButton())), C.el('h1', '', sc.title));
|
|
9
|
+
// The page has its own tabs; the card says only what is still open.
|
|
10
|
+
if (sc.openQuestions) card.appendChild(C.el('p', 'sub', sc.openQuestions + ' open question' + (sc.openQuestions === 1 ? '' : 's') + ' · ' + (sc.sections || []).length + ' sections'));
|
|
11
|
+
if (sc.html) {
|
|
12
|
+
var frame = C.el('iframe', 'frame');
|
|
13
|
+
frame.title = 'The Game Codex for ' + sc.title;
|
|
14
|
+
frame.setAttribute('sandbox', 'allow-scripts');
|
|
15
|
+
frame.setAttribute('referrerpolicy', 'no-referrer');
|
|
16
|
+
frame.srcdoc = sc.html;
|
|
17
|
+
card.appendChild(frame);
|
|
18
|
+
} else card.appendChild(C.el('p', 'note', 'The page is too big to show here.'));
|
|
19
|
+
if (todo) card.appendChild(C.el('p', 'fine', 'Not decided yet: ' + sc.missing.join(', ') + '.'));
|
|
20
|
+
return card;
|
|
21
|
+
}
|
|
22
|
+
C.start({ kind: 'codex', label: 'Game Codex', fullscreen: true, render: render, poll: null });
|
|
23
|
+
})();
|
package/mcp/ui/setup.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* The setup card: the new-studio checklist, this computer's setup status (what each row unlocks and its fix), the
|
|
2
|
+
* studio in use, and its toolkit install as it runs. */
|
|
3
|
+
(function () {
|
|
4
|
+
var C = Card;
|
|
5
|
+
var MARK = { ok: '✓', act: '→', missing: '!', optional: '○', later: '…', unknown: '?' };
|
|
6
|
+
function step(s) {
|
|
7
|
+
var li = C.el('li', s.state);
|
|
8
|
+
C.add(li, C.el('span', 'cn', s.state === 'done' ? '✓' : String(s.n)), C.add(C.el('div', 'cb'), C.el('b', '', s.label), s.state === 'now' ? C.el('span', '', 'Now') : null));
|
|
9
|
+
return li;
|
|
10
|
+
}
|
|
11
|
+
function row(r) {
|
|
12
|
+
var li = C.el('li', 'state-' + r.state);
|
|
13
|
+
var body = C.add(C.el('div', 'cb'), C.el('b', '', r.label + (r.need === 'optional' ? ' (optional)' : '')), C.el('span', '', r.detail), C.el('span', '', 'Unlocks ' + r.unlocks));
|
|
14
|
+
if (r.fix && r.state !== 'ok') body.appendChild(C.el('span', 'fix', r.fix.say));
|
|
15
|
+
C.add(li, C.el('span', 'cn', MARK[r.state] || '?'), body, r.fix && r.fix.open && r.state !== 'ok' ? C.btn('Open', 'ghost small', function () { C.open(r.fix.open); }) : null);
|
|
16
|
+
return li;
|
|
17
|
+
}
|
|
18
|
+
function render(sc) {
|
|
19
|
+
var card = C.el('main', 'card');
|
|
20
|
+
var cur = sc.current;
|
|
21
|
+
var install = sc.install;
|
|
22
|
+
var head = install && install.state === 'running' ? C.pill('Installing · ' + install.seconds + ' s', 'live', true)
|
|
23
|
+
: cur ? C.pill(cur.games.length ? cur.games.length + (cur.games.length === 1 ? ' game' : ' games') : 'No game yet', cur.games.length ? 'ok' : '') : C.pill('New studio', '');
|
|
24
|
+
C.add(card, C.top('Studio setup', head), C.el('h1', '', cur ? cur.name : 'Set up a studio'),
|
|
25
|
+
C.el('p', 'sub', cur ? (cur.games.length ? 'On this computer, in ' + cur.folder : 'On this computer, in ' + cur.folder + '. Its home page says "First game coming soon".') : 'Studios live in ' + (sc.studiosDir || 'the folder set in the Homie extension') + '.'));
|
|
26
|
+
var ol = C.el('ol', 'list');
|
|
27
|
+
(sc.checklist || []).forEach(function (s) { ol.appendChild(step(s)); });
|
|
28
|
+
card.appendChild(ol);
|
|
29
|
+
if (install) {
|
|
30
|
+
var msg = install.state === 'running' ? 'Installing the studio\'s toolkit and Wrangler (' + install.seconds + ' s)…' : install.state === 'done' ? 'The studio\'s toolkit is installed.' : 'The install failed: ' + (install.tail || []).slice(-2).join(' ');
|
|
31
|
+
card.appendChild(C.el('p', 'note' + (install.state === 'failed' ? ' bad' : ''), msg));
|
|
32
|
+
}
|
|
33
|
+
var rows = (sc.status && sc.status.rows) || [];
|
|
34
|
+
if (rows.length) {
|
|
35
|
+
// What needs the person comes in full; what is ready is one line.
|
|
36
|
+
var todo = rows.filter(function (r) { return r.state !== 'ok'; });
|
|
37
|
+
var ready = rows.filter(function (r) { return r.state === 'ok'; });
|
|
38
|
+
card.appendChild(C.el('h2', '', 'This computer and your accounts'));
|
|
39
|
+
if (ready.length) card.appendChild(C.add(C.el('p', 'ready'), C.el('span', 'ok', '✓ Ready: '), C.el('span', '', ready.map(function (r) { return r.label; }).join(' · '))));
|
|
40
|
+
if (todo.length) {
|
|
41
|
+
var ul = C.el('ul', 'list');
|
|
42
|
+
todo.forEach(function (r) { ul.appendChild(row(r)); });
|
|
43
|
+
card.appendChild(ul);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ((sc.studios || []).length > 1) card.appendChild(C.el('p', 'fine', 'Studios here: ' + sc.studios.map(function (s) { return s.name; }).join(', ')));
|
|
47
|
+
var foot = C.el('div', 'foot');
|
|
48
|
+
foot.appendChild(C.el('span', 'when', 'Optional rows never block anything.'));
|
|
49
|
+
var acts = C.el('div', 'actions');
|
|
50
|
+
if (cur && !cur.games.length) {
|
|
51
|
+
acts.appendChild(C.btn(C.state.demoing ? 'Opening…' : 'Play a live game', '', function () {
|
|
52
|
+
if (C.state.demoing) return; C.state.demoing = true; C.render();
|
|
53
|
+
C.call('game_demo', {}).then(function (r) {
|
|
54
|
+
C.state.demoing = false; C.render();
|
|
55
|
+
var d = r && r.structuredContent; var p = d && d.pick;
|
|
56
|
+
if (p && p.play) { C.open(p.play); C.tell('demo', 'The person opened the live demo game ' + p.name + ' (' + p.play + ') from the setup card: step 2, see a working game. Nothing was copied into the studio.'); }
|
|
57
|
+
}).catch(function () { C.state.demoing = false; C.render(); });
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
acts.appendChild(C.btn('Check again', 'ghost small', function () {
|
|
61
|
+
C.call('setup_status', { fresh: true }).then(function (r) { C.take(r); }).catch(function () {});
|
|
62
|
+
}));
|
|
63
|
+
foot.appendChild(acts);
|
|
64
|
+
card.appendChild(foot);
|
|
65
|
+
if (install && install.state === 'done') C.tell('install-' + install.job, (cur ? cur.name : 'The studio') + '\'s toolkit finished installing; the studio is ready to build.');
|
|
66
|
+
return card;
|
|
67
|
+
}
|
|
68
|
+
C.start({
|
|
69
|
+
kind: 'setup', label: 'Studio setup', render: render,
|
|
70
|
+
poll: function (sc) { return sc.install && sc.install.state === 'running' ? { tool: 'setup_status', args: {}, every: 3000 } : null; },
|
|
71
|
+
});
|
|
72
|
+
})();
|
package/mcp/ui/studio.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* The studio card: its games with Play (on this computer and live), songs, videos and posts, where it runs, its
|
|
2
|
+
* live rooms, and a live game to try while it has none. */
|
|
3
|
+
(function () {
|
|
4
|
+
var C = Card;
|
|
5
|
+
function initials(name) { return String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map(function (w) { return w[0]; }).join('').toUpperCase(); }
|
|
6
|
+
function render(sc) {
|
|
7
|
+
var card = C.el('main', 'card');
|
|
8
|
+
C.add(card, C.top('Studio', C.pill(sc.site ? 'Online' : sc.dev ? 'Running here' : 'On this computer', sc.site ? 'ok' : '')), C.el('h1', '', sc.name), C.el('p', 'sub', sc.tagline || sc.folder));
|
|
9
|
+
var stats = C.el('div', 'stats');
|
|
10
|
+
[[sc.games.length, sc.games.length === 1 ? 'game' : 'games'], [sc.songs.length, sc.songs.length === 1 ? 'song' : 'songs'], [sc.videos.length, sc.videos.length === 1 ? 'video' : 'videos'], [sc.posts, sc.posts === 1 ? 'post' : 'posts'], [(sc.rooms || []).length, 'live rooms']].forEach(function (x) {
|
|
11
|
+
stats.appendChild(C.add(C.el('span'), C.el('b', '', String(x[0])), C.el('span', '', ' ' + x[1])));
|
|
12
|
+
});
|
|
13
|
+
card.appendChild(stats);
|
|
14
|
+
if (sc.games.length) {
|
|
15
|
+
var grid = C.el('div', 'games');
|
|
16
|
+
sc.games.forEach(function (g) {
|
|
17
|
+
var art = C.el('div', 'art'); var pic = C.img(g.cover, g.name);
|
|
18
|
+
if (pic) art.appendChild(pic); else art.textContent = initials(g.name);
|
|
19
|
+
var acts = C.el('div', 'actions');
|
|
20
|
+
if (g.play.dev) acts.appendChild(C.btn('Play here', 'small', function () { C.open(g.play.dev); }));
|
|
21
|
+
if (g.play.live) acts.appendChild(C.btn('Play live', g.play.dev ? 'ghost small' : 'small', function () { C.open(g.play.live); }));
|
|
22
|
+
grid.appendChild(C.add(C.el('div', 'game'), art, C.el('b', '', g.name), g.blurb ? C.el('span', '', g.blurb) : null, sc.site && !g.live ? C.el('span', 'fine', 'Not online yet: deploy puts it there') : null, acts.childNodes.length ? acts : null));
|
|
23
|
+
});
|
|
24
|
+
card.appendChild(grid);
|
|
25
|
+
} else {
|
|
26
|
+
var soon = C.add(C.el('div', 'soon'), C.el('b', '', 'First game coming soon'), C.el('span', '', 'That is what the studio\'s home page says until its first game is made. Plan it first: it is made the way you decide.'));
|
|
27
|
+
if (sc.demo) soon.appendChild(C.add(C.el('div', 'actions'), C.btn('Play ' + sc.demo.name, 'small', function () { C.open(sc.demo.play); C.tell('demo', 'The person opened the live demo ' + sc.demo.name + ' from the studio card.'); }), C.el('span', 'fine', 'A live game on ' + sc.demo.studio + ', made with Homie')));
|
|
28
|
+
card.appendChild(soon);
|
|
29
|
+
}
|
|
30
|
+
if ((sc.rooms || []).length) card.appendChild(C.el('p', 'fine', 'Playing now: ' + sc.rooms.map(function (r) { return r.game + ' (' + r.players + ')'; }).join(', ')));
|
|
31
|
+
var foot = C.el('div', 'foot');
|
|
32
|
+
foot.appendChild(C.el('span', 'when', sc.site ? sc.site.replace(/^https?:\/\//, '') : sc.dev ? sc.dev.url.replace(/^https?:\/\//, '') : 'Not online yet'));
|
|
33
|
+
var acts = C.el('div', 'actions');
|
|
34
|
+
if (sc.dev) acts.appendChild(C.btn('Open here', 'ghost small', function () { C.open(sc.dev.url + '/'); }));
|
|
35
|
+
if (sc.site) acts.appendChild(C.btn('Open site', 'small', function () { C.open(sc.site + '/'); }));
|
|
36
|
+
foot.appendChild(acts);
|
|
37
|
+
card.appendChild(foot);
|
|
38
|
+
return card;
|
|
39
|
+
}
|
|
40
|
+
C.start({ kind: 'studio', label: 'Studio', render: render, poll: null });
|
|
41
|
+
})();
|