11import type { DevframeJsonRenderSpec , JsonRenderIndex , JsonRenderIndexEntry } from '@devframes/json-render'
2+ import type { DevframeRpcClient } from 'devframe/client'
3+ import type { Ref } from 'vue'
24import type { ActionBridgeRpc } from '../action-bridge'
35import { JSON_RENDER_INDEX_KEY } from '@devframes/json-render'
46import { connectDevframe } from 'devframe/client'
57import { computed , createApp , defineComponent , h , ref , shallowReactive , shallowRef , watch } from 'vue'
68import { JsonRenderView } from '../renderer'
9+ import { applyBranding , initColorScheme } from './theme'
710import 'virtual:uno.css'
811import '@antfu/design/styles.css'
12+ // After uno so the brand-primary override wins (see primary-ramp.css).
13+ import './primary-ramp.css'
914
10- // Shared design tokens flip on the `.dark` class; mirror the OS preference.
11- const mq = window . matchMedia ( '(prefers-color-scheme: dark)' )
12- function applyScheme ( dark : boolean ) : void {
13- document . documentElement . classList . toggle ( 'dark' , dark )
14- }
15- applyScheme ( mq . matches )
16- mq . addEventListener ( 'change' , e => applyScheme ( e . matches ) )
15+ // Follow the host's color scheme + brand primary (both best-effort standalone).
16+ initColorScheme ( )
17+ void applyBranding ( )
1718
1819const surface = 'flex min-h-screen items-center justify-center color-faint text-sm'
1920
20- async function main ( ) : Promise < void > {
21- const root = document . getElementById ( 'app' )
22- if ( ! root )
23- throw new Error ( '#app mount node missing' )
24-
25- const rpc = await connectDevframe ( )
26- const interactive = rpc . connectionMeta . backend !== 'static'
27-
28- // Discover every live view from the single view-index shared state, then
29- // subscribe to each view's own state. The author never wires a view id into
30- // the client — publishing a view is enough for it to appear here.
31- const indexState = await rpc . sharedState . get < JsonRenderIndex > ( JSON_RENDER_INDEX_KEY , { initialValue : { } } )
32- // Seed the current server value before mounting so an empty tree isn't
33- // mistaken for "no views" during the first round-trip.
34- if ( interactive ) {
35- try {
36- const value = await rpc . call ( 'devframe:rpc:server-state:get' , JSON_RENDER_INDEX_KEY )
37- if ( value && typeof value === 'object' )
38- indexState . mutate ( ( ) => value as JsonRenderIndex )
39- }
40- catch {
41- // Non-fatal: fall back to live 'updated' events below.
42- }
43- }
21+ /** Subscribe to a view's live spec, keyed by `stateKey`. */
22+ function subscribeSpec (
23+ rpc : DevframeRpcClient ,
24+ stateKey : string ,
25+ into : ( spec : DevframeJsonRenderSpec | null ) => void ,
26+ ) : void {
27+ void rpc . sharedState
28+ . get < DevframeJsonRenderSpec > ( stateKey , { initialValue : null as unknown as DevframeJsonRenderSpec } )
29+ . then ( ( state ) => {
30+ into ( state . value ( ) as DevframeJsonRenderSpec | null )
31+ state . on ( 'updated' , ( ) => into ( state . value ( ) as DevframeJsonRenderSpec | null ) )
32+ } )
33+ }
4434
45- const indexRef = shallowRef < JsonRenderIndex > ( indexState . value ( ) as JsonRenderIndex )
46- indexState . on ( 'updated' , ( ) => {
47- indexRef . value = indexState . value ( ) as JsonRenderIndex
35+ /**
36+ * Single-view mode (`?view=<stateKey>`): render exactly one view, no nav — the
37+ * shape the hub's iframe view-provider mounts (one pooled iframe per view).
38+ */
39+ function mountSingleView ( root : HTMLElement , rpc : DevframeRpcClient , stateKey : string , interactive : boolean ) : void {
40+ const spec = shallowRef < DevframeJsonRenderSpec | null | undefined > ( undefined )
41+ subscribeSpec ( rpc , stateKey , s => ( spec . value = s ) )
42+ const App = defineComponent ( {
43+ name : 'JsonRenderSingleView' ,
44+ setup ( ) {
45+ return ( ) => h ( 'div' , { class : 'min-h-screen bg-base color-base font-sans p6' } , [
46+ h ( JsonRenderView , {
47+ spec : spec . value ?? null ,
48+ rpc : rpc as unknown as ActionBridgeRpc ,
49+ viewId : stateKey ,
50+ interactive,
51+ loading : spec . value === undefined ,
52+ } ) ,
53+ ] )
54+ } ,
4855 } )
56+ createApp ( App ) . mount ( root )
57+ }
4958
59+ /**
60+ * Dashboard mode: discover every live view from the view index and render a
61+ * segmented switcher. The standalone experience when no single view is pinned.
62+ */
63+ function mountDashboard ( root : HTMLElement , rpc : DevframeRpcClient , indexRef : Ref < JsonRenderIndex > , interactive : boolean ) : void {
5064 const App = defineComponent ( {
5165 name : 'JsonRenderSpa' ,
5266 setup ( ) {
5367 const entries = computed < JsonRenderIndexEntry [ ] > ( ( ) =>
5468 Object . values ( indexRef . value ) . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) )
5569
56- // Per-view specs, keyed by stateKey. `undefined` = subscribing (loading),
57- // `null` = subscribed with no spec yet.
5870 const specs = shallowReactive < Record < string , DevframeJsonRenderSpec | null | undefined > > ( { } )
5971 const subscribed = new Set < string > ( )
6072 const active = ref < string | null > ( null )
@@ -65,14 +77,7 @@ async function main(): Promise<void> {
6577 continue
6678 subscribed . add ( entry . stateKey )
6779 specs [ entry . stateKey ] = undefined
68- void rpc . sharedState
69- . get < DevframeJsonRenderSpec > ( entry . stateKey , { initialValue : null as unknown as DevframeJsonRenderSpec } )
70- . then ( ( state ) => {
71- specs [ entry . stateKey ] = state . value ( ) as DevframeJsonRenderSpec | null
72- state . on ( 'updated' , ( ) => {
73- specs [ entry . stateKey ] = state . value ( ) as DevframeJsonRenderSpec | null
74- } )
75- } )
80+ subscribeSpec ( rpc , entry . stateKey , s => ( specs [ entry . stateKey ] = s ) )
7681 }
7782 if ( ! active . value || ! list . some ( e => e . stateKey === active . value ) )
7883 active . value = list [ 0 ] ?. stateKey ?? null
@@ -101,9 +106,6 @@ async function main(): Promise<void> {
101106
102107 const activeEntry = list . find ( e => e . stateKey === active . value ) ?? list [ 0 ]
103108 const spec = specs [ activeEntry . stateKey ]
104-
105- // A single view renders on its own — no nav. The top bar (brand +
106- // segmented view switcher) only appears once there's more than one.
107109 const multiple = list . length > 1
108110
109111 return h ( 'div' , { class : 'min-h-screen bg-base color-base font-sans' } , [
@@ -131,10 +133,47 @@ async function main(): Promise<void> {
131133 }
132134 } ,
133135 } )
134-
135136 createApp ( App ) . mount ( root )
136137}
137138
139+ async function main ( ) : Promise < void > {
140+ const root = document . getElementById ( 'app' )
141+ if ( ! root )
142+ throw new Error ( '#app mount node missing' )
143+
144+ const rpc = await connectDevframe ( )
145+ const interactive = rpc . connectionMeta . backend !== 'static'
146+
147+ // The hub's iframe view-provider pins one view via `?view=<stateKey>`.
148+ const pinned = new URLSearchParams ( location . search ) . get ( 'view' )
149+ if ( pinned ) {
150+ mountSingleView ( root , rpc , pinned , interactive )
151+ return
152+ }
153+
154+ // Dashboard: discover every live view from the single view-index shared
155+ // state. The author never wires a view id into the client — publishing a
156+ // view is enough for it to appear here.
157+ const indexState = await rpc . sharedState . get < JsonRenderIndex > ( JSON_RENDER_INDEX_KEY , { initialValue : { } } )
158+ if ( interactive ) {
159+ try {
160+ const value = await rpc . call ( 'devframe:rpc:server-state:get' , JSON_RENDER_INDEX_KEY )
161+ if ( value && typeof value === 'object' )
162+ indexState . mutate ( ( ) => value as JsonRenderIndex )
163+ }
164+ catch {
165+ // Non-fatal: fall back to live 'updated' events below.
166+ }
167+ }
168+
169+ const indexRef = shallowRef < JsonRenderIndex > ( indexState . value ( ) as JsonRenderIndex )
170+ indexState . on ( 'updated' , ( ) => {
171+ indexRef . value = indexState . value ( ) as JsonRenderIndex
172+ } )
173+
174+ mountDashboard ( root , rpc , indexRef , interactive )
175+ }
176+
138177main ( ) . catch ( ( error ) => {
139178 console . error ( error )
140179 document . body . textContent = `Failed to start: ${ ( error as Error ) . message } `
0 commit comments