@homie-rocks/studio 0.48.4 → 0.50.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/CHANGELOG.md +46 -0
- package/app-starters/gathering/README.md +4 -0
- package/app-starters/gathering/app.json +162 -0
- package/app-starters/gathering/index.html +3 -0
- package/app-starters/gathering/map/main.json +1 -0
- package/app-starters/gathering/src/move.ts +3 -0
- package/app-starters/gathering/src/rules.ts +21 -0
- package/app-starters/gathering/src/view.ts +74 -0
- package/app-starters/giving/README.md +5 -0
- package/app-starters/giving/app.json +162 -0
- package/app-starters/giving/index.html +3 -0
- package/app-starters/giving/map/main.json +1 -0
- package/app-starters/giving/src/move.ts +3 -0
- package/app-starters/giving/src/rules.ts +21 -0
- package/app-starters/giving/src/view.ts +74 -0
- package/app-starters/jobs/README.md +4 -0
- package/app-starters/jobs/app.json +162 -0
- package/app-starters/jobs/index.html +3 -0
- package/app-starters/jobs/map/main.json +1 -0
- package/app-starters/jobs/src/move.ts +3 -0
- package/app-starters/jobs/src/rules.ts +21 -0
- package/app-starters/jobs/src/view.ts +74 -0
- package/app-starters/quiz/README.md +4 -0
- package/app-starters/quiz/app.json +162 -0
- package/app-starters/quiz/index.html +3 -0
- package/app-starters/quiz/map/main.json +1 -0
- package/app-starters/quiz/src/move.ts +3 -0
- package/app-starters/quiz/src/rules.ts +21 -0
- package/app-starters/quiz/src/view.ts +74 -0
- package/app-starters/welcome/app.json +25 -2
- package/app-starters/welcome/index.html +1 -1
- package/app-starters/welcome/src/view.ts +20 -8
- package/bin/homie-studio.mjs +20 -16
- package/functions/FUNCTIONS.md +1 -1
- package/guides/app/SKILL.md +178 -0
- package/guides/art/SKILL.md +154 -0
- package/guides/art/references/RIGHTS.md +20 -0
- package/guides/art/scripts/art.mjs +308 -0
- package/guides/models/SKILL.md +198 -0
- package/guides/models/references/RIGHTS.md +46 -0
- package/guides/models/references/models.json +175 -0
- package/guides/models/scripts/models.mjs +483 -0
- package/guides/music/SKILL.md +197 -0
- package/guides/music/references/COMPOSITION.md +59 -0
- package/guides/music/references/GAME-AUDIO.md +50 -0
- package/guides/music/references/RIGHTS.md +43 -0
- package/guides/music/scripts/lib/audio.mjs +258 -0
- package/guides/music/scripts/lib/eleven.mjs +260 -0
- package/guides/music/scripts/lib/studio.mjs +147 -0
- package/guides/music/scripts/music.mjs +644 -0
- package/guides/parts/SKILL.md +158 -0
- package/guides/plan/SKILL.md +126 -0
- package/guides/plan/references/CODEX.md +105 -0
- package/guides/plan/references/INTERVIEW.md +86 -0
- package/guides/playtest/SKILL.md +205 -0
- package/guides/playtest/references/METHOD.md +124 -0
- package/guides/playtest/references/REVIEWER.md +78 -0
- package/guides/playtest/scripts/lib/exit.mjs +31 -0
- package/guides/playtest/scripts/lib/judge.mjs +502 -0
- package/guides/playtest/scripts/lib/pixels.mjs +128 -0
- package/guides/playtest/scripts/lib/preflight.mjs +69 -0
- package/guides/playtest/scripts/playtest.mjs +870 -0
- package/guides/shop/SKILL.md +153 -0
- package/guides/sound/SKILL.md +156 -0
- package/guides/sound/references/GAME-SOUND.md +100 -0
- package/guides/sound/references/SCORE.md +131 -0
- package/guides/sound/references/TRAPS.md +93 -0
- package/guides/sound/scripts/lib/measure.mjs +252 -0
- package/guides/sound/scripts/lib/score.mjs +415 -0
- package/guides/sound/scripts/lib/sfx.mjs +222 -0
- package/guides/sound/scripts/lib/synth.mjs +509 -0
- package/guides/sound/scripts/sound.mjs +520 -0
- package/guides/studio-setup/SKILL.md +419 -0
- package/guides/style/SKILL.md +105 -0
- package/lib/app-needs.mjs +63 -0
- package/lib/authoring-guides.mjs +5 -4
- package/lib/build.mjs +5 -4
- package/lib/check-site.mjs +6 -6
- package/lib/check.mjs +18 -5
- package/lib/codex.mjs +95 -18
- package/lib/decisions.mjs +9 -7
- package/lib/dev.mjs +4 -2
- package/lib/doctor.mjs +1 -1
- package/lib/handoff.mjs +13 -8
- package/lib/mcp-tools.mjs +19 -17
- package/lib/mcp.mjs +1 -1
- package/lib/parts.mjs +6 -6
- package/lib/scaffold.mjs +45 -11
- package/lib/shop-links.mjs +2 -2
- package/lib/shop.mjs +5 -3
- package/lib/studio.mjs +7 -6
- package/lib/template-history.json +84 -0
- package/package.json +4 -4
- package/shop/SHOP.md +4 -0
- package/shop/shop.ts +4 -4
- package/worker/app-format.mjs +1 -1
- package/worker/build-id.mjs +1 -1
- package/worker/functions-schema.mjs +15 -0
- package/worker/index.mjs +2 -1
- package/worker/parts.mjs +4 -4
- package/worker/shop-page.mjs +22 -16
- package/worker/shop-rules.mjs +4 -3
- package/worker/shop.mjs +6 -2
- package/worker/site.mjs +4 -2
- package/worker/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,52 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
|
|
|
17
17
|
`npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
|
|
18
18
|
file) and what the upgrade would change, and changes nothing until you agree.
|
|
19
19
|
|
|
20
|
+
## [0.50.0] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
**Plugin 0.50.0** · Build the person's app from their dream.
|
|
23
|
+
|
|
24
|
+
Apps now follow their owner’s words through planning, a living scene and evidence of each need.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Apps use the shared Codex planning page: their own words, pictured scene, people, offers and demonstrable needs. A plan can precede app plumbing and survives creation.
|
|
29
|
+
- Opt-in app checks run the Codex's real UI flows and attach wall/phone captures to each need; the Codex shows their status. Existing apps without an executable needs block retain their checks.
|
|
30
|
+
- App checkout accepts a reference to a booking, ticket or gift. Verified paid events include that reference, the app and order ID, including reopened Payment Links.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- App making and playtesting follow the person's needs, living scene, records and individual customer journey. Starters are plumbing. The packaged toolkit includes shared planning, style, media and playtest guides.
|
|
35
|
+
- Scaffold and app creation direct the AI through that path with the same engines and shared parts available to games. No new studio caps or defaults.
|
|
36
|
+
|
|
37
|
+
### Upgrade notes
|
|
38
|
+
|
|
39
|
+
- Existing apps and games keep their behavior. Add the executable needs block to an app Codex to opt into needs evidence; omitted payment references remain null. The new migration updates only the paid-event payload.
|
|
40
|
+
|
|
41
|
+
## [0.49.0] - 2026-10-13
|
|
42
|
+
|
|
43
|
+
**Plugin 0.49.0** · Apps from the first request.
|
|
44
|
+
|
|
45
|
+
Businesses and causes can start, check and preview their first live app without hand fixes.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Business and cause scaffolds, gathering, quiz, service-request and giving app starters; custom pages and Shop in the header.
|
|
50
|
+
- Giving preset with the organisation’s currency, suggested one-time and monthly Stripe Payment Links, and organisation-supplied receipt wording.
|
|
51
|
+
- Local shop preview before Stripe is connected, and a visible fallback when WebGL is unavailable.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Fresh app installs include Three.js types; strict builds need no hand-installed dependencies.
|
|
56
|
+
- App text checks wait for visual readiness, prove wall/two-phone actions and reconnect, and leave receipts visible to deployment hooks.
|
|
57
|
+
- Deployment command parsing excludes quoted documentation and heredocs; Codex exec user events retain task authorization.
|
|
58
|
+
- Starter words and blurb reach the wall. Busy local dev ports select and print a free port.
|
|
59
|
+
- App setup, paths, guides and parts messages use app wording. Grok setup explains trust through the AI.
|
|
60
|
+
- Updated Agents/MCP dependencies and Wrangler to resolve the reported audit advisories.
|
|
61
|
+
|
|
62
|
+
### Upgrade notes
|
|
63
|
+
|
|
64
|
+
Existing studios and games keep their authored files and policies. New app starters are opt-in. Calendar capacity and booking availability are planned next; service starters collect requests, not confirmed reservations.
|
|
65
|
+
|
|
20
66
|
## [0.48.4] - 2026-10-13
|
|
21
67
|
|
|
22
68
|
**Plugin 0.48.4** · Deploys go right.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gathering",
|
|
3
|
+
"name": "Gather together",
|
|
4
|
+
"blurb": "Check in, choose a task, and let the team know you are here.",
|
|
5
|
+
"entry": "src/view.ts",
|
|
6
|
+
"roles": {
|
|
7
|
+
"customer": {
|
|
8
|
+
"signIn": false,
|
|
9
|
+
"can": [
|
|
10
|
+
"read:entries",
|
|
11
|
+
"create:entries"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"wall": {
|
|
15
|
+
"signIn": false,
|
|
16
|
+
"can": [
|
|
17
|
+
"read:entries"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"staff": {
|
|
21
|
+
"signIn": true,
|
|
22
|
+
"can": [
|
|
23
|
+
"read:entries",
|
|
24
|
+
"update:entries",
|
|
25
|
+
"delete:entries"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"surfaces": {
|
|
30
|
+
"phone": "customer",
|
|
31
|
+
"wall": "wall",
|
|
32
|
+
"kiosk": "customer",
|
|
33
|
+
"tablet": "staff"
|
|
34
|
+
},
|
|
35
|
+
"words": {
|
|
36
|
+
"open": "Open",
|
|
37
|
+
"join": "Scan to join",
|
|
38
|
+
"one": "person",
|
|
39
|
+
"many": "people",
|
|
40
|
+
"action": "Check in",
|
|
41
|
+
"total": "Check-ins",
|
|
42
|
+
"choice": "Choose an option",
|
|
43
|
+
"monthly": "Gift frequency",
|
|
44
|
+
"once": "One time",
|
|
45
|
+
"month": "Monthly",
|
|
46
|
+
"give": "Give securely on Stripe",
|
|
47
|
+
"goal": "Intention goal \u00b7 not payments received",
|
|
48
|
+
"prompt": "Choose what works for you.",
|
|
49
|
+
"finished": "All three answers are in. Thank you!",
|
|
50
|
+
"complete": "Mark complete",
|
|
51
|
+
"done": "Complete",
|
|
52
|
+
"pending": "Checked in",
|
|
53
|
+
"thanks": "Thank you \u2014 the wall is up to date.",
|
|
54
|
+
"live": "Together \u00b7 live",
|
|
55
|
+
"reconnecting": "Reconnecting\u2026"
|
|
56
|
+
},
|
|
57
|
+
"records": {
|
|
58
|
+
"persist": true,
|
|
59
|
+
"collections": {
|
|
60
|
+
"entries": {
|
|
61
|
+
"fields": {
|
|
62
|
+
"choice": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"required": true,
|
|
65
|
+
"maxLength": 100
|
|
66
|
+
},
|
|
67
|
+
"status": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"required": true,
|
|
70
|
+
"maxLength": 60
|
|
71
|
+
},
|
|
72
|
+
"amount": {
|
|
73
|
+
"type": "number",
|
|
74
|
+
"required": true
|
|
75
|
+
},
|
|
76
|
+
"monthly": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"required": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"parts": [],
|
|
85
|
+
"netplay": {
|
|
86
|
+
"v": 1,
|
|
87
|
+
"public": true,
|
|
88
|
+
"version": "1",
|
|
89
|
+
"params": [
|
|
90
|
+
"role",
|
|
91
|
+
"surface",
|
|
92
|
+
"ticket"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"playerCard": false,
|
|
96
|
+
"chat": false,
|
|
97
|
+
"agents": {
|
|
98
|
+
"vote": false
|
|
99
|
+
},
|
|
100
|
+
"watch": "overview",
|
|
101
|
+
"screen": {
|
|
102
|
+
"join": "bottom-right",
|
|
103
|
+
"share": "top-right"
|
|
104
|
+
},
|
|
105
|
+
"needs": {
|
|
106
|
+
"three": "0.185.1",
|
|
107
|
+
"@homie-rocks/camera": "0.2.1",
|
|
108
|
+
"@homie-rocks/geom": "0.1.1",
|
|
109
|
+
"@types/three": "0.185.1"
|
|
110
|
+
},
|
|
111
|
+
"check": {
|
|
112
|
+
"action": "[data-action]",
|
|
113
|
+
"observe": "[data-count]"
|
|
114
|
+
},
|
|
115
|
+
"landing": {
|
|
116
|
+
"pitch": "Check in, choose a task, and let the team know you are here."
|
|
117
|
+
},
|
|
118
|
+
"room": {
|
|
119
|
+
"host": "server"
|
|
120
|
+
},
|
|
121
|
+
"players": {
|
|
122
|
+
"min": 1,
|
|
123
|
+
"max": 32
|
|
124
|
+
},
|
|
125
|
+
"starter": {
|
|
126
|
+
"kind": "gathering",
|
|
127
|
+
"choices": [
|
|
128
|
+
"Welcome desk",
|
|
129
|
+
"Packing",
|
|
130
|
+
"Deliveries"
|
|
131
|
+
],
|
|
132
|
+
"color": "#184b43",
|
|
133
|
+
"currency": "CAD",
|
|
134
|
+
"goal": 2500,
|
|
135
|
+
"questions": [
|
|
136
|
+
{
|
|
137
|
+
"text": "Which planet is called the red planet?",
|
|
138
|
+
"choices": [
|
|
139
|
+
"Venus",
|
|
140
|
+
"Mars",
|
|
141
|
+
"Jupiter"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"text": "How many sides does a hexagon have?",
|
|
146
|
+
"choices": [
|
|
147
|
+
"5",
|
|
148
|
+
"6",
|
|
149
|
+
"8"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"text": "Which is a mammal?",
|
|
154
|
+
"choices": [
|
|
155
|
+
"Dolphin",
|
|
156
|
+
"Trout",
|
|
157
|
+
"Octopus"
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Live together</title><style>
|
|
2
|
+
*{box-sizing:border-box}body{margin:0;background:var(--tone,#183b55);color:#fff7df;font-family:system-ui,sans-serif}main{min-height:100svh;padding:clamp(24px,5vw,64px);display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:center}h1{font-size:clamp(44px,6vw,88px);line-height:1;letter-spacing:-.05em;margin:20px 0}p{line-height:1.6;max-width:40em}#connection{font-size:12px;text-transform:uppercase;letter-spacing:.18em}.panel{background:#fff7df;color:#20352e;border-radius:28px;padding:28px;box-shadow:0 30px 80px #0003}#count{font-size:80px;font-weight:800;line-height:1}ul{padding:0;list-style:none}li{padding:12px 0;border-bottom:1px solid #20352e22}button,select,a{font:inherit;min-height:48px;border-radius:12px}button,select{padding:10px 16px;width:100%;margin-top:8px}button{background:var(--tone);color:#fff7df;border:0;cursor:pointer}button:disabled{opacity:.5}a{display:inline-flex;align-items:center;color:inherit}label{display:block;margin-top:14px}select{background:white;color:#20352e;border:1px solid #20352e66}progress{width:100%;height:22px;accent-color:var(--tone)}#status{min-height:24px}#amount{font-size:40px;font-weight:700}button:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #df9325;outline-offset:4px}.wall #controls{display:none}#giving-link[hidden],[hidden]{display:none!important}@media(max-width:650px){main{display:block;padding:72px 22px 24px}h1{font-size:46px}.panel{margin-top:24px;padding:22px}#count{font-size:58px}li{font-size:14px}}@media(max-height:520px) and (min-width:651px){main{padding:65px 25px 20px;gap:22px}h1{font-size:38px}.panel{padding:18px}}
|
|
3
|
+
</style><main><header><span id="connection"></span><h1></h1><p id="intro"></p><div id="goal"><p id="goal-label"></p><div><span id="amount"></span> / <span id="target"></span></div><progress></progress></div><ul id="entries" aria-live="polite"></ul></header><section class="panel"><div id="label"></div><div id="count" data-count>0</div><div id="controls"><p id="prompt"></p><label><span id="choice-label"></span><select id="choice"></select></label><label id="frequency"><span id="frequency-label"></span><select id="monthly"><option id="once" value="once"></option><option id="month" value="month"></option></select></label><button data-action></button><a id="giving-link" href="/shop/" target="_blank" rel="noopener"></a></div><p id="status" role="status"></p></section></main><script type="module" src="./assets/main.js"></script></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"bounds":{"min":[-1,-1],"max":[1,1]},"boxes":[],"spots":{"start":[[0,0]]}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Transient scene truth lives in the room; authorized queue records live in AppRecords.
|
|
2
|
+
// A phone has a logical stationary seat anchor. The wall watches without a seat.
|
|
3
|
+
import { defineRules, f } from '@homie-rocks/studio/rules';
|
|
4
|
+
import { move } from './move';
|
|
5
|
+
export default defineRules({
|
|
6
|
+
contract: 2, space: { dims: 2 }, move,
|
|
7
|
+
shared: { revision: f.u32() },
|
|
8
|
+
shapes: { events: { refresh: {} }, commands: { refresh: {} }, effects: { refresh: {} } },
|
|
9
|
+
entities: {
|
|
10
|
+
participant: {
|
|
11
|
+
player: true,
|
|
12
|
+
body: { shape: 'circle', radius: 0.1, maxSpeed: 0 },
|
|
13
|
+
commands: { refresh(world, self) { world.sendRoom('refresh', {}); } },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
room: {
|
|
17
|
+
rounds: { seconds: 0, breakSeconds: 0 }, bots: { keep: 0 },
|
|
18
|
+
join() { return { kind: 'participant', at: { x: 0, y: 0 } }; },
|
|
19
|
+
on: { refresh(world) { world.shared.revision += 1; world.emit('refresh', { x: 0, y: 0 }, {}); } },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import app from '../app.json';
|
|
2
|
+
import { openRoom } from '@homie-rocks/studio/rules/view';
|
|
3
|
+
import type rules from './rules';
|
|
4
|
+
import { createAppRecords, type AppRecord } from '@homie-rocks/studio/apps';
|
|
5
|
+
import { randomId } from '@homie-rocks/studio/links';
|
|
6
|
+
type Entry = { choice: string; status: string; amount: number; monthly: boolean };
|
|
7
|
+
const room = openRoom<typeof rules>();
|
|
8
|
+
const net = room.net;
|
|
9
|
+
const role = net.params.role ?? 'customer';
|
|
10
|
+
const words: Record<string, string> = app.words;
|
|
11
|
+
const config = app.starter;
|
|
12
|
+
const $ = <T extends HTMLElement = HTMLElement>(s: string) => document.querySelector<T>(s)!;
|
|
13
|
+
const records = createAppRecords<Entry>({ collection: 'entries' });
|
|
14
|
+
let rows: AppRecord<Entry>[] = [], busy = false, ready = false, question = 0;
|
|
15
|
+
const action = $<HTMLButtonElement>('[data-action]');
|
|
16
|
+
document.body.className = role;
|
|
17
|
+
document.body.style.setProperty('--tone', config.color);
|
|
18
|
+
$('h1').textContent = app.name; $('#intro').textContent = app.blurb;
|
|
19
|
+
$('#label').textContent = words.total; action.textContent = words.action;
|
|
20
|
+
$('#choice-label').textContent = words.choice;
|
|
21
|
+
$('#frequency-label').textContent = words.monthly;
|
|
22
|
+
$('#once').textContent = words.once; $('#month').textContent = words.month;
|
|
23
|
+
$('#giving-link').textContent = words.give;
|
|
24
|
+
$('#giving-link').hidden = config.kind !== 'giving';
|
|
25
|
+
$('#frequency').hidden = config.kind !== 'giving';
|
|
26
|
+
$('#goal').hidden = config.kind !== 'giving';
|
|
27
|
+
$('#controls').hidden = role === 'wall';
|
|
28
|
+
const select = $<HTMLSelectElement>('#choice');
|
|
29
|
+
function choices() {
|
|
30
|
+
select.replaceChildren();
|
|
31
|
+
const list = config.kind === 'quiz' ? config.questions[question]?.choices ?? [] : config.choices;
|
|
32
|
+
for (const value of list) { const option = document.createElement('option'); option.value = value; option.textContent = value; select.append(option); }
|
|
33
|
+
$('#prompt').textContent = config.kind === 'quiz' ? config.questions[question]?.text ?? words.finished : words.prompt;
|
|
34
|
+
action.hidden = config.kind === 'quiz' && question >= config.questions.length;
|
|
35
|
+
}
|
|
36
|
+
choices();
|
|
37
|
+
function paint() {
|
|
38
|
+
$('[data-count]').textContent = String(rows.length);
|
|
39
|
+
const amount = rows.reduce((n, r) => n + r.data.amount, 0);
|
|
40
|
+
$('#amount').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(amount);
|
|
41
|
+
$('#target').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(config.goal);
|
|
42
|
+
$<HTMLProgressElement>('progress').max = config.goal; $<HTMLProgressElement>('progress').value = amount;
|
|
43
|
+
$('#goal-label').textContent = words.goal;
|
|
44
|
+
action.disabled = busy || !ready;
|
|
45
|
+
$('#entries').replaceChildren();
|
|
46
|
+
for (const row of rows.slice(-6).reverse()) {
|
|
47
|
+
const li = document.createElement('li');
|
|
48
|
+
li.textContent = `${row.data.choice} · ${row.data.status}${row.data.monthly ? ' · ' + words.month : ''}`;
|
|
49
|
+
if (role === 'staff' && row.data.status !== words.done) {
|
|
50
|
+
const button = document.createElement('button'); button.textContent = words.complete;
|
|
51
|
+
button.onclick = () => { void records.update(row, { ...row.data, status: words.done }).then(refresh).catch(e => { $('#status').textContent = e.message; }); }; li.append(button);
|
|
52
|
+
}
|
|
53
|
+
$('#entries').append(li);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function refresh() {
|
|
57
|
+
try { rows = await records.list(); ready = true; paint(); net.playable(); }
|
|
58
|
+
catch (error) { $('#status').textContent = (error as Error).message; }
|
|
59
|
+
}
|
|
60
|
+
action.onclick = async () => {
|
|
61
|
+
busy = true; paint();
|
|
62
|
+
try {
|
|
63
|
+
const choice = select.value;
|
|
64
|
+
await records.create({ choice: config.kind === 'quiz' ? `${question + 1}. ${choice}` : choice, status: words.pending, amount: config.kind === 'giving' ? Number(choice) : 0, monthly: $<HTMLSelectElement>('#monthly').value === 'month' }, randomId());
|
|
65
|
+
$('#status').textContent = words.thanks;
|
|
66
|
+
if (config.kind === 'quiz') { question++; choices(); }
|
|
67
|
+
room.command('refresh', {}); await refresh();
|
|
68
|
+
} catch (error) { $('#status').textContent = (error as Error).message; }
|
|
69
|
+
finally { busy = false; paint(); }
|
|
70
|
+
};
|
|
71
|
+
room.on('refresh', () => { void refresh(); });
|
|
72
|
+
net.on('link', () => { void refresh(); });
|
|
73
|
+
setInterval(() => { $('#connection').textContent = net.link === 'online' ? words.live : words.reconnecting; void refresh(); }, 2000);
|
|
74
|
+
void refresh();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Together we can
|
|
2
|
+
|
|
3
|
+
Edit app.json words and starter settings. Phone: /<id>/open; wall: /<id>/tv. A site/pages/<id>/index.html replaces only the landing. Records are public; use references, never private contact details. Staff updates require an account grant.
|
|
4
|
+
|
|
5
|
+
Giving totals are public intentions, not received money. The gift link opens the studio shop. Use `shop init --giving --currency CAD` and connect the organisation’s Stripe account. Monthly renewals are managed in Stripe.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "giving",
|
|
3
|
+
"name": "Together we can",
|
|
4
|
+
"blurb": "Support our goal with a one-time or monthly gift. Intentions below are not payments.",
|
|
5
|
+
"entry": "src/view.ts",
|
|
6
|
+
"roles": {
|
|
7
|
+
"customer": {
|
|
8
|
+
"signIn": false,
|
|
9
|
+
"can": [
|
|
10
|
+
"read:entries",
|
|
11
|
+
"create:entries"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"wall": {
|
|
15
|
+
"signIn": false,
|
|
16
|
+
"can": [
|
|
17
|
+
"read:entries"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"staff": {
|
|
21
|
+
"signIn": true,
|
|
22
|
+
"can": [
|
|
23
|
+
"read:entries",
|
|
24
|
+
"update:entries",
|
|
25
|
+
"delete:entries"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"surfaces": {
|
|
30
|
+
"phone": "customer",
|
|
31
|
+
"wall": "wall",
|
|
32
|
+
"kiosk": "customer",
|
|
33
|
+
"tablet": "staff"
|
|
34
|
+
},
|
|
35
|
+
"words": {
|
|
36
|
+
"open": "Open",
|
|
37
|
+
"join": "Scan to join",
|
|
38
|
+
"one": "person",
|
|
39
|
+
"many": "people",
|
|
40
|
+
"action": "Record my intention",
|
|
41
|
+
"total": "Giving intentions",
|
|
42
|
+
"choice": "Choose an option",
|
|
43
|
+
"monthly": "Gift frequency",
|
|
44
|
+
"once": "One time",
|
|
45
|
+
"month": "Monthly",
|
|
46
|
+
"give": "Give securely on Stripe",
|
|
47
|
+
"goal": "Intention goal \u00b7 not payments received",
|
|
48
|
+
"prompt": "Choose what works for you.",
|
|
49
|
+
"finished": "All three answers are in. Thank you!",
|
|
50
|
+
"complete": "Mark complete",
|
|
51
|
+
"done": "Complete",
|
|
52
|
+
"pending": "Recorded",
|
|
53
|
+
"thanks": "Thank you \u2014 the wall is up to date.",
|
|
54
|
+
"live": "Together \u00b7 live",
|
|
55
|
+
"reconnecting": "Reconnecting\u2026"
|
|
56
|
+
},
|
|
57
|
+
"records": {
|
|
58
|
+
"persist": true,
|
|
59
|
+
"collections": {
|
|
60
|
+
"entries": {
|
|
61
|
+
"fields": {
|
|
62
|
+
"choice": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"required": true,
|
|
65
|
+
"maxLength": 100
|
|
66
|
+
},
|
|
67
|
+
"status": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"required": true,
|
|
70
|
+
"maxLength": 60
|
|
71
|
+
},
|
|
72
|
+
"amount": {
|
|
73
|
+
"type": "number",
|
|
74
|
+
"required": true
|
|
75
|
+
},
|
|
76
|
+
"monthly": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"required": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"parts": [],
|
|
85
|
+
"netplay": {
|
|
86
|
+
"v": 1,
|
|
87
|
+
"public": true,
|
|
88
|
+
"version": "1",
|
|
89
|
+
"params": [
|
|
90
|
+
"role",
|
|
91
|
+
"surface",
|
|
92
|
+
"ticket"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"playerCard": false,
|
|
96
|
+
"chat": false,
|
|
97
|
+
"agents": {
|
|
98
|
+
"vote": false
|
|
99
|
+
},
|
|
100
|
+
"watch": "overview",
|
|
101
|
+
"screen": {
|
|
102
|
+
"join": "bottom-right",
|
|
103
|
+
"share": "top-right"
|
|
104
|
+
},
|
|
105
|
+
"needs": {
|
|
106
|
+
"three": "0.185.1",
|
|
107
|
+
"@homie-rocks/camera": "0.2.1",
|
|
108
|
+
"@homie-rocks/geom": "0.1.1",
|
|
109
|
+
"@types/three": "0.185.1"
|
|
110
|
+
},
|
|
111
|
+
"check": {
|
|
112
|
+
"action": "[data-action]",
|
|
113
|
+
"observe": "[data-count]"
|
|
114
|
+
},
|
|
115
|
+
"landing": {
|
|
116
|
+
"pitch": "Support our goal with a one-time or monthly gift. Intentions below are not payments."
|
|
117
|
+
},
|
|
118
|
+
"room": {
|
|
119
|
+
"host": "server"
|
|
120
|
+
},
|
|
121
|
+
"players": {
|
|
122
|
+
"min": 1,
|
|
123
|
+
"max": 32
|
|
124
|
+
},
|
|
125
|
+
"starter": {
|
|
126
|
+
"kind": "giving",
|
|
127
|
+
"choices": [
|
|
128
|
+
"10",
|
|
129
|
+
"25",
|
|
130
|
+
"50"
|
|
131
|
+
],
|
|
132
|
+
"color": "#354d2e",
|
|
133
|
+
"currency": "CAD",
|
|
134
|
+
"goal": 2500,
|
|
135
|
+
"questions": [
|
|
136
|
+
{
|
|
137
|
+
"text": "Which planet is called the red planet?",
|
|
138
|
+
"choices": [
|
|
139
|
+
"Venus",
|
|
140
|
+
"Mars",
|
|
141
|
+
"Jupiter"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"text": "How many sides does a hexagon have?",
|
|
146
|
+
"choices": [
|
|
147
|
+
"5",
|
|
148
|
+
"6",
|
|
149
|
+
"8"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"text": "Which is a mammal?",
|
|
154
|
+
"choices": [
|
|
155
|
+
"Dolphin",
|
|
156
|
+
"Trout",
|
|
157
|
+
"Octopus"
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Live together</title><style>
|
|
2
|
+
*{box-sizing:border-box}body{margin:0;background:var(--tone,#183b55);color:#fff7df;font-family:system-ui,sans-serif}main{min-height:100svh;padding:clamp(24px,5vw,64px);display:grid;grid-template-columns:1.1fr 1fr;gap:40px;align-items:center}h1{font-size:clamp(44px,6vw,88px);line-height:1;letter-spacing:-.05em;margin:20px 0}p{line-height:1.6;max-width:40em}#connection{font-size:12px;text-transform:uppercase;letter-spacing:.18em}.panel{background:#fff7df;color:#20352e;border-radius:28px;padding:28px;box-shadow:0 30px 80px #0003}#count{font-size:80px;font-weight:800;line-height:1}ul{padding:0;list-style:none}li{padding:12px 0;border-bottom:1px solid #20352e22}button,select,a{font:inherit;min-height:48px;border-radius:12px}button,select{padding:10px 16px;width:100%;margin-top:8px}button{background:var(--tone);color:#fff7df;border:0;cursor:pointer}button:disabled{opacity:.5}a{display:inline-flex;align-items:center;color:inherit}label{display:block;margin-top:14px}select{background:white;color:#20352e;border:1px solid #20352e66}progress{width:100%;height:22px;accent-color:var(--tone)}#status{min-height:24px}#amount{font-size:40px;font-weight:700}button:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #df9325;outline-offset:4px}.wall #controls{display:none}#giving-link[hidden],[hidden]{display:none!important}@media(max-width:650px){main{display:block;padding:72px 22px 24px}h1{font-size:46px}.panel{margin-top:24px;padding:22px}#count{font-size:58px}li{font-size:14px}}@media(max-height:520px) and (min-width:651px){main{padding:65px 25px 20px;gap:22px}h1{font-size:38px}.panel{padding:18px}}
|
|
3
|
+
</style><main><header><span id="connection"></span><h1></h1><p id="intro"></p><div id="goal"><p id="goal-label"></p><div><span id="amount"></span> / <span id="target"></span></div><progress></progress></div><ul id="entries" aria-live="polite"></ul></header><section class="panel"><div id="label"></div><div id="count" data-count>0</div><div id="controls"><p id="prompt"></p><label><span id="choice-label"></span><select id="choice"></select></label><label id="frequency"><span id="frequency-label"></span><select id="monthly"><option id="once" value="once"></option><option id="month" value="month"></option></select></label><button data-action></button><a id="giving-link" href="/shop/" target="_blank" rel="noopener"></a></div><p id="status" role="status"></p></section></main><script type="module" src="./assets/main.js"></script></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"bounds":{"min":[-1,-1],"max":[1,1]},"boxes":[],"spots":{"start":[[0,0]]}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Transient scene truth lives in the room; authorized queue records live in AppRecords.
|
|
2
|
+
// A phone has a logical stationary seat anchor. The wall watches without a seat.
|
|
3
|
+
import { defineRules, f } from '@homie-rocks/studio/rules';
|
|
4
|
+
import { move } from './move';
|
|
5
|
+
export default defineRules({
|
|
6
|
+
contract: 2, space: { dims: 2 }, move,
|
|
7
|
+
shared: { revision: f.u32() },
|
|
8
|
+
shapes: { events: { refresh: {} }, commands: { refresh: {} }, effects: { refresh: {} } },
|
|
9
|
+
entities: {
|
|
10
|
+
participant: {
|
|
11
|
+
player: true,
|
|
12
|
+
body: { shape: 'circle', radius: 0.1, maxSpeed: 0 },
|
|
13
|
+
commands: { refresh(world, self) { world.sendRoom('refresh', {}); } },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
room: {
|
|
17
|
+
rounds: { seconds: 0, breakSeconds: 0 }, bots: { keep: 0 },
|
|
18
|
+
join() { return { kind: 'participant', at: { x: 0, y: 0 } }; },
|
|
19
|
+
on: { refresh(world) { world.shared.revision += 1; world.emit('refresh', { x: 0, y: 0 }, {}); } },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import app from '../app.json';
|
|
2
|
+
import { openRoom } from '@homie-rocks/studio/rules/view';
|
|
3
|
+
import type rules from './rules';
|
|
4
|
+
import { createAppRecords, type AppRecord } from '@homie-rocks/studio/apps';
|
|
5
|
+
import { randomId } from '@homie-rocks/studio/links';
|
|
6
|
+
type Entry = { choice: string; status: string; amount: number; monthly: boolean };
|
|
7
|
+
const room = openRoom<typeof rules>();
|
|
8
|
+
const net = room.net;
|
|
9
|
+
const role = net.params.role ?? 'customer';
|
|
10
|
+
const words: Record<string, string> = app.words;
|
|
11
|
+
const config = app.starter;
|
|
12
|
+
const $ = <T extends HTMLElement = HTMLElement>(s: string) => document.querySelector<T>(s)!;
|
|
13
|
+
const records = createAppRecords<Entry>({ collection: 'entries' });
|
|
14
|
+
let rows: AppRecord<Entry>[] = [], busy = false, ready = false, question = 0;
|
|
15
|
+
const action = $<HTMLButtonElement>('[data-action]');
|
|
16
|
+
document.body.className = role;
|
|
17
|
+
document.body.style.setProperty('--tone', config.color);
|
|
18
|
+
$('h1').textContent = app.name; $('#intro').textContent = app.blurb;
|
|
19
|
+
$('#label').textContent = words.total; action.textContent = words.action;
|
|
20
|
+
$('#choice-label').textContent = words.choice;
|
|
21
|
+
$('#frequency-label').textContent = words.monthly;
|
|
22
|
+
$('#once').textContent = words.once; $('#month').textContent = words.month;
|
|
23
|
+
$('#giving-link').textContent = words.give;
|
|
24
|
+
$('#giving-link').hidden = config.kind !== 'giving';
|
|
25
|
+
$('#frequency').hidden = config.kind !== 'giving';
|
|
26
|
+
$('#goal').hidden = config.kind !== 'giving';
|
|
27
|
+
$('#controls').hidden = role === 'wall';
|
|
28
|
+
const select = $<HTMLSelectElement>('#choice');
|
|
29
|
+
function choices() {
|
|
30
|
+
select.replaceChildren();
|
|
31
|
+
const list = config.kind === 'quiz' ? config.questions[question]?.choices ?? [] : config.choices;
|
|
32
|
+
for (const value of list) { const option = document.createElement('option'); option.value = value; option.textContent = value; select.append(option); }
|
|
33
|
+
$('#prompt').textContent = config.kind === 'quiz' ? config.questions[question]?.text ?? words.finished : words.prompt;
|
|
34
|
+
action.hidden = config.kind === 'quiz' && question >= config.questions.length;
|
|
35
|
+
}
|
|
36
|
+
choices();
|
|
37
|
+
function paint() {
|
|
38
|
+
$('[data-count]').textContent = String(rows.length);
|
|
39
|
+
const amount = rows.reduce((n, r) => n + r.data.amount, 0);
|
|
40
|
+
$('#amount').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(amount);
|
|
41
|
+
$('#target').textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency: config.currency }).format(config.goal);
|
|
42
|
+
$<HTMLProgressElement>('progress').max = config.goal; $<HTMLProgressElement>('progress').value = amount;
|
|
43
|
+
$('#goal-label').textContent = words.goal;
|
|
44
|
+
action.disabled = busy || !ready;
|
|
45
|
+
$('#entries').replaceChildren();
|
|
46
|
+
for (const row of rows.slice(-6).reverse()) {
|
|
47
|
+
const li = document.createElement('li');
|
|
48
|
+
li.textContent = `${row.data.choice} · ${row.data.status}${row.data.monthly ? ' · ' + words.month : ''}`;
|
|
49
|
+
if (role === 'staff' && row.data.status !== words.done) {
|
|
50
|
+
const button = document.createElement('button'); button.textContent = words.complete;
|
|
51
|
+
button.onclick = () => { void records.update(row, { ...row.data, status: words.done }).then(refresh).catch(e => { $('#status').textContent = e.message; }); }; li.append(button);
|
|
52
|
+
}
|
|
53
|
+
$('#entries').append(li);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function refresh() {
|
|
57
|
+
try { rows = await records.list(); ready = true; paint(); net.playable(); }
|
|
58
|
+
catch (error) { $('#status').textContent = (error as Error).message; }
|
|
59
|
+
}
|
|
60
|
+
action.onclick = async () => {
|
|
61
|
+
busy = true; paint();
|
|
62
|
+
try {
|
|
63
|
+
const choice = select.value;
|
|
64
|
+
await records.create({ choice: config.kind === 'quiz' ? `${question + 1}. ${choice}` : choice, status: words.pending, amount: config.kind === 'giving' ? Number(choice) : 0, monthly: $<HTMLSelectElement>('#monthly').value === 'month' }, randomId());
|
|
65
|
+
$('#status').textContent = words.thanks;
|
|
66
|
+
if (config.kind === 'quiz') { question++; choices(); }
|
|
67
|
+
room.command('refresh', {}); await refresh();
|
|
68
|
+
} catch (error) { $('#status').textContent = (error as Error).message; }
|
|
69
|
+
finally { busy = false; paint(); }
|
|
70
|
+
};
|
|
71
|
+
room.on('refresh', () => { void refresh(); });
|
|
72
|
+
net.on('link', () => { void refresh(); });
|
|
73
|
+
setInterval(() => { $('#connection').textContent = net.link === 'online' ? words.live : words.reconnecting; void refresh(); }, 2000);
|
|
74
|
+
void refresh();
|