@oddessentials/agent-guild 1.28.0 → 1.30.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.
Files changed (133) hide show
  1. package/bin/native/win32-arm64/agent-guild-report.exe +0 -0
  2. package/bin/native/win32-x64/agent-guild-report.exe +0 -0
  3. package/bin/windows/arm64/agent-guild-start.exe +0 -0
  4. package/bin/windows/x64/agent-guild-start.exe +0 -0
  5. package/docs/api.md +10 -2
  6. package/node_modules/@jsquash/jpeg/CHANGELOG.md +54 -0
  7. package/node_modules/@jsquash/jpeg/LICENSE +201 -0
  8. package/node_modules/@jsquash/jpeg/README.md +105 -0
  9. package/node_modules/@jsquash/jpeg/codec/LICENSE.codec.md +132 -0
  10. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.d.ts +7 -0
  11. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.js +15 -0
  12. package/node_modules/@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm +0 -0
  13. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.d.ts +37 -0
  14. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.js +15 -0
  15. package/node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm +0 -0
  16. package/node_modules/@jsquash/jpeg/codec/pre.js +24 -0
  17. package/node_modules/@jsquash/jpeg/decode.d.ts +16 -0
  18. package/node_modules/@jsquash/jpeg/decode.js +36 -0
  19. package/node_modules/@jsquash/jpeg/emscripten-types.d.ts +137 -0
  20. package/node_modules/@jsquash/jpeg/encode.d.ts +21 -0
  21. package/node_modules/@jsquash/jpeg/encode.js +35 -0
  22. package/node_modules/@jsquash/jpeg/index.d.ts +3 -0
  23. package/node_modules/@jsquash/jpeg/index.js +2 -0
  24. package/node_modules/@jsquash/jpeg/meta.d.ts +24 -0
  25. package/node_modules/@jsquash/jpeg/meta.js +25 -0
  26. package/node_modules/@jsquash/jpeg/package.json +32 -0
  27. package/node_modules/@jsquash/jpeg/utils.d.ts +17 -0
  28. package/node_modules/@jsquash/jpeg/utils.js +31 -0
  29. package/node_modules/@jsquash/png/CHANGELOG.md +69 -0
  30. package/node_modules/@jsquash/png/LICENSE +201 -0
  31. package/node_modules/@jsquash/png/README.md +114 -0
  32. package/node_modules/@jsquash/png/codec/LICENSE.codec.md +29 -0
  33. package/node_modules/@jsquash/png/codec/README.md +5 -0
  34. package/node_modules/@jsquash/png/codec/pkg/README.md +5 -0
  35. package/node_modules/@jsquash/png/codec/pkg/squoosh_png.d.ts +71 -0
  36. package/node_modules/@jsquash/png/codec/pkg/squoosh_png.js +314 -0
  37. package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm +0 -0
  38. package/node_modules/@jsquash/png/codec/pkg/squoosh_png_bg.wasm.d.ts +13 -0
  39. package/node_modules/@jsquash/png/codec/pre.js +24 -0
  40. package/node_modules/@jsquash/png/decode.d.ts +29 -0
  41. package/node_modules/@jsquash/png/decode.js +35 -0
  42. package/node_modules/@jsquash/png/encode.d.ts +31 -0
  43. package/node_modules/@jsquash/png/encode.js +40 -0
  44. package/node_modules/@jsquash/png/index.d.ts +3 -0
  45. package/node_modules/@jsquash/png/index.js +2 -0
  46. package/node_modules/@jsquash/png/meta.d.ts +16 -0
  47. package/node_modules/@jsquash/png/meta.js +15 -0
  48. package/node_modules/@jsquash/png/package.json +34 -0
  49. package/node_modules/@jsquash/webp/CHANGELOG.md +48 -0
  50. package/node_modules/@jsquash/webp/LICENSE +201 -0
  51. package/node_modules/@jsquash/webp/README.md +100 -0
  52. package/node_modules/@jsquash/webp/codec/LICENSE.codec.md +29 -0
  53. package/node_modules/@jsquash/webp/codec/Makefile.animated.backup +88 -0
  54. package/node_modules/@jsquash/webp/codec/README.md +5 -0
  55. package/node_modules/@jsquash/webp/codec/dec/README.md +18 -0
  56. package/node_modules/@jsquash/webp/codec/dec/webp_dec.d.ts +7 -0
  57. package/node_modules/@jsquash/webp/codec/dec/webp_dec.js +16 -0
  58. package/node_modules/@jsquash/webp/codec/dec/webp_dec.wasm +0 -0
  59. package/node_modules/@jsquash/webp/codec/enc/README.md +22 -0
  60. package/node_modules/@jsquash/webp/codec/enc/webp_enc.d.ts +42 -0
  61. package/node_modules/@jsquash/webp/codec/enc/webp_enc.js +16 -0
  62. package/node_modules/@jsquash/webp/codec/enc/webp_enc.wasm +0 -0
  63. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.d.ts +1 -0
  64. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.js +16 -0
  65. package/node_modules/@jsquash/webp/codec/enc/webp_enc_simd.wasm +0 -0
  66. package/node_modules/@jsquash/webp/codec/pre.js +24 -0
  67. package/node_modules/@jsquash/webp/decode.d.ts +15 -0
  68. package/node_modules/@jsquash/webp/decode.js +34 -0
  69. package/node_modules/@jsquash/webp/emscripten-types.d.ts +137 -0
  70. package/node_modules/@jsquash/webp/encode.d.ts +23 -0
  71. package/node_modules/@jsquash/webp/encode.js +43 -0
  72. package/node_modules/@jsquash/webp/index.d.ts +3 -0
  73. package/node_modules/@jsquash/webp/index.js +2 -0
  74. package/node_modules/@jsquash/webp/meta.d.ts +19 -0
  75. package/node_modules/@jsquash/webp/meta.js +33 -0
  76. package/node_modules/@jsquash/webp/package.json +35 -0
  77. package/node_modules/@jsquash/webp/utils.d.ts +17 -0
  78. package/node_modules/@jsquash/webp/utils.js +31 -0
  79. package/node_modules/argparse/lib/argparse.js +1 -1
  80. package/node_modules/argparse/lib/textwrap.js +13 -2
  81. package/node_modules/argparse/package.json +1 -1
  82. package/node_modules/markdown-it/dist/browser/markdown-it.esm.min.mjs +737 -686
  83. package/node_modules/markdown-it/dist/browser/markdown-it.esm.min.mjs.map +1 -1
  84. package/node_modules/markdown-it/dist/browser/markdown-it.umd.min.js +8 -8
  85. package/node_modules/markdown-it/dist/browser/markdown-it.umd.min.js.map +1 -1
  86. package/node_modules/markdown-it/dist/markdown-it.cjs.js +660 -518
  87. package/node_modules/markdown-it/dist/markdown-it.cjs.js.map +1 -1
  88. package/node_modules/markdown-it/dist/markdown-it.d.mts +1 -0
  89. package/node_modules/markdown-it/dist/markdown-it.mjs +179 -124
  90. package/node_modules/markdown-it/dist/markdown-it.mjs.map +1 -1
  91. package/node_modules/markdown-it/package.json +11 -11
  92. package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  93. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  94. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  95. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  96. package/node_modules/wasm-feature-detect/LICENSE +202 -0
  97. package/node_modules/wasm-feature-detect/README.md +109 -0
  98. package/node_modules/wasm-feature-detect/README.md.ejs +89 -0
  99. package/node_modules/wasm-feature-detect/dist/cjs/index.cjs +1 -0
  100. package/node_modules/wasm-feature-detect/dist/esm/index.js +1 -0
  101. package/node_modules/wasm-feature-detect/dist/index.d.cts +24 -0
  102. package/node_modules/wasm-feature-detect/dist/index.d.ts +24 -0
  103. package/node_modules/wasm-feature-detect/dist/umd/index.js +1 -0
  104. package/node_modules/wasm-feature-detect/package.json +53 -0
  105. package/node_modules/ws/README.md +21 -29
  106. package/node_modules/ws/lib/buffer-util.js +13 -2
  107. package/node_modules/ws/lib/sender.js +4 -1
  108. package/node_modules/ws/lib/websocket.js +8 -3
  109. package/node_modules/ws/package.json +1 -1
  110. package/package.json +7 -4
  111. package/src/manager/config.mjs +2 -0
  112. package/src/manager/folder-browser.mjs +39 -3
  113. package/src/manager/main.mjs +4 -0
  114. package/src/manager/server.mjs +32 -5
  115. package/src/manager/steam-workshop.mjs +64 -0
  116. package/src/manager/yard-art.mjs +1 -1
  117. package/src/manager/yard-piece-check.mjs +424 -0
  118. package/src/manager/yard-pieces.mjs +188 -0
  119. package/src/manager/yard-worlds.mjs +214 -0
  120. package/web/app.js +175 -11
  121. package/web/index.html +20 -2
  122. package/web/skins/grove/ui/companion-bird.webp +0 -0
  123. package/web/skins/guild/skin.css +2 -13
  124. package/web/styles.css +15 -1
  125. package/web/yard/art.json +1 -1
  126. package/web/yard/model.mjs +44 -2
  127. package/web/yard/photo.js +3 -4
  128. package/web/yard/renderer.js +150 -38
  129. package/web/yard/vendor/engine.js +500 -495
  130. package/web/yard/view.js +160 -14
  131. package/web/yard/yard.css +55 -12
  132. package/web/skins/grove/ui/install-now-dark.svg +0 -91
  133. package/web/skins/grove/ui/install-now-light.svg +0 -91
