@oddessentials/agent-guild 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/bin/agent-guild-report.mjs +0 -5
- package/docs/api.md +850 -0
- package/examples/claude-code-settings.json +4 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/package.json +3 -2
- package/src/manager/server.mjs +5 -3
- package/src/manager/session-hooks.mjs +3 -4
- package/src/manager/session-manager.mjs +0 -8
- package/src/manager/session.mjs +4 -106
- package/src/report/hooks.mjs +0 -24
- package/web/api-doc.js +30 -0
- package/web/api-explorer.js +164 -0
- package/web/api.html +40 -0
- package/web/app.js +4 -1
- package/web/host.js +1 -0
- package/web/index.html +23 -4
- package/web/yard/art.json +1 -1
- package/web/yard/model.mjs +13 -0
- package/web/yard/photo.js +228 -0
- package/web/yard/renderer.js +46 -1
- package/web/yard/view.js +16 -2
- package/web/yard/yard.css +19 -10
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { WORLDS, photoName } from './model.mjs';
|
|
2
|
+
|
|
3
|
+
const CREST = new URL('../brand/crest.png', import.meta.url).href;
|
|
4
|
+
const SITE = 'agentguild.oddessentials.ai';
|
|
5
|
+
const INK = '#f8ecd4', FAINT = '#c9c2b4', PANEL = 'rgba(13,20,22,.85)', PANEL_END = 'rgba(13,20,22,.6)';
|
|
6
|
+
|
|
7
|
+
async function crestImage() {
|
|
8
|
+
const image = new Image();
|
|
9
|
+
image.src = CREST;
|
|
10
|
+
await image.decode();
|
|
11
|
+
return image;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canCopy() {
|
|
15
|
+
return window.isSecureContext && typeof ClipboardItem === 'function' && typeof navigator.clipboard?.write === 'function'
|
|
16
|
+
&& (ClipboardItem.supports?.('image/png') ?? true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function canShare() {
|
|
20
|
+
try { return Boolean(navigator.canShare?.({ files: [new File([], 'yard.png', { type: 'image/png' })] })); } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fit(context, text, width) {
|
|
24
|
+
if (context.measureText(text).width <= width + .5) return text;
|
|
25
|
+
let cut = text;
|
|
26
|
+
while (cut && context.measureText(cut + '…').width > width) cut = cut.slice(0, -1);
|
|
27
|
+
return cut + '…';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function write(context, line, x, color) {
|
|
31
|
+
context.font = line.font;
|
|
32
|
+
context.fillStyle = color;
|
|
33
|
+
context.fillText(fit(context, line.text, line.box.w), x, line.box.y + line.box.h / 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shadow(context, color, blur, y) {
|
|
37
|
+
context.shadowColor = color; context.shadowBlur = blur; context.shadowOffsetY = y;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Tags as yard.css draws them unselected, in stage pixels.
|
|
41
|
+
function drawTag(context, tag) {
|
|
42
|
+
context.save();
|
|
43
|
+
context.textBaseline = 'middle';
|
|
44
|
+
if (tag.kind === 'chip') {
|
|
45
|
+
const { x, y, w, h } = tag.box;
|
|
46
|
+
shadow(context, 'rgba(0,0,0,.33)', 10, 2);
|
|
47
|
+
context.beginPath(); context.roundRect(x, y, w, h, 10);
|
|
48
|
+
context.fillStyle = PANEL; context.fill();
|
|
49
|
+
shadow(context, 'transparent', 0, 0);
|
|
50
|
+
context.strokeStyle = 'rgba(224,191,121,.27)'; context.lineWidth = 1; context.stroke();
|
|
51
|
+
context.textAlign = 'center';
|
|
52
|
+
shadow(context, '#000', 3, 1);
|
|
53
|
+
write(context, tag.lines[0], x + w / 2, INK);
|
|
54
|
+
context.restore();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const bottom = tag.box.y + tag.box.h, name = tag.lines[0].box;
|
|
58
|
+
const box = tag.kind === 'session' ? { x: name.x - 6, y: bottom - name.h - 8, w: name.w + 12, h: name.h + 8 } : tag.box;
|
|
59
|
+
const lines = tag.kind === 'session' ? [{ ...tag.lines[0], box: { ...name, y: box.y + 3 } }] : tag.lines;
|
|
60
|
+
const color = tag.color || '#ac9362';
|
|
61
|
+
context.globalAlpha = tag.status === 'exited' ? .5 : tag.kind === 'session' ? .82 : 1;
|
|
62
|
+
shadow(context, 'rgba(0,0,0,.33)', 12, 2);
|
|
63
|
+
context.beginPath(); context.roundRect(box.x, box.y, box.w, box.h, 4);
|
|
64
|
+
const fill = context.createLinearGradient(0, box.y, 0, bottom);
|
|
65
|
+
fill.addColorStop(0, PANEL); fill.addColorStop(1, PANEL_END);
|
|
66
|
+
context.fillStyle = fill; context.fill();
|
|
67
|
+
shadow(context, 'transparent', 0, 0);
|
|
68
|
+
context.save();
|
|
69
|
+
context.clip();
|
|
70
|
+
if (tag.status === 'locked') {
|
|
71
|
+
context.strokeStyle = color; context.lineWidth = 2; context.setLineDash([4, 3]);
|
|
72
|
+
context.beginPath(); context.moveTo(box.x, bottom - 1); context.lineTo(box.x + box.w, bottom - 1); context.stroke();
|
|
73
|
+
} else {
|
|
74
|
+
context.fillStyle = color; context.fillRect(box.x, bottom - 2, box.w, 2);
|
|
75
|
+
}
|
|
76
|
+
context.restore();
|
|
77
|
+
shadow(context, '#000', 3, 1);
|
|
78
|
+
lines.forEach((line, i) => write(context, line, line.box.x, i ? FAINT : INK));
|
|
79
|
+
context.restore();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function drawSignature(context, skin, crest, width, height) {
|
|
83
|
+
const title = WORLDS[skin].title;
|
|
84
|
+
const display = getComputedStyle(document.documentElement).getPropertyValue('--font-display').trim() || 'serif';
|
|
85
|
+
const titleFont = `600 15px ${display}`, siteFont = `500 10px ${getComputedStyle(document.body).fontFamily}`;
|
|
86
|
+
context.save();
|
|
87
|
+
context.font = titleFont; const titleWidth = context.measureText(title).width;
|
|
88
|
+
context.font = siteFont; const siteWidth = context.measureText(SITE).width;
|
|
89
|
+
const mark = crest && skin !== 'professional' ? 34 : 0, markWidth = mark && mark * crest.naturalWidth / crest.naturalHeight;
|
|
90
|
+
const pad = 10, w = pad * 2 + (mark ? markWidth + 10 : 0) + Math.max(titleWidth, siteWidth), h = pad * 2 + 34;
|
|
91
|
+
const x = width - w - 14, y = height - h - 14, left = x + pad + (mark ? markWidth + 10 : 0);
|
|
92
|
+
shadow(context, 'rgba(0,0,0,.33)', 12, 2);
|
|
93
|
+
context.beginPath(); context.roundRect(x, y, w, h, 8);
|
|
94
|
+
context.fillStyle = 'rgba(13,20,22,.72)'; context.fill();
|
|
95
|
+
shadow(context, 'transparent', 0, 0);
|
|
96
|
+
if (mark) context.drawImage(crest, x + pad, y + (h - mark) / 2, markWidth, mark);
|
|
97
|
+
context.textBaseline = 'top';
|
|
98
|
+
context.font = titleFont; context.fillStyle = INK; context.fillText(title, left, y + pad + 1);
|
|
99
|
+
context.font = siteFont; context.fillStyle = FAINT; context.fillText(SITE, left, y + pad + 22);
|
|
100
|
+
context.restore();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class YardPhoto {
|
|
104
|
+
constructor(dialog) {
|
|
105
|
+
this.dialog = dialog;
|
|
106
|
+
this.preview = dialog.querySelector('canvas');
|
|
107
|
+
this.labels = dialog.querySelector('[data-labels]');
|
|
108
|
+
this.status = dialog.querySelector('[role="status"]');
|
|
109
|
+
this.copyButton = dialog.querySelector('[data-copy]');
|
|
110
|
+
this.saveButton = dialog.querySelector('[data-save]');
|
|
111
|
+
this.shareButton = dialog.querySelector('[data-share]');
|
|
112
|
+
this.shot = null; this.png = null; this.blob = null; this.url = ''; this.version = 0; this.opening = false;
|
|
113
|
+
this.crest = crestImage().catch(() => null);
|
|
114
|
+
this.labels.addEventListener('change', () => { this.draw(); this.status.textContent = ''; });
|
|
115
|
+
this.copyButton.addEventListener('click', () => this.copy());
|
|
116
|
+
this.saveButton.addEventListener('click', () => this.save());
|
|
117
|
+
this.shareButton.addEventListener('click', () => this.share());
|
|
118
|
+
dialog.querySelector('[data-done]').addEventListener('click', () => dialog.close());
|
|
119
|
+
dialog.addEventListener('close', () => {
|
|
120
|
+
if (dialog.open) return;
|
|
121
|
+
this.clear(); this.opener?.focus();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async open(renderer, opener) {
|
|
126
|
+
if (this.dialog.open || this.opening) return;
|
|
127
|
+
const version = this.version;
|
|
128
|
+
this.opening = true;
|
|
129
|
+
try {
|
|
130
|
+
const display = getComputedStyle(document.documentElement).getPropertyValue('--font-display');
|
|
131
|
+
const [crest] = await Promise.all([this.crest, document.fonts.load(`600 15px ${display}`).catch(() => {})]);
|
|
132
|
+
const stage = document.getElementById('yard-stage');
|
|
133
|
+
if (version !== this.version || !(stage.checkVisibility?.() ?? stage.isConnected) || !stage.clientWidth || !stage.clientHeight) return;
|
|
134
|
+
let shot = null;
|
|
135
|
+
try { shot = renderer.capture(); } catch {}
|
|
136
|
+
Object.assign(this, { shot, opener, crestImage: crest, taken: new Date(), background: getComputedStyle(document.getElementById('yard')).backgroundColor });
|
|
137
|
+
this.labels.checked = false;
|
|
138
|
+
try { this.draw(); } catch { shot.canvas.width = 0; this.shot = shot = null; this.draw(); }
|
|
139
|
+
this.labels.parentElement.hidden = !shot;
|
|
140
|
+
this.copyButton.hidden = !shot || !canCopy();
|
|
141
|
+
this.shareButton.hidden = !shot || !canShare();
|
|
142
|
+
this.saveButton.hidden = !shot;
|
|
143
|
+
this.status.textContent = shot ? '' : 'Couldn’t take the picture.';
|
|
144
|
+
this.dialog.showModal();
|
|
145
|
+
[this.copyButton, this.saveButton, this.dialog.querySelector('[data-done]')].find(button => !button.hidden).focus();
|
|
146
|
+
} finally { this.opening = false; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
draw() {
|
|
150
|
+
this.version++;
|
|
151
|
+
this.png = null; this.blob = null;
|
|
152
|
+
const { shot, preview } = this;
|
|
153
|
+
if (!shot) { preview.width = preview.height = 0; return; }
|
|
154
|
+
preview.width = shot.canvas.width; preview.height = shot.canvas.height;
|
|
155
|
+
const context = preview.getContext('2d');
|
|
156
|
+
context.fillStyle = this.background;
|
|
157
|
+
context.fillRect(0, 0, preview.width, preview.height);
|
|
158
|
+
context.drawImage(shot.canvas, 0, 0);
|
|
159
|
+
context.save();
|
|
160
|
+
context.scale(shot.scale, shot.scale);
|
|
161
|
+
if (this.labels.checked) for (const tag of shot.tags) drawTag(context, tag);
|
|
162
|
+
drawSignature(context, shot.skin, this.crestImage, preview.width / shot.scale, preview.height / shot.scale);
|
|
163
|
+
context.restore();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
encode() {
|
|
167
|
+
if (this.png) return this.png;
|
|
168
|
+
const png = this.png = new Promise((resolve, reject) => this.preview.toBlob(blob => blob ? resolve(blob) : reject(new Error('The picture could not be encoded.')), 'image/png'));
|
|
169
|
+
png.then(blob => { if (this.png === png) this.blob = blob; }, () => { if (this.png === png) this.png = null; });
|
|
170
|
+
return png;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
name() { return photoName(WORLDS[this.shot.skin].title, this.taken); }
|
|
174
|
+
|
|
175
|
+
isCurrent(version) { return version === this.version && this.dialog.open; }
|
|
176
|
+
|
|
177
|
+
report(version, text) {
|
|
178
|
+
if (this.isCurrent(version)) this.status.textContent = text;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async copy() {
|
|
182
|
+
const version = this.version;
|
|
183
|
+
try {
|
|
184
|
+
await navigator.clipboard.write([new ClipboardItem({ 'image/png': this.encode() })]);
|
|
185
|
+
this.report(version, 'Copied');
|
|
186
|
+
} catch { this.report(version, 'Couldn’t copy the picture.'); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async save() {
|
|
190
|
+
const version = this.version, name = this.name();
|
|
191
|
+
try {
|
|
192
|
+
const blob = await this.encode();
|
|
193
|
+
if (!this.isCurrent(version)) return;
|
|
194
|
+
URL.revokeObjectURL(this.url);
|
|
195
|
+
this.url = URL.createObjectURL(blob);
|
|
196
|
+
Object.assign(document.createElement('a'), { href: this.url, download: name }).click();
|
|
197
|
+
this.report(version, 'Saved ' + name);
|
|
198
|
+
} catch { this.report(version, 'Couldn’t save the picture.'); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async share() {
|
|
202
|
+
const version = this.version, name = this.name(), ready = this.blob;
|
|
203
|
+
try {
|
|
204
|
+
const blob = ready || await this.encode();
|
|
205
|
+
if (!this.isCurrent(version)) return;
|
|
206
|
+
const file = new File([blob], name, { type: 'image/png' });
|
|
207
|
+
await navigator.share({ files: [file] });
|
|
208
|
+
this.report(version, 'Shared');
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (err?.name === 'AbortError') return;
|
|
211
|
+
this.report(version, err?.name === 'NotAllowedError' && !ready ? 'The picture is ready. Press Share again.' : 'Couldn’t share the picture.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
close() {
|
|
216
|
+
this.clear();
|
|
217
|
+
if (this.dialog.open) this.dialog.close();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
clear() {
|
|
221
|
+
this.version++;
|
|
222
|
+
URL.revokeObjectURL(this.url);
|
|
223
|
+
if (this.shot) this.shot.canvas.width = 0;
|
|
224
|
+
this.shot = null; this.png = null; this.blob = null; this.url = '';
|
|
225
|
+
this.preview.width = this.preview.height = 0;
|
|
226
|
+
this.status.textContent = '';
|
|
227
|
+
}
|
|
228
|
+
}
|
package/web/yard/renderer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as T from './vendor/engine.js';
|
|
2
2
|
import { WORLDS, PROVIDER_ORDER, characterIndex, CAMERA, SUN, minZoom, overviewFor, clampPan, viewBasis, providerPositions, sessionPose, familiarPose, hash,
|
|
3
|
-
stage, nextStagingChange, doorFor, arrives, route, clipFor, helperOffset, stationProp, hallActivity, lookTarget, lookLimits, STAGING } from './model.mjs';
|
|
3
|
+
stage, nextStagingChange, doorFor, arrives, route, clipFor, helperOffset, stationProp, hallActivity, lookTarget, lookLimits, STAGING, photoSize } from './model.mjs';
|
|
4
4
|
|
|
5
5
|
const ASSETS = new URL('./assets/', import.meta.url);
|
|
6
6
|
const vector = new T.Vector3();
|
|
@@ -940,6 +940,51 @@ export class YardRenderer {
|
|
|
940
940
|
}
|
|
941
941
|
this.dirty=true;this.drawOnce();
|
|
942
942
|
}
|
|
943
|
+
// The frame on screen at twice its size, without the selection ring, and
|
|
944
|
+
// the tags shown over it. The live view is left exactly as it was.
|
|
945
|
+
capture() {
|
|
946
|
+
if(this.disposed||!this.world||this.renderer.getContext().isContextLost())return null;
|
|
947
|
+
const w=this.host.clientWidth,h=this.host.clientHeight,buffer=this.renderer.getDrawingBufferSize(new T.Vector2());
|
|
948
|
+
const size=photoSize(w,h,buffer.x,buffer.y);
|
|
949
|
+
const viewport=this.renderer.getViewport({copy:v=>v.clone()}),ratio=this.renderer.getPixelRatio();
|
|
950
|
+
const particleScale=this.effects?.points.material.uniforms.scale,scale=particleScale?.value;
|
|
951
|
+
const radius=(this.effects?Math.max(...this.effects.preset.size):0)*size.height*this.camera.projectionMatrix.elements[5]/4;
|
|
952
|
+
const pad=Math.ceil(radius)+1,tw=buffer.x+2*pad,th=buffer.y+2*pad;
|
|
953
|
+
const canvas=Object.assign(document.createElement('canvas'),{width:size.width,height:size.height});
|
|
954
|
+
let tags;
|
|
955
|
+
try {
|
|
956
|
+
this.positionLabels();
|
|
957
|
+
tags=this.tags();
|
|
958
|
+
for(const unit of this.units.values()){unit.ring.material.color.set(unit.session?.provider.color||0xbaaa80);unit.ring.scale.setScalar(1);}
|
|
959
|
+
const context=canvas.getContext('2d');
|
|
960
|
+
// Keep particle centers within a padded frustum. Extending the viewport
|
|
961
|
+
// beyond the canvas crops the margins without enlarging the live buffer.
|
|
962
|
+
this.renderer.setViewport(-pad/ratio,-pad/ratio,tw/ratio,th/ratio);
|
|
963
|
+
if(particleScale)particleScale.value=th/2;
|
|
964
|
+
for(const {x,y}of size.tiles) {
|
|
965
|
+
this.camera.setViewOffset(size.width,size.height,x-pad,y-pad,tw,th);
|
|
966
|
+
this.renderer.render(this.scene,this.camera);
|
|
967
|
+
context.drawImage(this.canvas,x,y);
|
|
968
|
+
}
|
|
969
|
+
} finally {
|
|
970
|
+
this.renderer.setViewport(viewport);
|
|
971
|
+
if(particleScale)particleScale.value=scale;
|
|
972
|
+
this.camera.clearViewOffset();
|
|
973
|
+
this.select(this.selected);
|
|
974
|
+
this.renderer.render(this.scene,this.camera);
|
|
975
|
+
}
|
|
976
|
+
return {canvas,scale:size.scale,skin:this.skin,theme:this.theme,tags};
|
|
977
|
+
}
|
|
978
|
+
tags() {
|
|
979
|
+
const origin=this.host.getBoundingClientRect();
|
|
980
|
+
const box=el=>{const r=el.getBoundingClientRect();return {x:r.left-origin.left,y:r.top-origin.top,w:r.width,h:r.height};};
|
|
981
|
+
const text=el=>{const style=getComputedStyle(el);return {text:el.textContent,font:style.fontWeight+' '+style.fontSize+' '+style.fontFamily,box:box(el)};};
|
|
982
|
+
return [...this.labels.children].filter(el=>el.style.visibility!=='hidden'&&(el.classList.contains('yard-label')||el.classList.contains('yard-crowd'))).map(el=>{
|
|
983
|
+
if(el.classList.contains('yard-crowd'))return {kind:'chip',box:box(el),lines:[text(el)]};
|
|
984
|
+
const kind=el.dataset.kind,lines=kind==='session'?[text(el.children[0])]:[...el.children].map(text);
|
|
985
|
+
return {kind,status:el.dataset.status,color:el.style.getPropertyValue('--unit-color'),box:box(el),lines};
|
|
986
|
+
});
|
|
987
|
+
}
|
|
943
988
|
pick(event) {
|
|
944
989
|
const rect=this.canvas.getBoundingClientRect();
|
|
945
990
|
this.pointer.set((event.clientX-rect.left)/rect.width*2-1,-(event.clientY-rect.top)/rect.height*2+1);
|
package/web/yard/view.js
CHANGED
|
@@ -21,7 +21,8 @@ const megabytes = bytes => (bytes / 1048576).toFixed(1) + ' MB';
|
|
|
21
21
|
/** A layout with injected canonical controls. No API, sockets, or business rules. */
|
|
22
22
|
export function initYard(controller) {
|
|
23
23
|
let selected = null, renderer = null, loading = null, failed = false, disposed = false;
|
|
24
|
-
let sceneAttempt = 0, worldAttempt = 0, pendingRenderer = null, swapReveal = 0;
|
|
24
|
+
let sceneAttempt = 0, worldAttempt = 0, pendingRenderer = null, swapReveal = 0, photo = null;
|
|
25
|
+
const closePhoto = () => photo?.close();
|
|
25
26
|
const rows = new Map();
|
|
26
27
|
const motion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
27
28
|
const root = document.documentElement;
|
|
@@ -59,6 +60,7 @@ export function initYard(controller) {
|
|
|
59
60
|
if (allowed) yardChoice.parentElement.removeAttribute('title');
|
|
60
61
|
else yardChoice.parentElement.title = `${skinEntry()?.name || 'This skin'} has no yard yet.`;
|
|
61
62
|
$('yard').hidden = view !== 'yard';
|
|
63
|
+
if (view !== 'yard') closePhoto();
|
|
62
64
|
$('providers').hidden = view === 'yard';
|
|
63
65
|
$('providers').inert = view === 'yard';
|
|
64
66
|
for (const section of document.querySelectorAll('#app > section.sessions, #app > section.news')) {
|
|
@@ -158,6 +160,7 @@ export function initYard(controller) {
|
|
|
158
160
|
renderer.setActive(sceneVisible());
|
|
159
161
|
hideLoading();
|
|
160
162
|
$('yard-stage').dataset.ready = 'true';
|
|
163
|
+
$('yard-photo').disabled = false;
|
|
161
164
|
} catch (err) {
|
|
162
165
|
candidate?.dispose();
|
|
163
166
|
if (attempt === sceneAttempt && !disposed) {
|
|
@@ -169,6 +172,7 @@ export function initYard(controller) {
|
|
|
169
172
|
await loading;
|
|
170
173
|
}
|
|
171
174
|
function failScene() {
|
|
175
|
+
closePhoto();
|
|
172
176
|
const restoreFocus = $('yard-stage').contains(document.activeElement);
|
|
173
177
|
sceneAttempt++; worldAttempt++; loading = null;
|
|
174
178
|
pendingRenderer?.dispose(); pendingRenderer = null;
|
|
@@ -178,6 +182,7 @@ export function initYard(controller) {
|
|
|
178
182
|
$('yard-swap-failure').hidden = true;
|
|
179
183
|
$('yard-failure').hidden = false;
|
|
180
184
|
$('yard-stage').dataset.ready = 'false';
|
|
185
|
+
$('yard-photo').disabled = true;
|
|
181
186
|
if (restoreFocus) $('yard-retry').focus();
|
|
182
187
|
}
|
|
183
188
|
function roster() {
|
|
@@ -304,6 +309,14 @@ export function initYard(controller) {
|
|
|
304
309
|
$('yard-focus').addEventListener('click',() => renderer?.focus(selected));
|
|
305
310
|
$('yard-zoom-in').addEventListener('click',() => renderer?.zoom(1.2));
|
|
306
311
|
$('yard-zoom-out').addEventListener('click',() => renderer?.zoom(1/1.2));
|
|
312
|
+
$('yard-photo').hidden = !controller.photo;
|
|
313
|
+
$('yard-photo').addEventListener('click',async () => {
|
|
314
|
+
let YardPhoto;
|
|
315
|
+
try { ({ YardPhoto } = await import('./photo.js')); }
|
|
316
|
+
catch { controller.toast('Couldn’t open Photo. Reload the page and try again.'); return; }
|
|
317
|
+
photo ||= new YardPhoto($('yard-photo-dialog'));
|
|
318
|
+
if (renderer && !disposed && sceneVisible()) photo.open(renderer, $('yard-photo'));
|
|
319
|
+
});
|
|
307
320
|
$('yard-stage').addEventListener('keydown',event => {
|
|
308
321
|
if (event.target !== $('yard-stage') || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
309
322
|
if (event.key === 'Escape') { select(null); event.preventDefault(); }
|
|
@@ -335,7 +348,8 @@ export function initYard(controller) {
|
|
|
335
348
|
window.addEventListener('pagehide',pageHide);
|
|
336
349
|
window.addEventListener('pageshow',visibility);
|
|
337
350
|
setView(view,{remember:false});
|
|
338
|
-
return { update, select, setView, dispose() {
|
|
351
|
+
return { update, select, setView, closePhoto, dispose() {
|
|
352
|
+
closePhoto();
|
|
339
353
|
disposed=true; sceneAttempt++; worldAttempt++; clearTimeout(swapReveal); observer.disconnect(); pendingRenderer?.dispose(); renderer?.dispose();
|
|
340
354
|
document.removeEventListener('visibilitychange',visibility);
|
|
341
355
|
motion.removeEventListener('change',motionChange);
|
package/web/yard/yard.css
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
#yard-title { font-size:clamp(22px,2.2vw,34px); letter-spacing:.015em; text-shadow:0 2px 14px var(--bg); }
|
|
17
17
|
#yard-subtitle { margin:6px 0 0; font-size:12px; color:var(--muted); }
|
|
18
18
|
.yard-summary { align-self:flex-start; 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; }
|
|
19
|
-
.yard-stage { flex:1; min-height:400px; position:relative; overflow:hidden; outline-offset:-3px; }
|
|
19
|
+
.yard-stage { flex:1; min-height:400px; position:relative; overflow:hidden; outline-offset:-3px; display:flex; flex-direction:column; justify-content:flex-end; }
|
|
20
20
|
.yard-stage canvas { display:block; position:absolute; inset:0; width:100%; height:100%; cursor:grab; touch-action:none; }
|
|
21
21
|
.yard-stage canvas:active { cursor:grabbing; }
|
|
22
22
|
.yard-stage:focus-visible,.yard-row:focus-visible,.yard-label:focus-visible,.yard-crowd:focus-visible,.yard-crowd-pick:focus-visible,.yard-crowd-open:focus-visible,.yard-search input:focus-visible { outline:2px solid var(--focus); outline-offset:-2px; }
|
|
@@ -41,17 +41,19 @@
|
|
|
41
41
|
.yard-crowd-pick .label-state { font-size:9px; color:#c9c2b4; }
|
|
42
42
|
.yard-crowd-open { font-size:10px; white-space:nowrap; color:#e0bf79; }
|
|
43
43
|
.yard-crowd-pick:hover, .yard-crowd-open:hover { background:#1d3236; }
|
|
44
|
-
.yard-
|
|
45
|
-
.yard-
|
|
46
|
-
|
|
44
|
+
.yard-overlay { z-index:2; display:flex; flex-direction:column; align-items:center; gap:10px; padding:0 18px 10px; pointer-events:none; }
|
|
45
|
+
.yard-overlay > * { pointer-events:auto; max-width:100%; }
|
|
46
|
+
.yard-camera { align-self:flex-end; display:flex; gap:5px; flex-wrap:wrap; justify-content:flex-end; }
|
|
47
|
+
.yard-camera .btn { min-height:30px; padding:5px 10px; font-size:11px; background:var(--surface); white-space:nowrap; }
|
|
48
|
+
#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); }
|
|
47
49
|
.yard-loading { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); display:grid; grid-template-columns:1fr auto; align-items:center; gap:10px 16px; min-width:240px; padding:18px 26px; color:var(--text); border:1px solid var(--yard-line); background:var(--surface); z-index:4; border-radius:var(--radius); }
|
|
48
50
|
.yard-loading-size { font-size:11px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
49
51
|
.yard-loading-bar { grid-column:1 / -1; height:3px; border-radius:3px; overflow:hidden; background:color-mix(in srgb,var(--gold) 18%,transparent); }
|
|
50
52
|
.yard-loading-bar::before { content:""; display:block; width:35%; height:100%; border-radius:inherit; background:var(--gold); animation:yard-loading 1.3s ease-in-out infinite; }
|
|
51
53
|
@keyframes yard-loading { from { transform:translateX(-100%); } to { transform:translateX(290%); } }
|
|
52
54
|
/* A swap keeps the current world on screen, so its notices stay out of the way. */
|
|
53
|
-
.yard-loading[data-mode="swap"],.yard-swap-failure {
|
|
54
|
-
.yard-swap-failure {
|
|
55
|
+
.yard-loading[data-mode="swap"],.yard-swap-failure { position:static; transform:none; min-width:0; padding:8px 14px; font-size:12px; border-radius:30px; background:color-mix(in srgb,var(--surface) 92%,transparent); }
|
|
56
|
+
.yard-swap-failure { display:flex; align-items:center; gap:12px; color:var(--text); border:1px solid var(--border); }
|
|
55
57
|
.yard-swap-failure .btn { min-height:26px; padding:3px 10px; font-size:11px; }
|
|
56
58
|
.yard-failure { position:absolute; top:40%; left:50%; transform:translate(-50%,-50%); max-width:380px; width:85%; padding:24px; background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); z-index:4; }
|
|
57
59
|
.yard-failure p { color:var(--muted); line-height:1.5; font-size:13px; }
|
|
@@ -112,8 +114,8 @@
|
|
|
112
114
|
.yard-heading { padding:18px; }
|
|
113
115
|
:root[data-view="yard"] #app > .launcher { padding:10px 14px; }
|
|
114
116
|
:root[data-view="yard"] #launch-heading { display:none; }
|
|
115
|
-
.yard-
|
|
116
|
-
#yard-help { font-size:9px; }
|
|
117
|
+
.yard-overlay { padding:0 12px 10px; }
|
|
118
|
+
#yard-help { font-size:9px; margin-top:16px; }
|
|
117
119
|
}
|
|
118
120
|
@media(max-width:640px) { :root[data-view="yard"] .cwd { flex-basis:100%; } }
|
|
119
121
|
:root[data-layout="compact"] .topbar, :root[data-layout="compact"] #app > .launcher, :root[data-layout="compact"] .yard-heading, :root[data-layout="compact"] .yard-news, :root[data-layout="compact"] .yard-sidebar, :root[data-layout="compact"] #yard-help, :root[data-layout="compact"] #yard-failure > p { display:none; }
|
|
@@ -121,8 +123,8 @@
|
|
|
121
123
|
:root[data-layout="compact"] #yard { display:flex; height:100dvh; min-height:0; }
|
|
122
124
|
:root[data-layout="compact"] .yard-main { flex:1; height:auto; min-height:0; }
|
|
123
125
|
:root[data-layout="compact"] .yard-stage { min-height:0; }
|
|
124
|
-
:root[data-layout="compact"] .yard-
|
|
125
|
-
:root[data-layout="compact"] #yard-offline { top:8px; bottom:auto; left:8px; right:8px; transform:none; flex-wrap:wrap; justify-content:center; text-align:center; border-radius:var(--radius); background:var(--surface); }
|
|
126
|
+
:root[data-layout="compact"] .yard-overlay { padding:0 8px 8px; }
|
|
127
|
+
:root[data-layout="compact"] #yard-offline { position:absolute; z-index:4; top:8px; bottom:auto; left:8px; right:8px; transform:none; flex-wrap:wrap; justify-content:center; text-align:center; border-radius:var(--radius); background:var(--surface); }
|
|
126
128
|
:root[data-layout="compact"] #yard-stage:has(#yard-offline:not([hidden])) :is(canvas, .yard-labels) { opacity:.5; filter:grayscale(1); }
|
|
127
129
|
:root[data-layout="compact"] #yard-stage:has(#yard-offline:not([hidden])) :is(#yard-download, #yard-loading) { display:none; }
|
|
128
130
|
:root[data-expand="none"] :is(#yard-offline-open, #yard-cards, #yard-download-cards, .yard-crowd-open) { display:none; }
|
|
@@ -138,3 +140,10 @@
|
|
|
138
140
|
.yard-row[data-key^="provider:"] .yard-row-sub { display:none; }
|
|
139
141
|
.yard-row[data-status="locked"] .yard-row-sub { display:block; }
|
|
140
142
|
.yard-search input { color:var(--text); background:var(--surface-2); border:1px solid var(--border); border-radius:var(--radius); }
|
|
143
|
+
.yard-photo { width:min(1120px,calc(100vw - 32px)); height:min(860px,calc(100dvh - 48px)); }
|
|
144
|
+
.yard-photo .models-body { display:flex; flex-direction:column; gap:12px; padding-top:14px; overflow:hidden; }
|
|
145
|
+
.yard-photo-preview { flex:1 1 0; min-height:0; width:100%; object-fit:contain; }
|
|
146
|
+
.yard-photo-labels { flex:none; display:flex; flex-wrap:wrap; align-items:center; gap:8px; font-size:13px; }
|
|
147
|
+
.yard-photo-labels span { color:var(--muted); font-size:12px; }
|
|
148
|
+
.yard-photo-footer { display:flex; flex-wrap:wrap; justify-content:flex-end; align-items:center; gap:10px; }
|
|
149
|
+
.yard-photo-footer p { flex:1 1 160px; min-width:0; margin:0; color:var(--muted); font-size:13px; }
|