@origostudio/origo-design 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/dist/packages/angular-renderer/CHANGELOG.md +21 -0
  3. package/dist/packages/angular-renderer/package.json +3 -3
  4. package/dist/packages/angular-renderer/src/devtools/devtools-bridge.d.ts +3 -1
  5. package/dist/packages/angular-renderer/src/devtools/devtools-bridge.js +64 -4
  6. package/dist/packages/angular-renderer/src/devtools/devtools-bridge.js.map +1 -1
  7. package/dist/packages/angular-renderer/src/index.d.ts +1 -0
  8. package/dist/packages/angular-renderer/src/index.js +1 -0
  9. package/dist/packages/angular-renderer/src/index.js.map +1 -1
  10. package/dist/packages/cli/CHANGELOG.md +8 -0
  11. package/dist/packages/cli/package.json +2 -2
  12. package/dist/packages/cli/package.json.bak +2 -2
  13. package/dist/packages/core/CHANGELOG.md +8 -0
  14. package/dist/packages/core/package.json +1 -1
  15. package/dist/packages/core/package.json.bak +1 -1
  16. package/dist/packages/design-tokens/CHANGELOG.md +8 -0
  17. package/dist/packages/design-tokens/package.json +1 -1
  18. package/dist/packages/devtools/browser/devtools.html +19 -0
  19. package/dist/packages/devtools/browser/devtools.js +10 -0
  20. package/dist/packages/devtools/browser/main.js +4 -0
  21. package/dist/packages/devtools/browser/manifest.json +24 -0
  22. package/dist/packages/devtools/browser/panel.html +13 -0
  23. package/dist/packages/devtools/prerendered-routes.json +3 -0
  24. package/dist/packages/playground/browser/{chunk-3_MCIwP9.js → chunk-0atszSIl.js} +1 -1
  25. package/dist/packages/playground/browser/{chunk-Db-66oBG.js → chunk-BU8WdTDW.js} +1 -1
  26. package/dist/packages/playground/browser/{chunk-DTWyZJQn.js → chunk-Bl2XUkt9.js} +1 -1
  27. package/dist/packages/playground/browser/{chunk-BfRxMFzC.js → chunk-BnQm5_3H.js} +1 -1
  28. package/dist/packages/playground/browser/{chunk-BNWpW9dI.js → chunk-BoABL5Us.js} +1 -1
  29. package/dist/packages/playground/browser/{chunk-DaAkzkdj.js → chunk-CCQ1RKCp.js} +1 -1
  30. package/dist/packages/playground/browser/{chunk-ukC1CGrX.js → chunk-CCwqbbdB.js} +1 -1
  31. package/dist/packages/playground/browser/{chunk-CxuQUL9T.js → chunk-Cs5LSVZ4.js} +1 -1
  32. package/dist/packages/playground/browser/{chunk-Cw0imz7o.js → chunk-D-D574YC.js} +1 -1
  33. package/dist/packages/playground/browser/{chunk-CTOW2Wjn.js → chunk-DGDHcpBL.js} +1 -1
  34. package/dist/packages/playground/browser/{chunk-Csk-gzld.js → chunk-DblWUBCc.js} +1 -1
  35. package/dist/packages/playground/browser/{chunk-GJ-FyJ-_.js → chunk-DmH1uLzp.js} +1 -1
  36. package/dist/packages/playground/browser/{chunk-ChHaxBw8.js → chunk-DoeRt62n.js} +1 -1
  37. package/dist/packages/playground/browser/{chunk-CbkfOqNs.js → chunk-fvcTt7Iz.js} +1 -1
  38. package/dist/packages/playground/browser/{chunk-ZMJY_Wrl.js → chunk-iUjqeDU_.js} +1 -1
  39. package/dist/packages/playground/browser/index.html +1 -1
  40. package/dist/packages/playground/browser/main-YU4BRK3G.js +711 -0
  41. package/graph.json +1052 -0
  42. package/package.json +2 -1
  43. package/packages/angular-renderer/CHANGELOG.md +21 -0
  44. package/packages/angular-renderer/package.json +3 -3
  45. package/packages/angular-renderer/src/devtools/devtools-bridge.ts +67 -5
  46. package/packages/angular-renderer/src/index.ts +1 -0
  47. package/packages/angular-renderer/src/test-setup.ts +9 -0
  48. package/packages/cli/CHANGELOG.md +8 -0
  49. package/packages/cli/package.json +2 -2
  50. package/packages/core/CHANGELOG.md +8 -0
  51. package/packages/core/package.json +1 -1
  52. package/packages/design-tokens/CHANGELOG.md +8 -0
  53. package/packages/design-tokens/package.json +1 -1
  54. package/packages/devtools/CHANGELOG.md +22 -0
  55. package/packages/devtools/devtools.html +19 -0
  56. package/packages/devtools/devtools.js +10 -0
  57. package/packages/devtools/manifest.json +24 -0
  58. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/deps___vitest_vm__/_metadata.json +8 -0
  59. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/deps___vitest_vm__/package.json +3 -0
  60. package/packages/devtools/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  61. package/packages/devtools/package.json +14 -0
  62. package/packages/devtools/panel.html +16 -0
  63. package/packages/devtools/src/background/background.ts +57 -0
  64. package/packages/devtools/src/content-script/content-script.ts +43 -0
  65. package/packages/devtools/src/content-script/injected.ts +124 -0
  66. package/packages/devtools/src/devtools-panel/app.component.ts +302 -0
  67. package/packages/devtools/src/devtools-panel/component-tree/component-tree.component.ts +108 -0
  68. package/packages/devtools/src/devtools-panel/main.ts +7 -0
  69. package/packages/devtools/src/devtools-panel/metadata-detail/metadata-detail.component.ts +160 -0
  70. package/packages/devtools/src/test-setup.ts +49 -0
  71. package/packages/devtools/src/types/messages.ts +99 -0
  72. package/packages/devtools/vitest.config.ts +14 -0
  73. package/packages/playground/CHANGELOG.md +20 -0
  74. package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/deps___vitest_vm__/_metadata.json +3 -3
  75. package/packages/playground/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  76. package/packages/playground/package.json +5 -2
  77. package/packages/playground/src/main.ts +83 -0
  78. package/packages/playground/src/preview/preview.service.ts +18 -0
  79. package/dist/packages/playground/browser/main-V25Z5OWF.js +0 -711
