@homie-rocks/studio 0.13.0 → 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 +31 -4
- package/bin/homie-studio.mjs +32 -5
- package/lib/cloudflare.mjs +2 -2
- 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/scaffold.mjs +56 -24
- package/lib/studio.mjs +4 -1
- package/lib/template-history.json +27 -0
- package/lib/upgrade.mjs +1 -1
- 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/package.json +2 -1
- package/worker/site.mjs +24 -2
- package/worker/version.mjs +1 -1
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
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, optional R2 storage for large media, deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"netplay/",
|
|
40
40
|
"port/",
|
|
41
41
|
"worker/",
|
|
42
|
+
"mcp/",
|
|
42
43
|
"saves/",
|
|
43
44
|
"starters/",
|
|
44
45
|
"media/",
|
package/worker/site.mjs
CHANGED
|
@@ -692,6 +692,7 @@ export function homePage(cat, { origin = '', rooms = [], live = {} } = {}) {
|
|
|
692
692
|
const f = featuredOf(cat);
|
|
693
693
|
const tagline = cat.studio?.tagline ?? '';
|
|
694
694
|
let hero;
|
|
695
|
+
let soon = '';
|
|
695
696
|
if (f) {
|
|
696
697
|
const playing = live[f.id] ?? 0;
|
|
697
698
|
hero = `<section class="hero home" aria-labelledby="hero-title">
|
|
@@ -704,6 +705,27 @@ export function homePage(cat, { origin = '', rooms = [], live = {} } = {}) {
|
|
|
704
705
|
<a class="also" href="/${esc(f.id)}/">${icon('arrow')}<span>About ${esc(f.name)}</span></a>${games.length > 1 ? `<a class="also" href="/games/">${icon('games')}<span>All ${games.length} games</span></a>` : ''}
|
|
705
706
|
</div>
|
|
706
707
|
</section>`;
|
|
708
|
+
} else if (!videos.length && !songs.length) {
|
|
709
|
+
// A new studio with nothing published yet: its own Home, its name, "First game coming soon", and what is on the
|
|
710
|
+
// way (a studio is never given a starter game it did not ask for). Its posts show below as soon as it has any.
|
|
711
|
+
hero = `<section class="hero home soon" aria-labelledby="hero-title">
|
|
712
|
+
${heroMedia({}, { word: name })}
|
|
713
|
+
<div class="hero-copy">
|
|
714
|
+
<p class="kicker">${esc(tagline || 'A Homie studio')}</p>
|
|
715
|
+
<h1 class="title ${titleClass(name)}" id="hero-title">${esc(name)}</h1>
|
|
716
|
+
<p class="line">First game coming soon.</p>
|
|
717
|
+
<p class="live"><span class="live-dot" aria-hidden="true"></span><span>${posts.length ? 'In the making · the latest news is below' : 'In the making'}</span></p>
|
|
718
|
+
</div>
|
|
719
|
+
</section>`;
|
|
720
|
+
const next = [
|
|
721
|
+
['Games', 'The first game', 'It opens here with a Play button: a live public room on a phone, a computer or a TV, with bots in the empty seats.'],
|
|
722
|
+
['Posts', 'News and drops', 'The studio\'s posts land on this page, with feeds to follow.'],
|
|
723
|
+
['Music and videos', 'Songs and trailers', 'Scores, songs and trailers get pages of their own.'],
|
|
724
|
+
];
|
|
725
|
+
soon = `<section class="band glowband" aria-labelledby="soon-title"><div class="band-in">
|
|
726
|
+
<div class="head-row reveal"><div><p class="kicker">On the way</p><h2 class="small-h" id="soon-title">${esc(`Coming to ${name}`)}</h2></div></div>
|
|
727
|
+
<div class="cards">${next.map(([k, t, d]) => `<div class="pcard reveal" data-soon><p class="post-meta">${esc(k)}</p><h3>${esc(t)}</h3><p>${esc(d)}</p></div>`).join('')}</div>
|
|
728
|
+
</div></section>`;
|
|
707
729
|
} else {
|
|
708
730
|
const lead = videos[0] ?? songs[0] ?? null;
|
|
709
731
|
const art = lead ? mediaArt(lead, videos[0] ? 'videos' : 'music', cat) : null;
|
|
@@ -736,10 +758,10 @@ export function homePage(cat, { origin = '', rooms = [], live = {} } = {}) {
|
|
|
736
758
|
const extra = partial(cat, 'home');
|
|
737
759
|
return layout(cat, {
|
|
738
760
|
title: tagline ? `${name} — ${tagline}` : name,
|
|
739
|
-
description: tagline || (f ? `${name}: ${games.map((g) => g.name).join(', ')}. Free in your browser, on a phone, a computer or a TV.` : name),
|
|
761
|
+
description: tagline || (f ? `${name}: ${games.map((g) => g.name).join(', ')}. Free in your browser, on a phone, a computer or a TV.` : soon ? `${name}: first game coming soon.` : name),
|
|
740
762
|
origin, path: '/', image: f ? (f.landing?.hero?.wideImage ?? coverOf(f)) : null, page: 'home', hero: true,
|
|
741
763
|
head: f?.landing?.hero?.wideImage ? `<link rel="preload" as="image" href="${esc(f.landing.hero.tallImage ?? f.landing.hero.wideImage)}">` : '',
|
|
742
|
-
main: `${hero}${extra ? `<section class="band">${extra}</section>` : ''}${roomsBand}${gamesBand}${postsBand}${mediaBand(videos, 'videos', 'Trailers and clips')}${mediaBand(songs, 'music', 'Songs and scores')}`,
|
|
764
|
+
main: `${hero}${extra ? `<section class="band">${extra}</section>` : ''}${roomsBand}${gamesBand}${postsBand}${soon}${mediaBand(videos, 'videos', 'Trailers and clips')}${mediaBand(songs, 'music', 'Songs and scores')}`,
|
|
743
765
|
});
|
|
744
766
|
}
|
|
745
767
|
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.14.0';
|