Skip to content

Commit 7d941ad

Browse files
bloveclaude
andcommitted
feat(examples-chat): App-mode cockpit layout (map bg + itinerary overlay + sidenav→drawer)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 62b456f commit 7d941ad

4 files changed

Lines changed: 121 additions & 14 deletions

File tree

examples/chat/angular/src/app/shell/demo-shell.component.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,39 @@
218218
.demo-shell__app-toggle.is-on .demo-shell__app-toggle-thumb {
219219
background: var(--tplane-chat-on-primary);
220220
}
221+
222+
/* App-mode layout: full-bleed map with the itinerary panel floating as an
223+
* overlay card (top-left) and the chat sidebar alongside via router-outlet. */
224+
.demo-shell__app-body {
225+
flex: 1 1 auto;
226+
min-height: 0;
227+
position: relative;
228+
display: flex;
229+
}
230+
/* Popup mode only: the map is the full-bleed backdrop with the chat bubble
231+
floating over it. (Sidebar mode renders the map inside the chat-sidebar's
232+
flex content slot instead — see SidebarMode, Task 12 — so it gets its
233+
column there.) */
234+
.demo-shell__map {
235+
position: absolute;
236+
inset: 0;
237+
}
238+
.demo-shell__itinerary-overlay {
239+
position: absolute;
240+
top: 16px;
241+
left: 16px;
242+
width: min(360px, 36vw);
243+
max-height: calc(100vh - 120px);
244+
overflow-y: auto;
245+
background: var(--tplane-chat-bg);
246+
border: 1px solid var(--tplane-chat-separator);
247+
border-radius: var(--tplane-chat-radius-card);
248+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
249+
z-index: 10;
250+
}
251+
.demo-shell__app-body .demo-shell__main {
252+
position: relative;
253+
z-index: 5;
254+
flex: 1 1 auto;
255+
min-width: 0;
256+
}

examples/chat/angular/src/app/shell/demo-shell.component.html

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,38 @@
123123
</button>
124124
</chat-sidenav>
125125

126-
<div
127-
class="demo-shell__main"
128-
[attr.data-sidenav-mode]="sidenavMode() !== 'drawer' ? sidenavMode() : null"
129-
>
130-
<router-outlet />
131-
@if (agent.interrupt && agent.interrupt()) {
132-
<div class="demo-shell__interrupt-panel" role="region" aria-label="Approval required">
133-
<chat-interrupt-panel [agent]="agent" (action)="onInterruptAction($event)" />
126+
@if (appMode() === 'on') {
127+
<div class="demo-shell__app-body">
128+
<!-- Sidebar mode renders the map inside the chat-sidebar's flex content
129+
slot (see SidebarMode, Task 12). Popup mode has no content slot, so
130+
the map is the full-bleed backdrop here, with the chat bubble
131+
floating over it. -->
132+
@if (mode() === 'popup') {
133+
<app-map-canvas class="demo-shell__map" />
134+
}
135+
<app-itinerary-panel class="demo-shell__itinerary-overlay" />
136+
<div class="demo-shell__main">
137+
<router-outlet />
138+
@if (agent.interrupt && agent.interrupt()) {
139+
<div class="demo-shell__interrupt-panel" role="region" aria-label="Approval required">
140+
<chat-interrupt-panel [agent]="agent" (action)="onInterruptAction($event)" />
141+
</div>
142+
}
134143
</div>
135-
}
136-
</div>
144+
</div>
145+
} @else {
146+
<div
147+
class="demo-shell__main"
148+
[attr.data-sidenav-mode]="sidenavMode() !== 'drawer' ? sidenavMode() : null"
149+
>
150+
<router-outlet />
151+
@if (agent.interrupt && agent.interrupt()) {
152+
<div class="demo-shell__interrupt-panel" role="region" aria-label="Approval required">
153+
<chat-interrupt-panel [agent]="agent" (action)="onInterruptAction($event)" />
154+
</div>
155+
}
156+
</div>
157+
}
137158

138159
<chat-history-search-palette
139160
[(open)]="paletteOpen"

examples/chat/angular/src/app/shell/demo-shell.component.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,49 @@ describe('DemoShell — App mode routing', () => {
468468
});
469469
});
470470