package/web/yard/view.js CHANGED
@@ -1,4 +1,4 @@
1
- import { WORLDS, sessionPose } from './model.mjs';
1
+ import { WORLDS, PROVIDER_ORDER, sessionPose } from './model.mjs';
2
2
 
3
3
  const $ = id => document.getElementById(id);
4
4
  const make = (tag, cls, text) => Object.assign(document.createElement(tag), { className: cls, ...(text === undefined ? {} : { textContent: text }) });
@@ -17,6 +17,8 @@ async function boundedLoad(start, onProgress) {
17
17
  } finally { settled = true; clearTimeout(timer); }
18
18
  }
19
19
  const megabytes = bytes => (bytes / 1048576).toFixed(1) + ' MB';
20
+ const CREWS = { anthropic: 'Claude Code', openai: 'Codex CLI', google: 'Antigravity CLI', xai: 'Grok Build', shell: 'Shell', docker: 'Docker Agent' };
21
+ const pieceKind = slot => slot === 'companion' ? 'companion' : 'character';
20
22
 
21
23
  /** A layout with injected canonical controls. No API, sockets, or business rules. */
22
24
  export function initYard(controller) {
@@ -28,7 +30,139 @@ export function initYard(controller) {
28
30
  const root = document.documentElement;
29
31
  let view = root.dataset.view === 'yard' ? 'yard' : 'cards';
30
32
  let snapshot = controller.snapshot();
31
- let lastSkin = '', lastTheme = '', wasConnected = false;
33
+ let lastSkin = '', lastTheme = '', lastRevision = null, wasConnected = false;
34
+ // Worlds that failed to draw here: skin to revision. The Yard shows its original until the world changes.
35
+ const unloadable = new Map();
36
+ const customFor = (skin) => {
37
+ const world = controller.yardWorld(skin);
38
+ return world && !world.problem && unloadable.get(skin) !== world.revision ? world : null;
39
+ };
40
+ const titleFor = (skin) => customFor(skin)?.title ?? WORLDS[skin].title;
41
+ const worldShown = new Map();
42
+ const loadWorld = (target, skin, theme, progress) => {
43
+ if (target.skin !== skin) {
44
+ worldShown.delete(skin);
45
+ for (const [key, shown] of pieceShown) if (key.startsWith(skin + '/') && shown.ok) pieceShown.delete(key);
46
+ target.setPieces(skin, piecesFor(skin));
47
+ worldSpot();
48
+ }
49
+ const custom = customFor(skin);
50
+ return target.setWorld(skin, theme, { progress, custom }).then(() => {
51
+ if (custom) { worldShown.set(skin, custom.revision); worldSpot(); }
52
+ }, (err) => {
53
+ if (!custom || disposed) throw err;
54
+ unloadable.set(skin, custom.revision);
55
+ if (skin === lastSkin) lastRevision = null;
56
+ worldSpot();
57
+ return target.setWorld(skin, theme, { progress });
58
+ });
59
+ };
60
+ const pieceShown = new Map();
61
+ const pieceKey = (skin, slot) => skin + '/' + slot;
62
+ const piecesOf = (skin) => controller.yardPieces?.(skin) ?? {};
63
+ const piecesFor = (skin) => {
64
+ const pieces = {};
65
+ for (const [slot, piece] of Object.entries(piecesOf(skin))) {
66
+ const shown = pieceShown.get(pieceKey(skin, slot));
67
+ if (piece.revision && !(shown?.revision === piece.revision && !shown.ok)) pieces[slot] = { revision: piece.revision };
68
+ }
69
+ return pieces;
70
+ };
71
+ const syncPieces = (target) => {
72
+ const skin = root.dataset.skin;
73
+ if (target && WORLDS[skin]) target.setPieces(skin, piecesFor(skin));
74
+ };
75
+ const pieceDrawn = ({ skin, slot, revision, ok }) => {
76
+ if (disposed || piecesOf(skin)[slot]?.revision !== revision) return;
77
+ const key = pieceKey(skin, slot), was = pieceShown.get(key);
78
+ if (was?.revision === revision && (!was.ok || was.ok === ok)) return;
79
+ pieceShown.set(key, { revision, ok });
80
+ if (!ok) for (const target of [renderer, pendingRenderer]) target?.setPieces(skin, piecesFor(skin));
81
+ worldSpot();
82
+ };
83
+ function worldStatus(skin) {
84
+ const world = controller.yardWorld(skin);
85
+ if (!world) return { state: 'original', text: 'Original' };
86
+ if (world.problem) return { state: 'failed', text: world.problem + ' Showing the original.' };
87
+ if (!customFor(skin)) return { state: 'failed', text: world.title + ' could not load. Showing the original.' };
88
+ return worldShown.get(skin) === world.revision ? { state: 'custom', text: 'Customized: ' + world.title } : { state: 'loading', text: 'Loading ' + world.title + '…' };
89
+ }
90
+ function pieceStatus(skin, slot) {
91
+ const piece = piecesOf(skin)[slot], kind = pieceKind(slot);
92
+ if (!piece) return { state: 'original', text: 'Original' };
93
+ if (piece.problem) return { state: 'failed', text: piece.problem + ' Showing the original ' + kind + '.' };
94
+ const shown = pieceShown.get(pieceKey(skin, slot));
95
+ if (shown?.revision !== piece.revision) return { state: 'loading', text: 'Loading ' + piece.title + '…' };
96
+ return shown.ok ? { state: 'custom', text: 'Customized: ' + piece.title } : { state: 'failed', text: piece.title + ' could not load. Showing the original ' + kind + '.' };
97
+ }
98
+ const slotName = (slot) => slot === 'world' ? 'World' : slot === 'companion' ? 'Companions' : snapshot.providers.find(p => p.id === slot)?.tool || CREWS[slot];
99
+ const customizeRows = new Map();
100
+ function customizeRow(slot) {
101
+ if (customizeRows.has(slot)) return customizeRows.get(slot);
102
+ const row = make('li', 'flag-row yard-customize-row'), copy = make('div', 'yard-customize-copy'), actions = make('div', 'yard-customize-actions');
103
+ const name = make('span', 'flag-name'), status = make('span', 'yard-customize-status'), note = make('span', 'yard-customize-note');
104
+ const replace = make('button', 'btn', 'Replace'), restore = make('button', 'btn', 'Restore original');
105
+ replace.type = restore.type = 'button';
106
+ note.hidden = slot === 'world';
107
+ const skin = () => root.dataset.skin;
108
+ replace.addEventListener('click', () => slot === 'world' ? controller.replaceYard(skin()) : controller.replacePiece(skin(), slot));
109
+ restore.addEventListener('click', () => slot === 'world' ? controller.restoreYard(skin()) : controller.restorePiece(skin(), slot));
110
+ copy.append(name, status, note);
111
+ actions.append(replace, restore);
112
+ row.append(copy, actions);
113
+ row.dataset.slot = slot;
114
+ const parts = { row, name, status, note, replace, restore };
115
+ customizeRows.set(slot, parts);
116
+ return parts;
117
+ }
118
+ function paintCustomize() {
119
+ const dialog = $('yard-customize'), skin = root.dataset.skin;
120
+ if (!dialog.open) return;
121
+ if (!WORLDS[skin]) { dialog.close(); return; }
122
+ $('yard-customize-sub').textContent = WORLDS[skin].title + ' only. Other Yards keep their own.';
123
+ const busy = Boolean(controller.yardBusy?.()) || !snapshot.connected;
124
+ const heading = (text) => {
125
+ if (!customizeRows.has(text)) { const item = make('li', 'yard-customize-heading', text); item.setAttribute('role', 'presentation'); customizeRows.set(text, item); }
126
+ return customizeRows.get(text);
127
+ };
128
+ const items = [];
129
+ for (const [title, slots] of [['Yard', ['world']], ['Characters', PROVIDER_ORDER], ['Companions', ['companion']]]) {
130
+ items.push(heading(title));
131
+ for (const slot of slots) {
132
+ const parts = customizeRow(slot), status = controller.yardChecking?.() === pieceKey(skin, slot) ? { state: 'loading', text: 'Checking replacement…' } : slot === 'world' ? worldStatus(skin) : pieceStatus(skin, slot);
133
+ const installed = slot === 'world' ? Boolean(controller.yardWorld(skin)) : Boolean(piecesOf(skin)[slot]);
134
+ parts.name.textContent = slotName(slot);
135
+ parts.status.textContent = status.text;
136
+ if (slot !== 'world') parts.note.textContent = slot === 'companion' ? 'Shows beside sessions with helpers in this Yard.'
137
+ : 'Shows on your ' + slotName(slot) + ' sessions in this Yard' + (slot === 'shell' ? ', and in gold on Terminal AI Chat’s.' : '.');
138
+ parts.row.dataset.state = status.state;
139
+ parts.replace.disabled = busy;
140
+ parts.restore.disabled = busy || !installed;
141
+ parts.replace.setAttribute('aria-label', 'Replace ' + slotName(slot));
142
+ parts.restore.setAttribute('aria-label', 'Restore the original ' + slotName(slot));
143
+ items.push(parts.row);
144
+ }
145
+ }
146
+ const list = $('yard-customize-list');
147
+ if (items.some((item, i) => list.children[i] !== item) || list.children.length !== items.length) {
148
+ const focused = list.contains(document.activeElement) ? document.activeElement : null;
149
+ list.replaceChildren(...items);
150
+ focused?.focus();
151
+ }
152
+ }
153
+ function worldSpot() {
154
+ const skin = root.dataset.skin;
155
+ if (!WORLDS[skin]) return;
156
+ const world = controller.yardWorld(skin);
157
+ $('yard-title').textContent = titleFor(skin);
158
+ $('yard-title').classList.toggle('yard-title-custom', Boolean(customFor(skin)));
159
+ const reason = world?.problem || (world && !customFor(skin) ? world.title + ' could not load.' : '');
160
+ const failed = !reason && Object.keys(piecesOf(skin)).map(slot => pieceStatus(skin, slot)).find(status => status.state === 'failed');
161
+ $('yard-customize-open').hidden = !controller.canReplaceYard();
162
+ $('yard-world-note').hidden = !reason && !failed;
163
+ $('yard-world-note').textContent = reason ? reason + ' This Yard is showing its original.' : failed ? failed.text : '';
164
+ paintCustomize();
165
+ }
32
166
  const celebrate = () => { if (!disposed && sceneVisible()) renderer?.fireworks(); };
33
167
  const fireworksButton = () => {
34
168
  const button = $('yard-fireworks');
@@ -66,7 +200,7 @@ export function initYard(controller) {
66
200
  if (allowed) yardChoice.parentElement.removeAttribute('title');
67
201
  else yardChoice.parentElement.title = `${skinEntry()?.name || 'This skin'} has no yard yet.`;
68
202
  $('yard').hidden = view !== 'yard';
69
- if (view !== 'yard') closePhoto();
203
+ if (view !== 'yard') { closePhoto(); $('yard-customize').close(); }
70
204
  $('providers').hidden = view === 'yard';
71
205
  $('providers').inert = view === 'yard';
72
206
  for (const section of document.querySelectorAll('#app > section.sessions, #app > section.news')) {
@@ -146,20 +280,24 @@ export function initYard(controller) {
146
280
  select: next => select(next),
147
281
  open: id => controller.openSession(id),
148
282
  error: () => { if (attempt === sceneAttempt) failScene(); },
283
+ piece: pieceDrawn,
149
284
  });
150
285
  pendingRenderer = candidate;
151
286
  // Appearance can change while the first world is loading.
152
- let skin, theme;
287
+ let skin, theme, revision;
153
288
  do {
154
289
  skin = root.dataset.skin; theme = root.dataset.theme;
155
290
  if (!WORLDS[skin]) { candidate.dispose(); return; }
156
- showLoading('Loading ' + WORLDS[skin].title + '…', 'scene');
157
- await boundedLoad(progress => candidate.setWorld(skin, theme, { progress }), loadingProgress);
158
- } while (!disposed && !candidate.disposed && attempt === sceneAttempt && (skin !== root.dataset.skin || theme !== root.dataset.theme));
291
+ revision = customFor(skin)?.revision ?? null;
292
+ syncPieces(candidate);
293
+ showLoading('Loading ' + titleFor(skin) + '…', 'scene');
294
+ await boundedLoad(progress => loadWorld(candidate, skin, theme, progress), loadingProgress);
295
+ } while (!disposed && !candidate.disposed && attempt === sceneAttempt && (skin !== root.dataset.skin || theme !== root.dataset.theme || revision !== (customFor(skin)?.revision ?? null)));
159
296
  if (disposed || candidate.disposed || attempt !== sceneAttempt) { candidate.dispose(); return; }
297
+ syncPieces(candidate);
160
298
  pendingRenderer = null;
161
299
  renderer = candidate;
162
- lastSkin = skin; lastTheme = theme;
300
+ lastSkin = skin; lastTheme = theme; lastRevision = revision;
163
301
  renderer.setReducedMotion(motion.matches);
164
302
  renderer.update(snapshot.providers, snapshot.sessions);
165
303
  renderer.select(selected);
@@ -237,13 +375,13 @@ export function initYard(controller) {
237
375
  // The current world stays up and usable while another loads, and after it
238
376
  // fails. A swap that is ready almost at once shows no progress.
239
377
  function swapWorld() {
240
- const current = renderer, attempt = ++worldAttempt, title = WORLDS[lastSkin].title;
378
+ const current = renderer, attempt = ++worldAttempt, title = titleFor(lastSkin);
241
379
  $('yard-swap-failure').hidden = true;
242
380
  hideLoading();
243
381
  showLoading('Loading ' + title + '…', 'swap');
244
382
  swapReveal = setTimeout(() => { if (attempt === worldAttempt) $('yard-loading').hidden = false; }, 250);
245
383
  const settle = () => renderer === current && attempt === worldAttempt && (hideLoading(), true);
246
- boundedLoad(progress => current.setWorld(lastSkin, lastTheme, { progress }), loadingProgress).then(() => {
384
+ boundedLoad(progress => loadWorld(current, lastSkin, lastTheme, progress), loadingProgress).then(() => {
247
385
  if (!settle()) return;
248
386
  current.update(snapshot.providers,snapshot.sessions); current.select(selected);
249
387
  }).catch(() => {
@@ -267,7 +405,8 @@ export function initYard(controller) {
267
405
  }
268
406
  const world = WORLDS[root.dataset.skin];
269
407
  if (!world) return;
270
- $('yard-title').textContent = world.title;
408
+ const revision = customFor(root.dataset.skin)?.revision ?? null;
409
+ worldSpot();
271
410
  $('yard-subtitle').textContent = world.subtitle;
272
411
  const working = snapshot.sessions.filter(s => sessionPose(s) === 'working').length;
273
412
  const running = snapshot.sessions.filter(s => s.status === 'running').length;
@@ -281,14 +420,15 @@ export function initYard(controller) {
281
420
  $('yard-news-empty').hidden = Boolean(href);
282
421
  if (href) { $('yard-headline').textContent = news.title; $('yard-headline').href = href; }
283
422
  if (renderer) {
284
- if (lastSkin !== root.dataset.skin || lastTheme !== root.dataset.theme) {
423
+ if (lastSkin !== root.dataset.skin || lastTheme !== root.dataset.theme || lastRevision !== revision) {
285
424
  if (!WORLDS[root.dataset.skin]) return;
286
- lastSkin = root.dataset.skin; lastTheme = root.dataset.theme;
425
+ lastSkin = root.dataset.skin; lastTheme = root.dataset.theme; lastRevision = revision;
287
426
  swapWorld();
288
427
  }
428
+ syncPieces(renderer);
289
429
  renderer.update(snapshot.providers,snapshot.sessions);
290
430
  renderer.setActive(sceneVisible());
291
- } else ensureScene();
431
+ } else { syncPieces(pendingRenderer); ensureScene(); }
292
432
  }
293
433
  $('view-cards').addEventListener('change',() => setView('cards'));
294
434
  $('view-yard').addEventListener('change',() => { if (skinAllowsYard()) setView('yard'); });
@@ -310,6 +450,12 @@ export function initYard(controller) {
310
450
  lastSkin = ''; lastTheme = '';
311
451
  update();
312
452
  });
453
+ $('yard-customize-open').addEventListener('click',() => {
454
+ if (!$('yard-customize').open) $('yard-customize').showModal();
455
+ paintCustomize();
456
+ });
457
+ $('yard-customize-close').addEventListener('click',() => $('yard-customize').close());
458
+ $('yard-customize').addEventListener('click',event => { if (event.target === $('yard-customize')) $('yard-customize').close(); });
313
459
  $('yard-filter').addEventListener('input',roster);
314
460
  $('yard-news').addEventListener('click',controller.openNews);
315
461
  $('yard-deselect').addEventListener('click',() => { select(null); $('yard-filter').focus(); });
package/web/yard/yard.css CHANGED
@@ -15,7 +15,7 @@
15
15
  .yard-heading > div:first-child::before { content:""; position:absolute; inset:-12px -22px -14px; z-index:-1; border-radius:28px; background:color-mix(in srgb,var(--bg) 72%,transparent); filter:blur(12px); }
16
16
  .yard-eyebrow { margin:0 0 6px; font-size:9px; font-weight:650; letter-spacing:.22em; color:var(--gold); }
17
17
  #yard-title { font-size:clamp(22px,2.2vw,34px); letter-spacing:.015em; text-shadow:0 2px 14px var(--bg); }
18
- :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"]) #yard-title { font-size:min(clamp(22px,2.2vw,34px),max(22px,4.4cqi)); }
18
+ :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"],[data-skin="grove"],[data-skin="guild"],[data-skin="professional"]) #yard-title { font-size:min(clamp(22px,2.2vw,34px),max(22px,4.4cqi)); }
19
19
  #yard-subtitle { margin:6px 0 0; font-size:12px; color:var(--muted); }
20
20
  .yard-heading-side { align-self:flex-start; display:flex; flex-direction:column; align-items:flex-end; gap:10px; }
21
21
  .yard-summary { margin-top:3px; font-size:11px; color:var(--text); padding:8px 11px; border:1px solid var(--yard-line); border-radius:30px; background:color-mix(in srgb,var(--surface) 75%,transparent); white-space:nowrap; }
@@ -46,7 +46,15 @@
46
46
  .yard-crowd-pick:hover, .yard-crowd-open:hover { background:#1d3236; }
47
47
  .yard-overlay { z-index:2; display:flex; flex-direction:column; align-items:center; gap:10px; padding:0 18px 10px; pointer-events:none; }
48
48
  .yard-overlay > * { pointer-events:auto; max-width:100%; }
49
- .yard-camera { align-self:flex-end; display:flex; gap:5px; flex-wrap:wrap; justify-content:flex-end; }
49
+ .yard-overlay > .yard-bar { align-self:stretch; display:flex; flex-wrap:wrap-reverse; align-items:flex-start; justify-content:space-between; gap:8px; pointer-events:none; }
50
+ .yard-bar > * { pointer-events:auto; }
51
+ .yard-world { display:flex; flex-direction:column; align-items:flex-start; gap:5px; min-width:0; max-width:100%; }
52
+ .yard-world-actions { display:flex; flex-wrap:wrap; gap:5px; }
53
+ .yard-world .btn { min-height:30px; padding:5px 10px; font-size:11px; background:var(--surface); white-space:nowrap; text-decoration:none; }
54
+ .yard-world-note { margin:0; padding:4px 10px; max-width:340px; border-radius:8px; font-size:11px; color:var(--text); background:var(--surface); }
55
+ #yard-title.yard-title-custom { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; overflow-wrap:anywhere; }
56
+ .folder-row.folder-file::before { border-radius:2px; border-style:dotted; }
57
+ .yard-camera { margin-left:auto; display:flex; gap:5px; flex-wrap:wrap; justify-content:flex-end; }
50
58
  .yard-camera .btn { min-height:30px; padding:5px 10px; font-size:11px; background:var(--surface); white-space:nowrap; }
51
59
  .yard-camera .btn svg { display:block; width:16px; height:16px; }
52
60
  #yard-help { width:max-content; margin:6px 0 0; padding:4px 12px; border-radius:30px; font-size:10px; color:var(--muted); text-align:center; pointer-events:none; background:color-mix(in srgb,var(--surface) 80%,transparent); }
@@ -153,18 +161,27 @@
153
161
  .yard-photo-footer p { flex:1 1 160px; min-width:0; margin:0; color:var(--muted); font-size:13px; }
154
162
 
155
163
  .yard-play { --play-gold:#e8c47c; --play-ray:#e8c47c; --play-aura:#7c5cff; --play-h:46px; position:relative; display:none; isolation:isolate; border-radius:8px; }
156
- :root[data-skin="orbital"] .yard-play:not(.yard-play-solo), :root:is([data-skin="gnomeland"],[data-skin="goblinville"]) .yard-play-solo { display:flex; }
164
+ :root[data-skin="orbital"] .yard-play:not(.yard-play-solo), :root:is([data-skin="gnomeland"],[data-skin="goblinville"]) .yard-play-solo:not(.yard-play-grove,.yard-play-arena,.yard-play-builders), :root[data-skin="grove"] .yard-play-grove:has(#yard-install:not([hidden])), :root[data-skin="guild"] .yard-play-arena, :root[data-skin="professional"] .yard-play-builders { display:flex; }
157
165
  .yard-play-solo { --play-aura:#ff6a1f; }
166
+ .yard-play-grove { --play-aura:#6fae72; }
167
+ .yard-play-arena { --play-aura:#e8431c; }
168
+ .yard-play-builders { --play-aura:#2f6bed; }
158
169
  :root[data-theme="light"] .yard-play { --play-ray:#b8893d; }
159
170
  .yard-play:has(.yard-play-chip :focus-visible) { outline:2px solid var(--focus); outline-offset:3px; }
160
- .yard-install { --play-h:46px; display:none; pointer-events:auto; padding:0; border:0; background:none; cursor:pointer; margin:calc(var(--play-h) * -42 / 128) calc(var(--play-h) * -46 / 128); }
161
- :root[data-skin="grove"] .yard-install:not([hidden]) { display:block; }
162
- .yard-install img { display:block; height:calc(var(--play-h) * 212 / 128); transition:filter .2s; }
163
- .yard-install:hover:not(:disabled) img { filter:brightness(1.12); }
164
- .yard-install:disabled { cursor:default; opacity:.6; }
165
- .yard-install:focus-visible { outline:2px solid var(--focus); outline-offset:-12px; }
166
- :root[data-theme="light"] .yard-install-dark, :root:not([data-theme="light"]) .yard-install-light { display:none; }
167
171
  .yard-play-defs { position:absolute; width:0; height:0; overflow:hidden; }
172
+ .break-buttons { display:grid; gap:8px; padding-top:10px; border-top:1px solid var(--border); }
173
+ .break-buttons[hidden] { display:none; }
174
+ .break-label { margin:0; font-size:.8rem; color:var(--muted); }
175
+ .break-row { display:flex; flex-wrap:wrap; gap:8px; }
176
+ .break-row .yard-play { --play-h:46px; display:block; }
177
+ .break-row .yard-play-chip { --play-more-w:44px; }
178
+ .break-row .yard-play-link { padding:0 7px; }
179
+ .break-row .yard-play-more { padding:0; }
180
+ .break-row .yard-play-more::before { display:none; }
181
+ .break-row .yard-play-more-orb { display:block; grid-area:1 / 1; }
182
+ .break-row .yard-play-caret { grid-area:1 / 1; place-self:end; width:9px; height:6px; margin:0 7px 6px 0; stroke-width:2.2; }
183
+ .break-row .yard-play-orb { width:38px; height:38px; }
184
+ .break-row .armed .yard-play-chip { outline:2px solid var(--focus); outline-offset:2px; }
168
185
  .yard-play-burst { position:absolute; left:50%; top:50%; width:calc(var(--play-h) * 7); height:calc(var(--play-h) * 2.6); translate:-50% -50%; pointer-events:none; z-index:0; opacity:.55; transition:opacity .3s;
169
186
  background:radial-gradient(closest-side, color-mix(in srgb, var(--play-aura) 55%, transparent), color-mix(in srgb, var(--play-aura) 22%, transparent) 40%, transparent 72%);
170
187
  -webkit-mask:radial-gradient(closest-side, #000 0, #000c 30%, #0004 55%, transparent 74%); mask:radial-gradient(closest-side, #000 0, #000c 30%, #0004 55%, transparent 74%); }
@@ -183,6 +200,23 @@
183
200
  .yard-play-link { display:inline-flex; align-items:center; gap:10px; padding:0 16px 0 9px; color:#fff3cf; text-decoration:none; font-family:var(--font-display); }
184
201
  .yard-play-link > * { position:relative; z-index:1; }
185
202
  .yard-play-solo .yard-play-chip::before { background:radial-gradient(90px 60px at 12% 50%,#ff6a1f6b,transparent 70%),radial-gradient(90px 60px at 92% 50%,#3f9dff66,transparent 70%),linear-gradient(180deg,#171b2c,#0b0e17 55%,#05070e); }
203
+ .yard-play-arena .yard-play-chip::before { background:radial-gradient(90px 60px at 12% 50%,#e8431c6b,transparent 70%),radial-gradient(90px 60px at 92% 50%,#7a55e866,transparent 70%),linear-gradient(180deg,#231b14,#110d0a 55%,#080604); }
204
+ .yard-play-arena .yard-play-orb { width:38px; height:38px; }
205
+ .yard-play-arena .yard-play-sub { color:#c4b89f; }
206
+ .yard-play-builders .yard-play-chip::before { background:radial-gradient(120px 60px at 12% 50%,#2f6bed75,transparent 70%),linear-gradient(180deg,#1b2029,#0d1015 55%,#07090d); }
207
+ .yard-play-builders .yard-play-orb { width:38px; height:38px; }
208
+ .yard-play-builders .yard-play-sub { color:#a9b0c4; }
209
+ .yard-play-link > .yard-play-stamp { position:absolute; top:3px; right:13px; padding:0 4px; border:1px solid #ff4a3d; border-radius:2px; box-shadow:inset 0 0 0 1px #0d1015,inset 0 0 0 1.5px #ff4a3d; color:#ff4a3d; font:900 8.5px/12px var(--font-body,sans-serif); letter-spacing:.12em; text-transform:uppercase; rotate:-8deg; pointer-events:none; }
210
+ .break-row .yard-play-stamp { display:none; }
211
+ .yard-play-grove .yard-play-chip::before { background:radial-gradient(90px 60px at 12% 50%,#3b7550b0,transparent 70%),radial-gradient(90px 60px at 94% 50%,#7b9a3a66,transparent 70%),linear-gradient(180deg,#16261b,#0d1a12 55%,#081109); }
212
+ .yard-play-grove .yard-play-link { margin:0; border:0; border-radius:0; background:none; text-align:left; }
213
+ .yard-play-grove .yard-play-orb { position:relative; width:38px; height:38px; }
214
+ .yard-play-grove .yard-play-orb svg { display:block; width:100%; height:100%; overflow:visible; }
215
+ .yard-play-bird { position:absolute; inset:calc(50% - 10px); border-radius:50%; background:url(/skins/grove/ui/companion-bird.webp) center / 100% no-repeat; }
216
+ .yard-play-grove .yard-play-sub { color:#d6dcb4; }
217
+ .yard-play-grove .yard-play-chip:has(:disabled) { opacity:.6; translate:none; }
218
+ .yard-play-grove:has(:disabled) .yard-play-burst { display:none; }
219
+ .yard-play-grove .yard-play-link:disabled { background:none; cursor:default; }
186
220
  .yard-play-more { display:grid; place-items:center; flex:none; width:var(--play-more-w); margin:0; padding:0 3px 0 0; border:0; border-radius:0; background:none; color:var(--play-gold); }
187
221
  .yard-play-more::before { content:""; position:absolute; left:0; top:9px; bottom:9px; width:1px; background:linear-gradient(180deg,transparent,#f6dc9c,transparent); }
188
222
  .yard-play-link:hover { background:#e8c47c14; }
@@ -213,12 +247,21 @@
213
247
  .yard-play-chev path:nth-child(3) { animation-delay:.4s; }
214
248
  .yard-play-solo .yard-play-chev path:nth-child(1) { stroke:#ffb45e; }
215
249
  .yard-play-solo .yard-play-chev path:nth-child(3) { stroke:#bfe6ff; }
250
+ .yard-play-grove .yard-play-chev path:nth-child(n) { stroke:#d7ee6e; }
251
+ .yard-play-builders .yard-play-chev path:nth-child(n) { stroke:#e8c47c; }
252
+ .yard-play-arena .yard-play-chev path:nth-child(1) { stroke:#ff9d4d; }
253
+ .yard-play-arena .yard-play-chev path:nth-child(2) { stroke:#f6dc9c; }
254
+ .yard-play-arena .yard-play-chev path:nth-child(3) { stroke:#cfc0ff; }
216
255
  @keyframes yard-play-spin { to { transform:rotate(360deg); } }
217
256
  @keyframes yard-play-sweep { 0% { left:-30%; } 55%,100% { left:130%; } }
218
257
  @keyframes yard-play-pulse { 0%,100% { opacity:.45; transform:scale(.94); } 50% { opacity:.9; transform:scale(1.06); } }
219
258
  @keyframes yard-play-chev { 0%,100% { opacity:.35; } 40% { opacity:1; } }
220
- @container (max-width:660px) { :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"]) .yard-heading { padding-inline:18px; } :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"]) .yard-summary { display:none; } .yard-play { --play-h:44px; } .yard-play-chip { --play-more-w:40px; } .yard-play-chev, .yard-play-sub-more { display:none; } }
259
+ @container (max-width:660px) { :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"],[data-skin="grove"],[data-skin="guild"],[data-skin="professional"]) .yard-heading { padding-inline:18px; } :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"],[data-skin="grove"],[data-skin="guild"],[data-skin="professional"]) .yard-summary { display:none; } .yard-play { --play-h:44px; } .yard-play-chip { --play-more-w:40px; } .yard-play-chev, .yard-play-sub-more { display:none; } }
260
+ @container (max-width:660px) { .yard-play-stamp { display:none; } }
261
+ @container (max-width:520px) { .yard-play-builders .yard-play-now { display:none; } }
262
+ @container (max-width:460px) { .yard-play-builders .yard-play-text { display:none; } .yard-play-builders .yard-play-link { padding:0 7px; } }
263
+ @container (max-width:360px) { :root[data-skin="professional"] #yard-title { font-size:20px; } }
221
264
  @container (max-width:480px) { .yard-play-sub, .yard-play-now { display:none; } }
222
265
  @container (max-width:440px) { .yard-play-text { display:none; } .yard-play-link { padding:0 7px; } }
223
266
  @container (max-width:380px) { .yard-play:not(.yard-play-solo) .yard-play-link { display:none; } .yard-play-chip { --play-more-w:44px; } .yard-play-more { padding:0; } .yard-play-more::before { display:none; } .yard-play-more-orb { display:block; grid-area:1 / 1; } .yard-play-caret { grid-area:1 / 1; place-self:end; width:9px; height:6px; margin:0 7px 6px 0; stroke-width:2.2; } }
224
- @container (max-width:340px) { :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"]) :is(.yard-eyebrow, #yard-subtitle) { display:none; } }
267
+ @container (max-width:340px) { :root:is([data-skin="orbital"],[data-skin="gnomeland"],[data-skin="goblinville"],[data-skin="grove"],[data-skin="guild"],[data-skin="professional"]) :is(.yard-eyebrow, #yard-subtitle) { display:none; } }