@oddessentials/agent-guild 1.29.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 (107) 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 +6 -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/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  80. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  81. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  82. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  83. package/node_modules/wasm-feature-detect/LICENSE +202 -0
  84. package/node_modules/wasm-feature-detect/README.md +109 -0
  85. package/node_modules/wasm-feature-detect/README.md.ejs +89 -0
  86. package/node_modules/wasm-feature-detect/dist/cjs/index.cjs +1 -0
  87. package/node_modules/wasm-feature-detect/dist/esm/index.js +1 -0
  88. package/node_modules/wasm-feature-detect/dist/index.d.cts +24 -0
  89. package/node_modules/wasm-feature-detect/dist/index.d.ts +24 -0
  90. package/node_modules/wasm-feature-detect/dist/umd/index.js +1 -0
  91. package/node_modules/wasm-feature-detect/package.json +53 -0
  92. package/package.json +4 -1
  93. package/src/manager/config.mjs +1 -0
  94. package/src/manager/folder-browser.mjs +31 -2
  95. package/src/manager/main.mjs +2 -0
  96. package/src/manager/server.mjs +19 -5
  97. package/src/manager/steam-workshop.mjs +64 -0
  98. package/src/manager/yard-piece-check.mjs +424 -0
  99. package/src/manager/yard-pieces.mjs +188 -0
  100. package/web/app.js +54 -16
  101. package/web/index.html +14 -1
  102. package/web/skins/guild/skin.css +2 -13
  103. package/web/styles.css +14 -0
  104. package/web/yard/art.json +1 -1
  105. package/web/yard/model.mjs +44 -2
  106. package/web/yard/renderer.js +136 -27
  107. package/web/yard/view.js +123 -11
