@miadi/stateloom-web 0.2.5 → 0.3.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/package.json +1 -1
- package/server/.next/BUILD_ID +1 -1
- package/server/.next/app-path-routes-manifest.json +2 -0
- package/server/.next/build-manifest.json +4 -4
- package/server/.next/prerender-manifest.json +3 -3
- package/server/.next/routes-manifest.json +16 -1
- package/server/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/server/.next/server/app/_global-error.html +2 -2
- package/server/.next/server/app/_global-error.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/server/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/server/.next/server/app/_not-found.html +1 -1
- package/server/.next/server/app/_not-found.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/api/companion/[...path]/route/app-paths-manifest.json +3 -0
- package/server/.next/server/app/api/companion/[...path]/route/build-manifest.json +11 -0
- package/server/.next/server/app/api/companion/[...path]/route/server-reference-manifest.json +4 -0
- package/server/.next/server/app/api/companion/[...path]/route.js +7 -0
- package/server/.next/server/app/api/companion/[...path]/route.js.map +5 -0
- package/server/.next/server/app/api/companion/[...path]/route.js.nft.json +1 -0
- package/server/.next/server/app/api/companion/[...path]/route_client-reference-manifest.js +2 -0
- package/server/.next/server/app/api/docs/route.js +1 -1
- package/server/.next/server/app/api/docs/route.js.nft.json +1 -1
- package/server/.next/server/app/api/file/route.js +1 -1
- package/server/.next/server/app/api/file/route.js.nft.json +1 -1
- package/server/.next/server/app/api/marks/route/app-paths-manifest.json +3 -0
- package/server/.next/server/app/api/marks/route/build-manifest.json +11 -0
- package/server/.next/server/app/api/marks/route/server-reference-manifest.json +4 -0
- package/server/.next/server/app/api/marks/route.js +8 -0
- package/server/.next/server/app/api/marks/route.js.map +5 -0
- package/server/.next/server/app/api/marks/route.js.nft.json +1 -0
- package/server/.next/server/app/api/marks/route_client-reference-manifest.js +2 -0
- package/server/.next/server/app/api/rispec/route.js +1 -1
- package/server/.next/server/app/api/rispec/route.js.nft.json +1 -1
- package/server/.next/server/app/api/watch/route.js +1 -1
- package/server/.next/server/app/api/watch/route.js.nft.json +1 -1
- package/server/.next/server/app/index.html +1 -1
- package/server/.next/server/app/index.rsc +3 -3
- package/server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/server/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/server/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/server/.next/server/app/page/build-manifest.json +2 -2
- package/server/.next/server/app/page.js.nft.json +1 -1
- package/server/.next/server/app/page_client-reference-manifest.js +1 -1
- package/server/.next/server/app-paths-manifest.json +2 -0
- package/server/.next/server/chunks/[root-of-the-server]__13c3d2a8._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__2046b812._.js +2 -2
- package/server/.next/server/chunks/[root-of-the-server]__87bf4b91._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__8b8844cb._.js +1 -1
- package/server/.next/server/chunks/[root-of-the-server]__8d9e281e._.js +3 -0
- package/server/.next/server/chunks/[root-of-the-server]__95b05635._.js +2 -2
- package/server/.next/server/chunks/[root-of-the-server]__aedef6d1._.js +3 -0
- package/server/.next/server/chunks/bridge-protocol_dist_index_179d02a5.js +1 -1
- package/server/.next/server/chunks/ssr/{[root-of-the-server]__b1b2bf74._.js → [root-of-the-server]__9e661f75._.js} +1 -1
- package/server/.next/server/chunks/ssr/[root-of-the-server]__edbb995c._.js +7 -4
- package/server/.next/server/chunks/web__next-internal_server_app_api_companion_[___path]_route_actions_cfae35e7.js +3 -0
- package/server/.next/server/chunks/web__next-internal_server_app_api_marks_route_actions_6f18e4f5.js +3 -0
- package/server/.next/server/middleware-build-manifest.js +2 -2
- package/server/.next/server/pages/404.html +1 -1
- package/server/.next/server/pages/500.html +2 -2
- package/server/.next/server/server-reference-manifest.js +1 -1
- package/server/.next/server/server-reference-manifest.json +1 -1
- package/server/.next/static/chunks/0d24f3e3dbdef25e.css +2 -0
- package/server/.next/static/chunks/{5e1795a806bd6c51.js → d54b3ced5d4f7ae5.js} +1 -1
- package/server/.next/static/chunks/f03f883bab5194ab.js +14 -0
- package/server/.next/static/chunks/{turbopack-34fc7de1568e2a7f.js → turbopack-b3cae9131ae5086f.js} +1 -1
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
- package/server/src/app/api/companion/[...path]/route.ts +112 -0
- package/server/src/app/api/config/route.ts +6 -0
- package/server/src/app/api/marks/route.ts +146 -0
- package/server/src/app/globals.css +94 -0
- package/server/src/app/page.tsx +13 -6
- package/server/src/components/erd/ErdWorkspace.tsx +7 -1
- package/server/src/components/sequence/SequenceReading.tsx +186 -0
- package/server/src/components/sequence/SequenceWorkspace.tsx +126 -13
- package/server/src/components/stage/CompanionLayer.tsx +373 -0
- package/server/src/components/stage/MachineStageLayer.tsx +56 -0
- package/server/src/components/stage/StageLayer.tsx +338 -0
- package/server/src/components/stage/StageProvider.tsx +257 -0
- package/server/src/lib/projectFile.ts +4 -2
- package/server/src/lib/runtimeConfig.ts +10 -1
- package/server/stateloom-web.json +2 -2
- package/server/.next/static/chunks/285279863965ba9b.js +0 -11
- package/server/.next/static/chunks/c86e96e4d6eeb632.css +0 -2
- /package/server/.next/static/{1_kqQZahUkpX4bIvXeeqX → HNER2j3kS6-hB98CL_HRG}/_buildManifest.js +0 -0
- /package/server/.next/static/{1_kqQZahUkpX4bIvXeeqX → HNER2j3kS6-hB98CL_HRG}/_clientMiddlewareManifest.json +0 -0
- /package/server/.next/static/{1_kqQZahUkpX4bIvXeeqX → HNER2j3kS6-hB98CL_HRG}/_ssgManifest.js +0 -0
package/server/.next/static/chunks/{turbopack-34fc7de1568e2a7f.js → turbopack-b3cae9131ae5086f.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/662d1b0ce457c16b.js","static/chunks/
|
|
1
|
+
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/662d1b0ce457c16b.js","static/chunks/d54b3ced5d4f7ae5.js","static/chunks/96818f5620f039a1.js"],runtimeModuleIds:[95506]}]),(()=>{let e;if(!Array.isArray(globalThis.TURBOPACK))return;let t="/_next/",r=(self.TURBOPACK_CHUNK_SUFFIX??document?.currentScript?.getAttribute?.("src")?.replace(/^(.*(?=\?)|^.*$)/,""))||"",n=new WeakMap;function o(e,t){this.m=e,this.e=t}let l=o.prototype,i=Object.prototype.hasOwnProperty,s="u">typeof Symbol&&Symbol.toStringTag;function u(e,t,r){i.call(e,t)||Object.defineProperty(e,t,r)}function a(e,t){let r=e[t];return r||(r=c(t),e[t]=r),r}function c(e){return{exports:{},error:void 0,id:e,namespaceObject:void 0}}function f(e,t){u(e,"__esModule",{value:!0}),s&&u(e,s,{value:"Module"});let r=0;for(;r<t.length;){let n=t[r++],o=t[r++];if("number"==typeof o)if(0===o)u(e,n,{value:t[r++],enumerable:!0,writable:!1});else throw Error(`unexpected tag: ${o}`);else"function"==typeof t[r]?u(e,n,{get:o,set:t[r++],enumerable:!0}):u(e,n,{get:o,enumerable:!0})}Object.seal(e)}l.s=function(e,t){let r,n;null!=t?n=(r=a(this.c,t)).exports:(r=this.m,n=this.e),r.namespaceObject=n,f(n,e)},l.j=function(e,t){var r,o;let l,s,u;null!=t?s=(l=a(this.c,t)).exports:(l=this.m,s=this.e);let c=(r=l,o=s,(u=n.get(r))||(n.set(r,u=[]),r.exports=r.namespaceObject=new Proxy(o,{get(e,t){if(i.call(e,t)||"default"===t||"__esModule"===t)return Reflect.get(e,t);for(let e of u){let r=Reflect.get(e,t);if(void 0!==r)return r}},ownKeys(e){let t=Reflect.ownKeys(e);for(let e of u)for(let r of Reflect.ownKeys(e))"default"===r||t.includes(r)||t.push(r);return t}})),u);"object"==typeof e&&null!==e&&c.push(e)},l.v=function(e,t){(null!=t?a(this.c,t):this.m).exports=e},l.n=function(e,t){let r;(r=null!=t?a(this.c,t):this.m).exports=r.namespaceObject=e};let p=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,h=[null,p({}),p([]),p(p)];function d(e,t,r){let n=[],o=-1;for(let t=e;("object"==typeof t||"function"==typeof t)&&!h.includes(t);t=p(t))for(let r of Object.getOwnPropertyNames(t))n.push(r,function(e,t){return()=>e[t]}(e,r)),-1===o&&"default"===r&&(o=n.length-1);return r&&o>=0||(o>=0?n.splice(o,1,0,e):n.push("default",0,e)),f(t,n),t}function m(e){let t=B(e,this.m);if(t.namespaceObject)return t.namespaceObject;let r=t.exports;return t.namespaceObject=d(r,"function"==typeof r?function(...e){return r.apply(this,e)}:Object.create(null),r&&r.__esModule)}function b(e){let t=e.indexOf("#");-1!==t&&(e=e.substring(0,t));let r=e.indexOf("?");return -1!==r&&(e=e.substring(0,r)),e}function y(){let e,t;return{promise:new Promise((r,n)=>{t=n,e=r}),resolve:e,reject:t}}l.i=m,l.A=function(e){return this.r(e)(m.bind(this))},l.t="function"==typeof require?require:function(){throw Error("Unexpected use of runtime require")},l.r=function(e){return B(e,this.m).exports},l.f=function(e){function t(t){if(t=b(t),i.call(e,t))return e[t].module();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r}return t.keys=()=>Object.keys(e),t.resolve=t=>{if(t=b(t),i.call(e,t))return e[t].id();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r},t.import=async e=>await t(e),t};let O=Symbol("turbopack queues"),g=Symbol("turbopack exports"),w=Symbol("turbopack error");function C(e){e&&1!==e.status&&(e.status=1,e.forEach(e=>e.queueCount--),e.forEach(e=>e.queueCount--?e.queueCount++:e()))}l.a=function(e,t){let r=this.m,n=t?Object.assign([],{status:-1}):void 0,o=new Set,{resolve:l,reject:i,promise:s}=y(),u=Object.assign(s,{[g]:r.exports,[O]:e=>{n&&e(n),o.forEach(e),u.catch(()=>{})}}),a={get:()=>u,set(e){e!==u&&(u[g]=e)}};Object.defineProperty(r,"exports",a),Object.defineProperty(r,"namespaceObject",a),e(function(e){let t=e.map(e=>{if(null!==e&&"object"==typeof e){if(O in e)return e;if(null!=e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then){let t=Object.assign([],{status:0}),r={[g]:{},[O]:e=>e(t)};return e.then(e=>{r[g]=e,C(t)},e=>{r[w]=e,C(t)}),r}}return{[g]:e,[O]:()=>{}}}),r=()=>t.map(e=>{if(e[w])throw e[w];return e[g]}),{promise:l,resolve:i}=y(),s=Object.assign(()=>i(r),{queueCount:0});function u(e){e!==n&&!o.has(e)&&(o.add(e),e&&0===e.status&&(s.queueCount++,e.push(s)))}return t.map(e=>e[O](u)),s.queueCount?l:r()},function(e){e?i(u[w]=e):l(u[g]),C(n)}),n&&-1===n.status&&(n.status=0)};let U=function(e){let t=new URL(e,"x:/"),r={};for(let e in t)r[e]=t[e];for(let t in r.href=e,r.pathname=e.replace(/[?#].*/,""),r.origin=r.protocol="",r.toString=r.toJSON=(...t)=>e,r)Object.defineProperty(this,t,{enumerable:!0,configurable:!0,value:r[t]})};function R(e,t){throw Error(`Invariant: ${t(e)}`)}U.prototype=URL.prototype,l.U=U,l.z=function(e){throw Error("dynamic usage of require is not supported")},l.g=globalThis;let j=o.prototype;var _,k=((_=k||{})[_.Runtime=0]="Runtime",_[_.Parent=1]="Parent",_[_.Update=2]="Update",_);let v=new Map;l.M=v;let $=new Map,P=new Map;async function S(e,t,r){let n;if("string"==typeof r)return E(e,t,K(r));let o=r.included||[],l=o.map(e=>!!v.has(e)||$.get(e));if(l.length>0&&l.every(e=>e))return void await Promise.all(l);let i=r.moduleChunks||[],s=i.map(e=>P.get(e)).filter(e=>e);if(s.length>0){if(s.length===i.length)return void await Promise.all(s);let r=new Set;for(let e of i)P.has(e)||r.add(e);for(let n of r){let r=E(e,t,K(n));P.set(n,r),s.push(r)}n=Promise.all(s)}else{for(let o of(n=E(e,t,K(r.path)),i))P.has(o)||P.set(o,n)}for(let e of o)$.has(e)||$.set(e,n);await n}j.l=function(e){return S(1,this.m.id,e)};let T=Promise.resolve(void 0),A=new WeakMap;function E(t,r,n){let o=e.loadChunkCached(t,n),l=A.get(o);if(void 0===l){let e=A.set.bind(A,o,T);l=o.then(e).catch(e=>{let o;switch(t){case 0:o=`as a runtime dependency of chunk ${r}`;break;case 1:o=`from module ${r}`;break;case 2:o="from an HMR update";break;default:R(t,e=>`Unknown source type: ${e}`)}let l=Error(`Failed to load chunk ${n} ${o}${e?`: ${e}`:""}`,e?{cause:e}:void 0);throw l.name="ChunkLoadError",l}),A.set(o,l)}return l}function K(e){return`${t}${e.split("/").map(e=>encodeURIComponent(e)).join("/")}${r}`}j.L=function(e){return E(1,this.m.id,e)},j.R=function(e){let t=this.r(e);return t?.default??t},j.P=function(e){return`/ROOT/${e??""}`},j.b=function(e){let t=new Blob([`self.TURBOPACK_WORKER_LOCATION = ${JSON.stringify(location.origin)};
|
|
2
2
|
self.TURBOPACK_CHUNK_SUFFIX = ${JSON.stringify(r)};
|
|
3
3
|
self.TURBOPACK_NEXT_CHUNK_URLS = ${JSON.stringify(e.reverse().map(K),null,2)};
|
|
4
4
|
importScripts(...self.TURBOPACK_NEXT_CHUNK_URLS.map(c => self.TURBOPACK_WORKER_LOCATION + c).reverse());`],{type:"text/javascript"});return URL.createObjectURL(t)};let x=/\.js(?:\?[^#]*)?(?:#.*)?$/,N=/\.css(?:\?[^#]*)?(?:#.*)?$/;function M(e){return N.test(e)}l.w=function(t,r,n){return e.loadWebAssembly(1,this.m.id,t,r,n)},l.u=function(t,r){return e.loadWebAssemblyModule(1,this.m.id,t,r)};let L={};l.c=L;let B=(e,t)=>{let r=L[e];if(r){if(r.error)throw r.error;return r}return q(e,k.Parent,t.id)};function q(e,t,r){let n=v.get(e);if("function"!=typeof n)throw Error(function(e,t,r){let n;switch(t){case 0:n=`as a runtime entry of chunk ${r}`;break;case 1:n=`because it was required from module ${r}`;break;case 2:n="because of an HMR update";break;default:R(t,e=>`Unknown source type: ${e}`)}return`Module ${e} was instantiated ${n}, but the module factory is not available.`}(e,t,r));let l=c(e),i=l.exports;L[e]=l;let s=new o(l,i);try{n(s,l,i)}catch(e){throw l.error=e,e}return l.namespaceObject&&l.exports!==l.namespaceObject&&d(l.exports,l.namespaceObject),l}function I(r){let n,o=function(e){if("string"==typeof e)return e;let r=decodeURIComponent(("u">typeof TURBOPACK_NEXT_CHUNK_URLS?TURBOPACK_NEXT_CHUNK_URLS.pop():e.getAttribute("src")).replace(/[?#].*$/,""));return r.startsWith(t)?r.slice(t.length):r}(r[0]);return 2===r.length?n=r[1]:(n=void 0,!function(e,t,r,n){let o=1;for(;o<e.length;){let t=e[o],n=o+1;for(;n<e.length&&"function"!=typeof e[n];)n++;if(n===e.length)throw Error("malformed chunk format, expected a factory function");if(!r.has(t)){let l=e[n];for(Object.defineProperty(l,"name",{value:"module evaluation"});o<n;o++)t=e[o],r.set(t,l)}o=n+1}}(r,0,v)),e.registerChunk(o,n)}let W=new Map;function H(e){let t=W.get(e);if(!t){let r,n;t={resolved:!1,loadingStarted:!1,promise:new Promise((e,t)=>{r=e,n=t}),resolve:()=>{t.resolved=!0,r()},reject:n},W.set(e,t)}return t}e={async registerChunk(e,t){if(H(K(e)).resolve(),null!=t){for(let e of t.otherChunks)H(K("string"==typeof e?e:e.path));if(await Promise.all(t.otherChunks.map(t=>S(0,e,t))),t.runtimeModuleIds.length>0)for(let r of t.runtimeModuleIds)!function(e,t){let r=L[t];if(r){if(r.error)throw r.error;return}q(t,k.Runtime,e)}(e,r)}},loadChunkCached:(e,t)=>(function(e,t){let r=H(t);if(r.loadingStarted)return r.promise;if(e===k.Runtime)return r.loadingStarted=!0,M(t)&&r.resolve(),r.promise;if("function"==typeof importScripts)if(M(t));else if(x.test(t))self.TURBOPACK_NEXT_CHUNK_URLS.push(t),importScripts(TURBOPACK_WORKER_LOCATION+t);else throw Error(`can't infer type of chunk from URL ${t} in worker`);else{let e=decodeURI(t);if(M(t))if(document.querySelectorAll(`link[rel=stylesheet][href="${t}"],link[rel=stylesheet][href^="${t}?"],link[rel=stylesheet][href="${e}"],link[rel=stylesheet][href^="${e}?"]`).length>0)r.resolve();else{let e=document.createElement("link");e.rel="stylesheet",e.href=t,e.onerror=()=>{r.reject()},e.onload=()=>{r.resolve()},document.head.appendChild(e)}else if(x.test(t)){let n=document.querySelectorAll(`script[src="${t}"],script[src^="${t}?"],script[src="${e}"],script[src^="${e}?"]`);if(n.length>0)for(let e of Array.from(n))e.addEventListener("error",()=>{r.reject()});else{let e=document.createElement("script");e.src=t,e.onerror=()=>{r.reject()},document.head.appendChild(e)}}else throw Error(`can't infer type of chunk from URL ${t}`)}return r.loadingStarted=!0,r.promise})(e,t),async loadWebAssembly(e,t,r,n,o){let l=fetch(K(r)),{instance:i}=await WebAssembly.instantiateStreaming(l,o);return i.exports},async loadWebAssemblyModule(e,t,r,n){let o=fetch(K(r));return await WebAssembly.compileStreaming(o)}};let F=globalThis.TURBOPACK;globalThis.TURBOPACK={push:I},F.forEach(I)})();
|
package/server/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/stateloom-web-source",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@miadi/stateloom-web-source",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@miadi/stateloom-canvas": "file:../bridge-canvas",
|
|
12
12
|
"@miadi/stateloom-client": "file:../bridge-client",
|
package/server/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/companion/* — the companion service, as this page reaches it (Spec 84).
|
|
3
|
+
*
|
|
4
|
+
* The Talk dock records takes and reads the conversation of a Mia episode companion
|
|
5
|
+
* (jgwill/miadi-orchestration-kit `claude/mia-episode-companion/phone-capture`). The
|
|
6
|
+
* service listens on its own host and port; the browser asks this server, which passes
|
|
7
|
+
* the request on to `STATELOOM_COMPANION_URL` — the same origin for the page, no CORS
|
|
8
|
+
* to configure, and the service's address never reaches the bundle.
|
|
9
|
+
*
|
|
10
|
+
* Only what the phone page itself does passes: read the thread, the episodes, the
|
|
11
|
+
* health, the recorder's state and the voices; send a take, its backup parts, a
|
|
12
|
+
* diagnostic, the recorder's heartbeat, a thumb, a request to voice a reply. A reply is
|
|
13
|
+
* never posted through here — the service takes replies from its own host only, and
|
|
14
|
+
* every request this route forwards carries `x-forwarded-for`, which is how the service
|
|
15
|
+
* tells a forwarded request from its own seat's.
|
|
16
|
+
*
|
|
17
|
+
* Bodies and audio stream both ways (a take is minutes of audio; a voice is played by
|
|
18
|
+
* byte range).
|
|
19
|
+
*/
|
|
20
|
+
import { NextRequest } from "next/server";
|
|
21
|
+
import { envAlias } from "@miadi/stateloom-protocol";
|
|
22
|
+
|
|
23
|
+
export const dynamic = "force-dynamic";
|
|
24
|
+
export const revalidate = 0;
|
|
25
|
+
|
|
26
|
+
const ID = "[0-9a-f-]{36}";
|
|
27
|
+
const READ = [
|
|
28
|
+
/^health$/,
|
|
29
|
+
/^episodes$/,
|
|
30
|
+
/^thread$/,
|
|
31
|
+
/^recorder$/,
|
|
32
|
+
/^replies$/,
|
|
33
|
+
new RegExp(`^replies/${ID}/audio$`),
|
|
34
|
+
new RegExp(`^replies/${ID}/steps/\\d{1,2}/audio$`),
|
|
35
|
+
/^captures\/audio\/[^/]+$/,
|
|
36
|
+
];
|
|
37
|
+
const WRITE = [
|
|
38
|
+
/^takes$/,
|
|
39
|
+
/^takes\/part$/,
|
|
40
|
+
/^takes\/diagnostic$/,
|
|
41
|
+
/^recorder$/,
|
|
42
|
+
new RegExp(`^replies/${ID}/voice$`),
|
|
43
|
+
new RegExp(`^replies/${ID}/feedback$`),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** The request headers the service reads. */
|
|
47
|
+
const PASS_IN = ["content-type", "range", "accept"];
|
|
48
|
+
/** The response headers the page needs, for JSON and for ranged audio. */
|
|
49
|
+
const PASS_OUT = ["content-type", "content-range", "accept-ranges", "content-length", "cache-control"];
|
|
50
|
+
|
|
51
|
+
function refuse(status: number, error: string): Response {
|
|
52
|
+
return Response.json({ success: false, error }, { status, headers: { "cache-control": "no-store" } });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function allowed(method: string, path: string): boolean {
|
|
56
|
+
const list = method === "POST" ? WRITE : method === "GET" || method === "HEAD" ? READ : [];
|
|
57
|
+
return list.some((re) => re.test(path));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function forward(req: NextRequest, ctx: { params: Promise<{ path?: string[] }> }): Promise<Response> {
|
|
61
|
+
const base = envAlias("COMPANION_URL");
|
|
62
|
+
if (!base) return refuse(503, "no companion on this designer: set STATELOOM_COMPANION_URL to the phone-capture service");
|
|
63
|
+
const { path: parts = [] } = await ctx.params;
|
|
64
|
+
if (parts.some((p) => p === ".." || p === "." || p.includes("/"))) return refuse(400, "bad path");
|
|
65
|
+
const path = parts.join("/");
|
|
66
|
+
if (!allowed(req.method, path)) {
|
|
67
|
+
return refuse(path === "replies" && req.method === "POST" ? 403 : 404, path === "replies" && req.method === "POST"
|
|
68
|
+
? "replies are posted from the companion's own host, not through the canvas"
|
|
69
|
+
: `not passed to the companion: ${req.method} ${path}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const target = new URL(`api/${parts.map(encodeURIComponent).join("/")}`, base.endsWith("/") ? base : `${base}/`);
|
|
73
|
+
target.search = req.nextUrl.search;
|
|
74
|
+
|
|
75
|
+
const headers = new Headers();
|
|
76
|
+
for (const name of PASS_IN) {
|
|
77
|
+
const value = req.headers.get(name);
|
|
78
|
+
if (value) headers.set(name, value);
|
|
79
|
+
}
|
|
80
|
+
// Never empty: an empty or absent header is what the service reads as its own host.
|
|
81
|
+
const client = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "stateloom-web";
|
|
82
|
+
headers.set("x-forwarded-for", client);
|
|
83
|
+
const login = req.headers.get("tailscale-user-login");
|
|
84
|
+
if (login) headers.set("tailscale-user-login", login);
|
|
85
|
+
|
|
86
|
+
let upstream: Response;
|
|
87
|
+
try {
|
|
88
|
+
upstream = await fetch(target, {
|
|
89
|
+
method: req.method,
|
|
90
|
+
headers,
|
|
91
|
+
body: req.method === "POST" ? req.body : undefined,
|
|
92
|
+
// A streamed request body (a take as it is sent) needs half duplex in Node's fetch.
|
|
93
|
+
...(req.method === "POST" ? { duplex: "half" } : {}),
|
|
94
|
+
cache: "no-store",
|
|
95
|
+
redirect: "manual",
|
|
96
|
+
signal: req.signal,
|
|
97
|
+
} as RequestInit);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return refuse(502, `the companion did not answer at ${target.origin}: ${e instanceof Error ? e.message : String(e)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const out = new Headers({ "cache-control": "no-store" });
|
|
103
|
+
for (const name of PASS_OUT) {
|
|
104
|
+
const value = upstream.headers.get(name);
|
|
105
|
+
if (value) out.set(name, value);
|
|
106
|
+
}
|
|
107
|
+
return new Response(req.method === "HEAD" ? null : upstream.body, { status: upstream.status, headers: out });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const GET = forward;
|
|
111
|
+
export const HEAD = forward;
|
|
112
|
+
export const POST = forward;
|
|
@@ -23,6 +23,12 @@ export async function GET(): Promise<Response> {
|
|
|
23
23
|
{
|
|
24
24
|
bridgeUrl: envAlias("BRIDGE_URL") ?? null,
|
|
25
25
|
projectFile: envAlias("PROJECT_FILE") ?? null,
|
|
26
|
+
// The stage this designer presents on (Spec 83); a page's `?stage=` wins.
|
|
27
|
+
stage: envAlias("STAGE") ?? null,
|
|
28
|
+
// Whether a companion answers behind /api/companion (Spec 84), and the episode the
|
|
29
|
+
// Talk dock speaks in when the page names none. The service's address stays here.
|
|
30
|
+
companion: !!envAlias("COMPANION_URL"),
|
|
31
|
+
companionEpisode: envAlias("COMPANION_EPISODE") ?? null,
|
|
26
32
|
},
|
|
27
33
|
{ headers: { "cache-control": "no-store" } }
|
|
28
34
|
);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/marks — a drawing's marks (Spec 83), kept beside it in its sidecar
|
|
3
|
+
* (`wave.smdf.json` → `wave.smdf.marks.json`).
|
|
4
|
+
*
|
|
5
|
+
* GET `?doc=<drawing>` → `{ doc, path, marks }`. No sidecar yet reads as no marks.
|
|
6
|
+
* POST `?doc=<drawing>` with one change:
|
|
7
|
+
* `{ action: "add", mark: { elements?, region?, level?, text, kind?, author } }`
|
|
8
|
+
* `{ action: "reply", id, text, author }`
|
|
9
|
+
* `{ action: "resolve" | "reopen" | "remove", id }`
|
|
10
|
+
* → `{ doc, path, marks, change: { action, id } }`
|
|
11
|
+
*
|
|
12
|
+
* The drawing passes the same root allowlist as the file API, and so does the
|
|
13
|
+
* sidecar itself — it sits in the same folder, so whatever admits the drawing
|
|
14
|
+
* admits its marks, and a symlink placed at the sidecar is refused the same
|
|
15
|
+
* way. The page tells the hub after the write (durable first, as every edit).
|
|
16
|
+
*/
|
|
17
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
18
|
+
import { existsSync, readFileSync } from "fs";
|
|
19
|
+
import { basename } from "path";
|
|
20
|
+
import {
|
|
21
|
+
addMark,
|
|
22
|
+
marksPathOf,
|
|
23
|
+
readMarksFile,
|
|
24
|
+
removeMark,
|
|
25
|
+
reopenMark,
|
|
26
|
+
replyToMark,
|
|
27
|
+
resolveMark,
|
|
28
|
+
type MarkAuthor,
|
|
29
|
+
type MarkInput,
|
|
30
|
+
type MarksChange,
|
|
31
|
+
type MarksFile,
|
|
32
|
+
} from "@miadi/stateloom-protocol";
|
|
33
|
+
import { getProjectFilePath, resolveDocPath, writeDocFile } from "@/lib/projectFile";
|
|
34
|
+
|
|
35
|
+
export const dynamic = "force-dynamic";
|
|
36
|
+
|
|
37
|
+
type Resolved = { ok: true; doc: string; path: string } | { ok: false; error: string };
|
|
38
|
+
|
|
39
|
+
function resolveMarks(doc: string | null): Resolved {
|
|
40
|
+
const drawing = resolveDocPath(doc ?? getProjectFilePath());
|
|
41
|
+
if (!drawing.ok) return { ok: false, error: drawing.error };
|
|
42
|
+
const sidecar = resolveDocPath(marksPathOf(drawing.path));
|
|
43
|
+
if (!sidecar.ok) return { ok: false, error: sidecar.error };
|
|
44
|
+
return { ok: true, doc: drawing.path, path: sidecar.path };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function read(r: { doc: string; path: string }): MarksFile {
|
|
48
|
+
if (!existsSync(r.path)) return { doc: basename(r.doc), marks: [] };
|
|
49
|
+
try {
|
|
50
|
+
return readMarksFile(JSON.parse(readFileSync(r.path, "utf8")), basename(r.doc));
|
|
51
|
+
} catch {
|
|
52
|
+
// A sidecar that does not parse is not overwritten by a reader: it reads as
|
|
53
|
+
// empty, and the next write says so instead of replacing it silently.
|
|
54
|
+
return { doc: basename(r.doc), marks: [] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const unreadable = (path: string): boolean => {
|
|
59
|
+
if (!existsSync(path)) return false;
|
|
60
|
+
try {
|
|
61
|
+
JSON.parse(readFileSync(path, "utf8"));
|
|
62
|
+
return false;
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function author(v: unknown): MarkAuthor {
|
|
69
|
+
const o = (v && typeof v === "object" ? v : {}) as Record<string, unknown>;
|
|
70
|
+
const name = typeof o.name === "string" && o.name.trim() ? o.name.trim().slice(0, 80) : "someone";
|
|
71
|
+
const id = typeof o.id === "string" && o.id.trim() ? o.id.trim().slice(0, 80) : name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
72
|
+
// A page speaks for a person. An agent writes through MCP or the CLI.
|
|
73
|
+
return { id, name, role: "person" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function GET(req: NextRequest) {
|
|
77
|
+
const r = resolveMarks(req.nextUrl.searchParams.get("doc"));
|
|
78
|
+
if (!r.ok) return NextResponse.json({ error: r.error }, { status: 403 });
|
|
79
|
+
return NextResponse.json({ doc: r.doc, path: r.path, marks: read(r).marks }, { headers: { "cache-control": "no-store" } });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function POST(req: NextRequest) {
|
|
83
|
+
const r = resolveMarks(req.nextUrl.searchParams.get("doc"));
|
|
84
|
+
if (!r.ok) return NextResponse.json({ error: r.error }, { status: 403 });
|
|
85
|
+
let body: Record<string, unknown>;
|
|
86
|
+
try {
|
|
87
|
+
body = (await req.json()) as Record<string, unknown>;
|
|
88
|
+
} catch {
|
|
89
|
+
return NextResponse.json({ error: "Body must be JSON: { action, ... }" }, { status: 400 });
|
|
90
|
+
}
|
|
91
|
+
if (unreadable(r.path)) {
|
|
92
|
+
return NextResponse.json({ error: `${r.path} is not readable JSON; fix or move it before marking again` }, { status: 409 });
|
|
93
|
+
}
|
|
94
|
+
const at = new Date().toISOString();
|
|
95
|
+
const file = read(r);
|
|
96
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
97
|
+
let next: MarksFile;
|
|
98
|
+
let change: { action: MarksChange; id: string };
|
|
99
|
+
try {
|
|
100
|
+
switch (body.action) {
|
|
101
|
+
case "add": {
|
|
102
|
+
const m = (body.mark && typeof body.mark === "object" ? body.mark : {}) as Partial<MarkInput>;
|
|
103
|
+
next = addMark(
|
|
104
|
+
file,
|
|
105
|
+
{
|
|
106
|
+
text: typeof m.text === "string" ? m.text.slice(0, 4000) : "",
|
|
107
|
+
kind: m.kind,
|
|
108
|
+
elements: Array.isArray(m.elements) ? m.elements.filter((e): e is string => typeof e === "string").slice(0, 50) : undefined,
|
|
109
|
+
region: m.region,
|
|
110
|
+
level: typeof m.level === "string" ? m.level : undefined,
|
|
111
|
+
author: author(m.author),
|
|
112
|
+
},
|
|
113
|
+
at,
|
|
114
|
+
);
|
|
115
|
+
change = { action: "add", id: next.marks[next.marks.length - 1].id };
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "reply":
|
|
119
|
+
next = replyToMark(file, id, { author: author(body.author), text: typeof body.text === "string" ? body.text.slice(0, 4000) : "" }, at);
|
|
120
|
+
change = { action: "reply", id };
|
|
121
|
+
break;
|
|
122
|
+
case "resolve":
|
|
123
|
+
next = resolveMark(file, id);
|
|
124
|
+
change = { action: "resolve", id };
|
|
125
|
+
break;
|
|
126
|
+
case "reopen":
|
|
127
|
+
next = reopenMark(file, id);
|
|
128
|
+
change = { action: "reopen", id };
|
|
129
|
+
break;
|
|
130
|
+
case "remove":
|
|
131
|
+
next = removeMark(file, id);
|
|
132
|
+
change = { action: "remove", id };
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
return NextResponse.json({ error: "action is one of add, reply, resolve, reopen, remove" }, { status: 400 });
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 400 });
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
writeDocFile(r.path, JSON.stringify(next, null, 2) + "\n");
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 400 });
|
|
144
|
+
}
|
|
145
|
+
return NextResponse.json({ doc: r.doc, path: r.path, marks: next.marks, change });
|
|
146
|
+
}
|
|
@@ -268,3 +268,97 @@ body {
|
|
|
268
268
|
designer and forgewright animate the same way from one source. Nothing in
|
|
269
269
|
this app referenced the old `sm-*` class names outside the canvas, which is
|
|
270
270
|
why they could simply leave. */
|
|
271
|
+
|
|
272
|
+
/* The stage's caption (Spec 83): a fixed layer the size of the screen that lets
|
|
273
|
+
every pointer through except on the caption itself, which sits above the
|
|
274
|
+
phone dock and, from `md` up, at the bottom edge. */
|
|
275
|
+
.stage-caption-host {
|
|
276
|
+
position: fixed;
|
|
277
|
+
inset: 0 0 calc(var(--sm-dock-h) + env(safe-area-inset-bottom)) 0;
|
|
278
|
+
pointer-events: none;
|
|
279
|
+
z-index: 45;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (min-width: 768px) {
|
|
283
|
+
.stage-caption-host { bottom: 0; }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* The stage's controls on a board: top-right, clear of the status line (top
|
|
287
|
+
left), the machine's zoom bar (bottom left) and the view controls (bottom
|
|
288
|
+
right). */
|
|
289
|
+
.stage-board-controls {
|
|
290
|
+
position: absolute;
|
|
291
|
+
right: 0.75rem;
|
|
292
|
+
top: 0.5rem;
|
|
293
|
+
z-index: 20;
|
|
294
|
+
display: flex;
|
|
295
|
+
gap: 0.375rem;
|
|
296
|
+
pointer-events: auto;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* The mark cards open under those controls. */
|
|
300
|
+
.stage-board-card {
|
|
301
|
+
position: absolute;
|
|
302
|
+
right: 0.5rem;
|
|
303
|
+
top: 3.25rem;
|
|
304
|
+
z-index: 25;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@media (max-width: 767px) {
|
|
308
|
+
.stage-board-card { left: 0.25rem; right: 0.25rem; }
|
|
309
|
+
.stage-board-card > * { width: auto; }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* The Talk dock (Spec 84): bottom-left, above the phone dock and above the
|
|
313
|
+
machine's zoom bar, clear of the stage's caption (bottom centre) and the
|
|
314
|
+
board's controls (top right). */
|
|
315
|
+
.stage-talk-host {
|
|
316
|
+
position: fixed;
|
|
317
|
+
left: calc(0.75rem + env(safe-area-inset-left));
|
|
318
|
+
/* On a phone the caption spans the width: the dock stands above it while it shows. */
|
|
319
|
+
bottom: calc(var(--sm-dock-h) + env(safe-area-inset-bottom) + var(--stage-caption-h, 3rem) + 0.5rem);
|
|
320
|
+
z-index: 46;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@media (min-width: 768px) {
|
|
324
|
+
.stage-talk-host { bottom: 3.5rem; }
|
|
325
|
+
/* Open beside a caption, the conversation stands above it rather than over its edge. */
|
|
326
|
+
.stage-talk-host:has(.slc-talk--open) { bottom: calc(3.5rem + var(--stage-caption-h, 0px)); }
|
|
327
|
+
.stage-talk-host .slc-talk-panel { max-height: calc(100dvh - 3.5rem - var(--stage-caption-h, 0px) - 4.5rem); }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* On a phone the open conversation is a sheet across the bottom, over the caption: one
|
|
331
|
+
thing at a time on a small screen. A reply that walks the board folds it again. */
|
|
332
|
+
@media (max-width: 767px) {
|
|
333
|
+
.stage-talk-host:has(.slc-talk--open) {
|
|
334
|
+
left: calc(0.25rem + env(safe-area-inset-left));
|
|
335
|
+
right: calc(0.25rem + env(safe-area-inset-right));
|
|
336
|
+
bottom: calc(var(--sm-dock-h) + env(safe-area-inset-bottom) + 0.25rem);
|
|
337
|
+
z-index: 47;
|
|
338
|
+
}
|
|
339
|
+
.stage-talk-host .slc-talk-panel { width: auto; max-height: min(72dvh, 40rem); }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* While marking is on: what a tap and a drag do, said on the board itself. */
|
|
343
|
+
.stage-board-hint {
|
|
344
|
+
position: absolute;
|
|
345
|
+
left: 50%;
|
|
346
|
+
top: 0.75rem;
|
|
347
|
+
z-index: 20;
|
|
348
|
+
transform: translateX(-50%);
|
|
349
|
+
max-width: calc(100% - 7.5rem);
|
|
350
|
+
padding: 0.35rem 0.75rem;
|
|
351
|
+
border-radius: 999px;
|
|
352
|
+
border: 1px solid rgb(251 191 36 / 0.45);
|
|
353
|
+
background: rgb(17 24 39 / 0.92);
|
|
354
|
+
color: #fcd34d;
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
line-height: 1.35;
|
|
357
|
+
text-align: center;
|
|
358
|
+
pointer-events: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@media (max-width: 767px) {
|
|
362
|
+
/* Under the board's controls, across the width, so it never covers them. */
|
|
363
|
+
.stage-board-hint { left: 0.5rem; right: 0.5rem; max-width: none; transform: none; top: 3.4rem; }
|
|
364
|
+
}
|
package/server/src/app/page.tsx
CHANGED
|
@@ -17,6 +17,8 @@ import SystemWorkspace from "@/components/system/SystemWorkspace";
|
|
|
17
17
|
import SystemStrip from "@/components/system/SystemStrip";
|
|
18
18
|
import BoardBoundary from "@/components/BoardBoundary";
|
|
19
19
|
import IssueIcon from "@/components/IssueIcon";
|
|
20
|
+
import StageProvider from "@/components/stage/StageProvider";
|
|
21
|
+
import MachineStageLayer from "@/components/stage/MachineStageLayer";
|
|
20
22
|
import { docKindOfPath, isSysdfPath, type DocKind } from "@miadi/stateloom-protocol";
|
|
21
23
|
import { useRequestedDoc } from "@/lib/docParam";
|
|
22
24
|
import { useMachineFocus } from "@/lib/machineFocus";
|
|
@@ -102,12 +104,16 @@ export default function Home() {
|
|
|
102
104
|
{workspace}
|
|
103
105
|
</BoardBoundary>
|
|
104
106
|
);
|
|
105
|
-
|
|
107
|
+
// The stage (Spec 83) wraps whichever workspace is open, so a tour that
|
|
108
|
+
// moves between drawings keeps playing across the switch.
|
|
109
|
+
if (!systemParam || !isSysdfPath(systemParam)) return <StageProvider>{guarded}</StageProvider>;
|
|
106
110
|
return (
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
<StageProvider>
|
|
112
|
+
<div className="system-frame">
|
|
113
|
+
<SystemStrip systemPath={systemParam} />
|
|
114
|
+
{guarded}
|
|
115
|
+
</div>
|
|
116
|
+
</StageProvider>
|
|
111
117
|
);
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -161,10 +167,11 @@ function MachineDesigner() {
|
|
|
161
167
|
<Toolbar />
|
|
162
168
|
<div className="flex flex-1 min-h-0 overflow-hidden relative">
|
|
163
169
|
{/* Canvas */}
|
|
164
|
-
<div className="flex-1 overflow-hidden">
|
|
170
|
+
<div className="relative flex-1 overflow-hidden" id="machine-board">
|
|
165
171
|
<BoardBoundary what="state machine">
|
|
166
172
|
<Canvas />
|
|
167
173
|
</BoardBoundary>
|
|
174
|
+
<MachineStageLayer />
|
|
168
175
|
</div>
|
|
169
176
|
|
|
170
177
|
{/* Scrim. Phone only — it is what makes "tap the canvas to dismiss"
|
|
@@ -61,7 +61,9 @@ import LockIcon from "@/components/LockIcon";
|
|
|
61
61
|
import { docQuery, navigateToDoc, useRequestedDoc } from "@/lib/docParam";
|
|
62
62
|
import { loadRuntimeConfig } from "@/lib/runtimeConfig";
|
|
63
63
|
import { useSheetDrag } from "@/lib/useSheetDrag";
|
|
64
|
-
import { urlParam, useFocusRequest, writeFocus } from "@/lib/systemParam";
|
|
64
|
+
import { urlParam, useFocusRequest, useUrlParam, writeFocus } from "@/lib/systemParam";
|
|
65
|
+
import StageLayer from "@/components/stage/StageLayer";
|
|
66
|
+
import { erdElementBoxes } from "@miadi/stateloom-protocol";
|
|
65
67
|
import { safely } from "@/lib/safely";
|
|
66
68
|
|
|
67
69
|
type Moved = Record<string, { x: number; y: number }>;
|
|
@@ -380,6 +382,8 @@ export default function ErdWorkspace() {
|
|
|
380
382
|
// show it. `attribute:E.a` shows its entity. A person's selection becomes the
|
|
381
383
|
// focus in turn, so the strip can list what it links to.
|
|
382
384
|
const focusRequest = useFocusRequest();
|
|
385
|
+
const stageFocus = useUrlParam("focus");
|
|
386
|
+
const stageBoxes = useMemo(() => (def ? safely(() => erdElementBoxes(def, positions, { rows: notation !== "chen" }), []) : []), [def, positions, notation]);
|
|
383
387
|
const appliedFocus = useRef(-1);
|
|
384
388
|
useEffect(() => {
|
|
385
389
|
if (!def || appliedFocus.current === focusRequest.nonce) return;
|
|
@@ -533,6 +537,8 @@ export default function ErdWorkspace() {
|
|
|
533
537
|
/>
|
|
534
538
|
</BoardBoundary>
|
|
535
539
|
)}
|
|
540
|
+
{/* The stage (Spec 83): what an agent lights, and the person's marks. */}
|
|
541
|
+
{def && <StageLayer docPath={docPath || null} kind="erd" viewport={viewport} setViewport={setViewport} boxes={stageBoxes} boardRef={boardRef} focus={stageFocus} />}
|
|
536
542
|
|
|
537
543
|
{/* Status costs no layout: it is a line over the board's corner. */}
|
|
538
544
|
<div
|