@@ -0,0 +1,302 @@
1
+ import { Component, OnInit, signal } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import type { DevToolsMessage, ErrorTelemetryResponseMessage } from '../types/messages';
4
+ import { ComponentTreeComponent } from './component-tree/component-tree.component';
5
+ import { MetadataDetailComponent } from './metadata-detail/metadata-detail.component';
6
+ import type {
7
+ RenderingPath,
8
+ MetadataSource,
9
+ ResolutionChain,
10
+ ErrorContext,
11
+ } from '@origo/angular-renderer';
12
+
13
+ export type ConnectionState = 'INITIALIZING' | 'CONNECTED' | 'NOT_AVAILABLE' | 'CONNECTION_LOST';
14
+
15
+ @Component({
16
+ selector: 'origo-devtools-root',
17
+ standalone: true,
18
+ imports: [CommonModule, ComponentTreeComponent, MetadataDetailComponent],
19
+ template: `
20
+ <div class="devtools-container">
21
+ <header class="header">
22
+ <div class="title-group">
23
+ <h1>Origo DevTools</h1>
24
+ <button class="icon-btn" (click)="reconnect()" title="Refresh Connection">🔄</button>
25
+ </div>
26
+ <span class="status-badge" [class]="connectionState().toLowerCase()">
27
+ {{ connectionState() }}
28
+ </span>
29
+ </header>
30
+
31
+ <main class="main-content">
32
+ <ng-container *ngIf="connectionState() === 'CONNECTED'; else fallback">
33
+ <div class="split-pane">
34
+ <div class="tree-pane">
35
+ <origo-devtools-tree
36
+ [renderingPath]="renderingPath()"
37
+ (nodeSelected)="onNodeSelected($event)"
38
+ ></origo-devtools-tree>
39
+ </div>
40
+ <div class="detail-pane">
41
+ <origo-devtools-metadata-detail
42
+ [metadataSource]="metadataSource()"
43
+ [resolutionChain]="resolutionChain()"
44
+ >
45
+ </origo-devtools-metadata-detail>
46
+ </div>
47
+ </div>
48
+
49
+ <div *ngIf="errorTelemetry().length > 0" class="error-telemetry-pane">
50
+ <h3>Error Telemetry</h3>
51
+ <ul>
52
+ <li *ngFor="let err of errorTelemetry()">
53
+ <strong>{{ err.message }}</strong
54
+ ><br />
55
+ <small>{{ err.badlPath }}</small>
56
+ </li>
57
+ </ul>
58
+ </div>
59
+ </ng-container>
60
+
61
+ <ng-template #fallback>
62
+ <div class="fallback-container">
63
+ <div *ngIf="connectionState() === 'NOT_AVAILABLE'" class="message not-available">
64
+ <h2>Origo DevTools Not Available</h2>
65
+ <p>Please run the application in development mode.</p>
66
+ </div>
67
+ <div *ngIf="connectionState() === 'CONNECTION_LOST'" class="message connection-lost">
68
+ <h2>Connection Lost</h2>
69
+ <p>Trying to reconnect to the application...</p>
70
+ </div>
71
+ <div *ngIf="connectionState() === 'INITIALIZING'" class="message initializing">
72
+ <h2>Connecting...</h2>
73
+ </div>
74
+ </div>
75
+ </ng-template>
76
+ </main>
77
+ </div>
78
+ `,
79
+ styles: [
80
+ `
81
+ :host {
82
+ display: block;
83
+ height: 100vh;
84
+ font-family: sans-serif;
85
+ overflow: hidden;
86
+ background-color: var(--origo-color-bg-base, #ffffff);
87
+ color: var(--origo-color-text-base, #333333);
88
+ }
89
+ .devtools-container {
90
+ display: flex;
91
+ flex-direction: column;
92
+ height: 100%;
93
+ }
94
+ .header {
95
+ padding: 8px 16px;
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ border-bottom: 1px solid var(--origo-color-border, #e0e0e0);
100
+ background-color: var(--origo-color-bg-subtle, #f5f5f5);
101
+ }
102
+ .title-group {
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 8px;
106
+ }
107
+ .title-group h1 {
108
+ margin: 0;
109
+ font-size: 14px;
110
+ font-weight: 600;
111
+ }
112
+ .icon-btn {
113
+ background: none;
114
+ border: none;
115
+ cursor: pointer;
116
+ font-size: 14px;
117
+ padding: 4px;
118
+ border-radius: 4px;
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ }
123
+ .icon-btn:hover {
124
+ background-color: var(--origo-color-bg-hover, #e0e0e0);
125
+ }
126
+ .status-badge {
127
+ font-size: 11px;
128
+ padding: 2px 6px;
129
+ border-radius: 4px;
130
+ background: var(--origo-color-bg-muted, #ccc);
131
+ color: var(--origo-color-text-base, #333);
132
+ }
133
+ .status-badge.connected {
134
+ background: var(--origo-color-success-bg, #4caf50);
135
+ color: var(--origo-color-success-text, white);
136
+ }
137
+ .status-badge.not_available {
138
+ background: var(--origo-color-error-bg, #f44336);
139
+ color: var(--origo-color-error-text, white);
140
+ }
141
+ .status-badge.connection_lost {
142
+ background: var(--origo-color-warning-bg, #ff9800);
143
+ color: var(--origo-color-warning-text, white);
144
+ }
145
+ .main-content {
146
+ flex: 1;
147
+ overflow: hidden;
148
+ }
149
+ .split-pane {
150
+ display: flex;
151
+ height: 100%;
152
+ }
153
+ .tree-pane {
154
+ flex: 1;
155
+ border-right: 1px solid var(--origo-color-border, #e0e0e0);
156
+ overflow-y: auto;
157
+ }
158
+ .detail-pane {
159
+ flex: 1;
160
+ overflow-y: auto;
161
+ }
162
+ .fallback-container {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ height: 100%;
167
+ }
168
+ .message {
169
+ text-align: center;
170
+ padding: 24px;
171
+ border-radius: 8px;
172
+ background: var(--origo-color-bg-subtle, #f5f5f5);
173
+ }
174
+ `,
175
+ ],
176
+ })
177
+ export class AppComponent implements OnInit {
178
+ connectionState = signal<ConnectionState>('INITIALIZING');
179
+
180
+ renderingPath = signal<RenderingPath | null>(null);
181
+ metadataSource = signal<MetadataSource | null>(null);
182
+ resolutionChain = signal<ResolutionChain | null>(null);
183
+ errorTelemetry = signal<ErrorContext[]>([]);
184
+
185
+ private backgroundPageConnection!: chrome.runtime.Port;
186
+ private reconnectTimeoutId: any;
187
+
188
+ ngOnInit() {
189
+ this.connectToBackground();
190
+ }
191
+
192
+ reconnect() {
193
+ if (this.connectionState() === 'INITIALIZING') return;
194
+ this.connectionState.set('INITIALIZING');
195
+ if (this.backgroundPageConnection) {
196
+ try {
197
+ this.backgroundPageConnection.disconnect();
198
+ } catch (e) {
199
+ console.warn('Error disconnecting from background page:', e);
200
+ }
201
+ }
202
+ if (this.reconnectTimeoutId) {
203
+ clearTimeout(this.reconnectTimeoutId);
204
+ }
205
+ // Small delay to ensure cleanup before reconnecting
206
+ this.reconnectTimeoutId = setTimeout(() => {
207
+ this.connectToBackground();
208
+ }, 100);
209
+ }
210
+
211
+ private connectToBackground() {
212
+ if (
213
+ typeof chrome === 'undefined' ||
214
+ !chrome.runtime ||
215
+ typeof chrome.runtime.connect !== 'function'
216
+ ) {
217
+ this.connectionState.set('NOT_AVAILABLE');
218
+ return;
219
+ }
220
+
221
+ if (!chrome.devtools?.inspectedWindow) {
222
+ this.connectionState.set('NOT_AVAILABLE');
223
+ return;
224
+ }
225
+
226
+ this.backgroundPageConnection = chrome.runtime.connect({
227
+ name: 'origo-devtools-panel',
228
+ });
229
+
230
+ try {
231
+ this.backgroundPageConnection.postMessage({
232
+ name: 'init',
233
+ tabId: chrome.devtools.inspectedWindow.tabId,
234
+ });
235
+ } catch (e) {
236
+ this.connectionState.set('NOT_AVAILABLE');
237
+ return;
238
+ }
239
+
240
+ this.backgroundPageConnection.onMessage.addListener((msg: any) => {
241
+ if (msg.source === 'origo-devtools-injected') {
242
+ this.handleMessage(msg.payload as DevToolsMessage);
243
+ }
244
+ });
245
+
246
+ this.backgroundPageConnection.onDisconnect.addListener(() => {
247
+ this.connectionState.set('CONNECTION_LOST');
248
+ });
249
+
250
+ // Send a ping to check availability
251
+ this.sendMessage({ type: 'PING' });
252
+ }
253
+
254
+ private sendMessage(message: unknown) {
255
+ if (this.connectionState() === 'CONNECTION_LOST') return;
256
+ if (this.backgroundPageConnection && chrome.devtools?.inspectedWindow) {
257
+ try {
258
+ this.backgroundPageConnection.postMessage({
259
+ tabId: chrome.devtools.inspectedWindow.tabId,
260
+ data: message,
261
+ });
262
+ } catch (e) {
263
+ console.warn('Failed to send message to background page', e);
264
+ }
265
+ }
266
+ }
267
+
268
+ private handleMessage(message: DevToolsMessage) {
269
+ switch (message.type) {
270
+ case 'NOT_AVAILABLE':
271
+ this.connectionState.set('NOT_AVAILABLE');
272
+ break;
273
+ case 'PONG':
274
+ this.connectionState.set('CONNECTED');
275
+ // Initialize by requesting rendering path
276
+ this.sendMessage({ type: 'GET_RENDERING_PATH', payload: { badlPath: '' } });
277
+ // Also fetch initial error telemetry
278
+ this.sendMessage({ type: 'GET_ERROR_TELEMETRY', payload: {} });
279
+ break;
280
+ case 'ERROR_TELEMETRY_RESPONSE':
281
+ this.errorTelemetry.set((message as ErrorTelemetryResponseMessage).payload || []);
282
+ break;
283
+ case 'RENDERING_PATH_RESPONSE':
284
+ this.renderingPath.set(message.payload);
285
+ break;
286
+ case 'METADATA_SOURCE_RESPONSE':
287
+ this.metadataSource.set(message.payload);
288
+ break;
289
+ case 'RESOLUTION_CHAIN_RESPONSE':
290
+ this.resolutionChain.set(message.payload);
291
+ break;
292
+ default:
293
+ console.warn('Unhandled devtools message type:', message.type);
294
+ break;
295
+ }
296
+ }
297
+
298
+ onNodeSelected(badlPath: string) {
299
+ this.sendMessage({ type: 'GET_METADATA_SOURCE', payload: { badlPath } });
300
+ this.sendMessage({ type: 'GET_RESOLUTION_CHAIN', payload: { badlPath } });
301
+ }
302
+ }
@@ -0,0 +1,108 @@
1
+ import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import type { RenderingPath } from '@origo/angular-renderer';
4
+
5
+ interface FlatNode {
6
+ name: string;
7
+ badlPath: string;
8
+ depth: number;
9
+ }
10
+
11
+ @Component({
12
+ selector: 'origo-devtools-tree',
13
+ standalone: true,
14
+ imports: [CommonModule],
15
+ template: `
16
+ <div class="tree-container">
17
+ <h3>Component Tree</h3>
18
+ <div *ngIf="!renderingPath" class="empty-state">No rendering path data available.</div>
19
+
20
+ <div *ngIf="renderingPath" class="tree-content">
21
+ <!-- Flattened list is virtualized-ready -->
22
+ <div class="node" *ngFor="let node of flatTree" [style.padding-left.px]="node.depth * 16">
23
+ <div class="node-header" (click)="selectNode(node.badlPath)">
24
+ <span class="node-name">{{ node.name }}</span>
25
+ <span class="node-path">{{ node.badlPath }}</span>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ `,
31
+ styles: [
32
+ `
33
+ .tree-container {
34
+ padding: 16px;
35
+ }
36
+ h3 {
37
+ margin-top: 0;
38
+ font-size: 14px;
39
+ color: var(--origo-color-text-muted, #666);
40
+ }
41
+ .empty-state {
42
+ padding: 24px;
43
+ text-align: center;
44
+ color: var(--origo-color-text-muted, #666);
45
+ }
46
+ .node-header {
47
+ cursor: pointer;
48
+ padding: 4px 8px;
49
+ border-radius: 4px;
50
+ }
51
+ .node-header:hover {
52
+ background-color: var(--origo-color-bg-hover, #f0f0f0);
53
+ }
54
+ .node-name {
55
+ font-weight: 600;
56
+ margin-right: 8px;
57
+ }
58
+ .node-path {
59
+ font-size: 11px;
60
+ color: var(--origo-color-text-muted, #666);
61
+ }
62
+ .node-depth-limit {
63
+ font-size: 11px;
64
+ color: var(--origo-color-text-muted, #666);
65
+ padding-left: 16px;
66
+ font-style: italic;
67
+ }
68
+ `,
69
+ ],
70
+ })
71
+ export class ComponentTreeComponent implements OnChanges {
72
+ @Input() renderingPath: RenderingPath | null = null;
73
+ @Output() nodeSelected = new EventEmitter<string>();
74
+
75
+ flatTree: FlatNode[] = [];
76
+
77
+ ngOnChanges(changes: SimpleChanges) {
78
+ if (changes['renderingPath'] && this.renderingPath) {
79
+ this.flatTree = this.flatten(this.renderingPath);
80
+ } else if (!this.renderingPath) {
81
+ this.flatTree = [];
82
+ }
83
+ }
84
+
85
+ flatten(node: RenderingPath, depth = 0, seen = new Set<RenderingPath>()): FlatNode[] {
86
+ if (!node || seen.has(node) || typeof node !== 'object') return [];
87
+ seen.add(node);
88
+
89
+ // Guard against missing properties or empty string
90
+ if (node.badlPath === undefined || node.badlPath === null || node.badlPath === '') return [];
91
+
92
+ const flatNode: FlatNode = { name: node.name || 'Unknown', badlPath: node.badlPath, depth };
93
+ const result = [flatNode];
94
+
95
+ if (Array.isArray(node.children)) {
96
+ for (const child of node.children) {
97
+ result.push(...this.flatten(child, depth + 1, seen));
98
+ }
99
+ }
100
+ return result;
101
+ }
102
+
103
+ selectNode(badlPath: string) {
104
+ if (badlPath) {
105
+ this.nodeSelected.emit(badlPath);
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,7 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { provideZonelessChangeDetection } from '@angular/core';
3
+ import { AppComponent } from './app.component';
4
+
5
+ bootstrapApplication(AppComponent, {
6
+ providers: [provideZonelessChangeDetection()],
7
+ }).catch(err => console.error(err));
@@ -0,0 +1,160 @@
1
+ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import type { MetadataSource, ResolutionChain } from '@origo/angular-renderer';
4
+
5
+ @Component({
6
+ selector: 'origo-devtools-metadata-detail',
7
+ standalone: true,
8
+ imports: [CommonModule],
9
+ template: `
10
+ <div class="detail-container">
11
+ <h3>Metadata Details</h3>
12
+
13
+ <div *ngIf="!metadataSource && !resolutionChain" class="empty-state">
14
+ Select a node in the component tree to view details.
15
+ </div>
16
+
17
+ <div *ngIf="metadataSource" class="section">
18
+ <h4>Source Definition</h4>
19
+ <div class="data-table-container">
20
+ <table class="data-table">
21
+ <tbody>
22
+ <tr *ngFor="let row of flatMetadata">
23
+ <td class="key-col">{{ row.path }}</td>
24
+ <td class="val-col">{{ row.value }}</td>
25
+ </tr>
26
+ </tbody>
27
+ </table>
28
+ </div>
29
+ </div>
30
+
31
+ <div *ngIf="resolutionChain" class="section">
32
+ <h4>Resolution Chain</h4>
33
+ <div class="data-table-container">
34
+ <table class="data-table">
35
+ <tbody>
36
+ <tr *ngFor="let row of flatResolution">
37
+ <td class="key-col">{{ row.path }}</td>
38
+ <td class="val-col">{{ row.value }}</td>
39
+ </tr>
40
+ </tbody>
41
+ </table>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Adding placeholder for theme token resolutions -->
46
+ <div class="section">
47
+ <h4>Theme Token Resolutions</h4>
48
+ <div class="empty-state">No theme tokens mapped to this element.</div>
49
+ </div>
50
+ </div>
51
+ `,
52
+ styles: [
53
+ `
54
+ .detail-container {
55
+ padding: 16px;
56
+ }
57
+ h3 {
58
+ margin-top: 0;
59
+ font-size: 14px;
60
+ color: var(--origo-color-text-muted, #666);
61
+ }
62
+ h4 {
63
+ font-size: 13px;
64
+ margin-bottom: 8px;
65
+ }
66
+ .empty-state {
67
+ padding: 24px;
68
+ text-align: center;
69
+ color: var(--origo-color-text-muted, #666);
70
+ }
71
+ .section {
72
+ margin-bottom: 24px;
73
+ }
74
+ .data-table-container {
75
+ background: var(--origo-color-bg-subtle, #f5f5f5);
76
+ border: 1px solid var(--origo-color-border, #e0e0e0);
77
+ border-radius: 4px;
78
+ overflow-x: auto;
79
+ }
80
+ .data-table {
81
+ width: 100%;
82
+ border-collapse: collapse;
83
+ font-family: var(--origo-font-mono, monospace);
84
+ font-size: var(--origo-font-size-sm, 12px);
85
+ }
86
+ .data-table td {
87
+ padding: 4px 8px;
88
+ border-bottom: 1px solid var(--origo-color-border-subtle, #eee);
89
+ }
90
+ .data-table tr:last-child td {
91
+ border-bottom: none;
92
+ }
93
+ .key-col {
94
+ font-weight: 600;
95
+ color: var(--origo-color-text-muted, #666);
96
+ white-space: nowrap;
97
+ }
98
+ .val-col {
99
+ color: var(--origo-color-text-base, #333);
100
+ word-break: break-all;
101
+ }
102
+ `,
103
+ ],
104
+ })
105
+ export class MetadataDetailComponent implements OnChanges {
106
+ @Input() metadataSource: MetadataSource | null = null;
107
+ @Input() resolutionChain: ResolutionChain | null = null;
108
+
109
+ flatMetadata: { path: string; value: string }[] = [];
110
+ flatResolution: { path: string; value: string }[] = [];
111
+
112
+ ngOnChanges(changes: SimpleChanges) {
113
+ if (changes['metadataSource']) {
114
+ this.flatMetadata = this.flattenObject(this.metadataSource);
115
+ }
116
+ if (changes['resolutionChain']) {
117
+ this.flatResolution = this.flattenObject(this.resolutionChain);
118
+ }
119
+ }
120
+
121
+ // Flattens an object to an array of paths and values to avoid recursive templates and cycle bugs
122
+ private flattenObject(
123
+ obj: unknown,
124
+ prefix = '',
125
+ seen = new Set(),
126
+ result: { path: string; value: string }[] = []
127
+ ): { path: string; value: string }[] {
128
+ if (obj === null || obj === undefined) {
129
+ if (prefix) result.push({ path: prefix, value: String(obj) });
130
+ return result;
131
+ }
132
+
133
+ if (typeof obj !== 'object') {
134
+ result.push({ path: prefix, value: String(obj) });
135
+ return result;
136
+ }
137
+
138
+ if (seen.has(obj)) {
139
+ result.push({ path: prefix, value: '[Circular]' });
140
+ return result;
141
+ }
142
+ seen.add(obj);
143
+
144
+ const isArray = Array.isArray(obj);
145
+ const keys = Object.keys(obj as any);
146
+
147
+ if (keys.length === 0) {
148
+ result.push({ path: prefix, value: isArray ? '[]' : '{}' });
149
+ } else {
150
+ for (const key of keys) {
151
+ const val = (obj as any)[key];
152
+ const newPrefix = prefix ? (isArray ? `${prefix}[${key}]` : `${prefix}.${key}`) : key;
153
+ this.flattenObject(val, newPrefix, seen, result);
154
+ }
155
+ }
156
+
157
+ seen.delete(obj);
158
+ return result;
159
+ }
160
+ }
@@ -0,0 +1,49 @@
1
+ import '@analogjs/vite-plugin-angular/setup-vitest';
2
+ import { getTestBed } from '@angular/core/testing';
3
+ import {
4
+ BrowserDynamicTestingModule,
5
+ platformBrowserDynamicTesting,
6
+ } from '@angular/platform-browser-dynamic/testing';
7
+
8
+ // Manual initialization just in case setup-vitest fails
9
+ try {
10
+ getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
11
+ } catch (e) {
12
+ if (!(e as Error).message.includes('has already been')) {
13
+ throw e;
14
+ }
15
+ }
16
+
17
+ beforeEach(() => {
18
+ // Mock chrome API for tests
19
+ global.chrome = {
20
+ runtime: {
21
+ connect: vi.fn(),
22
+ sendMessage: vi.fn(),
23
+ onConnect: {
24
+ addListener: vi.fn(),
25
+ },
26
+ onMessage: {
27
+ addListener: vi.fn(),
28
+ },
29
+ getURL: vi.fn(),
30
+ },
31
+ tabs: {
32
+ sendMessage: vi.fn(),
33
+ },
34
+ devtools: {
35
+ panels: {
36
+ create: vi.fn(),
37
+ },
38
+ inspectedWindow: {
39
+ tabId: 123,
40
+ },
41
+ },
42
+ } as unknown;
43
+ });
44
+
45
+ afterEach(() => {
46
+ getTestBed().resetTestingModule();
47
+ vi.restoreAllMocks();
48
+ delete (global as any).chrome;
49
+ });