@idosgames/app-shell 0.1.0 → 0.1.2

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/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- 'use strict';var client=require('react-dom/client'),react$1=require('@idosgames/react'),react=require('react'),jsxRuntime=require('react/jsx-runtime');function S(){let r=new Map;return {emit(n,t){let o=r.get(n);if(o)for(let s of [...o])s(t);},on(n,t){let o=t,s=r.get(n);return s||(s=new Set,r.set(n,s)),s.add(o),()=>{s.delete(o),s.size===0&&r.delete(n);}}}}var k={position:"absolute",inset:"0"};function g(r){return {request(n){let t=document.createElement("div");return t.dataset.surface=n,Object.assign(t.style,k),t.style.pointerEvents=n==="fullbleed-canvas"?"auto":"none",r.appendChild(t),{host:t,release(){t.remove();}}}}}function h(r,n){return r.map(t=>{let o={module:t,scenes:[],panels:[],routes:[]},s={client:n.client,events:n.events,surface:n.surface,registerScene:a=>o.scenes.push(a),registerPanel:a=>o.panels.push(a),registerRoute:a=>o.routes.push(a)};return t.setup(s),o})}var u=class{constructor(n){this.container=n;}container;hosts=new Map;active=new Set;ensureMounted(n){let t=this.hosts.get(n);return t||(t=document.createElement("div"),Object.assign(t.style,{position:"absolute",inset:"0"}),this.container.appendChild(t),this.hosts.set(n,t),n.mount({host:t})),t}setActive(n){let t=new Set(n);for(let o of this.active){if(t.has(o))continue;o.suspend?.();let s=this.hosts.get(o);s&&(s.style.display="none");}for(let o of t){if(this.active.has(o))continue;let s=this.ensureMounted(o);s.style.display="block",o.activate?.();}this.active=t;}destroy(){for(let[n,t]of this.hosts)n.destroy(),t.remove();this.hosts.clear(),this.active.clear();}};function E({registrations:r}){let n=react.useRef(null),t=react.useRef(null),o=react.useMemo(()=>r.flatMap(e=>e.routes.map(i=>({id:i.id,label:i.label,icon:i.icon,order:i.order??0,moduleId:e.module.id}))).sort((e,i)=>e.order-i.order),[r]),[s,a]=react.useState(()=>o[0]?.id??null);react.useEffect(()=>{s!==null&&o.some(e=>e.id===s)||a(o[0]?.id??null);},[o,s]);let c=react.useMemo(()=>o.find(e=>e.id===s)?.moduleId??null,[o,s]);react.useEffect(()=>{let e=n.current;if(!e)return;let i=t.current??=new u(e),y=r.find(R=>R.module.id===c)?.scenes??[];i.setActive(y);},[r,c]),react.useEffect(()=>()=>{t.current?.destroy(),t.current=null;},[]);let v=react.useMemo(()=>r.flatMap(e=>e.panels.filter(i=>i.activeOnly===false||e.module.id===c).map(i=>{let y=i.component;return {key:`${e.module.id}:${i.id}`,slot:i.slot,order:i.order??0,moduleId:e.module.id,component:jsxRuntime.jsx(y,{})}})).sort((e,i)=>e.order-i.order),[r,c]),d=e=>v.filter(i=>i.slot===e),p=r.length>0;return jsxRuntime.jsxs("div",{style:w,children:[jsxRuntime.jsx("div",{ref:n,style:z}),jsxRuntime.jsx("div",{style:A,children:d("overlay").map(e=>jsxRuntime.jsx("div",{style:m,children:e.component},e.key))}),jsxRuntime.jsx("div",{style:B,children:d("hud").map(e=>jsxRuntime.jsx("div",{style:m,children:e.component},e.key))}),jsxRuntime.jsx("div",{style:L,children:d("sidebar").map(e=>jsxRuntime.jsx("div",{style:m,children:e.component},e.key))}),o.length>1&&jsxRuntime.jsx("nav",{style:D,children:o.map(e=>jsxRuntime.jsxs("button",{type:"button",onClick:()=>a(e.id),style:e.id===s?T:P,children:[e.icon?`${e.icon} `:"",e.label]},e.id))}),d("modal").length>0&&jsxRuntime.jsx("div",{style:G,children:d("modal").map(e=>jsxRuntime.jsx("div",{style:m,children:e.component},e.key))}),!p&&jsxRuntime.jsx(H,{})]})}function H(){return jsxRuntime.jsxs("div",{style:N,children:[jsxRuntime.jsx("div",{style:{fontSize:40,marginBottom:12},children:"\u{1F9E9}"}),jsxRuntime.jsx("div",{style:{fontWeight:700,fontSize:18,marginBottom:6},children:"No modules installed"}),jsxRuntime.jsxs("div",{style:{opacity:.7,maxWidth:380,lineHeight:1.5},children:["Add a module under ",jsxRuntime.jsx("code",{children:"src/modules/"})," and register it in"," ",jsxRuntime.jsx("code",{children:"src/modules.ts"}),". The host shell will pick it up here."]})]})}var w={position:"absolute",inset:0,overflow:"hidden",color:"#fff",fontFamily:"system-ui, sans-serif"},z={position:"absolute",inset:0,zIndex:0},A={position:"absolute",inset:0,zIndex:1,pointerEvents:"none"},B={position:"absolute",top:0,left:0,right:0,zIndex:2,display:"flex",gap:12,padding:16,pointerEvents:"none"},L={position:"absolute",top:0,right:0,bottom:0,width:340,maxWidth:"40vw",zIndex:2,display:"flex",flexDirection:"column",gap:12,padding:16,overflowY:"auto",pointerEvents:"none"},D={position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",zIndex:3,display:"flex",gap:8,padding:6,borderRadius:14,background:"rgba(20,18,40,0.72)",backdropFilter:"blur(6px)",pointerEvents:"auto"},P={border:"none",borderRadius:10,padding:"8px 16px",fontWeight:700,fontSize:14,color:"#cfc9ee",background:"transparent",cursor:"pointer"},T={...P,color:"#fff",background:"#6c5ce7"},G={position:"absolute",inset:0,zIndex:4,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(0,0,0,0.5)",pointerEvents:"none"},m={pointerEvents:"auto"},N={position:"absolute",inset:0,zIndex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",textAlign:"center",padding:24};function $(r){let{container:n,client:t,modules:o}=r;n.style.position="relative";let s=document.createElement("div");Object.assign(s.style,{position:"absolute",inset:"0",zIndex:"0",pointerEvents:"none"});let a=document.createElement("div");Object.assign(a.style,{position:"absolute",inset:"0",zIndex:"1"}),n.append(s,a);let c=S(),v=g(s),d=h(o,{client:t,events:c,surface:v}),p=client.createRoot(a);return p.render(jsxRuntime.jsx(react$1.IDosGamesProvider,{client:t,children:jsxRuntime.jsx(react$1.StatusProvider,{children:jsxRuntime.jsx(E,{registrations:d})})})),{unmount(){p.unmount(),s.remove(),a.remove();}}}exports.HostShell=E;exports.SceneStage=u;exports.createSharedEventBus=S;exports.createSurfaceAllocator=g;exports.mountHost=$;exports.runModules=h;
1
+ 'use strict';var client=require('react-dom/client'),react$1=require('@idosgames/react'),react=require('react'),core=require('@idosgames/core'),jsxRuntime=require('react/jsx-runtime');function h(){let s=new Map;return {emit(o,e){let n=s.get(o);if(n)for(let r of [...n])r(e);},on(o,e){let n=e,r=s.get(o);return r||(r=new Set,s.set(o,r)),r.add(n),()=>{r.delete(n),r.size===0&&s.delete(o);}}}}var B={position:"absolute",inset:"0"};function b(s){return {request(o){let e=document.createElement("div");return e.dataset.surface=o,Object.assign(e.style,B),e.style.pointerEvents=o==="fullbleed-canvas"?"auto":"none",s.appendChild(e),{host:e,release(){e.remove();}}}}}function x(s,o){return s.map(e=>{let n={module:e,scenes:[],panels:[],routes:[]},r={client:o.client,titleId:o.client.titleID,events:o.events,surface:o.surface,registerScene:l=>n.scenes.push(l),registerPanel:l=>n.panels.push(l),registerRoute:l=>n.routes.push(l)};return e.setup(r),n})}var f=class{constructor(o){this.container=o;}container;hosts=new Map;active=new Set;ensureMounted(o){let e=this.hosts.get(o);return e||(e=document.createElement("div"),Object.assign(e.style,{position:"absolute",inset:"0"}),this.container.appendChild(e),this.hosts.set(o,e),o.mount({host:e})),e}setActive(o){let e=new Set(o);for(let n of this.active){if(e.has(n))continue;n.suspend?.();let r=this.hosts.get(n);r&&(r.style.display="none");}for(let n of e){if(this.active.has(n))continue;let r=this.ensureMounted(n);r.style.display="block",n.activate?.();}this.active=e;}destroy(){for(let[o,e]of this.hosts)o.destroy(),e.remove();this.hosts.clear(),this.active.clear();}};function k({registrations:s}){let o=react.useRef(null),e=react.useRef(null),n=react.useMemo(()=>s.flatMap(t=>t.routes.map(i=>({id:i.id,label:i.label,icon:i.icon,order:i.order??0,moduleId:t.module.id}))).sort((t,i)=>t.order-i.order),[s]),[r,l]=react.useState(()=>n[0]?.id??null);react.useEffect(()=>{r!==null&&n.some(t=>t.id===r)||l(n[0]?.id??null);},[n,r]);let d=react.useMemo(()=>n.find(t=>t.id===r)?.moduleId??null,[n,r]);react.useEffect(()=>{let t=o.current;if(!t)return;let i=e.current??=new f(t),m=s.find(S=>S.module.id===d)?.scenes??[];i.setActive(m);},[s,d]),react.useEffect(()=>()=>{e.current?.destroy(),e.current=null;},[]);let p=react.useMemo(()=>s.flatMap(t=>t.panels.filter(i=>i.activeOnly===false||t.module.id===d).map(i=>{let m=i.component;return {key:`${t.module.id}:${i.id}`,slot:i.slot,order:i.order??0,moduleId:t.module.id,component:jsxRuntime.jsx(m,{})}})).sort((t,i)=>t.order-i.order),[s,d]),c=t=>p.filter(i=>i.slot===t),u=s.length>0;return jsxRuntime.jsxs("div",{style:T,children:[jsxRuntime.jsx("div",{ref:o,style:O}),jsxRuntime.jsx("div",{style:W,children:c("overlay").map(t=>jsxRuntime.jsx("div",{style:y,children:t.component},t.key))}),jsxRuntime.jsx("div",{style:j,children:c("hud").map(t=>jsxRuntime.jsx("div",{style:y,children:t.component},t.key))}),jsxRuntime.jsx("div",{style:U,children:c("sidebar").map(t=>jsxRuntime.jsx("div",{style:y,children:t.component},t.key))}),n.length>1&&jsxRuntime.jsx("nav",{style:$,children:n.map(t=>jsxRuntime.jsxs("button",{type:"button",onClick:()=>l(t.id),style:t.id===r?F:A,children:[t.icon?`${t.icon} `:"",t.label]},t.id))}),c("modal").length>0&&jsxRuntime.jsx("div",{style:K,children:c("modal").map(t=>jsxRuntime.jsx("div",{style:y,children:t.component},t.key))}),!u&&jsxRuntime.jsx(N,{})]})}function N(){return jsxRuntime.jsxs("div",{style:q,children:[jsxRuntime.jsx("div",{style:{fontSize:40,marginBottom:12},children:"\u{1F9E9}"}),jsxRuntime.jsx("div",{style:{fontWeight:700,fontSize:18,marginBottom:6},children:"No modules installed"}),jsxRuntime.jsxs("div",{style:{opacity:.7,maxWidth:380,lineHeight:1.5},children:["Add a module under ",jsxRuntime.jsx("code",{children:"src/modules/"})," and register it in"," ",jsxRuntime.jsx("code",{children:"src/modules.ts"}),". The host shell will pick it up here."]})]})}var T={position:"absolute",inset:0,overflow:"hidden",color:"#fff",fontFamily:"system-ui, sans-serif"},O={position:"absolute",inset:0,zIndex:0},W={position:"absolute",inset:0,zIndex:1,pointerEvents:"none"},j={position:"absolute",top:0,left:0,right:0,zIndex:2,display:"flex",gap:12,padding:16,pointerEvents:"none"},U={position:"absolute",top:0,right:0,bottom:0,width:340,maxWidth:"40vw",zIndex:2,display:"flex",flexDirection:"column",gap:12,padding:16,overflowY:"auto",pointerEvents:"none"},$={position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",zIndex:3,display:"flex",gap:8,padding:6,borderRadius:14,background:"rgba(20,18,40,0.72)",backdropFilter:"blur(6px)",pointerEvents:"auto"},A={border:"none",borderRadius:10,padding:"8px 16px",fontWeight:700,fontSize:14,color:"#cfc9ee",background:"transparent",cursor:"pointer"},F={...A,color:"#fff",background:"#6c5ce7"},K={position:"absolute",inset:0,zIndex:4,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(0,0,0,0.5)",pointerEvents:"none"},y={pointerEvents:"auto"},q={position:"absolute",inset:0,zIndex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",textAlign:"center",padding:24};function M(s){let{client:o,modules:e,events:n,surface:r,renderLogin:l}=s,[d,p]=react.useState(true),[c,u]=react.useState(null),t=()=>{u(x(e,{client:o,events:n,surface:r}));},i=react.useRef(false);return react.useEffect(()=>{if(!i.current){if(i.current=true,o.auth.lastAuthType===core.AuthType.None){p(false);return}(async()=>((await o.auth.autoLogin()).ok&&t(),p(false)))();}},[o]),react.useEffect(()=>{let m=()=>u(null),S=o.on("auth:unauthorized",m),z=o.on("auth:loggedOut",m);return ()=>{S(),z();}},[o]),c?jsxRuntime.jsx(k,{registrations:c}):d?null:l({client:o,onAuthenticated:t})}function C({client:s,onAuthenticated:o}){let[e,n]=react.useState(false),[r,l]=react.useState(null),[d,p]=react.useState(true),c=async()=>{n(true),l(null),s.auth.setRememberSession(d);let u=await s.auth.loginWithDeviceID();if(u.ok){o();return}l(u.error),n(false);};return jsxRuntime.jsx("div",{style:{position:"absolute",inset:0,display:"grid",placeItems:"center",gap:"12px",color:"#e8e6f3",font:"14px system-ui, sans-serif"},children:jsxRuntime.jsxs("div",{style:{display:"grid",gap:"12px",justifyItems:"center"},children:[jsxRuntime.jsx("button",{type:"button",onClick:()=>{c();},disabled:e,style:{padding:"10px 20px",borderRadius:"8px",border:"1px solid #4c4470",background:e?"#2a2545":"#3b3363",color:"inherit",cursor:e?"default":"pointer",font:"inherit"},children:e?"Signing in\u2026":"Play as guest"}),jsxRuntime.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"6px",cursor:e?"default":"pointer",opacity:e?.6:1},children:[jsxRuntime.jsx("input",{type:"checkbox",checked:d,disabled:e,onChange:u=>p(u.target.checked)}),"Remember me"]}),r&&jsxRuntime.jsx("span",{style:{color:"#ff9b9b"},children:r})]})})}function _(s){let{container:o,client:e,modules:n,renderLogin:r}=s;o.style.position="relative";let l=document.createElement("div");Object.assign(l.style,{position:"absolute",inset:"0",zIndex:"0",pointerEvents:"none"});let d=document.createElement("div");Object.assign(d.style,{position:"absolute",inset:"0",zIndex:"1"}),o.append(l,d);let p=h(),c=b(l),u=client.createRoot(d);return u.render(jsxRuntime.jsx(react$1.IDosGamesProvider,{client:e,children:jsxRuntime.jsx(react$1.StatusProvider,{children:jsxRuntime.jsx(M,{client:e,modules:n,events:p,surface:c,renderLogin:r??C})})})),{unmount(){u.unmount(),l.remove(),d.remove();}}}exports.AuthGate=M;exports.DefaultLoginScreen=C;exports.HostShell=k;exports.SceneStage=f;exports.createSharedEventBus=h;exports.createSurfaceAllocator=b;exports.mountHost=_;exports.runModules=x;
package/dist/index.d.cts CHANGED
@@ -1,13 +1,44 @@
1
- import { IDosGamesClient, Module, EngineScene, UiPanel, RouteEntry, SharedEventBus, SurfaceAllocator } from '@idosgames/module-sdk';
1
+ import { IDosGamesClient, Module, SharedEventBus, SurfaceAllocator, EngineScene, UiPanel, RouteEntry } from '@idosgames/module-sdk';
2
2
  import { ReactNode } from 'react';
3
3
 
4
+ /** What a login screen receives from the host. */
5
+ interface LoginScreenProps {
6
+ /** The (not yet authenticated) shared client. Call any `client.auth.login*` method on it. */
7
+ client: IDosGamesClient;
8
+ /** Call after a login succeeds to enter the game. */
9
+ onAuthenticated(): void;
10
+ }
11
+ type RenderLogin = (props: LoginScreenProps) => ReactNode;
12
+ /**
13
+ * The Login ↔ Main gate — the analog of the Unity SDK's two scenes.
14
+ *
15
+ * It lives in the host runtime rather than in a project or a feature module, on purpose:
16
+ *
17
+ * - Modules receive an ALREADY-AUTHENTICATED client (see `ModuleContext`), and are installed only
18
+ * once that is true. A login module would be a cycle — you would need modules running to render
19
+ * the screen that lets you get a session.
20
+ * - Getting this wrong is expensive and silent (modules mounted before auth, a dead session never
21
+ * re-gated), so the state machine is not something each project should re-implement. What a game
22
+ * genuinely wants to own is how the login screen LOOKS — that is the `renderLogin` prop.
23
+ */
24
+ declare function AuthGate(props: {
25
+ client: IDosGamesClient;
26
+ modules: readonly Module[];
27
+ events: SharedEventBus;
28
+ surface: SurfaceAllocator;
29
+ renderLogin: RenderLogin;
30
+ }): ReactNode;
31
+
4
32
  interface HostHandle {
5
33
  unmount(): void;
6
34
  }
7
35
  /**
8
- * Mounts the host shell into `container` with the given modules, all driven by one shared,
9
- * already-authenticated SDK client. This is the composable-module analog of the old per-template
10
- * `mountBoardGame` — except the app owns it and the games are plugged in as modules.
36
+ * Mounts the host into `container`: the Login ↔ Main gate, and behind it the module shell, all
37
+ * driven by one shared SDK client.
38
+ *
39
+ * The client is handed over NOT yet authenticated — the host signs in. Callers must not log in
40
+ * first: the gate replays the previous session itself (`autoLogin`) and shows the login screen when
41
+ * there is nothing to replay, which is also what makes "switch account" and a dead session work.
11
42
  *
12
43
  * DOM layout inside `container`:
13
44
  * container (position: relative)
@@ -18,8 +49,23 @@ declare function mountHost(options: {
18
49
  container: HTMLElement;
19
50
  client: IDosGamesClient;
20
51
  modules: readonly Module[];
52
+ /**
53
+ * The Login scene's content. Optional so a project that deleted its screen still boots with a
54
+ * usable one; games are expected to pass their own (see the host template's LoginScreen).
55
+ */
56
+ renderLogin?: RenderLogin;
21
57
  }): HostHandle;