471+
describe('DemoShell — App-mode cockpit layout', () => {
472+
beforeEach(() => {
473+
localStorage.clear();
474+
TestBed.configureTestingModule({
475+
providers: [
476+
threadsAdapterProvider,
477+
ItineraryStore,
478+
provideRouter([
479+
{ path: 'embed', component: DemoShell },
480+
{ path: 'popup', component: DemoShell },
481+
{ path: 'sidebar', component: DemoShell },
482+
{ path: '', pathMatch: 'full', redirectTo: 'embed' },
483+
{ path: '**', redirectTo: 'embed' },
484+
]),
485+
],
486+
});
487+
});
488+
489+
it('renders the map backdrop + itinerary overlay and forces drawer sidenav in popup App mode', async () => {
490+
const router = TestBed.inject(Router);
491+
await router.navigateByUrl('/popup');
492+
const fx = TestBed.createComponent(DemoShell);
493+
fx.detectChanges();
494+
495+
const cmp = fx.componentInstance as unknown as {
496+
appMode: { (): 'on' | 'off'; set(v: 'on' | 'off'): void };
497+
sidenavMode: () => string;
498+
};
499+
500+
cmp.appMode.set('on');
501+
fx.detectChanges();
502+
503+
// App mode collapses the sidenav to its hamburger drawer.
504+
expect(cmp.sidenavMode()).toBe('drawer');
505+
506+
// Popup App mode renders the full-bleed map backdrop + floating itinerary.
507+
const el = fx.nativeElement as HTMLElement;
508+
expect(el.querySelector('.demo-shell__app-body')).toBeTruthy();
509+
expect(el.querySelector('app-map-canvas')).toBeTruthy();
510+
expect(el.querySelector('app-itinerary-panel')).toBeTruthy();
511+
});
512+
});
513+
471514
// ── Itinerary ↔ checkpoint sync (Task 9) ────────────────────────────────────
472515

473516
/** A FakeStreamTransport that records the payload of the most recent

examples/chat/angular/src/app/shell/demo-shell.component.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
} from '@threadplane/chat';
4242
import { PalettePersistence } from './palette-persistence.service';
4343
import { ProjectsService } from './projects.service';
44+
import { MapCanvasComponent } from '../map-canvas.component';
45+
import { ItineraryPanelComponent } from '../itinerary-panel.component';
4446
import { DEMO_AGENT } from './shell-tokens';
4547
import { createCanonicalDemoRuntimeTelemetrySink } from './runtime-telemetry';
4648
import { environment } from '../../environments/environment';
@@ -117,6 +119,8 @@ export function extractItinerary(value: unknown): ItineraryStop[] | null {
117119
ChatSidenavScrimComponent,
118120
ChatHistorySearchPaletteComponent,
119121
ChatSelectComponent,
122+
MapCanvasComponent,
123+
ItineraryPanelComponent,
120124
],
121125
changeDetection: ChangeDetectionStrategy.OnPush,
122126
templateUrl: './demo-shell.component.html',
@@ -402,10 +406,13 @@ export class DemoShell {
402406
(this.persistence.read('sidenavMode') as 'expanded' | 'collapsed' | null) ?? 'expanded',
403407
);
404408

405-
/** Computed sidenav mode: viewport forces drawer below 768px, else user preference. */
406-
protected readonly sidenavMode = computed<ChatSidenavMode>(() =>
407-
this.viewportWidth() >= 768 ? this.storedDesktopMode() : 'drawer',
408-
);
409+
/** Computed sidenav mode: App mode forces drawer (the cockpit reclaims the
410+
* full width for the map + overlay), then viewport forces drawer below
411+
* 768px, else user preference. */
412+
protected readonly sidenavMode = computed<ChatSidenavMode>(() => {
413+
if (this.appMode() === 'on') return 'drawer';
414+
return this.viewportWidth() >= 768 ? this.storedDesktopMode() : 'drawer';
415+
});
409416

410417
/** Active threads filtered by the selected project (or all when none selected). */
411418
protected readonly visibleThreads = computed<Thread[]>(() => {

0 commit comments

Comments
 (0)