11import { createEffect , createMemo , createSignal , For , onCleanup , onMount , Show } from "solid-js" ;
22import { AgentMark } from "./agentMarks.tsx" ;
3- import { api , isReadonly , layoutMode , relTime , sessionLabel , type SessionRow } from "./api.ts" ;
3+ import {
4+ api ,
5+ isReadonly ,
6+ layoutMode ,
7+ relTime ,
8+ sessionLabel ,
9+ type SessionRow ,
10+ type Surface ,
11+ } from "./api.ts" ;
412import { host , isShadow , navHostEl , root , SLOTS } from "./host.ts" ;
513import { applyFrameHeight , Card , cardEls , frameForSource } from "./Card.tsx" ;
614import { renderNotes } from "./notes.ts" ;
715import { SessionTimeline } from "./SessionTimeline.tsx" ;
816import { activeTheme , initTheme , setTheme , themeOptions } from "./theme.ts" ;
917import {
1018 applyRoute ,
19+ bootstrap ,
1120 checkVersion ,
1221 connect ,
1322 dismissUpdate ,
@@ -18,7 +27,6 @@ import {
1827 navOpen ,
1928 nearBottom ,
2029 pillTarget ,
21- refreshSessions ,
2230 refreshSessionsQuiet ,
2331 select ,
2432 selectAdjacent ,
@@ -29,6 +37,7 @@ import {
2937 setPillTarget ,
3038 setUnread ,
3139 setViewMode ,
40+ standaloneSurface ,
3241 streamLoading ,
3342 surfaces ,
3443 toast ,
@@ -73,12 +82,13 @@ export default function App() {
7382 } ) ;
7483
7584 onMount ( ( ) => {
76- // Await the first session fetch, then mark the board decided and tell the
77- // host (onReady). Until then #onboard stays hidden, so neither the empty
78- // board nor a host's loading overlay flips to real content before we know
79- // what to show. .catch keeps it unblocking — a failed fetch still resolves
80- // to the (empty) onboarding board, and the host overlay still clears.
81- void refreshSessions ( host ( ) . router . get ( ) . surfaceId )
85+ // Await the initial route resolution (the standalone surface fetch, or the
86+ // first session fetch), then mark the board decided and tell the host
87+ // (onReady). Until then #onboard stays hidden, so neither the empty board
88+ // nor a host's loading overlay flips to real content before we know what to
89+ // show. .catch keeps it unblocking — a failed fetch still resolves to the
90+ // (empty) onboarding board, and the host overlay still clears.
91+ void bootstrap ( )
8292 . catch ( ( ) => { } )
8393 . finally ( ( ) => {
8494 setInitialLoaded ( true ) ;
@@ -126,9 +136,13 @@ export default function App() {
126136 } ) ;
127137
128138 // unseen activity badges the tab title — self-hosted only; an embedding host
129- // owns its own document title.
139+ // owns its own document title. The standalone page titles itself after the
140+ // surface instead (set below), so don't fight it here.
130141 createEffect ( ( ) => {
131- if ( ! isShadow ( ) ) document . title = unread ( ) . size ? `(${ unread ( ) . size } ) sideshow` : "sideshow" ;
142+ if ( isShadow ( ) ) return ;
143+ const solo = standaloneSurface ( ) ;
144+ if ( solo ) document . title = solo . title ? `${ solo . title } · sideshow` : "sideshow" ;
145+ else document . title = unread ( ) . size ? `(${ unread ( ) . size } ) sideshow` : "sideshow" ;
132146 } ) ;
133147 // the mobile drawer slides in via a class on the host element (see styles.css
134148 // `body.nav-open`; self-hosted that element is <body>)
@@ -140,108 +154,136 @@ export default function App() {
140154 const sessionGroups = createMemo ( ( ) => groupSessions ( sessions , new Date ( ) ) ) ;
141155
142156 return (
143- < >
144- < div id = "app" >
145- < header class = "topbar" >
146- < Show when = { ! streamMode ( ) } >
147- < button
148- class = "menu"
149- id = "menuBtn"
150- aria-label = "Show sessions"
151- onClick = { ( ) => setNavOpen ( ! navOpen ( ) ) }
152- >
153- ☰< span class = "dot" id = "menuDot" classList = { { show : unread ( ) . size > 0 } } > </ span >
154- </ button >
155- </ Show >
156- < Brand />
157- </ header >
158- < Show when = { ! streamMode ( ) } >
159- < aside >
160- < Brand />
161- < UpdateBanner />
162- < div id = "sessionList" >
163- < For each = { sessionGroups ( ) } >
164- { ( group ) => (
165- < >
166- < div class = "sess-group" > { group . label } </ div >
167- < For each = { group . sessions } > { ( s ) => < SessionItem session = { s } /> } </ For >
168- </ >
169- ) }
170- </ For >
171- </ div >
172- < div class = "aside-foot" >
173- { /* ThemePicker is a generic feature, not deployment-specific
174- guidance — it stays engine-owned and works under any host. */ }
175- < Show when = { ! isReadonly ( ) } >
176- < ThemePicker />
157+ < Show
158+ when = { standaloneSurface ( ) }
159+ keyed
160+ fallback = {
161+ < >
162+ < div id = "app" >
163+ < header class = "topbar" >
164+ < Show when = { ! streamMode ( ) } >
165+ < button
166+ class = "menu"
167+ id = "menuBtn"
168+ aria-label = "Show sessions"
169+ onClick = { ( ) => setNavOpen ( ! navOpen ( ) ) }
170+ >
171+ ☰< span class = "dot" id = "menuDot" classList = { { show : unread ( ) . size > 0 } } > </ span >
172+ </ button >
177173 </ Show >
178- { /* Host-overridable region (SLOTS.asideFoot): the footer's
174+ < Brand />
175+ </ header >
176+ < Show when = { ! streamMode ( ) } >
177+ < aside >
178+ < Brand />
179+ < UpdateBanner />
180+ < div id = "sessionList" >
181+ < For each = { sessionGroups ( ) } >
182+ { ( group ) => (
183+ < >
184+ < div class = "sess-group" > { group . label } </ div >
185+ < For each = { group . sessions } > { ( s ) => < SessionItem session = { s } /> } </ For >
186+ </ >
187+ ) }
188+ </ For >
189+ </ div >
190+ < div class = "aside-foot" >
191+ { /* ThemePicker is a generic feature, not deployment-specific
192+ guidance — it stays engine-owned and works under any host. */ }
193+ < Show when = { ! isReadonly ( ) } >
194+ < ThemePicker />
195+ </ Show >
196+ { /* Host-overridable region (SLOTS.asideFoot): the footer's
179197 instructional links/actions. An embedder projects
180198 deployment-appropriate ones here; the children below are the
181199 self-hosted fallback — shown verbatim when nothing is projected
182200 (and outside a shadow root, where <slot> just renders them). */ }
183- < slot name = { SLOTS . asideFoot } >
184- < a href = "/guide" target = "_blank" >
185- design guide
186- </ a > { " " }
187- · { " " }
188- < a href = "/setup" target = "_blank" >
189- agent setup
190- </ a > { " " }
191- < Show when = { ! isReadonly ( ) } >
192- · { " " }
193- < a
194- href = "#"
195- onClick = { ( e ) => {
196- e . preventDefault ( ) ;
197- setConnectOpen ( true ) ;
198- } }
199- >
200- connect Claude Code
201- </ a >
202- </ Show >
203- </ slot >
204- </ div >
205- </ aside >
206- </ Show >
207- < main
208- onScroll = { ( ) => {
209- if ( nearBottom ( ) ) setPillTarget ( null ) ;
210- } }
211- >
212- { /* Host-overridable main pane (SLOTS.main). Fallback is the normal
201+ < slot name = { SLOTS . asideFoot } >
202+ < a href = "/guide" target = "_blank" >
203+ design guide
204+ </ a > { " " }
205+ · { " " }
206+ < a href = "/setup" target = "_blank" >
207+ agent setup
208+ </ a > { " " }
209+ < Show when = { ! isReadonly ( ) } >
210+ · { " " }
211+ < a
212+ href = "#"
213+ onClick = { ( e ) => {
214+ e . preventDefault ( ) ;
215+ setConnectOpen ( true ) ;
216+ } }
217+ >
218+ connect Claude Code
219+ </ a >
220+ </ Show >
221+ </ slot >
222+ </ div >
223+ </ aside >
224+ </ Show >
225+ < main
226+ onScroll = { ( ) => {
227+ if ( nearBottom ( ) ) setPillTarget ( null ) ;
228+ } }
229+ >
230+ { /* Host-overridable main pane (SLOTS.main). Fallback is the normal
213231 board; an embedder projects a `slot="ss:main"` child to take over the
214232 pane (e.g. a cloud Settings page) while the sidebar stays. */ }
215- < slot name = { SLOTS . main } >
216- < Show when = { ! streamMode ( ) } >
217- < Onboard />
218- </ Show >
219- < SessionView />
220- </ slot >
221- </ main >
222- </ div >
223- < Show when = { ! streamMode ( ) } >
224- < div id = "scrim" onClick = { ( ) => setNavOpen ( false ) } > </ div >
225- </ Show >
226- < Show when = { connectOpen ( ) } >
227- < ConnectModal onClose = { ( ) => setConnectOpen ( false ) } />
228- </ Show >
229- < div id = "toast" role = "status" aria-live = "polite" classList = { { show : toastShow ( ) } } >
230- { toastText ( ) }
231- </ div >
232- < button
233- id = "newPill"
234- hidden = { pillTarget ( ) === null }
235- onClick = { ( ) => {
236- const target = pillTarget ( ) ;
237- if ( target )
238- cardEls . get ( target ) ?. card . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
239- setPillTarget ( null ) ;
240- } }
241- >
242- new surface ↓
243- </ button >
244- </ >
233+ < slot name = { SLOTS . main } >
234+ < Show when = { ! streamMode ( ) } >
235+ < Onboard />
236+ </ Show >
237+ < SessionView />
238+ </ slot >
239+ </ main >
240+ </ div >
241+ < Show when = { ! streamMode ( ) } >
242+ < div id = "scrim" onClick = { ( ) => setNavOpen ( false ) } > </ div >
243+ </ Show >
244+ < Show when = { connectOpen ( ) } >
245+ < ConnectModal onClose = { ( ) => setConnectOpen ( false ) } />
246+ </ Show >
247+ < div id = "toast" role = "status" aria-live = "polite" classList = { { show : toastShow ( ) } } >
248+ { toastText ( ) }
249+ </ div >
250+ < button
251+ id = "newPill"
252+ hidden = { pillTarget ( ) === null }
253+ onClick = { ( ) => {
254+ const target = pillTarget ( ) ;
255+ if ( target )
256+ cardEls . get ( target ) ?. card . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
257+ setPillTarget ( null ) ;
258+ } }
259+ >
260+ new surface ↓
261+ </ button >
262+ </ >
263+ }
264+ >
265+ { ( surface ) => < StandaloneView surface = { surface } /> }
266+ </ Show >
267+ ) ;
268+ }
269+
270+ // The full-page view a bare /s/:id direct link lands on: just the one surface,
271+ // no sidebar/session chrome/comments, with a small sideshow watermark beneath
272+ // it. The Card renders in `standalone` mode (title + parts only); its part
273+ // iframes are sized by the same postMessage bridge the board uses (it resolves
274+ // any registered card, so a standalone card sizes identically).
275+ function StandaloneView ( props : { surface : Surface } ) {
276+ return (
277+ < div id = "standalone" >
278+ < main class = "standalone-main" >
279+ < Card surface = { props . surface } standalone />
280+ < footer class = "standalone-foot" >
281+ < a href = "https://sideshow.sh" target = "_blank" rel = "noopener" >
282+ made with < strong > sideshow</ strong >
283+ </ a >
284+ </ footer >
285+ </ main >
286+ </ div >
245287 ) ;
246288}
247289
0 commit comments