22
58
 
59
+ /**
60
+ * Last-resort login screen, used only when the host was mounted without a `renderLogin`.
61
+ *
62
+ * Deliberately plain and guest-only: a game's real login screen belongs in the project (branding,
63
+ * which providers it offers, art), and lives in the host template as `src/LoginScreen.tsx`. This
64
+ * one exists so a project that deleted or broke its screen still boots into something a player can
65
+ * get past, instead of a blank page.
66
+ */
67
+ declare function DefaultLoginScreen({ client, onAuthenticated, }: LoginScreenProps): ReactNode;
68
+
23
69
  interface ModuleRegistration {
24
70
  module: Module;
25
71
  scenes: EngineScene[];
@@ -58,4 +104,4 @@ declare function createSharedEventBus(): SharedEventBus;
58
104
  /** Creates an allocator whose surfaces are appended to `layer`. */
59
105
  declare function createSurfaceAllocator(layer: HTMLElement): SurfaceAllocator;
60
106
 
61
- export { type HostHandle, HostShell, type ModuleRegistration, SceneStage, type SharedServices, createSharedEventBus, createSurfaceAllocator, mountHost, runModules };
107
+ export { AuthGate, DefaultLoginScreen, type HostHandle, HostShell, type LoginScreenProps, type ModuleRegistration, type RenderLogin, SceneStage, type SharedServices, createSharedEventBus, createSurfaceAllocator, mountHost, runModules };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,44 @@
1
- import { IDosGamesClient, Module, EngineScene, UiPanel, RouteEntry, SharedEventBus, SurfaceAllocator } from '@idosgames/module-sdk';
1
+ import { IDosGamesClient, Module, SharedEventBus, SurfaceAllocator, EngineScene, UiPanel, RouteEntry } from '@idosgames/module-sdk';
2
2
  import { ReactNode } from 'react';
3
3
 
4
+ /** What a login screen receives from the host. */
5
+ interface LoginScreenProps {
6
+ /** The (not yet authenticated) shared client. Call any `client.auth.login*` method on it. */
7
+ client: IDosGamesClient;
8
+ /** Call after a login succeeds to enter the game. */
9
+ onAuthenticated(): void;
10
+ }
11
+ type RenderLogin = (props: LoginScreenProps) => ReactNode;
12
+ /**
13
+ * The Login ↔ Main gate — the analog of the Unity SDK's two scenes.
14
+ *
15
+ * It lives in the host runtime rather than in a project or a feature module, on purpose:
16
+ *
17
+ * - Modules receive an ALREADY-AUTHENTICATED client (see `ModuleContext`), and are installed only
18
+ * once that is true. A login module would be a cycle — you would need modules running to render
19
+ * the screen that lets you get a session.
20
+ * - Getting this wrong is expensive and silent (modules mounted before auth, a dead session never
21
+ * re-gated), so the state machine is not something each project should re-implement. What a game
22
+ * genuinely wants to own is how the login screen LOOKS — that is the `renderLogin` prop.
23
+ */
24
+ declare function AuthGate(props: {
25
+ client: IDosGamesClient;
26
+ modules: readonly Module[];
27
+ events: SharedEventBus;
28
+ surface: SurfaceAllocator;
29
+ renderLogin: RenderLogin;
30
+ }): ReactNode;
31
+
4
32
  interface HostHandle {
5
33
  unmount(): void;
6
34
  }
7
35
  /**
8
- * Mounts the host shell into `container` with the given modules, all driven by one shared,
9
- * already-authenticated SDK client. This is the composable-module analog of the old per-template
10
- * `mountBoardGame` — except the app owns it and the games are plugged in as modules.
36
+ * Mounts the host into `container`: the Login ↔ Main gate, and behind it the module shell, all
37
+ * driven by one shared SDK client.
38
+ *
39
+ * The client is handed over NOT yet authenticated — the host signs in. Callers must not log in
40
+ * first: the gate replays the previous session itself (`autoLogin`) and shows the login screen when
41
+ * there is nothing to replay, which is also what makes "switch account" and a dead session work.
11
42
  *
12
43
  * DOM layout inside `container`:
13
44
  * container (position: relative)
@@ -18,8 +49,23 @@ declare function mountHost(options: {
18
49
  container: HTMLElement;
19
50
  client: IDosGamesClient;
20
51
  modules: readonly Module[];
52
+ /**
53
+ * The Login scene's content. Optional so a project that deleted its screen still boots with a
54
+ * usable one; games are expected to pass their own (see the host template's LoginScreen).
55
+ */
56
+ renderLogin?: RenderLogin;
21
57
  }): HostHandle;
