Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,16 @@ This adds a purple FAB button (bottom-right) that opens the full devtools UI in

## Demo App

The repository includes a demo Angular app (`src/`) that showcases the devtools with a product catalog built using `@ngrx/signals`:
The repository includes a demo app, **Angular Travel** (`src/`), that looks and behaves like a real booking site so every inspector has something to show:

- **Home** — simple counter with `signal()`
- **Products** — product list and detail pages powered by a `signalStore` with `withState`, `withComputed`, and `withMethods`
- **About** — static page
- **Examples → SSR & HTTP** (`/examples/http`) — a product list fetched from `/api/products` during SSR and replayed from the transfer cache. The endpoint accepts `?delay=` and `?fail=` for backend scenarios; run the SSR server (`pnpm build --configuration development && node dist/angular-devtools/server/server.mjs`) to see server calls
- **Destinations**: search, region filter and sort kept in the URL, backed by an `@ngrx/signals` store (`withState`, `withComputed`, `withMethods`)
- **Trip pages**: loaded by a resolver that redirects unknown trips, with a route title resolver
- **Booking**: a Signal Forms checkout with cross-field rules, a seat limit and an unsaved-changes guard
- **My Trips**: behind a sign-in guard that redirects to a reactive form and back
- **DevTools Lab** (`/examples`): small, focused pages for signals, components, DI, routes and all three form APIs
- **SSR & HTTP** (`/examples/http`): a product list fetched from `/api/products` during SSR and replayed from the transfer cache. The endpoint accepts `?delay=` and `?fail=` for backend scenarios; run the SSR server (`pnpm build --configuration development && node dist/angular-devtools/server/server.mjs`) to see server calls

Run `pnpm start` and click the purple FAB button to open the devtools popup and see all inspectors in action.
Run `pnpm start` and click the floating button in the corner to open the devtools. Destination photos are from Unsplash, credited in `public/destinations/CREDITS.md`.

## Development