package/web/app.js CHANGED
@@ -202,7 +202,7 @@ function holdToast(held) {
202
202
  const firstClick = (run) => (e) => { if (e.detail < 2) run(); };
203
203
 
204
204
  /** Buttons that act through the session manager. While it cannot be reached they are unavailable rather than left to fail. */
205
- const MANAGER_ACTIONS = '.provider :is(.new, .existing, .install, .update), .session-card :is(.resume, .rename, .stop, .remove), .history-resume, #panel-stop, #yard-install, #yard-world button, .break-install';
205
+ const MANAGER_ACTIONS = '.provider :is(.new, .existing, .install, .update), .session-card :is(.resume, .rename, .stop, .remove), .history-resume, #panel-stop, #yard-install, #yard-customize-open, .break-install';
206
206
 
207
207
  function paintManagerActions(root = document) {
208
208
  for (const button of root.querySelectorAll(MANAGER_ACTIONS)) button.disabled = !state.connected;
@@ -1435,15 +1435,19 @@ const FOLDER_FIELDS = {
1435
1435
  cwd: { title: 'Choose working folder', input: 'cwd' },
1436
1436
  clone: { title: 'Choose clone folder', input: 'github-parent' },
1437
1437
  world: { title: 'Choose a world file', input: null },
1438
+ character: { title: 'Choose a character', input: null },
1439
+ companion: { title: 'Choose a companion', input: null },
1438
1440
  };
1439
1441
  const WORLD_FOLDER_KEY = 'agentGuild.worldFolder';
1442
+ const PIECE_FOLDER_KEY = 'agentGuild.pieceFolder';
1443
+ const PIECE_FIELDS = ['character', 'companion'];
1440
1444
  const folderView = { field: null, listing: null, loading: false, error: null, request: 0, opener: null };
1441
1445
 
1442
- function chooseFolder(field, skin = null) {
1446
+ function chooseFolder(field, skin = null, slot = null) {
1443
1447
  if (!state.connected) return;
1444
1448
  const dialog = $('folder-browser');
1445
- Object.assign(folderView, { field, skin, listing: null, loading: false, error: null });
1446
- $('folder-use').hidden = $('folder-new-open').hidden = field === 'world';
1449
+ Object.assign(folderView, { field, skin, slot, listing: null, loading: false, error: null, toWorkshop: PIECE_FIELDS.includes(field) && !load(PIECE_FOLDER_KEY) });
1450
+ $('folder-use').hidden = $('folder-new-open').hidden = field === 'world' || PIECE_FIELDS.includes(field);
1447
1451
  $('folder-title').textContent = FOLDER_FIELDS[field].title;
1448
1452
  $('folder-filter').value = '';
1449
1453
  $('folder-hidden').checked = load(HIDDEN_FOLDERS_KEY) === '1';
@@ -1451,7 +1455,7 @@ function chooseFolder(field, skin = null) {
1451
1455
  folderView.opener = document.activeElement;
1452
1456
  dialog.showModal();
1453
1457
  }
1454
- browseTo(field === 'world' ? load(WORLD_FOLDER_KEY) ?? '' : $(FOLDER_FIELDS[field].input).value.trim());
1458
+ browseTo(field === 'world' ? load(WORLD_FOLDER_KEY) ?? '' : PIECE_FIELDS.includes(field) ? load(PIECE_FOLDER_KEY) ?? '' : $(FOLDER_FIELDS[field].input).value.trim());
1455
1459
  }
1456
1460
 
1457
1461
  function showNewFolder(open) {
@@ -1505,8 +1509,13 @@ async function browseTo(dir) {
1505
1509
  folderView.error = null;
1506
1510
  renderFolderBrowser();
1507
1511
  try {
1508
- const listing = await api('GET', `/folders?path=${encodeURIComponent(dir)}${folderView.field === 'world' ? '&files=agworld' : ''}`);
1512
+ const listing = await api('GET', `/folders?path=${encodeURIComponent(dir)}${folderView.field === 'world' ? '&files=agworld' : PIECE_FIELDS.includes(folderView.field) ? `&pieces=${folderView.field}` : ''}`);
1509
1513
  if (request !== folderView.request) return;
1514
+ if (folderView.toWorkshop && listing.workshop && listing.path !== listing.workshop) {
1515
+ folderView.toWorkshop = false;
1516
+ return browseTo(listing.workshop);
1517
+ }
1518
+ folderView.toWorkshop = false;
1510
1519
  folderView.listing = listing;
1511
1520
  $('folder-filter').value = '';
1512
1521
  } catch (err) {
@@ -1534,6 +1543,8 @@ function renderFolderBrowser() {
1534
1543
  $('folder-roots').replaceChildren(...(listing?.roots ?? []).map((root) => button(root.name, () => browseTo(root.path), 'btn folder-chip')));
1535
1544
  $('folder-up').disabled = loading || !listing?.parent;
1536
1545
  $('folder-home').disabled = loading;
1546
+ $('folder-workshop').hidden = !listing?.workshop;
1547
+ $('folder-workshop').disabled = loading;
1537
1548
  const note = $('folder-note');
1538
1549
  note.hidden = !listing?.note;
1539
1550
  note.textContent = listing?.note ? `${listing.note} was not found, so this shows the nearest folder that exists.` : '';
@@ -1547,12 +1558,20 @@ function renderFolderBrowser() {
1547
1558
  const row = button(file.name, () => useWorldFile(file.path), 'folder-row folder-file');
1548
1559
  row.title = file.path;
1549
1560
  return row;
1561
+ }), ...(listing?.pieces ?? []).filter((piece) => !query || piece.title.toLowerCase().includes(query) || piece.folder.toLowerCase().includes(query)).map((piece) => {
1562
+ const row = button('', () => { if (!piece.problem) usePiece(piece.path); }, 'folder-row folder-piece');
1563
+ if (piece.problem) row.setAttribute('aria-disabled', 'true');
1564
+ row.append(Object.assign(document.createElement('span'), { className: 'folder-piece-title', textContent: piece.title }),
1565
+ Object.assign(document.createElement('span'), { className: 'folder-piece-sub', textContent: piece.problem ? `${piece.folder} · ${piece.problem}` : `${folderView.field === 'character' ? 'Character' : 'Companion'} · ${piece.folder}` }));
1566
+ row.title = piece.path;
1567
+ return row;
1550
1568
  }));
1551
1569
  const status = $('folder-status');
1552
1570
  status.textContent = loading ? 'Loading folders…'
1553
1571
  : error ? error
1554
1572
  : !listing ? ''
1555
- : listing.files?.length ? ''
1573
+ : listing.files?.length || listing.pieces?.length ? ''
1574
+ : PIECE_FIELDS.includes(folderView.field) ? `No ${folderView.field === 'character' ? 'characters' : 'companions'} here or in the folders inside it. Steam keeps Workshop items in its library under steamapps › workshop › content › 5462970.`
1556
1575
  : !entries.length ? (folderView.field === 'world' ? 'No world files here.' : listing.entries.length ? 'No folders match.' : 'No folders here.')
1557
1576
  : listing.truncated ? `Showing the first ${listing.entries.length.toLocaleString()} folders.` : '';
1558
1577
  status.classList.toggle('error', Boolean(error));
@@ -1562,17 +1581,19 @@ function renderFolderBrowser() {
1562
1581
  $('folder-new-create').disabled = loading || !listing || !$('folder-new-name').value.trim();
1563
1582
  }
1564
1583
 
1565
- async function yardWorldChange(request) {
1566
- const buttons = document.querySelectorAll('#yard-world button');
1567
- for (const item of buttons) item.disabled = true;
1584
+ async function yardChange(request, row = true) {
1585
+ state.yardBusy = row;
1586
+ notifyViews();
1568
1587
  try {
1569
- // The new state arrives as yard-worlds.updated.
1570
1588
  await request();
1589
+ return true;
1571
1590
  } catch (err) {
1572
- if (err instanceof AuthError) return showAuth(err.message);
1573
- toast(err.message, 8000);
1591
+ if (err instanceof AuthError) showAuth(err.message);
1592
+ else toast(err.message, 8000);
1593
+ return false;
1574
1594
  } finally {
1575
- for (const item of buttons) item.disabled = !state.connected;
1595
+ state.yardBusy = false;
1596
+ notifyViews();
1576
1597
  }
1577
1598
  }
1578
1599
 
@@ -1580,7 +1601,13 @@ function useWorldFile(file) {
1580
1601
  const { skin, listing } = folderView;
1581
1602
  $('folder-browser').close();
1582
1603
  save(WORLD_FOLDER_KEY, listing.path);
1583
- return yardWorldChange(() => api('PUT', `/yard-worlds/${skin}`, { path: file }));
1604
+ return yardChange(() => api('PUT', `/yard-worlds/${skin}`, { path: file }), `${skin}/world`);
1605
+ }
1606
+
1607
+ async function usePiece(folder) {
1608
+ const { skin, slot, listing } = folderView;
1609
+ $('folder-browser').close();
1610
+ if (await yardChange(() => api('PUT', `/yard-pieces/${skin}/${slot}`, { path: folder }), `${skin}/${slot}`)) save(PIECE_FOLDER_KEY, listing.path);
1584
1611
  }
1585
1612
 
1586
1613
  function folderBrowserClosed() {
@@ -7909,6 +7936,7 @@ function closeDialogs() {
7909
7936
  closeFlags();
7910
7937
  closeMemory();
7911
7938
  if ($('folder-browser').open) $('folder-browser').close();
7939
+ if ($('yard-customize').open) $('yard-customize').close();
7912
7940
  yardUi?.closePhoto();
7913
7941
  // An open question is answered as cancelled.
7914
7942
  if ($('ask').open) $('ask').close();
@@ -8058,6 +8086,7 @@ function connectEvents() {
8058
8086
  $('yard-install').hidden = !['win32', 'darwin'].includes(state.platform);
8059
8087
  $('yard-world-get').hidden = state.platform !== 'win32';
8060
8088
  state.yardWorlds = msg.yardWorlds ?? null;
8089
+ state.yardPieces = msg.yardPieces ?? null;
8061
8090
  state.remoteAccessUI?.setAvailable(msg.remoteAccess);
8062
8091
  state.remoteAccessUI?.signInsUpdated();
8063
8092
  state.glassesLinkUI?.setAvailable(msg.remoteAccess);
@@ -8102,6 +8131,9 @@ function connectEvents() {
8102
8131
  } else if (msg.type === 'yard-worlds.updated') {
8103
8132
  state.yardWorlds = msg.yardWorlds;
8104
8133
  notifyViews();
8134
+ } else if (msg.type === 'yard-pieces.updated') {
8135
+ state.yardPieces = msg.yardPieces;
8136
+ notifyViews();
8105
8137
  } else if (msg.type === 'yard.fireworks') {
8106
8138
  yardUi?.celebrate();
8107
8139
  } else if (msg.type === 'news.updated') {
@@ -8845,6 +8877,7 @@ $('folder-browser').addEventListener('keydown', folderBrowserKeys);
8845
8877
  $('folder-list').addEventListener('keydown', moveInFolders);
8846
8878
  $('folder-up').addEventListener('click', () => browseTo(folderView.listing.parent));
8847
8879
  $('folder-home').addEventListener('click', () => browseTo('~'));
8880
+ $('folder-workshop').addEventListener('click', () => browseTo(folderView.listing.workshop));
8848
8881
  $('folder-filter').addEventListener('input', renderFolderBrowser);
8849
8882
  $('folder-hidden').addEventListener('change', (e) => {
8850
8883
  save(HIDDEN_FOLDERS_KEY, e.target.checked ? '1' : null);
@@ -8937,7 +8970,12 @@ yardUi = initYard({
8937
8970
  yardWorld: (skin) => state.yardWorlds?.[skin] ?? null,
8938
8971
  canReplaceYard: () => Boolean(state.yardWorlds),
8939
8972
  replaceYard: (skin) => chooseFolder('world', skin),
8940
- restoreYard: (skin) => yardWorldChange(() => api('DELETE', `/yard-worlds/${skin}`)),
8973
+ restoreYard: (skin) => yardChange(() => api('DELETE', `/yard-worlds/${skin}`)),
8974
+ yardPieces: (skin) => state.yardPieces?.[skin] ?? {},
8975
+ yardBusy: () => Boolean(state.yardBusy),
8976
+ yardChecking: () => (typeof state.yardBusy === 'string' ? state.yardBusy : null),
8977
+ replacePiece: (skin, slot) => chooseFolder(slot === 'companion' ? 'companion' : 'character', skin, slot),
8978
+ restorePiece: (skin, slot) => yardChange(() => api('DELETE', `/yard-pieces/${skin}/${slot}`)),
8941
8979
  // The new state arrives as yard-art.updated, ordered with the progress events.
8942
8980
  yardArtAction: (action) => api('POST', `/yard-art/${action}`),
8943
8981
  mountInspector(host, selection) {
package/web/index.html CHANGED
@@ -151,7 +151,7 @@
151
151
  <div class="yard-overlay">
152
152
  <div id="yard-loading" class="yard-loading" role="status" data-mode="scene"><span id="yard-loading-text">Preparing your yard…</span><span id="yard-loading-size" class="yard-loading-size" aria-hidden="true"></span><span class="yard-loading-bar" aria-hidden="true"></span></div>
153
153
  <div id="yard-swap-failure" class="yard-swap-failure" hidden role="status"><span id="yard-swap-failure-text"></span><button id="yard-swap-retry" class="btn" type="button">Retry</button></div>
154
- <div class="yard-bar"><div id="yard-world" class="yard-world" role="group" aria-label="This Yard's world"><p id="yard-world-note" class="yard-world-note" role="status" hidden></p><div class="yard-world-actions"><button id="yard-world-restore" type="button" class="btn" hidden>Restore original</button><button id="yard-world-replace" type="button" class="btn">Replace this Yard</button><a id="yard-world-get" class="btn" href="https://github.com/oddessentials/agent-guild-builders/releases/latest" target="_blank" rel="noopener noreferrer" hidden>Build a world</a></div></div><div class="yard-camera" role="group" aria-label="Yard controls"><button id="yard-zoom-in" type="button" class="btn" aria-label="Zoom in">+</button><button id="yard-zoom-out" type="button" class="btn" aria-label="Zoom out">−</button><button id="yard-fit" type="button" class="btn">Overview</button><button id="yard-focus" type="button" class="btn" disabled>Focus selected</button><button id="yard-photo" type="button" class="btn" aria-haspopup="dialog" aria-label="Save a picture of the Yard" title="Save a picture of the Yard" disabled hidden><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h2.3l1.4-2h5.6l1.4 2h2.3A1.5 1.5 0 0 1 20 8.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"/><circle cx="12" cy="12.75" r="3.25"/></svg></button><button id="yard-fireworks" type="button" class="btn" aria-label="Fireworks in this Yard only" title="Fireworks in this Yard only" disabled><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M12 3v3m0 9v6M3 10h3m12 0h3M5.6 3.6l2.2 2.2m8.4 8.4 2.2 2.2M18.4 3.6l-2.2 2.2m-8.4 8.4-2.2 2.2"/><circle cx="12" cy="10" r="1"/></svg></button></div></div>
154
+ <div class="yard-bar"><div id="yard-world" class="yard-world" role="group" aria-label="This Yard's world"><p id="yard-world-note" class="yard-world-note" role="status" hidden></p><div class="yard-world-actions"><button id="yard-customize-open" type="button" class="btn" aria-haspopup="dialog">Customize this Yard</button><a id="yard-world-get" class="btn" href="https://github.com/oddessentials/agent-guild-builders/releases/latest" target="_blank" rel="noopener noreferrer" hidden>Build a Yard</a></div></div><div class="yard-camera" role="group" aria-label="Yard controls"><button id="yard-zoom-in" type="button" class="btn" aria-label="Zoom in">+</button><button id="yard-zoom-out" type="button" class="btn" aria-label="Zoom out">−</button><button id="yard-fit" type="button" class="btn">Overview</button><button id="yard-focus" type="button" class="btn" disabled>Focus selected</button><button id="yard-photo" type="button" class="btn" aria-haspopup="dialog" aria-label="Save a picture of the Yard" title="Save a picture of the Yard" disabled hidden><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h2.3l1.4-2h5.6l1.4 2h2.3A1.5 1.5 0 0 1 20 8.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"/><circle cx="12" cy="12.75" r="3.25"/></svg></button><button id="yard-fireworks" type="button" class="btn" aria-label="Fireworks in this Yard only" title="Fireworks in this Yard only" disabled><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M12 3v3m0 9v6M3 10h3m12 0h3M5.6 3.6l2.2 2.2m8.4 8.4 2.2 2.2M18.4 3.6l-2.2 2.2m-8.4 8.4-2.2 2.2"/><circle cx="12" cy="10" r="1"/></svg></button></div></div>
155
155
  <p id="yard-help"><span class="yard-help-pointer">Drag to explore · Scroll to zoom · Select to inspect · Double-click a session to open</span><span class="yard-help-touch">Drag to explore · Pinch to zoom · Tap to inspect · Open from the Inspector</span></p>
156
156
  </div>
157
157
  </div>
@@ -742,6 +742,18 @@
742
742
  </div>
743
743
  </dialog>
744
744
 
745
+ <dialog id="yard-customize" class="models yard-customize" aria-labelledby="yard-customize-title">
746
+ <div class="models-bar">
747
+ <div class="models-heading">
748
+ <h2 id="yard-customize-title">Customize this Yard</h2>
749
+ <p id="yard-customize-sub" class="sub"></p>
750
+ </div>
751
+ <button id="yard-customize-close" class="btn" type="button">Close</button>
752
+ </div>
753
+ <div class="models-body">
754
+ <ul id="yard-customize-list" class="flags-list" aria-label="This Yard's world, characters and companion"></ul>
755
+ </div>
756
+ </dialog>
745
757
  <dialog id="folder-browser" class="models folder-browser" aria-labelledby="folder-title">
746
758
  <div class="models-bar">
747
759
  <div class="models-heading">
@@ -754,6 +766,7 @@
754
766
  <div class="history-tools folder-places">
755
767
  <button id="folder-up" class="btn" type="button">Up</button>
756
768
  <button id="folder-home" class="btn folder-chip" type="button">Home</button>
769
+ <button id="folder-workshop" class="btn folder-chip" type="button" hidden>Steam Workshop</button>
757
770
  <div id="folder-roots" class="folder-roots"></div>
758
771
  <button id="folder-new-open" class="btn folder-new-open" type="button" aria-expanded="false" aria-controls="folder-new">New folder</button>
759
772
  </div>
@@ -235,13 +235,7 @@
235
235
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
236
236
  border: 54px solid transparent; border-image: url("ui/frame-ornate.png") 232;
237
237
  }
238
- .models-bar { padding: 30px 36px 12px; }
239
- .models-body { margin: 0 24px; padding: 0 12px 14px; }
240
- .models-source { padding: 10px 36px 30px; }
241
- .history-tools { padding: 10px 36px 0; }
242
- .history-form { padding: 10px 36px 30px; }
243
- .memory-body { margin: 0 24px 28px; }
244
- .github-accounts { padding: 10px 36px; }
238
+ .models { padding: 32px; }
245
239
  .model-group h4, .news-day, .changelog-type { font-family: var(--font-display); letter-spacing: 0.08em; color: var(--gold-ink); }
246
240
  .news-bar { padding: 16px 18px 12px; }
247
241
  .news-filters, .news-status { padding-left: 18px; padding-right: 18px; }
@@ -259,12 +253,7 @@
259
253
 
260
254
  @media (max-width: 640px) {
261
255
  .models::after, .auth::after { border-width: 40px; }
262
- .models-bar { padding: 24px 26px 10px; }
263
- .models-body { margin: 0 16px; padding: 0 10px 12px; }
264
- .models-source { padding: 10px 26px 24px; }
265
- .history-tools { padding: 10px 26px 0; }
266
- .history-form { padding: 10px 26px 24px; }
267
- .github-accounts { padding: 10px 26px; }
256
+ .models { padding: 26px; }
268
257
  .auth { padding: 32px 30px; }
269
258
  }
270
259
 
package/web/styles.css CHANGED
@@ -1408,6 +1408,20 @@ body:has(#terminal-panel:not([hidden]) .terminal-controls:not([hidden])) .toast
1408
1408
  .folder-row::before { content: ""; flex: none; width: 18px; height: 14px; border: 1.6px solid var(--muted); border-radius: 2px 4px 3px 3px; }
1409
1409
  .folder-row:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
1410
1410
  .folder-row.hidden-folder { color: var(--muted); }
1411
+ .folder-row.folder-piece { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 10px; }
1412
+ .folder-row.folder-piece::before { grid-row: span 2; width: 14px; height: 14px; border-radius: 50%; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 25%, transparent); }
1413
+ .folder-row.folder-piece[aria-disabled="true"] { cursor: default; background: none; color: var(--muted); }
1414
+ .folder-piece-title { font-weight: 600; }
1415
+ .folder-piece-sub { color: var(--muted); font-size: 0.8rem; }
1416
+ .yard-customize { width: min(560px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); }
1417
+ .yard-customize-heading { padding: 14px 0 4px; color: var(--muted); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
1418
+ .yard-customize-copy { display: flex; flex-direction: column; gap: 2px; min-width: 0; overflow-wrap: anywhere; }
1419
+ .yard-customize-status, .yard-customize-note { color: var(--muted); font-size: 0.82rem; }
1420
+ .yard-customize-row[data-state="custom"] .yard-customize-status { color: var(--text); }
1421
+ .yard-customize-row[data-state="failed"] .yard-customize-status { color: var(--danger); }
1422
+ .yard-customize-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
1423
+ .yard-customize-actions .btn { min-height: 36px; }
1424
+ @media (max-width: 480px) { .flag-row.yard-customize-row { grid-template-columns: minmax(0, 1fr); } .yard-customize-actions { justify-content: flex-start; } }
1411
1425
  #folder-status.error { color: var(--danger); }
1412
1426
  .accounts-dialog { width: min(560px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); }
1413
1427
  .accounts-list { margin: 0; padding: 0; list-style: none; }