22
58
 
59
+ /**
60
+ * Last-resort login screen, used only when the host was mounted without a `renderLogin`.
61
+ *
62
+ * Deliberately plain and guest-only: a game's real login screen belongs in the project (branding,
63
+ * which providers it offers, art), and lives in the host template as `src/LoginScreen.tsx`. This
64
+ * one exists so a project that deleted or broke its screen still boots into something a player can
65
+ * get past, instead of a blank page.
66
+ */
67
+ declare function DefaultLoginScreen({ client, onAuthenticated, }: LoginScreenProps): ReactNode;
68
+
23
69
  interface ModuleRegistration {
24
70
  module: Module;
25
71
  scenes: EngineScene[];
@@ -58,4 +104,4 @@ declare function createSharedEventBus(): SharedEventBus;
58
104
  /** Creates an allocator whose surfaces are appended to `layer`. */
59
105
  declare function createSurfaceAllocator(layer: HTMLElement): SurfaceAllocator;
60
106
 
61
- export { type HostHandle, HostShell, type ModuleRegistration, SceneStage, type SharedServices, createSharedEventBus, createSurfaceAllocator, mountHost, runModules };
107
+ export { AuthGate, DefaultLoginScreen, type HostHandle, HostShell, type LoginScreenProps, type ModuleRegistration, type RenderLogin, SceneStage, type SharedServices, createSharedEventBus, createSurfaceAllocator, mountHost, runModules };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import {createRoot}from'react-dom/client';import {IDosGamesProvider,StatusProvider}from'@idosgames/react';import {useRef,useMemo,useState,useEffect}from'react';import {jsx,jsxs}from'react/jsx-runtime';function S(){let r=new Map;return {emit(n,t){let o=r.get(n);if(o)for(let s of [...o])s(t);},on(n,t){let o=t,s=r.get(n);return s||(s=new Set,r.set(n,s)),s.add(o),()=>{s.delete(o),s.size===0&&r.delete(n);}}}}var k={position:"absolute",inset:"0"};function g(r){return {request(n){let t=document.createElement("div");return t.dataset.surface=n,Object.assign(t.style,k),t.style.pointerEvents=n==="fullbleed-canvas"?"auto":"none",r.appendChild(t),{host:t,release(){t.remove();}}}}}function h(r,n){return r.map(t=>{let o={module:t,scenes:[],panels:[],routes:[]},s={client:n.client,events:n.events,surface:n.surface,registerScene:a=>o.scenes.push(a),registerPanel:a=>o.panels.push(a),registerRoute:a=>o.routes.push(a)};return t.setup(s),o})}var u=class{constructor(n){this.container=n;}container;hosts=new Map;active=new Set;ensureMounted(n){let t=this.hosts.get(n);return t||(t=document.createElement("div"),Object.assign(t.style,{position:"absolute",inset:"0"}),this.container.appendChild(t),this.hosts.set(n,t),n.mount({host:t})),t}setActive(n){let t=new Set(n);for(let o of this.active){if(t.has(o))continue;o.suspend?.();let s=this.hosts.get(o);s&&(s.style.display="none");}for(let o of t){if(this.active.has(o))continue;let s=this.ensureMounted(o);s.style.display="block",o.activate?.();}this.active=t;}destroy(){for(let[n,t]of this.hosts)n.destroy(),t.remove();this.hosts.clear(),this.active.clear();}};function E({registrations:r}){let n=useRef(null),t=useRef(null),o=useMemo(()=>r.flatMap(e=>e.routes.map(i=>({id:i.id,label:i.label,icon:i.icon,order:i.order??0,moduleId:e.module.id}))).sort((e,i)=>e.order-i.order),[r]),[s,a]=useState(()=>o[0]?.id??null);useEffect(()=>{s!==null&&o.some(e=>e.id===s)||a(o[0]?.id??null);},[o,s]);let c=useMemo(()=>o.find(e=>e.id===s)?.moduleId??null,[o,s]);useEffect(()=>{let e=n.current;if(!e)return;let i=t.current??=new u(e),y=r.find(R=>R.module.id===c)?.scenes??[];i.setActive(y);},[r,c]),useEffect(()=>()=>{t.current?.destroy(),t.current=null;},[]);let v=useMemo(()=>r.flatMap(e=>e.panels.filter(i=>i.activeOnly===false||e.module.id===c).map(i=>{let y=i.component;return {key:`${e.module.id}:${i.id}`,slot:i.slot,order:i.order??0,moduleId:e.module.id,component:jsx(y,{})}})).sort((e,i)=>e.order-i.order),[r,c]),d=e=>v.filter(i=>i.slot===e),p=r.length>0;return jsxs("div",{style:w,children:[jsx("div",{ref:n,style:z}),jsx("div",{style:A,children:d("overlay").map(e=>jsx("div",{style:m,children:e.component},e.key))}),jsx("div",{style:B,children:d("hud").map(e=>jsx("div",{style:m,children:e.component},e.key))}),jsx("div",{style:L,children:d("sidebar").map(e=>jsx("div",{style:m,children:e.component},e.key))}),o.length>1&&jsx("nav",{style:D,children:o.map(e=>jsxs("button",{type:"button",onClick:()=>a(e.id),style:e.id===s?T:P,children:[e.icon?`${e.icon} `:"",e.label]},e.id))}),d("modal").length>0&&jsx("div",{style:G,children:d("modal").map(e=>jsx("div",{style:m,children:e.component},e.key))}),!p&&jsx(H,{})]})}function H(){return jsxs("div",{style:N,children:[jsx("div",{style:{fontSize:40,marginBottom:12},children:"\u{1F9E9}"}),jsx("div",{style:{fontWeight:700,fontSize:18,marginBottom:6},children:"No modules installed"}),jsxs("div",{style:{opacity:.7,maxWidth:380,lineHeight:1.5},children:["Add a module under ",jsx("code",{children:"src/modules/"})," and register it in"," ",jsx("code",{children:"src/modules.ts"}),". The host shell will pick it up here."]})]})}var w={position:"absolute",inset:0,overflow:"hidden",color:"#fff",fontFamily:"system-ui, sans-serif"},z={position:"absolute",inset:0,zIndex:0},A={position:"absolute",inset:0,zIndex:1,pointerEvents:"none"},B={position:"absolute",top:0,left:0,right:0,zIndex:2,display:"flex",gap:12,padding:16,pointerEvents:"none"},L={position:"absolute",top:0,right:0,bottom:0,width:340,maxWidth:"40vw",zIndex:2,display:"flex",flexDirection:"column",gap:12,padding:16,overflowY:"auto",pointerEvents:"none"},D={position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",zIndex:3,display:"flex",gap:8,padding:6,borderRadius:14,background:"rgba(20,18,40,0.72)",backdropFilter:"blur(6px)",pointerEvents:"auto"},P={border:"none",borderRadius:10,padding:"8px 16px",fontWeight:700,fontSize:14,color:"#cfc9ee",background:"transparent",cursor:"pointer"},T={...P,color:"#fff",background:"#6c5ce7"},G={position:"absolute",inset:0,zIndex:4,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(0,0,0,0.5)",pointerEvents:"none"},m={pointerEvents:"auto"},N={position:"absolute",inset:0,zIndex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",textAlign:"center",padding:24};function $(r){let{container:n,client:t,modules:o}=r;n.style.position="relative";let s=document.createElement("div");Object.assign(s.style,{position:"absolute",inset:"0",zIndex:"0",pointerEvents:"none"});let a=document.createElement("div");Object.assign(a.style,{position:"absolute",inset:"0",zIndex:"1"}),n.append(s,a);let c=S(),v=g(s),d=h(o,{client:t,events:c,surface:v}),p=createRoot(a);return p.render(jsx(IDosGamesProvider,{client:t,children:jsx(StatusProvider,{children:jsx(E,{registrations:d})})})),{unmount(){p.unmount(),s.remove(),a.remove();}}}export{E as HostShell,u as SceneStage,S as createSharedEventBus,g as createSurfaceAllocator,$ as mountHost,h as runModules};
1
+ import {createRoot}from'react-dom/client';import {IDosGamesProvider,StatusProvider}from'@idosgames/react';import {useRef,useMemo,useState,useEffect}from'react';import {AuthType}from'@idosgames/core';import {jsx,jsxs}from'react/jsx-runtime';function h(){let s=new Map;return {emit(o,e){let n=s.get(o);if(n)for(let r of [...n])r(e);},on(o,e){let n=e,r=s.get(o);return r||(r=new Set,s.set(o,r)),r.add(n),()=>{r.delete(n),r.size===0&&s.delete(o);}}}}var B={position:"absolute",inset:"0"};function b(s){return {request(o){let e=document.createElement("div");return e.dataset.surface=o,Object.assign(e.style,B),e.style.pointerEvents=o==="fullbleed-canvas"?"auto":"none",s.appendChild(e),{host:e,release(){e.remove();}}}}}function x(s,o){return s.map(e=>{let n={module:e,scenes:[],panels:[],routes:[]},r={client:o.client,titleId:o.client.titleID,events:o.events,surface:o.surface,registerScene:l=>n.scenes.push(l),registerPanel:l=>n.panels.push(l),registerRoute:l=>n.routes.push(l)};return e.setup(r),n})}var f=class{constructor(o){this.container=o;}container;hosts=new Map;active=new Set;ensureMounted(o){let e=this.hosts.get(o);return e||(e=document.createElement("div"),Object.assign(e.style,{position:"absolute",inset:"0"}),this.container.appendChild(e),this.hosts.set(o,e),o.mount({host:e})),e}setActive(o){let e=new Set(o);for(let n of this.active){if(e.has(n))continue;n.suspend?.();let r=this.hosts.get(n);r&&(r.style.display="none");}for(let n of e){if(this.active.has(n))continue;let r=this.ensureMounted(n);r.style.display="block",n.activate?.();}this.active=e;}destroy(){for(let[o,e]of this.hosts)o.destroy(),e.remove();this.hosts.clear(),this.active.clear();}};function k({registrations:s}){let o=useRef(null),e=useRef(null),n=useMemo(()=>s.flatMap(t=>t.routes.map(i=>({id:i.id,label:i.label,icon:i.icon,order:i.order??0,moduleId:t.module.id}))).sort((t,i)=>t.order-i.order),[s]),[r,l]=useState(()=>n[0]?.id??null);useEffect(()=>{r!==null&&n.some(t=>t.id===r)||l(n[0]?.id??null);},[n,r]);let d=useMemo(()=>n.find(t=>t.id===r)?.moduleId??null,[n,r]);useEffect(()=>{let t=o.current;if(!t)return;let i=e.current??=new f(t),m=s.find(S=>S.module.id===d)?.scenes??[];i.setActive(m);},[s,d]),useEffect(()=>()=>{e.current?.destroy(),e.current=null;},[]);let p=useMemo(()=>s.flatMap(t=>t.panels.filter(i=>i.activeOnly===false||t.module.id===d).map(i=>{let m=i.component;return {key:`${t.module.id}:${i.id}`,slot:i.slot,order:i.order??0,moduleId:t.module.id,component:jsx(m,{})}})).sort((t,i)=>t.order-i.order),[s,d]),c=t=>p.filter(i=>i.slot===t),u=s.length>0;return jsxs("div",{style:T,children:[jsx("div",{ref:o,style:O}),jsx("div",{style:W,children:c("overlay").map(t=>jsx("div",{style:y,children:t.component},t.key))}),jsx("div",{style:j,children:c("hud").map(t=>jsx("div",{style:y,children:t.component},t.key))}),jsx("div",{style:U,children:c("sidebar").map(t=>jsx("div",{style:y,children:t.component},t.key))}),n.length>1&&jsx("nav",{style:$,children:n.map(t=>jsxs("button",{type:"button",onClick:()=>l(t.id),style:t.id===r?F:A,children:[t.icon?`${t.icon} `:"",t.label]},t.id))}),c("modal").length>0&&jsx("div",{style:K,children:c("modal").map(t=>jsx("div",{style:y,children:t.component},t.key))}),!u&&jsx(N,{})]})}function N(){return jsxs("div",{style:q,children:[jsx("div",{style:{fontSize:40,marginBottom:12},children:"\u{1F9E9}"}),jsx("div",{style:{fontWeight:700,fontSize:18,marginBottom:6},children:"No modules installed"}),jsxs("div",{style:{opacity:.7,maxWidth:380,lineHeight:1.5},children:["Add a module under ",jsx("code",{children:"src/modules/"})," and register it in"," ",jsx("code",{children:"src/modules.ts"}),". The host shell will pick it up here."]})]})}var T={position:"absolute",inset:0,overflow:"hidden",color:"#fff",fontFamily:"system-ui, sans-serif"},O={position:"absolute",inset:0,zIndex:0},W={position:"absolute",inset:0,zIndex:1,pointerEvents:"none"},j={position:"absolute",top:0,left:0,right:0,zIndex:2,display:"flex",gap:12,padding:16,pointerEvents:"none"},U={position:"absolute",top:0,right:0,bottom:0,width:340,maxWidth:"40vw",zIndex:2,display:"flex",flexDirection:"column",gap:12,padding:16,overflowY:"auto",pointerEvents:"none"},$={position:"absolute",bottom:16,left:"50%",transform:"translateX(-50%)",zIndex:3,display:"flex",gap:8,padding:6,borderRadius:14,background:"rgba(20,18,40,0.72)",backdropFilter:"blur(6px)",pointerEvents:"auto"},A={border:"none",borderRadius:10,padding:"8px 16px",fontWeight:700,fontSize:14,color:"#cfc9ee",background:"transparent",cursor:"pointer"},F={...A,color:"#fff",background:"#6c5ce7"},K={position:"absolute",inset:0,zIndex:4,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(0,0,0,0.5)",pointerEvents:"none"},y={pointerEvents:"auto"},q={position:"absolute",inset:0,zIndex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",textAlign:"center",padding:24};function M(s){let{client:o,modules:e,events:n,surface:r,renderLogin:l}=s,[d,p]=useState(true),[c,u]=useState(null),t=()=>{u(x(e,{client:o,events:n,surface:r}));},i=useRef(false);return useEffect(()=>{if(!i.current){if(i.current=true,o.auth.lastAuthType===AuthType.None){p(false);return}(async()=>((await o.auth.autoLogin()).ok&&t(),p(false)))();}},[o]),useEffect(()=>{let m=()=>u(null),S=o.on("auth:unauthorized",m),z=o.on("auth:loggedOut",m);return ()=>{S(),z();}},[o]),c?jsx(k,{registrations:c}):d?null:l({client:o,onAuthenticated:t})}function C({client:s,onAuthenticated:o}){let[e,n]=useState(false),[r,l]=useState(null),[d,p]=useState(true),c=async()=>{n(true),l(null),s.auth.setRememberSession(d);let u=await s.auth.loginWithDeviceID();if(u.ok){o();return}l(u.error),n(false);};return jsx("div",{style:{position:"absolute",inset:0,display:"grid",placeItems:"center",gap:"12px",color:"#e8e6f3",font:"14px system-ui, sans-serif"},children:jsxs("div",{style:{display:"grid",gap:"12px",justifyItems:"center"},children:[jsx("button",{type:"button",onClick:()=>{c();},disabled:e,style:{padding:"10px 20px",borderRadius:"8px",border:"1px solid #4c4470",background:e?"#2a2545":"#3b3363",color:"inherit",cursor:e?"default":"pointer",font:"inherit"},children:e?"Signing in\u2026":"Play as guest"}),jsxs("label",{style:{display:"flex",alignItems:"center",gap:"6px",cursor:e?"default":"pointer",opacity:e?.6:1},children:[jsx("input",{type:"checkbox",checked:d,disabled:e,onChange:u=>p(u.target.checked)}),"Remember me"]}),r&&jsx("span",{style:{color:"#ff9b9b"},children:r})]})})}function _(s){let{container:o,client:e,modules:n,renderLogin:r}=s;o.style.position="relative";let l=document.createElement("div");Object.assign(l.style,{position:"absolute",inset:"0",zIndex:"0",pointerEvents:"none"});let d=document.createElement("div");Object.assign(d.style,{position:"absolute",inset:"0",zIndex:"1"}),o.append(l,d);let p=h(),c=b(l),u=createRoot(d);return u.render(jsx(IDosGamesProvider,{client:e,children:jsx(StatusProvider,{children:jsx(M,{client:e,modules:n,events:p,surface:c,renderLogin:r??C})})})),{unmount(){u.unmount(),l.remove(),d.remove();}}}export{M as AuthGate,C as DefaultLoginScreen,k as HostShell,f as SceneStage,h as createSharedEventBus,b as createSurfaceAllocator,_ as mountHost,x as runModules};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idosgames/app-shell",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "The host runtime for composable modules: module registry, shared event bus, surface allocator, scene stage, and the React Mode Router shell. A project seeds one host shell and plugs N modules into it.",
5
5
  "type": "module",
6
6
  "license": "MIT",