Expand Down
4 changes: 2 additions & 2 deletions packages/ng-devtools/src/__tests__/router-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ describe('router MCP tools', () => {

it('explain-render-mode reads the workspace server routes', async () => {
const { push, call } = await boot();
await push('push-router', report({ snapshot: { ...report().snapshot, url: '/products/9' } }));
const text = await call('explain-render-mode', { url: '/products/9' });
await push('push-router', report({ snapshot: { ...report().snapshot, url: '/book/lisbon' } }));
const text = await call('explain-render-mode', { url: '/book/lisbon' });
expect(text).toContain('renders with `Client`');
expect(text).toContain('app.routes.server.ts');
});
Expand Down
12 changes: 12 additions & 0 deletions public/destinations/CREDITS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Destination photos

From [Unsplash](https://unsplash.com), free to use under the [Unsplash License](https://unsplash.com/license).

- santorini.webp: photo by Raimond Klavins, https://unsplash.com/photos/Bmx1VE5jjy0
- kyoto.webp: photo by Juan Munoz, https://unsplash.com/photos/n4Dl8szrGJU
- lisbon.webp: photo by Sarah Markstaller, https://unsplash.com/photos/r_ShROkN3E8
- iceland.webp: photo by WeRoad, https://unsplash.com/photos/p8-oTF8Fh9I
- bali.webp: photo by Radoslav Bali, https://unsplash.com/photos/jN9JnZ-SyVc
- swiss-alps.webp: photo by Florian van Duyn, https://unsplash.com/photos/77mHoK2JZDQ
- cape-town.webp: photo by Xandro Vandewalle, https://unsplash.com/photos/2t-q7ONxb-w
- istanbul.webp: photo by Engin Yapici, https://unsplash.com/photos/WA1u0scVLZU
Binary file added public/destinations/bali.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/cape-town.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/iceland.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/istanbul.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/kyoto.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/lisbon.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/santorini.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/destinations/swiss-alps.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideHttpClient, withFetch } from '@angular/common/http';
Expand All @@ -8,7 +8,7 @@ import { provideNgDevtoolsHttp, withNgDevtools } from '@santoshyadavdev/ng-devto
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideRouter(routes, withComponentInputBinding()),
provideClientHydration(),
provideHttpClient(withFetch(), withNgDevtools()),
provideNgDevtoolsHttp(),
Expand Down
239 changes: 207 additions & 32 deletions src/app/app.css
Original file line number Diff line number Diff line change
@@ -1,66 +1,72 @@
:host {
display: block;
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100dvh;
background: var(--page);
}

.navbar {
display: flex;
align-items: center;
gap: 32px;
gap: 24px;
padding: 0 24px;
min-height: 56px;
background: var(--surface);
min-height: 64px;
background: color-mix(in srgb, var(--surface) 86%, transparent);
backdrop-filter: saturate(1.4) blur(12px);
-webkit-backdrop-filter: saturate(1.4) blur(12px);
border-bottom: 1px solid var(--line);
box-shadow: 0 1px 3px var(--shadow);
position: sticky;
top: 0;
z-index: 100;
}

/* The bar carries five items; below this it needs two rows rather than
clipping the last of them off the screen. */
@media (max-width: 700px) {
.navbar {
flex-wrap: wrap;
gap: 8px 16px;
padding: 8px 16px;
}
.nav-links {
order: 3;
flex-basis: 100%;
}
}

.brand {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 40px;
border-radius: var(--radius-sm);
white-space: nowrap;
font-weight: 700;
font-size: 16px;
text-decoration: none;
font-size: 17px;
letter-spacing: -0.01em;
color: var(--ink);
text-decoration: none;
}

.brand strong {
color: var(--brand);
}

.navbar app-theme-toggle {
margin-left: auto;
.mark {
flex: none;
transition: transform 0.3s var(--ease);
}

.brand:hover .mark {
transform: rotate(-12deg);
}

.nav-links {
display: flex;
flex-wrap: wrap;
gap: 4px;
}

.nav-links a {
padding: 6px 14px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0 12px;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
color: var(--muted);
text-decoration: none;
white-space: nowrap;
transition:
background 0.15s,
color 0.15s;
background-color 0.15s var(--ease),
color 0.15s var(--ease);
}

.nav-links a:hover {
Expand All @@ -71,18 +77,187 @@
.nav-links a.active {
background: var(--brand-soft);
color: var(--brand);
font-weight: 600;
}

.actions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}

.saved,
.account {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 34px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--ink);
font-size: 13px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
transition:
border-color 0.15s var(--ease),
background-color 0.15s var(--ease);
}

.saved:hover,
.account:hover {
border-color: var(--line-strong);
background: var(--subtle);
}

.heart {
flex: none;
fill: var(--brand);
}

.count {
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 99px;
background: var(--accent);
color: var(--accent-ink);
font-size: 12px;
font-weight: 700;
line-height: 20px;
text-align: center;
font-variant-numeric: tabular-nums;
transition:
background-color 0.2s var(--ease),
color 0.2s var(--ease);
}

.count.empty {
background: var(--subtle);
color: var(--muted);
}

@media (max-width: 900px) {
.navbar {
flex-wrap: wrap;
gap: 8px 16px;
padding: 12px 16px 8px;
}

.nav-links {
order: 3;
flex-basis: 100%;
overflow-x: auto;
scrollbar-width: none;
}

.nav-links::-webkit-scrollbar {
display: none;
}
}

@media (max-width: 520px) {
.nav-links a {
flex: 1 0 auto;
padding: 0 10px;
}

.saved {
padding: 0 10px;
}

.saved .label {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
}

@media (max-width: 400px) {
.brand-name {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}

.actions {
gap: 6px;
}
}

main {
flex: 1;
}

main:focus {
outline: none;
}

.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
gap: 8px 24px;
padding: 24px 96px 24px 24px;
border-top: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}

.footer p {
margin: 0;
max-width: 64ch;
text-wrap: pretty;
}

.footer a {
color: var(--brand);
text-underline-offset: 2px;
}

.footer a:hover {
color: var(--brand-strong);
}

.footer code {
overflow-wrap: anywhere;
}

@media (max-width: 600px) {
.footer {
padding: 20px 88px 24px 16px;
}
}

.skip-link {
position: absolute;
left: -9999px;
top: 0;
left: 8px;
top: 8px;
z-index: 200;
padding: 8px 16px;
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--brand);
font-weight: 600;
transform: translateY(-200%);
}

.skip-link:focus {
left: 0;
transform: none;
}
Loading
Loading