Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
38aa0ba
refactor(ui): split the pipes page into section components
erkamyaman Nov 8, 2026
33a12de
refactor(ui): move the network page templates and styles into their o…
erkamyaman Nov 8, 2026
8acaa90
refactor(ui): split the Analog page into section components
erkamyaman Nov 8, 2026
e552eaf
refactor(ui): keep one Analog tab panel across view switches
erkamyaman Nov 8, 2026
c253936
Merge remote-tracking branch 'origin/refactor/network-template-files'…
erkamyaman Nov 8, 2026
9441d47
Merge remote-tracking branch 'origin/refactor/split-pipes-inspector' …
erkamyaman Nov 8, 2026
fda367c
Merge remote-tracking branch 'origin/refactor/split-analog-inspector'…
erkamyaman Nov 8, 2026
f8aa952
refactor(ui): split the Components page into section components
erkamyaman Nov 8, 2026
9deb2dd
Merge remote-tracking branch 'origin/refactor/split-component-tree' i…
erkamyaman Nov 8, 2026
9bdb0d8
refactor(ui): split the signals page into section components
erkamyaman Nov 8, 2026
903a23d
Merge remote-tracking branch 'origin/refactor/split-signal-inspector'…
erkamyaman Nov 8, 2026
2002f5b
refactor(ui): split the Store page into section components
erkamyaman Nov 8, 2026
6671ab1
Merge remote-tracking branch 'origin/refactor/split-store-inspector' …
erkamyaman Nov 8, 2026
e33109e
refactor(ui): split the Injectors page into section components
erkamyaman Nov 8, 2026
e743b36
Merge remote-tracking branch 'origin/refactor/split-di-inspector' int…
erkamyaman Nov 8, 2026
d73da2f
docs: keep component templates and styles in their own files
erkamyaman Nov 8, 2026
3db8497
build(extension): rebuild the panel bundle
erkamyaman Nov 8, 2026
8b97ad7
fix(ui): inline panel templates in the package tests on Windows too
erkamyaman Nov 8, 2026
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
2 changes: 1 addition & 1 deletion .claude/rules/angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ These apply to every Angular and TypeScript file in the repository: the panel (`
- Use `model()` for two-way bound properties with `[(prop)]` syntax instead of pairing `input()` with `output()`
- Use `computed()` for derived state
- Use `linkedSignal()` for state derived from multiple reactive sources that must stay synchronized
- Prefer inline templates for small components
- Give every component its own files: `templateUrl: './x.html'` and `styleUrl: './x.scss'` next to `x.ts`. Do NOT use inline `template:` or `styles:` in new or split components
- Prefer Signal Forms (`@angular/forms/signals`) for new forms. They are stable in Angular v22+ and provide signal-based state, type-safe field access, and schema-based validation
- When not using Signal Forms, prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead
Expand Down
27 changes: 16 additions & 11 deletions app/src/__tests__/page-conventions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ async function pageSources(): Promise<Map<string, string>> {
readdirSync(path: string, options: { recursive: true }): string[];
};
// Recursive, so a page split into a folder (pages/network/) is still checked.
const files = fs
.readdirSync(PAGES, { recursive: true })
.map((file) => file.replaceAll('\\', '/'))
.filter((file) => file.endsWith('.ts'));
return new Map(files.map((file) => [file, fs.readFileSync(`${PAGES}${file}`, 'utf8')]));
const all = fs.readdirSync(PAGES, { recursive: true }).map((file) => file.replaceAll('\\', '/'));
const files = all.filter((file) => file.endsWith('.ts'));
// A component's template and style files count as part of its source.
const read = (file: string) =>
[file, file.replace(/\.ts$/, '.html'), file.replace(/\.ts$/, '.scss')]
.filter((path) => all.includes(path))
.map((path) => fs.readFileSync(`${PAGES}${path}`, 'utf8'))
.join('\n');
return new Map(files.map((file) => [file, read(file)]));
}

describe('every page uses the shared search field', () => {
Expand All @@ -36,7 +40,7 @@ describe('every page uses the shared search field', () => {
),
);
// The values filter sits under the component filter in the Components detail panel.
expect(labels).toEqual(['component-tree.ts: Filter values']);
expect(labels).toEqual(['components/component-detail.ts: Filter values']);
});

it('uses Filter, not Find, in every placeholder', async () => {
Expand All @@ -59,10 +63,10 @@ describe('pages with a splitter', () => {
const page = (name: string) => sources.get(name) ?? '';
expect(page('di-inspector.ts')).toMatch(/<app-splitter[^>]*view="injectors"/s);
expect(page('di-inspector.ts')).toContain("storedPaneSize('injectors', 420)");
expect(page('store-inspector.ts')).toMatch(
expect(page('store/store-log.ts')).toMatch(
/<app-splitter[^>]*view="store-log"[^>]*pane="before"/s,
);
expect(page('store-inspector.ts')).toContain("storedPaneSize('store-log', 300)");
expect(page('store/store-log.ts')).toContain("storedPaneSize('store-log', 300)");
});
});

Expand All @@ -76,7 +80,7 @@ describe('pages use the shared state component', () => {
'plugins-inspector.ts',
'audit-inspector.ts',
'query-inspector.ts',
'store-inspector.ts',
'store/store-live.ts',
'network-inspector.ts',
'live-route.ts',
'route-lint.ts',
Expand Down Expand Up @@ -128,12 +132,13 @@ describe('pages with a keyboard list use the shared roving list', () => {
const sources = await pageSources();
const lists = {
'capacitor-inspector.ts': '[data-cap-call]',
'pipes-inspector.ts': '[data-pipe-row]',
'pipes/pipe-list.ts': '[data-pipe-row]',
'network/call-timeline.ts': '',
'network/ssr-requests.ts': '',
'query-inspector.ts': '',
'signal-inspector.ts': '',
'store-inspector.ts': '',
'store/store-log.ts': '',
'store/store-events.ts': '',
'timeline.ts': '',
};
for (const [file, selector] of Object.entries(lists)) {
Expand Down
24 changes: 23 additions & 1 deletion app/src/__tests__/tint-contrast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,25 @@ describe('signal kind badges', () => {
// both themes get a value these tests check.
const TOKEN_ONLY_PAGES = [
'analog-inspector.ts',
'analog/analog-content.ts',
'analog/analog-lint.ts',
'analog/analog-render.ts',
'analog/analog-routes.ts',
'analog/analog-server.ts',
// The Analog page's styles, which moved out of the page into shared mixins.
'../styles/_analog.scss',
'route-timeline.ts',
'signal-graph-view.ts',
'signal-inspector.ts',
'signals/resource-list.ts',
'signals/signal-detail-state.ts',
'signals/signal-detail.ts',
'signals/signal-graph-section.ts',
'signals/signal-list.ts',
'signals/signal-model.ts',
'signals/signal-state.ts',
'signals/source-signals.ts',
'signals/watched-signals.ts',
];

describe('pages that use theme tokens only', () => {
Expand All @@ -233,8 +249,14 @@ describe('pages that use theme tokens only', () => {
const fsName = 'node:fs';
const fs = (await import(/* @vite-ignore */ fsName)) as {
readFileSync(path: string, encoding: 'utf8'): string;
existsSync(path: string): boolean;
};
const source = fs.readFileSync(`${PAGES}${file}`, 'utf8');
// A component's template and style files count as part of its source.
const source = [file, file.replace(/\.ts$/, '.html'), file.replace(/\.ts$/, '.scss')]
.filter((path, index, all) => all.indexOf(path) === index)
.filter((path) => fs.existsSync(`${PAGES}${path}`))
.map((path) => fs.readFileSync(`${PAGES}${path}`, 'utf8'))
.join('\n');
expect(source.match(/#[0-9a-f]{3,8}\b/gi) ?? []).toEqual([]);
});
}
Expand Down
107 changes: 107 additions & 0 deletions app/src/pages/analog-inspector.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
@if (project() === null && loadFailed()) {
<div class="empty" role="alert">
<h2 class="empty-title">Could not read the project.</h2>
<p class="muted">
Check that the dev server with Pangular Inspector is still running, then retry.
</p>
<button type="button" class="btn" (click)="retry()">Retry</button>
</div>
} @else if (project() === null) {
<div class="loading" role="status">
<span class="spinner" aria-hidden="true"></span>
<span>Reading the project…</span>
</div>
} @else if (!project()!.analog) {
<div class="empty">
<h2 class="empty-title">This app is not an Analog app.</h2>
<p class="muted">
Add <code>pangular()</code> from <code>@pangular-inspector/devtools/vite</code> next to
<code>analog()</code> in vite.config.ts and run the Analog dev server.
</p>
</div>
} @else {
<section class="summary" aria-label="Analog summary">
<div class="stat">
<span class="label">Analog</span>
<span class="value">{{ project()!.version || 'unknown' }}</span>
</div>
<div class="stat">
<span class="label">Pages</span>
<span class="value">{{ pageCount() }}</span>
</div>
<div class="stat">
<span class="label">API routes</span>
<span class="value">{{ project()!.api.length }}</span>
</div>
<div class="stat">
<span class="label">Server calls</span>
<span class="value">{{ tabCalls().length }}</span>
</div>
<div class="stat" [attr.data-tone]="lintTone() || 'good'">
<span class="label">Issues</span>
<span class="value">{{ lintCount().errors + lintCount().warnings }}</span>
</div>
@if (page(); as p) {
<div class="stat wide">
<span class="label">Open in the browser</span>
<span class="value mono" [title]="p.url">{{ p.url }}</span>
</div>
}
</section>

<div class="tabs" role="tablist" aria-label="Analog views" (keydown)="onKey($event)">
@for (v of views(); track v.id) {
<button
type="button"
role="tab"
[id]="'analog-tab-' + v.id"
[attr.aria-selected]="v.id === view()"
[attr.aria-controls]="'analog-panel-' + v.id"
[attr.tabindex]="v.id === view() ? 0 : -1"
(click)="view.set(v.id)"
>
{{ v.label }}
@if (v.count !== null) {
<span class="count" [attr.data-tone]="v.tone">{{ v.count }}</span>
}
</button>
}
</div>

<div
class="panel"
role="tabpanel"
[id]="'analog-panel-' + view()"
[attr.aria-labelledby]="'analog-tab-' + view()"
>
@switch (view()) {
@case ('routes') {
<div appAnalogRoutes></div>
}
@case ('server') {
<div appAnalogServer></div>
}
@case ('render') {
<div appAnalogRender></div>
}
@case ('content') {
<div appAnalogContent></div>
}
@case ('data') {
<app-analog-data
[rpc]="rpc()"
[page]="page()"
[serverFns]="serverFns()"
[duplicates]="state().duplicates ?? []"
[refetches]="state().refetches ?? []"
/>
}
@case ('runtime') {
<app-analog-runtime [rpc]="rpc()" />
}
@case ('lint') {
<div appAnalogLint></div>
}
}
</div>
}
Loading
Loading