@ng-render/angular 0.1.0

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.
@@ -0,0 +1,1527 @@
1
+ import { defineSchema, getByPath, setByPath, evaluateVisibility, resolveAction, executeAction, runValidation, resolveBindings, resolveElementProps, resolveActionParam, removeByPath, createMixedStreamParser, applySpecPatch, SPEC_DATA_PART_TYPE, nestedToFlat } from '@json-render/core';
2
+ export { defineCatalog } from '@json-render/core';
3
+ import * as i0 from '@angular/core';
4
+ import { InjectionToken, makeEnvironmentProviders, signal, Injectable, inject, computed, ErrorHandler, input, ViewContainerRef, Injector, EnvironmentInjector, effect, createEnvironmentInjector, ChangeDetectionStrategy, Component, Directive } from '@angular/core';
5
+
6
+ /**
7
+ * The schema for @ng-render/angular.
8
+ *
9
+ * Identical to the React schema — the schema is framework-agnostic.
10
+ */
11
+ const schema = defineSchema((s) => ({
12
+ spec: s.object({
13
+ root: s.string(),
14
+ elements: s.record(s.object({
15
+ type: s.ref('catalog.components'),
16
+ props: s.propsOf('catalog.components'),
17
+ children: s.array(s.string()),
18
+ visible: s.any(),
19
+ })),
20
+ }),
21
+ catalog: s.object({
22
+ components: s.map({
23
+ props: s.zod(),
24
+ slots: s.array(s.string()),
25
+ description: s.string(),
26
+ example: s.any(),
27
+ }),
28
+ actions: s.map({
29
+ params: s.zod(),
30
+ description: s.string(),
31
+ }),
32
+ }),
33
+ }), {
34
+ builtInActions: [
35
+ {
36
+ name: 'setState',
37
+ description: 'Update a value in the state model at the given statePath. Params: { statePath: string, value: any }',
38
+ },
39
+ {
40
+ name: 'pushState',
41
+ description: 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
42
+ },
43
+ {
44
+ name: 'removeState',
45
+ description: 'Remove an item from an array in state by index. Params: { statePath: string, index: number }',
46
+ },
47
+ ],
48
+ defaultRules: [
49
+ 'CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: [\'a\', \'b\'], then elements \'a\' and \'b\' MUST exist. A missing child element causes that entire branch of the UI to be invisible.',
50
+ 'SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.',
51
+ 'CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.',
52
+ 'CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',
53
+ 'When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).',
54
+ 'When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.',
55
+ 'Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.',
56
+ 'For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.',
57
+ 'Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.',
58
+ ],
59
+ });
60
+
61
+ const NG_RENDER_REGISTRY = new InjectionToken('NG_RENDER_REGISTRY');
62
+ const NG_RENDER_ACTION_HANDLERS = new InjectionToken('NG_RENDER_ACTION_HANDLERS');
63
+ const NG_RENDER_NAVIGATE = new InjectionToken('NG_RENDER_NAVIGATE');
64
+ const NG_RENDER_FALLBACK = new InjectionToken('NG_RENDER_FALLBACK');
65
+
66
+ /**
67
+ * Create a type-safe registry from a catalog.
68
+ * Maps component names to Angular component classes.
69
+ */
70
+ function defineRegistry(_catalog, options) {
71
+ const registry = {};
72
+ if (options.components) {
73
+ for (const [name, componentType] of Object.entries(options.components)) {
74
+ registry[name] = componentType;
75
+ }
76
+ }
77
+ const actionMap = options.actions
78
+ ? Object.entries(options.actions)
79
+ : [];
80
+ const handlers = (getSetState, getState) => {
81
+ const result = {};
82
+ for (const [name, actionFn] of actionMap) {
83
+ result[name] = async (params) => {
84
+ const setState = getSetState();
85
+ const state = getState();
86
+ if (setState) {
87
+ await actionFn(params, setState, state);
88
+ }
89
+ };
90
+ }
91
+ return result;
92
+ };
93
+ const executeAction = async (actionName, params, setState, state = {}) => {
94
+ const entry = actionMap.find(([name]) => name === actionName);
95
+ if (entry) {
96
+ await entry[1](params, setState, state);
97
+ }
98
+ else {
99
+ console.warn(`Unknown action: ${actionName}`);
100
+ }
101
+ };
102
+ return { registry, handlers, executeAction };
103
+ }
104
+ /**
105
+ * Provide ng-render configuration at the environment level.
106
+ * Replaces React's <JSONUIProvider>.
107
+ */
108
+ function provideNgRender(options) {
109
+ const providers = [
110
+ { provide: NG_RENDER_REGISTRY, useValue: options.registry },
111
+ ];
112
+ if (options.handlers) {
113
+ providers.push({
114
+ provide: NG_RENDER_ACTION_HANDLERS,
115
+ useValue: options.handlers,
116
+ });
117
+ }
118
+ if (options.navigate) {
119
+ providers.push({
120
+ provide: NG_RENDER_NAVIGATE,
121
+ useValue: options.navigate,
122
+ });
123
+ }
124
+ if (options.fallback) {
125
+ providers.push({
126
+ provide: NG_RENDER_FALLBACK,
127
+ useValue: options.fallback,
128
+ });
129
+ }
130
+ return makeEnvironmentProviders(providers);
131
+ }
132
+
133
+ class SpecStateService {
134
+ _state = signal({}, ...(ngDevMode ? [{ debugName: "_state" }] : []));
135
+ state = this._state.asReadonly();
136
+ get(path) {
137
+ return getByPath(this._state(), path);
138
+ }
139
+ set(path, value) {
140
+ this._state.update((prev) => {
141
+ const next = { ...prev };
142
+ setByPath(next, path, value);
143
+ return next;
144
+ });
145
+ }
146
+ update(updates) {
147
+ this._state.update((prev) => {
148
+ const next = { ...prev };
149
+ for (const [path, value] of Object.entries(updates)) {
150
+ setByPath(next, path, value);
151
+ }
152
+ return next;
153
+ });
154
+ }
155
+ initialize(initialState) {
156
+ this._state.set({ ...initialState });
157
+ }
158
+ mergeInitialState(state) {
159
+ this._state.update((prev) => ({ ...prev, ...state }));
160
+ }
161
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SpecStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
162
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SpecStateService });
163
+ }
164
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: SpecStateService, decorators: [{
165
+ type: Injectable
166
+ }] });
167
+
168
+ class VisibilityService {
169
+ stateService = inject(SpecStateService);
170
+ ctx = computed(() => ({
171
+ stateModel: this.stateService.state(),
172
+ }), ...(ngDevMode ? [{ debugName: "ctx" }] : []));
173
+ isVisible(condition) {
174
+ return evaluateVisibility(condition, this.ctx());
175
+ }
176
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: VisibilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
177
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: VisibilityService });
178
+ }
179
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: VisibilityService, decorators: [{
180
+ type: Injectable
181
+ }] });
182
+
183
+ let idCounter = 0;
184
+ function generateUniqueId() {
185
+ idCounter += 1;
186
+ return `${Date.now()}-${idCounter}`;
187
+ }
188
+ function deepResolveValue(value, get) {
189
+ if (value === null || value === undefined)
190
+ return value;
191
+ if (value === '$id')
192
+ return generateUniqueId();
193
+ if (typeof value === 'object' && !Array.isArray(value)) {
194
+ const obj = value;
195
+ const keys = Object.keys(obj);
196
+ if (keys.length === 1 && typeof obj['$state'] === 'string') {
197
+ return get(obj['$state']);
198
+ }
199
+ if (keys.length === 1 && '$id' in obj) {
200
+ return generateUniqueId();
201
+ }
202
+ }
203
+ if (Array.isArray(value)) {
204
+ return value.map((item) => deepResolveValue(item, get));
205
+ }
206
+ if (typeof value === 'object') {
207
+ const resolved = {};
208
+ for (const [key, val] of Object.entries(value)) {
209
+ resolved[key] = deepResolveValue(val, get);
210
+ }
211
+ return resolved;
212
+ }
213
+ return value;
214
+ }
215
+ class ActionDispatcherService {
216
+ stateService = inject(SpecStateService);
217
+ handlers = inject(NG_RENDER_ACTION_HANDLERS, { optional: true }) ?? {};
218
+ navigate = inject(NG_RENDER_NAVIGATE, {
219
+ optional: true,
220
+ });
221
+ loadingActions = signal(new Set(), ...(ngDevMode ? [{ debugName: "loadingActions" }] : []));
222
+ pendingConfirmation = signal(null, ...(ngDevMode ? [{ debugName: "pendingConfirmation" }] : []));
223
+ async execute(binding) {
224
+ const state = this.stateService.state();
225
+ const resolved = resolveAction(binding, state);
226
+ // Built-in: setState
227
+ if (resolved.action === 'setState' && resolved.params) {
228
+ const statePath = resolved.params['statePath'];
229
+ const value = resolved.params['value'];
230
+ if (statePath) {
231
+ this.stateService.set(statePath, value);
232
+ }
233
+ return;
234
+ }
235
+ // Built-in: pushState
236
+ if (resolved.action === 'pushState' && resolved.params) {
237
+ const statePath = resolved.params['statePath'];
238
+ const rawValue = resolved.params['value'];
239
+ if (statePath) {
240
+ const resolvedValue = deepResolveValue(rawValue, (p) => this.stateService.get(p));
241
+ const arr = this.stateService.get(statePath) ?? [];
242
+ this.stateService.set(statePath, [...arr, resolvedValue]);
243
+ const clearStatePath = resolved.params['clearStatePath'];
244
+ if (clearStatePath) {
245
+ this.stateService.set(clearStatePath, '');
246
+ }
247
+ }
248
+ return;
249
+ }
250
+ // Built-in: removeState
251
+ if (resolved.action === 'removeState' && resolved.params) {
252
+ const statePath = resolved.params['statePath'];
253
+ const index = resolved.params['index'];
254
+ if (statePath !== undefined && index !== undefined) {
255
+ const arr = this.stateService.get(statePath) ?? [];
256
+ this.stateService.set(statePath, arr.filter((_, i) => i !== index));
257
+ }
258
+ return;
259
+ }
260
+ // Built-in: push (navigation)
261
+ if (resolved.action === 'push' && resolved.params) {
262
+ const screen = resolved.params['screen'];
263
+ if (screen) {
264
+ const currentScreen = this.stateService.get('/currentScreen');
265
+ const navStack = this.stateService.get('/navStack') ?? [];
266
+ if (currentScreen) {
267
+ this.stateService.set('/navStack', [...navStack, currentScreen]);
268
+ }
269
+ else {
270
+ this.stateService.set('/navStack', [...navStack, '']);
271
+ }
272
+ this.stateService.set('/currentScreen', screen);
273
+ }
274
+ return;
275
+ }
276
+ // Built-in: pop (navigation)
277
+ if (resolved.action === 'pop') {
278
+ const navStack = this.stateService.get('/navStack') ?? [];
279
+ if (navStack.length > 0) {
280
+ const previousScreen = navStack[navStack.length - 1];
281
+ this.stateService.set('/navStack', navStack.slice(0, -1));
282
+ if (previousScreen) {
283
+ this.stateService.set('/currentScreen', previousScreen);
284
+ }
285
+ else {
286
+ this.stateService.set('/currentScreen', undefined);
287
+ }
288
+ }
289
+ return;
290
+ }
291
+ const handler = this.handlers[resolved.action];
292
+ if (!handler) {
293
+ console.warn(`No handler registered for action: ${resolved.action}`);
294
+ return;
295
+ }
296
+ // Confirmation flow
297
+ if (resolved.confirm) {
298
+ return new Promise((resolve, reject) => {
299
+ this.pendingConfirmation.set({
300
+ action: resolved,
301
+ handler,
302
+ resolve: () => {
303
+ this.pendingConfirmation.set(null);
304
+ resolve();
305
+ },
306
+ reject: () => {
307
+ this.pendingConfirmation.set(null);
308
+ reject(new Error('Action cancelled'));
309
+ },
310
+ });
311
+ }).then(async () => {
312
+ await this.executeHandler(resolved, handler);
313
+ });
314
+ }
315
+ await this.executeHandler(resolved, handler);
316
+ }
317
+ confirm() {
318
+ this.pendingConfirmation()?.resolve();
319
+ }
320
+ cancel() {
321
+ this.pendingConfirmation()?.reject();
322
+ }
323
+ async executeHandler(resolved, handler) {
324
+ this.loadingActions.update((prev) => {
325
+ const next = new Set(prev);
326
+ next.add(resolved.action);
327
+ return next;
328
+ });
329
+ try {
330
+ await executeAction({
331
+ action: resolved,
332
+ handler,
333
+ setState: (path, value) => this.stateService.set(path, value),
334
+ navigate: this.navigate ?? undefined,
335
+ executeAction: async (name) => {
336
+ await this.execute({ action: name });
337
+ },
338
+ });
339
+ }
340
+ finally {
341
+ this.loadingActions.update((prev) => {
342
+ const next = new Set(prev);
343
+ next.delete(resolved.action);
344
+ return next;
345
+ });
346
+ }
347
+ }
348
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ActionDispatcherService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
349
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ActionDispatcherService });
350
+ }
351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ActionDispatcherService, decorators: [{
352
+ type: Injectable
353
+ }] });
354
+
355
+ class ValidationService {
356
+ stateService = inject(SpecStateService);
357
+ customFunctions = {};
358
+ fieldConfigs = {};
359
+ fieldStates = signal({}, ...(ngDevMode ? [{ debugName: "fieldStates" }] : []));
360
+ setCustomFunctions(fns) {
361
+ this.customFunctions = fns;
362
+ }
363
+ registerField(path, config) {
364
+ this.fieldConfigs[path] = config;
365
+ }
366
+ validate(path, config) {
367
+ const currentState = this.stateService.state();
368
+ const segments = path.split('/').filter(Boolean);
369
+ let value = currentState;
370
+ for (const seg of segments) {
371
+ if (value != null && typeof value === 'object') {
372
+ value = value[seg];
373
+ }
374
+ else {
375
+ value = undefined;
376
+ break;
377
+ }
378
+ }
379
+ const result = runValidation(config, {
380
+ value,
381
+ stateModel: currentState,
382
+ customFunctions: this.customFunctions,
383
+ });
384
+ this.fieldStates.update((prev) => ({
385
+ ...prev,
386
+ [path]: {
387
+ touched: prev[path]?.touched ?? true,
388
+ validated: true,
389
+ result,
390
+ },
391
+ }));
392
+ return result;
393
+ }
394
+ touch(path) {
395
+ this.fieldStates.update((prev) => ({
396
+ ...prev,
397
+ [path]: {
398
+ ...prev[path],
399
+ touched: true,
400
+ validated: prev[path]?.validated ?? false,
401
+ result: prev[path]?.result ?? null,
402
+ },
403
+ }));
404
+ }
405
+ clear(path) {
406
+ this.fieldStates.update((prev) => {
407
+ const { [path]: _, ...rest } = prev;
408
+ return rest;
409
+ });
410
+ }
411
+ validateAll() {
412
+ let allValid = true;
413
+ for (const [path, config] of Object.entries(this.fieldConfigs)) {
414
+ const result = this.validate(path, config);
415
+ if (!result.valid) {
416
+ allValid = false;
417
+ }
418
+ }
419
+ return allValid;
420
+ }
421
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
422
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ValidationService });
423
+ }
424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ValidationService, decorators: [{
425
+ type: Injectable
426
+ }] });
427
+
428
+ class RepeatScopeService {
429
+ item = undefined;
430
+ index = 0;
431
+ basePath = '';
432
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RepeatScopeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
433
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RepeatScopeService });
434
+ }
435
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RepeatScopeService, decorators: [{
436
+ type: Injectable
437
+ }] });
438
+
439
+ /**
440
+ * Custom error handler that absorbs rendering errors for individual elements.
441
+ * Prevents one broken component from crashing the entire tree.
442
+ */
443
+ class ElementErrorHandler extends ErrorHandler {
444
+ handleError(error) {
445
+ console.error('[json-render] Rendering error in element:', error);
446
+ }
447
+ }
448
+
449
+ class ElementRendererComponent {
450
+ element = input.required(...(ngDevMode ? [{ debugName: "element" }] : []));
451
+ spec = input.required(...(ngDevMode ? [{ debugName: "spec" }] : []));
452
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
453
+ vcr = inject(ViewContainerRef);
454
+ injector = inject(Injector);
455
+ envInjector = inject(EnvironmentInjector);
456
+ stateService = inject(SpecStateService);
457
+ visibilityService = inject(VisibilityService);
458
+ actionDispatcher = inject(ActionDispatcherService);
459
+ repeatScope = inject(RepeatScopeService, {
460
+ optional: true,
461
+ });
462
+ registry = inject(NG_RENDER_REGISTRY);
463
+ fallbackComponent = inject(NG_RENDER_FALLBACK, {
464
+ optional: true,
465
+ });
466
+ componentRef = null;
467
+ childRenderers = [];
468
+ repeatInjectors = [];
469
+ constructor() {
470
+ effect(() => {
471
+ this.render();
472
+ });
473
+ }
474
+ render() {
475
+ this.cleanup();
476
+ const element = this.element();
477
+ const spec = this.spec();
478
+ const loading = this.loading();
479
+ // Read state signal to register dependency for re-rendering
480
+ const state = this.stateService.state();
481
+ // Build PropResolutionContext with repeat scope data
482
+ const baseCtx = this.visibilityService.ctx();
483
+ const fullCtx = this.repeatScope
484
+ ? {
485
+ ...baseCtx,
486
+ repeatItem: this.repeatScope.item,
487
+ repeatIndex: this.repeatScope.index,
488
+ repeatBasePath: this.repeatScope.basePath,
489
+ }
490
+ : baseCtx;
491
+ // Evaluate visibility
492
+ if (element.visible !== undefined &&
493
+ !evaluateVisibility(element.visible, fullCtx)) {
494
+ return;
495
+ }
496
+ // Handle repeat elements
497
+ if (element.repeat) {
498
+ this.renderRepeat(element, spec, loading, fullCtx);
499
+ return;
500
+ }
501
+ // Resolve bindings and props
502
+ const rawProps = element.props;
503
+ const elementBindings = resolveBindings(rawProps, fullCtx);
504
+ const resolvedProps = resolveElementProps(rawProps, fullCtx);
505
+ const resolvedElement = resolvedProps !== element.props
506
+ ? { ...element, props: resolvedProps }
507
+ : element;
508
+ // Create emit function
509
+ const onBindings = element.on;
510
+ const emit = (eventName) => {
511
+ const binding = onBindings?.[eventName];
512
+ if (!binding)
513
+ return;
514
+ const actionBindings = Array.isArray(binding)
515
+ ? binding
516
+ : [binding];
517
+ for (const b of actionBindings) {
518
+ if (!b.params) {
519
+ this.actionDispatcher.execute(b);
520
+ continue;
521
+ }
522
+ const resolved = {};
523
+ for (const [key, val] of Object.entries(b.params)) {
524
+ resolved[key] = resolveActionParam(val, fullCtx);
525
+ }
526
+ this.actionDispatcher.execute({ ...b, params: resolved });
527
+ }
528
+ };
529
+ // Create on() function
530
+ const on = (eventName) => {
531
+ const binding = onBindings?.[eventName];
532
+ if (!binding) {
533
+ return { emit: () => { }, shouldPreventDefault: false, bound: false };
534
+ }
535
+ const actionBindings = Array.isArray(binding)
536
+ ? binding
537
+ : [binding];
538
+ const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);
539
+ return {
540
+ emit: () => emit(eventName),
541
+ shouldPreventDefault,
542
+ bound: true,
543
+ };
544
+ };
545
+ // Look up component
546
+ const ComponentClass = this.registry[resolvedElement.type] ?? this.fallbackComponent;
547
+ if (!ComponentClass) {
548
+ console.warn(`[ng-render] No renderer for component type: "${resolvedElement.type}"`);
549
+ return;
550
+ }
551
+ // Create the component
552
+ try {
553
+ this.componentRef = this.vcr.createComponent(ComponentClass, {
554
+ injector: this.injector,
555
+ environmentInjector: this.envInjector,
556
+ });
557
+ this.componentRef.setInput('element', resolvedElement);
558
+ this.componentRef.setInput('emit', emit);
559
+ this.componentRef.setInput('on', on);
560
+ if (elementBindings) {
561
+ this.componentRef.setInput('bindings', elementBindings);
562
+ }
563
+ this.componentRef.setInput('loading', loading);
564
+ // Render children into the ChildrenOutletDirective if present
565
+ if (resolvedElement.children && resolvedElement.children.length > 0) {
566
+ // Trigger change detection so ViewChild queries resolve
567
+ this.componentRef.changeDetectorRef.detectChanges();
568
+ // Look for children outlet on the component instance.
569
+ // Components that accept children must declare:
570
+ // @ViewChild(ChildrenOutletDirective, { static: true })
571
+ // childrenOutlet!: ChildrenOutletDirective;
572
+ const instance = this.componentRef.instance;
573
+ const outlet = instance?.childrenOutlet;
574
+ if (outlet?.vcr) {
575
+ this.renderChildren(resolvedElement.children, spec, loading, outlet.vcr);
576
+ }
577
+ else {
578
+ // No outlet found — render children as siblings after the component.
579
+ // This handles components without explicit jrChildren directive.
580
+ this.renderChildren(resolvedElement.children, spec, loading, this.vcr);
581
+ }
582
+ }
583
+ }
584
+ catch (error) {
585
+ console.error(`[ng-render] Error creating component for type "${resolvedElement.type}":`, error);
586
+ }
587
+ }
588
+ renderRepeat(element, spec, loading, ctx) {
589
+ const repeat = element.repeat;
590
+ const items = getByPath(this.stateService.state(), repeat.statePath) ?? [];
591
+ for (let index = 0; index < items.length; index++) {
592
+ const itemValue = items[index];
593
+ const basePath = `${repeat.statePath}/${index}`;
594
+ // Create a child injector with a fresh RepeatScopeService
595
+ const repeatScopeService = new RepeatScopeService();
596
+ repeatScopeService.item = itemValue;
597
+ repeatScopeService.index = index;
598
+ repeatScopeService.basePath = basePath;
599
+ const childInjector = createEnvironmentInjector([{ provide: RepeatScopeService, useValue: repeatScopeService }], this.envInjector);
600
+ this.repeatInjectors.push(childInjector);
601
+ // Render children for this repeat iteration
602
+ if (element.children) {
603
+ for (const childKey of element.children) {
604
+ const childElement = spec.elements[childKey];
605
+ if (!childElement) {
606
+ if (!loading) {
607
+ console.warn(`[ng-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat).`);
608
+ }
609
+ continue;
610
+ }
611
+ const childRef = this.vcr.createComponent(ElementRendererComponent, {
612
+ environmentInjector: childInjector,
613
+ });
614
+ childRef.setInput('element', childElement);
615
+ childRef.setInput('spec', spec);
616
+ childRef.setInput('loading', loading);
617
+ this.childRenderers.push(childRef);
618
+ }
619
+ }
620
+ }
621
+ }
622
+ renderChildren(childKeys, spec, loading, vcr) {
623
+ for (const childKey of childKeys) {
624
+ const childElement = spec.elements[childKey];
625
+ if (!childElement) {
626
+ if (!loading) {
627
+ console.warn(`[ng-render] Missing element "${childKey}". This element will not render.`);
628
+ }
629
+ continue;
630
+ }
631
+ const childRef = vcr.createComponent(ElementRendererComponent, {
632
+ environmentInjector: this.envInjector,
633
+ });
634
+ childRef.setInput('element', childElement);
635
+ childRef.setInput('spec', spec);
636
+ childRef.setInput('loading', loading);
637
+ this.childRenderers.push(childRef);
638
+ }
639
+ }
640
+ cleanup() {
641
+ for (const ref of this.childRenderers) {
642
+ ref.destroy();
643
+ }
644
+ this.childRenderers = [];
645
+ for (const inj of this.repeatInjectors) {
646
+ inj.destroy();
647
+ }
648
+ this.repeatInjectors = [];
649
+ if (this.componentRef) {
650
+ this.componentRef.destroy();
651
+ this.componentRef = null;
652
+ }
653
+ this.vcr.clear();
654
+ }
655
+ ngOnDestroy() {
656
+ this.cleanup();
657
+ }
658
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
659
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.4", type: ElementRendererComponent, isStandalone: true, selector: "jr-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: ErrorHandler, useClass: ElementErrorHandler }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
660
+ }
661
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementRendererComponent, decorators: [{
662
+ type: Component,
663
+ args: [{
664
+ selector: 'jr-element-renderer',
665
+ standalone: true,
666
+ template: '',
667
+ changeDetection: ChangeDetectionStrategy.OnPush,
668
+ providers: [{ provide: ErrorHandler, useClass: ElementErrorHandler }],
669
+ }]
670
+ }], ctorParameters: () => [], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
671
+
672
+ class JsonRendererComponent {
673
+ spec = input(null, ...(ngDevMode ? [{ debugName: "spec" }] : []));
674
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
675
+ initialState = input({}, ...(ngDevMode ? [{ debugName: "initialState" }] : []));
676
+ onStateChange = input(undefined, ...(ngDevMode ? [{ debugName: "onStateChange" }] : []));
677
+ stateService = inject(SpecStateService);
678
+ previousSpecState;
679
+ previousInitialState;
680
+ rootElement = computed(() => {
681
+ const s = this.spec();
682
+ if (!s?.root)
683
+ return null;
684
+ return s.elements[s.root] ?? null;
685
+ }, ...(ngDevMode ? [{ debugName: "rootElement" }] : []));
686
+ constructor() {
687
+ // Initialize from initialState input — only when values actually change
688
+ effect(() => {
689
+ const initial = this.initialState();
690
+ const serialized = JSON.stringify(initial);
691
+ if (serialized !== this.previousInitialState) {
692
+ this.previousInitialState = serialized;
693
+ if (initial && Object.keys(initial).length > 0) {
694
+ this.stateService.mergeInitialState(initial);
695
+ }
696
+ }
697
+ });
698
+ // Merge spec.state when spec changes
699
+ effect(() => {
700
+ const s = this.spec();
701
+ if (s?.state && s.state !== this.previousSpecState) {
702
+ this.previousSpecState = s.state;
703
+ if (Object.keys(s.state).length > 0) {
704
+ this.stateService.mergeInitialState(s.state);
705
+ }
706
+ }
707
+ });
708
+ // Wire onStateChange callback — observe state and forward changes
709
+ effect(() => {
710
+ const callback = this.onStateChange();
711
+ const state = this.stateService.state();
712
+ // The callback is invoked with the full state snapshot
713
+ // Components should use the SpecStateService for granular path-level changes
714
+ if (callback && state) {
715
+ callback('', state);
716
+ }
717
+ });
718
+ }
719
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: JsonRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
720
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: JsonRendererComponent, isStandalone: true, selector: "json-renderer", inputs: { spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, initialState: { classPropertyName: "initialState", publicName: "initialState", isSignal: true, isRequired: false, transformFunction: null }, onStateChange: { classPropertyName: "onStateChange", publicName: "onStateChange", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
721
+ SpecStateService,
722
+ VisibilityService,
723
+ ActionDispatcherService,
724
+ ValidationService,
725
+ ], ngImport: i0, template: `
726
+ @if (rootElement(); as root) {
727
+ <jr-element-renderer
728
+ [element]="root"
729
+ [spec]="spec()!"
730
+ [loading]="loading()" />
731
+ }
732
+ `, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "jr-element-renderer", inputs: ["element", "spec", "loading"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
733
+ }
734
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: JsonRendererComponent, decorators: [{
735
+ type: Component,
736
+ args: [{
737
+ selector: 'json-renderer',
738
+ standalone: true,
739
+ imports: [ElementRendererComponent],
740
+ template: `
741
+ @if (rootElement(); as root) {
742
+ <jr-element-renderer
743
+ [element]="root"
744
+ [spec]="spec()!"
745
+ [loading]="loading()" />
746
+ }
747
+ `,
748
+ changeDetection: ChangeDetectionStrategy.OnPush,
749
+ providers: [
750
+ SpecStateService,
751
+ VisibilityService,
752
+ ActionDispatcherService,
753
+ ValidationService,
754
+ ],
755
+ }]
756
+ }], ctorParameters: () => [], propDecorators: { spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], initialState: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialState", required: false }] }], onStateChange: [{ type: i0.Input, args: [{ isSignal: true, alias: "onStateChange", required: false }] }] } });
757
+
758
+ /**
759
+ * Directive that marks where children should be rendered inside a
760
+ * registered component.
761
+ *
762
+ * Usage in component templates:
763
+ * ```html
764
+ * <div class="card">
765
+ * <ng-template jrChildren></ng-template>
766
+ * </div>
767
+ * ```
768
+ */
769
+ class ChildrenOutletDirective {
770
+ vcr = inject(ViewContainerRef);
771
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ChildrenOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
772
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.4", type: ChildrenOutletDirective, isStandalone: true, selector: "[jrChildren]", exportAs: ["jrChildren"], ngImport: i0 });
773
+ }
774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ChildrenOutletDirective, decorators: [{
775
+ type: Directive,
776
+ args: [{
777
+ selector: '[jrChildren]',
778
+ standalone: true,
779
+ exportAs: 'jrChildren',
780
+ }]
781
+ }] });
782
+
783
+ /**
784
+ * Default confirmation dialog component.
785
+ * Renders when an action has a `confirm` field.
786
+ *
787
+ * Place inside or near the `<json-renderer>` in your template:
788
+ * ```html
789
+ * <json-renderer [spec]="spec">
790
+ * <jr-confirm-dialog />
791
+ * </json-renderer>
792
+ * ```
793
+ *
794
+ * Or use it standalone anywhere the ActionDispatcherService is available.
795
+ */
796
+ class ConfirmDialogComponent {
797
+ dispatcher = inject(ActionDispatcherService);
798
+ confirm = computed(() => {
799
+ const pending = this.dispatcher.pendingConfirmation();
800
+ return pending?.action.confirm ?? null;
801
+ }, ...(ngDevMode ? [{ debugName: "confirm" }] : []));
802
+ onConfirm() {
803
+ this.dispatcher.confirm();
804
+ }
805
+ onCancel() {
806
+ this.dispatcher.cancel();
807
+ }
808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
809
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.4", type: ConfirmDialogComponent, isStandalone: true, selector: "jr-confirm-dialog", ngImport: i0, template: `
810
+ @if (confirm(); as c) {
811
+ <div class="jr-confirm-overlay" (click)="onCancel()">
812
+ <div class="jr-confirm-dialog" (click)="$event.stopPropagation()">
813
+ <h3 class="jr-confirm-title">{{ c.title }}</h3>
814
+ <p class="jr-confirm-message">{{ c.message }}</p>
815
+ <div class="jr-confirm-actions">
816
+ <button class="jr-confirm-btn jr-confirm-btn-cancel" (click)="onCancel()">
817
+ {{ c.cancelLabel ?? 'Cancel' }}
818
+ </button>
819
+ <button
820
+ class="jr-confirm-btn"
821
+ [class.jr-confirm-btn-danger]="c.variant === 'danger'"
822
+ [class.jr-confirm-btn-primary]="c.variant !== 'danger'"
823
+ (click)="onConfirm()">
824
+ {{ c.confirmLabel ?? 'Confirm' }}
825
+ </button>
826
+ </div>
827
+ </div>
828
+ </div>
829
+ }
830
+ `, isInline: true, styles: [".jr-confirm-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:50}.jr-confirm-dialog{background:#fff;border-radius:8px;padding:24px;max-width:400px;width:100%;box-shadow:0 20px 25px -5px #0000001a}.jr-confirm-title{margin:0 0 8px;font-size:18px;font-weight:600}.jr-confirm-message{margin:0 0 24px;color:#6b7280}.jr-confirm-actions{display:flex;gap:12px;justify-content:flex-end}.jr-confirm-btn{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.jr-confirm-btn-cancel{border:1px solid #d1d5db;background:#fff;color:#374151}.jr-confirm-btn-primary{border:none;background:#3b82f6;color:#fff}.jr-confirm-btn-danger{border:none;background:#dc2626;color:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
831
+ }
832
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ConfirmDialogComponent, decorators: [{
833
+ type: Component,
834
+ args: [{ selector: 'jr-confirm-dialog', standalone: true, template: `
835
+ @if (confirm(); as c) {
836
+ <div class="jr-confirm-overlay" (click)="onCancel()">
837
+ <div class="jr-confirm-dialog" (click)="$event.stopPropagation()">
838
+ <h3 class="jr-confirm-title">{{ c.title }}</h3>
839
+ <p class="jr-confirm-message">{{ c.message }}</p>
840
+ <div class="jr-confirm-actions">
841
+ <button class="jr-confirm-btn jr-confirm-btn-cancel" (click)="onCancel()">
842
+ {{ c.cancelLabel ?? 'Cancel' }}
843
+ </button>
844
+ <button
845
+ class="jr-confirm-btn"
846
+ [class.jr-confirm-btn-danger]="c.variant === 'danger'"
847
+ [class.jr-confirm-btn-primary]="c.variant !== 'danger'"
848
+ (click)="onConfirm()">
849
+ {{ c.confirmLabel ?? 'Confirm' }}
850
+ </button>
851
+ </div>
852
+ </div>
853
+ </div>
854
+ }
855
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jr-confirm-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:50}.jr-confirm-dialog{background:#fff;border-radius:8px;padding:24px;max-width:400px;width:100%;box-shadow:0 20px 25px -5px #0000001a}.jr-confirm-title{margin:0 0 8px;font-size:18px;font-weight:600}.jr-confirm-message{margin:0 0 24px;color:#6b7280}.jr-confirm-actions{display:flex;gap:12px;justify-content:flex-end}.jr-confirm-btn{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.jr-confirm-btn-cancel{border:1px solid #d1d5db;background:#fff;color:#374151}.jr-confirm-btn-primary{border:none;background:#3b82f6;color:#fff}.jr-confirm-btn-danger{border:none;background:#dc2626;color:#fff}\n"] }]
856
+ }] });
857
+
858
+ /**
859
+ * Default fallback component for unknown element types.
860
+ * Displays a warning in development.
861
+ *
862
+ * Register via provideNgRender:
863
+ * ```ts
864
+ * provideNgRender({
865
+ * registry: myRegistry,
866
+ * fallback: FallbackComponent,
867
+ * })
868
+ * ```
869
+ */
870
+ class FallbackComponent {
871
+ element = input.required(...(ngDevMode ? [{ debugName: "element" }] : []));
872
+ emit = input.required(...(ngDevMode ? [{ debugName: "emit" }] : []));
873
+ on = input.required(...(ngDevMode ? [{ debugName: "on" }] : []));
874
+ bindings = input(...(ngDevMode ? [undefined, { debugName: "bindings" }] : []));
875
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
876
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FallbackComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
877
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.4", type: FallbackComponent, isStandalone: true, selector: "jr-fallback", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, emit: { classPropertyName: "emit", publicName: "emit", isSignal: true, isRequired: true, transformFunction: null }, on: { classPropertyName: "on", publicName: "on", isSignal: true, isRequired: true, transformFunction: null }, bindings: { classPropertyName: "bindings", publicName: "bindings", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
878
+ <div class="jr-fallback">
879
+ Unknown component: "{{ element().type }}"
880
+ </div>
881
+ `, isInline: true, styles: [".jr-fallback{padding:8px 12px;border:1px dashed #f59e0b;border-radius:6px;background:#fffbeb;color:#92400e;font-size:12px;font-family:monospace}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
882
+ }
883
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: FallbackComponent, decorators: [{
884
+ type: Component,
885
+ args: [{ selector: 'jr-fallback', standalone: true, template: `
886
+ <div class="jr-fallback">
887
+ Unknown component: "{{ element().type }}"
888
+ </div>
889
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jr-fallback{padding:8px 12px;border:1px dashed #f59e0b;border-radius:6px;background:#fffbeb;color:#92400e;font-size:12px;font-family:monospace}\n"] }]
890
+ }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], emit: [{ type: i0.Input, args: [{ isSignal: true, alias: "emit", required: true }] }], on: [{ type: i0.Input, args: [{ isSignal: true, alias: "on", required: true }] }], bindings: [{ type: i0.Input, args: [{ isSignal: true, alias: "bindings", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
891
+
892
+ /**
893
+ * Parse a single JSONL line into a patch or usage metadata.
894
+ */
895
+ function parseLine(line) {
896
+ try {
897
+ const trimmed = line.trim();
898
+ if (!trimmed || trimmed.startsWith('//'))
899
+ return null;
900
+ const parsed = JSON.parse(trimmed);
901
+ if (parsed.__meta === 'usage') {
902
+ return {
903
+ type: 'usage',
904
+ usage: {
905
+ promptTokens: parsed.promptTokens ?? 0,
906
+ completionTokens: parsed.completionTokens ?? 0,
907
+ totalTokens: parsed.totalTokens ?? 0,
908
+ },
909
+ };
910
+ }
911
+ return { type: 'patch', patch: parsed };
912
+ }
913
+ catch {
914
+ return null;
915
+ }
916
+ }
917
+ function setSpecValue(newSpec, path, value) {
918
+ if (path === '/root') {
919
+ newSpec.root = value;
920
+ return;
921
+ }
922
+ if (path === '/state') {
923
+ newSpec.state = value;
924
+ return;
925
+ }
926
+ if (path.startsWith('/state/')) {
927
+ if (!newSpec.state)
928
+ newSpec.state = {};
929
+ const statePath = path.slice('/state'.length);
930
+ setByPath(newSpec.state, statePath, value);
931
+ return;
932
+ }
933
+ if (path.startsWith('/elements/')) {
934
+ const pathParts = path.slice('/elements/'.length).split('/');
935
+ const elementKey = pathParts[0];
936
+ if (!elementKey)
937
+ return;
938
+ if (pathParts.length === 1) {
939
+ newSpec.elements[elementKey] = value;
940
+ }
941
+ else {
942
+ const element = newSpec.elements[elementKey];
943
+ if (element) {
944
+ const propPath = '/' + pathParts.slice(1).join('/');
945
+ const newElement = { ...element };
946
+ setByPath(newElement, propPath, value);
947
+ newSpec.elements[elementKey] = newElement;
948
+ }
949
+ }
950
+ }
951
+ }
952
+ function removeSpecValue(newSpec, path) {
953
+ if (path === '/state') {
954
+ delete newSpec.state;
955
+ return;
956
+ }
957
+ if (path.startsWith('/state/') && newSpec.state) {
958
+ const statePath = path.slice('/state'.length);
959
+ removeByPath(newSpec.state, statePath);
960
+ return;
961
+ }
962
+ if (path.startsWith('/elements/')) {
963
+ const pathParts = path.slice('/elements/'.length).split('/');
964
+ const elementKey = pathParts[0];
965
+ if (!elementKey)
966
+ return;
967
+ if (pathParts.length === 1) {
968
+ const { [elementKey]: _, ...rest } = newSpec.elements;
969
+ newSpec.elements = rest;
970
+ }
971
+ else {
972
+ const element = newSpec.elements[elementKey];
973
+ if (element) {
974
+ const propPath = '/' + pathParts.slice(1).join('/');
975
+ const newElement = { ...element };
976
+ removeByPath(newElement, propPath);
977
+ newSpec.elements[elementKey] = newElement;
978
+ }
979
+ }
980
+ }
981
+ }
982
+ function getSpecValue(spec, path) {
983
+ if (path === '/root')
984
+ return spec.root;
985
+ if (path === '/state')
986
+ return spec.state;
987
+ if (path.startsWith('/state/') && spec.state) {
988
+ const statePath = path.slice('/state'.length);
989
+ return getByPath(spec.state, statePath);
990
+ }
991
+ return getByPath(spec, path);
992
+ }
993
+ /**
994
+ * Apply an RFC 6902 JSON patch to a Spec.
995
+ * Returns a new shallow-copied Spec (immutable pattern for signal updates).
996
+ */
997
+ function applyPatch(spec, patch) {
998
+ const newSpec = {
999
+ ...spec,
1000
+ elements: { ...spec.elements },
1001
+ ...(spec.state ? { state: { ...spec.state } } : {}),
1002
+ };
1003
+ switch (patch.op) {
1004
+ case 'add':
1005
+ case 'replace':
1006
+ setSpecValue(newSpec, patch.path, patch.value);
1007
+ break;
1008
+ case 'remove':
1009
+ removeSpecValue(newSpec, patch.path);
1010
+ break;
1011
+ case 'move':
1012
+ if (!patch.from)
1013
+ break;
1014
+ const moveValue = getSpecValue(newSpec, patch.from);
1015
+ removeSpecValue(newSpec, patch.from);
1016
+ setSpecValue(newSpec, patch.path, moveValue);
1017
+ break;
1018
+ case 'copy':
1019
+ if (!patch.from)
1020
+ break;
1021
+ const copyValue = getSpecValue(newSpec, patch.from);
1022
+ setSpecValue(newSpec, patch.path, copyValue);
1023
+ break;
1024
+ case 'test':
1025
+ break;
1026
+ }
1027
+ return newSpec;
1028
+ }
1029
+
1030
+ /**
1031
+ * Create a streaming UI generator.
1032
+ * Angular equivalent of React's `useUIStream` hook.
1033
+ *
1034
+ * Returns signal-based reactive state + imperative send/clear functions.
1035
+ *
1036
+ * @example
1037
+ * ```ts
1038
+ * // In a component
1039
+ * private stream = createUIStream({ api: '/api/generate' });
1040
+ * spec = this.stream.spec;
1041
+ * isStreaming = this.stream.isStreaming;
1042
+ *
1043
+ * async generate() {
1044
+ * await this.stream.send('Create a dashboard');
1045
+ * }
1046
+ *
1047
+ * ngOnDestroy() {
1048
+ * this.stream.destroy();
1049
+ * }
1050
+ * ```
1051
+ */
1052
+ function createUIStream(options) {
1053
+ const spec = signal(null, ...(ngDevMode ? [{ debugName: "spec" }] : []));
1054
+ const isStreaming = signal(false, ...(ngDevMode ? [{ debugName: "isStreaming" }] : []));
1055
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
1056
+ const usage = signal(null, ...(ngDevMode ? [{ debugName: "usage" }] : []));
1057
+ const rawLines = signal([], ...(ngDevMode ? [{ debugName: "rawLines" }] : []));
1058
+ let abortController = null;
1059
+ const clear = () => {
1060
+ spec.set(null);
1061
+ error.set(null);
1062
+ };
1063
+ const destroy = () => {
1064
+ abortController?.abort();
1065
+ abortController = null;
1066
+ };
1067
+ const send = async (prompt, context) => {
1068
+ abortController?.abort();
1069
+ abortController = new AbortController();
1070
+ isStreaming.set(true);
1071
+ error.set(null);
1072
+ usage.set(null);
1073
+ rawLines.set([]);
1074
+ const previousSpec = context?.['previousSpec'];
1075
+ let currentSpec = previousSpec && previousSpec.root
1076
+ ? { ...previousSpec, elements: { ...previousSpec.elements } }
1077
+ : { root: '', elements: {} };
1078
+ spec.set(currentSpec);
1079
+ try {
1080
+ const response = await fetch(options.api, {
1081
+ method: 'POST',
1082
+ headers: { 'Content-Type': 'application/json' },
1083
+ body: JSON.stringify({ prompt, context, currentSpec }),
1084
+ signal: abortController.signal,
1085
+ });
1086
+ if (!response.ok) {
1087
+ let errorMessage = `HTTP error: ${response.status}`;
1088
+ try {
1089
+ const errorData = await response.json();
1090
+ if (errorData.message)
1091
+ errorMessage = errorData.message;
1092
+ else if (errorData.error)
1093
+ errorMessage = errorData.error;
1094
+ }
1095
+ catch {
1096
+ // Ignore
1097
+ }
1098
+ throw new Error(errorMessage);
1099
+ }
1100
+ const reader = response.body?.getReader();
1101
+ if (!reader)
1102
+ throw new Error('No response body');
1103
+ const decoder = new TextDecoder();
1104
+ let buffer = '';
1105
+ while (true) {
1106
+ const { done, value } = await reader.read();
1107
+ if (done)
1108
+ break;
1109
+ buffer += decoder.decode(value, { stream: true });
1110
+ const lines = buffer.split('\n');
1111
+ buffer = lines.pop() ?? '';
1112
+ for (const line of lines) {
1113
+ const trimmed = line.trim();
1114
+ if (!trimmed)
1115
+ continue;
1116
+ const result = parseLine(trimmed);
1117
+ if (!result)
1118
+ continue;
1119
+ if (result.type === 'usage') {
1120
+ usage.set(result.usage);
1121
+ }
1122
+ else {
1123
+ rawLines.update((prev) => [...prev, trimmed]);
1124
+ currentSpec = applyPatch(currentSpec, result.patch);
1125
+ spec.set({ ...currentSpec });
1126
+ }
1127
+ }
1128
+ }
1129
+ // Process remaining buffer
1130
+ if (buffer.trim()) {
1131
+ const result = parseLine(buffer.trim());
1132
+ if (result) {
1133
+ if (result.type === 'usage') {
1134
+ usage.set(result.usage);
1135
+ }
1136
+ else {
1137
+ rawLines.update((prev) => [...prev, buffer.trim()]);
1138
+ currentSpec = applyPatch(currentSpec, result.patch);
1139
+ spec.set({ ...currentSpec });
1140
+ }
1141
+ }
1142
+ }
1143
+ options.onComplete?.(currentSpec);
1144
+ }
1145
+ catch (err) {
1146
+ if (err.name === 'AbortError')
1147
+ return;
1148
+ const resolvedError = err instanceof Error ? err : new Error(String(err));
1149
+ error.set(resolvedError);
1150
+ options.onError?.(resolvedError);
1151
+ }
1152
+ finally {
1153
+ isStreaming.set(false);
1154
+ }
1155
+ };
1156
+ return {
1157
+ spec: spec.asReadonly(),
1158
+ isStreaming: isStreaming.asReadonly(),
1159
+ error: error.asReadonly(),
1160
+ usage: usage.asReadonly(),
1161
+ rawLines: rawLines.asReadonly(),
1162
+ send,
1163
+ clear,
1164
+ destroy,
1165
+ };
1166
+ }
1167
+
1168
+ let chatMessageIdCounter = 0;
1169
+ function generateChatId() {
1170
+ if (typeof crypto !== 'undefined' &&
1171
+ typeof crypto.randomUUID === 'function') {
1172
+ return crypto.randomUUID();
1173
+ }
1174
+ chatMessageIdCounter += 1;
1175
+ return `msg-${Date.now()}-${chatMessageIdCounter}`;
1176
+ }
1177
+ /**
1178
+ * Create a chat + GenUI interface.
1179
+ * Angular equivalent of React's `useChatUI` hook.
1180
+ *
1181
+ * Manages a multi-turn conversation where each assistant message can contain
1182
+ * both conversational text and a json-render UI spec.
1183
+ *
1184
+ * @example
1185
+ * ```ts
1186
+ * private chat = createChatUI({ api: '/api/chat' });
1187
+ * messages = this.chat.messages;
1188
+ * isStreaming = this.chat.isStreaming;
1189
+ *
1190
+ * async send(text: string) {
1191
+ * await this.chat.send(text);
1192
+ * }
1193
+ *
1194
+ * ngOnDestroy() {
1195
+ * this.chat.destroy();
1196
+ * }
1197
+ * ```
1198
+ */
1199
+ function createChatUI(options) {
1200
+ const messages = signal([], ...(ngDevMode ? [{ debugName: "messages" }] : []));
1201
+ const isStreaming = signal(false, ...(ngDevMode ? [{ debugName: "isStreaming" }] : []));
1202
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
1203
+ let abortController = null;
1204
+ const clear = () => {
1205
+ messages.set([]);
1206
+ error.set(null);
1207
+ };
1208
+ const destroy = () => {
1209
+ abortController?.abort();
1210
+ abortController = null;
1211
+ };
1212
+ const send = async (text) => {
1213
+ if (!text.trim())
1214
+ return;
1215
+ abortController?.abort();
1216
+ abortController = new AbortController();
1217
+ const userMessage = {
1218
+ id: generateChatId(),
1219
+ role: 'user',
1220
+ text: text.trim(),
1221
+ spec: null,
1222
+ };
1223
+ const assistantId = generateChatId();
1224
+ const assistantMessage = {
1225
+ id: assistantId,
1226
+ role: 'assistant',
1227
+ text: '',
1228
+ spec: null,
1229
+ };
1230
+ messages.update((prev) => [...prev, userMessage, assistantMessage]);
1231
+ isStreaming.set(true);
1232
+ error.set(null);
1233
+ const historyForApi = messages()
1234
+ .filter((m) => m.id !== assistantId)
1235
+ .map((m) => ({ role: m.role, content: m.text }));
1236
+ historyForApi.push({ role: 'user', content: text.trim() });
1237
+ let accumulatedText = '';
1238
+ let currentSpec = { root: '', elements: {} };
1239
+ let hasSpec = false;
1240
+ try {
1241
+ const response = await fetch(options.api, {
1242
+ method: 'POST',
1243
+ headers: { 'Content-Type': 'application/json' },
1244
+ body: JSON.stringify({ messages: historyForApi }),
1245
+ signal: abortController.signal,
1246
+ });
1247
+ if (!response.ok) {
1248
+ let errorMessage = `HTTP error: ${response.status}`;
1249
+ try {
1250
+ const errorData = await response.json();
1251
+ if (errorData.message)
1252
+ errorMessage = errorData.message;
1253
+ else if (errorData.error)
1254
+ errorMessage = errorData.error;
1255
+ }
1256
+ catch {
1257
+ // Ignore
1258
+ }
1259
+ throw new Error(errorMessage);
1260
+ }
1261
+ const reader = response.body?.getReader();
1262
+ if (!reader)
1263
+ throw new Error('No response body');
1264
+ const decoder = new TextDecoder();
1265
+ const parser = createMixedStreamParser({
1266
+ onPatch(patch) {
1267
+ hasSpec = true;
1268
+ applySpecPatch(currentSpec, patch);
1269
+ messages.update((prev) => prev.map((m) => m.id === assistantId
1270
+ ? {
1271
+ ...m,
1272
+ spec: {
1273
+ root: currentSpec.root,
1274
+ elements: { ...currentSpec.elements },
1275
+ ...(currentSpec.state
1276
+ ? { state: { ...currentSpec.state } }
1277
+ : {}),
1278
+ },
1279
+ }
1280
+ : m));
1281
+ },
1282
+ onText(line) {
1283
+ accumulatedText += (accumulatedText ? '\n' : '') + line;
1284
+ messages.update((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: accumulatedText } : m));
1285
+ },
1286
+ });
1287
+ while (true) {
1288
+ const { done, value } = await reader.read();
1289
+ if (done)
1290
+ break;
1291
+ parser.push(decoder.decode(value, { stream: true }));
1292
+ }
1293
+ parser.flush();
1294
+ const finalMessage = {
1295
+ id: assistantId,
1296
+ role: 'assistant',
1297
+ text: accumulatedText,
1298
+ spec: hasSpec
1299
+ ? {
1300
+ root: currentSpec.root,
1301
+ elements: { ...currentSpec.elements },
1302
+ ...(currentSpec.state
1303
+ ? { state: { ...currentSpec.state } }
1304
+ : {}),
1305
+ }
1306
+ : null,
1307
+ };
1308
+ options.onComplete?.(finalMessage);
1309
+ }
1310
+ catch (err) {
1311
+ if (err.name === 'AbortError')
1312
+ return;
1313
+ const resolvedError = err instanceof Error ? err : new Error(String(err));
1314
+ error.set(resolvedError);
1315
+ messages.update((prev) => prev.filter((m) => m.id !== assistantId || m.text.length > 0));
1316
+ options.onError?.(resolvedError);
1317
+ }
1318
+ finally {
1319
+ isStreaming.set(false);
1320
+ }
1321
+ };
1322
+ return {
1323
+ messages: messages.asReadonly(),
1324
+ isStreaming: isStreaming.asReadonly(),
1325
+ error: error.asReadonly(),
1326
+ send,
1327
+ clear,
1328
+ destroy,
1329
+ };
1330
+ }
1331
+
1332
+ /**
1333
+ * Create a two-way bound prop helper.
1334
+ * Angular equivalent of React's `useBoundProp` hook.
1335
+ *
1336
+ * Must be called within an injection context (constructor, factory, etc.)
1337
+ * where SpecStateService is available.
1338
+ *
1339
+ * @example
1340
+ * ```ts
1341
+ * // In a component that receives bindings
1342
+ * private stateService = inject(SpecStateService);
1343
+ *
1344
+ * // Create bound prop for a form input
1345
+ * get emailBinding(): BoundProp<string> {
1346
+ * return boundProp(
1347
+ * this.element().props['value'] as string,
1348
+ * this.bindings()?.['value'],
1349
+ * this.stateService,
1350
+ * );
1351
+ * }
1352
+ *
1353
+ * onInput(event: Event) {
1354
+ * this.emailBinding.setValue((event.target as HTMLInputElement).value);
1355
+ * }
1356
+ * ```
1357
+ */
1358
+ function boundProp(propValue, bindingPath, stateService) {
1359
+ return {
1360
+ value: propValue,
1361
+ setValue: (value) => {
1362
+ if (bindingPath) {
1363
+ stateService.set(bindingPath, value);
1364
+ }
1365
+ },
1366
+ };
1367
+ }
1368
+ /**
1369
+ * Create a two-way bound prop helper using dependency injection.
1370
+ * Convenience wrapper that injects SpecStateService automatically.
1371
+ *
1372
+ * Must be called within an injection context.
1373
+ *
1374
+ * @example
1375
+ * ```ts
1376
+ * // In a component constructor or field initializer
1377
+ * private createBound = injectBoundProp();
1378
+ *
1379
+ * get emailBinding() {
1380
+ * return this.createBound(
1381
+ * this.element().props['value'] as string,
1382
+ * this.bindings()?.['value'],
1383
+ * );
1384
+ * }
1385
+ * ```
1386
+ */
1387
+ function injectBoundProp() {
1388
+ const stateService = inject(SpecStateService);
1389
+ return (propValue, bindingPath) => boundProp(propValue, bindingPath, stateService);
1390
+ }
1391
+
1392
+ /**
1393
+ * Type guard that validates a data part payload looks like a valid SpecDataPart.
1394
+ */
1395
+ function isSpecDataPart(data) {
1396
+ if (typeof data !== 'object' || data === null)
1397
+ return false;
1398
+ const obj = data;
1399
+ switch (obj['type']) {
1400
+ case 'patch':
1401
+ return typeof obj['patch'] === 'object' && obj['patch'] !== null;
1402
+ case 'flat':
1403
+ case 'nested':
1404
+ return typeof obj['spec'] === 'object' && obj['spec'] !== null;
1405
+ default:
1406
+ return false;
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Build a Spec by replaying all spec data parts from a message's parts array.
1411
+ * Returns null if no spec data parts are present.
1412
+ *
1413
+ * Works with the AI SDK's UIMessage.parts array.
1414
+ *
1415
+ * @example
1416
+ * ```ts
1417
+ * const spec = buildSpecFromParts(message.parts);
1418
+ * if (spec) {
1419
+ * // render with <json-renderer [spec]="spec" />
1420
+ * }
1421
+ * ```
1422
+ */
1423
+ function buildSpecFromParts(parts) {
1424
+ const spec = { root: '', elements: {} };
1425
+ let hasSpec = false;
1426
+ for (const part of parts) {
1427
+ if (part.type === SPEC_DATA_PART_TYPE) {
1428
+ if (!isSpecDataPart(part.data))
1429
+ continue;
1430
+ const payload = part.data;
1431
+ if (payload.type === 'patch') {
1432
+ hasSpec = true;
1433
+ applySpecPatch(spec, payload.patch);
1434
+ }
1435
+ else if (payload.type === 'flat') {
1436
+ hasSpec = true;
1437
+ Object.assign(spec, payload.spec);
1438
+ }
1439
+ else if (payload.type === 'nested') {
1440
+ hasSpec = true;
1441
+ const flat = nestedToFlat(payload.spec);
1442
+ Object.assign(spec, flat);
1443
+ }
1444
+ }
1445
+ }
1446
+ return hasSpec ? spec : null;
1447
+ }
1448
+ /**
1449
+ * Extract and join all text content from a message's parts array.
1450
+ *
1451
+ * @example
1452
+ * ```ts
1453
+ * const text = getTextFromParts(message.parts);
1454
+ * ```
1455
+ */
1456
+ function getTextFromParts(parts) {
1457
+ return parts
1458
+ .filter((p) => p.type === 'text' && typeof p.text === 'string')
1459
+ .map((p) => p.text.trim())
1460
+ .filter(Boolean)
1461
+ .join('\n\n');
1462
+ }
1463
+ /**
1464
+ * Convert a flat element list to a Spec.
1465
+ * Input elements use key/parentKey to establish identity and relationships.
1466
+ *
1467
+ * @example
1468
+ * ```ts
1469
+ * const spec = flatToTree([
1470
+ * { key: 'root', type: 'Stack', props: {}, children: [] },
1471
+ * { key: 'text', parentKey: 'root', type: 'Text', props: { content: 'Hello' } },
1472
+ * ]);
1473
+ * ```
1474
+ */
1475
+ function flatToTree(elements) {
1476
+ const elementMap = {};
1477
+ let root = '';
1478
+ for (const element of elements) {
1479
+ elementMap[element.key] = {
1480
+ type: element.type,
1481
+ props: element.props,
1482
+ children: [],
1483
+ visible: element.visible,
1484
+ };
1485
+ }
1486
+ for (const element of elements) {
1487
+ if (element.parentKey) {
1488
+ const parent = elementMap[element.parentKey];
1489
+ if (parent) {
1490
+ if (!parent.children)
1491
+ parent.children = [];
1492
+ parent.children.push(element.key);
1493
+ }
1494
+ }
1495
+ else {
1496
+ root = element.key;
1497
+ }
1498
+ }
1499
+ return { root, elements: elementMap };
1500
+ }
1501
+ /**
1502
+ * Extract both spec and text from message parts.
1503
+ * Combines buildSpecFromParts and getTextFromParts.
1504
+ *
1505
+ * @example
1506
+ * ```ts
1507
+ * const { spec, text, hasSpec } = extractFromParts(message.parts);
1508
+ * ```
1509
+ */
1510
+ function extractFromParts(parts) {
1511
+ const spec = buildSpecFromParts(parts);
1512
+ const text = getTextFromParts(parts);
1513
+ const hasSpec = spec !== null && Object.keys(spec.elements || {}).length > 0;
1514
+ return { spec, text, hasSpec };
1515
+ }
1516
+
1517
+ /*
1518
+ * Public API Surface of @ng-render/angular
1519
+ */
1520
+ // Schema
1521
+
1522
+ /**
1523
+ * Generated bundle index. Do not edit.
1524
+ */
1525
+
1526
+ export { ActionDispatcherService, ChildrenOutletDirective, ConfirmDialogComponent, ElementRendererComponent, FallbackComponent, JsonRendererComponent, NG_RENDER_ACTION_HANDLERS, NG_RENDER_FALLBACK, NG_RENDER_NAVIGATE, NG_RENDER_REGISTRY, RepeatScopeService, SpecStateService, ValidationService, VisibilityService, applyPatch, boundProp, buildSpecFromParts, createChatUI, createUIStream, defineRegistry, extractFromParts, flatToTree, getTextFromParts, injectBoundProp, provideNgRender, schema };
1527
+ //# sourceMappingURL=ng-render-angular.mjs.map