@daltonr/pathwrite-angular 0.11.0 → 0.12.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.
package/src/index.ts CHANGED
@@ -30,6 +30,13 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
30
30
  private _unsubscribeFromEngine: () => void = () => {};
31
31
  private readonly _stateSignal = signal<PathSnapshot<TData> | null>(null);
32
32
 
33
+ /**
34
+ * Arbitrary services object passed via `[services]` on `<pw-shell>`.
35
+ * Read it in step components via `usePathContext<TData, TServices>().services`.
36
+ * Set directly by `PathShellComponent` — do not set this manually.
37
+ */
38
+ public services: unknown = null;
39
+
33
40
  public readonly state$: Observable<PathSnapshot<TData> | null> = this._state$.asObservable();
34
41
  public readonly events$: Observable<PathEvent> = this._events$.asObservable();
35
42
  /** Signal version of state$. Updates on every path state change. Requires Angular 16+. */
@@ -72,8 +79,9 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
72
79
  this._state$.next(event.snapshot as PathSnapshot<TData>);
73
80
  this._stateSignal.set(event.snapshot as PathSnapshot<TData>);
74
81
  } else if (event.type === "completed" || event.type === "cancelled") {
75
- this._state$.next(null);
76
- this._stateSignal.set(null);
82
+ const snap = engine.snapshot() as PathSnapshot<TData> | null;
83
+ this._state$.next(snap);
84
+ this._stateSignal.set(snap);
77
85
  }
78
86
  });
79
87
  }
@@ -134,15 +142,15 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
134
142
  return this._engine.resetStep();
135
143
  }
136
144
 
137
- public goToStep(stepId: string): Promise<void> {
138
- return this._engine.goToStep(stepId);
145
+ public goToStep(stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> {
146
+ return this._engine.goToStep(stepId, options);
139
147
  }
140
148
 
141
149
  /** Jump to a step by ID, checking the current step's canMoveNext (forward) or
142
150
  * canMovePrevious (backward) guard first. Navigation is blocked if the guard
143
151
  * returns false. Throws if the step ID does not exist. */
144
- public goToStepChecked(stepId: string): Promise<void> {
145
- return this._engine.goToStepChecked(stepId);
152
+ public goToStepChecked(stepId: string, options?: { validateOnLeave?: boolean }): Promise<void> {
153
+ return this._engine.goToStepChecked(stepId, options);
146
154
  }
147
155
 
148
156
  public validate(): void {
@@ -163,7 +171,7 @@ export class PathFacade<TData extends PathData = PathData> implements OnDestroy
163
171
  * path state and strongly-typed navigation actions. Mirrors React's `usePathContext()`
164
172
  * return type for consistency across adapters.
165
173
  */
166
- export interface UsePathContextReturn<TData extends PathData = PathData> {
174
+ export interface UsePathContextReturn<TData extends PathData = PathData, TServices = unknown> {
167
175
  /** Current path snapshot as a signal. Returns `null` when no path is active. */
168
176
  snapshot: Signal<PathSnapshot<TData> | null>;
169
177
  /** Start (or restart) a path. */
@@ -180,10 +188,10 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
180
188
  setData: <K extends string & keyof TData>(key: K, value: TData[K]) => Promise<void>;
181
189
  /** Reset the current step's data to what it was when the step was entered. */
182
190
  resetStep: () => Promise<void>;
183
- /** Jump to a step by ID without checking guards. */
184
- goToStep: (stepId: string) => Promise<void>;
191
+ /** Jump to a step by ID without checking guards. Pass `{ validateOnLeave: true }` to mark the departing step as attempted before navigating. */
192
+ goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
185
193
  /** Jump to a step by ID, checking guards first. */
186
- goToStepChecked: (stepId: string) => Promise<void>;
194
+ goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
187
195
  /**
188
196
  * Tears down any active path and immediately starts the given path fresh.
189
197
  * Use for "Start over" / retry flows.
@@ -193,6 +201,12 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
193
201
  retry: () => Promise<void>;
194
202
  /** Pause with intent to return, preserving all state. Emits `suspended`. */
195
203
  suspend: () => Promise<void>;
204
+ /**
205
+ * The services object passed to the nearest `<pw-shell>` via `[services]`.
206
+ * Typed as `TServices` — pass your services interface as the second generic:
207
+ * `usePathContext<MyData, MyServices>().services`.
208
+ */
209
+ services: TServices;
196
210
  }
197
211
 
198
212
  /**
@@ -231,7 +245,7 @@ export interface UsePathContextReturn<TData extends PathData = PathData> {
231
245
  *
232
246
  * @throws Error if PathFacade is not provided in the injector tree
233
247
  */
234
- export function usePathContext<TData extends PathData = PathData>(): UsePathContextReturn<TData> {
248
+ export function usePathContext<TData extends PathData = PathData, TServices = unknown>(): UsePathContextReturn<TData, TServices> {
235
249
  const facade = inject(PathFacade, { optional: true }) as PathFacade<TData> | null;
236
250
 
237
251
  if (!facade) {
@@ -250,11 +264,12 @@ export function usePathContext<TData extends PathData = PathData>(): UsePathCont
250
264
  cancel: () => facade.cancel(),
251
265
  setData: (key, value) => facade.setData(key, value),
252
266
  resetStep: () => facade.resetStep(),
253
- goToStep: (stepId) => facade.goToStep(stepId),
254
- goToStepChecked: (stepId) => facade.goToStepChecked(stepId),
267
+ goToStep: (stepId, options) => facade.goToStep(stepId, options),
268
+ goToStepChecked: (stepId, options) => facade.goToStepChecked(stepId, options),
255
269
  restart: () => facade.restart(),
256
270
  retry: () => facade.retry(),
257
271
  suspend: () => facade.suspend(),
272
+ services: facade.services as TServices,
258
273
  };
259
274
  }
260
275
 
package/src/shell.ts CHANGED
@@ -45,8 +45,8 @@ export interface PathShellActions {
45
45
  next: () => Promise<void>;
46
46
  previous: () => Promise<void>;
47
47
  cancel: () => Promise<void>;
48
- goToStep: (stepId: string) => Promise<void>;
49
- goToStepChecked: (stepId: string) => Promise<void>;
48
+ goToStep: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
49
+ goToStepChecked: (stepId: string, options?: { validateOnLeave?: boolean }) => Promise<void>;
50
50
  setData: (key: string, value: unknown) => Promise<void>;
51
51
  /** Restart the shell's current path with its current `initialData`. */
52
52
  restart: () => Promise<void>;
@@ -127,6 +127,31 @@ export class PathShellFooterDirective {
127
127
  ) {}
128
128
  }
129
129
 
130
+ // ---------------------------------------------------------------------------
131
+ // PathShellCompletionDirective
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Replaces the default completion panel inside `<pw-shell>` when
136
+ * `snapshot.status === "completed"` (`completionBehaviour: "stayOnFinal"`).
137
+ * The template receives the current `PathSnapshot` as the implicit context.
138
+ *
139
+ * ```html
140
+ * <pw-shell [path]="myPath">
141
+ * <ng-template pwShellCompletion let-s>
142
+ * <my-completion-screen [data]="s.data" />
143
+ * </ng-template>
144
+ * <ng-template pwStep="details"><app-details-form /></ng-template>
145
+ * </pw-shell>
146
+ * ```
147
+ */
148
+ @Directive({ selector: "[pwShellCompletion]", standalone: true })
149
+ export class PathShellCompletionDirective {
150
+ public constructor(
151
+ public readonly templateRef: TemplateRef<{ $implicit: PathSnapshot }>
152
+ ) {}
153
+ }
154
+
130
155
  // ---------------------------------------------------------------------------
131
156
  // PathShellComponent
132
157
  // ---------------------------------------------------------------------------
@@ -160,7 +185,7 @@ export class PathShellFooterDirective {
160
185
  <!-- Active path -->
161
186
  <div class="pw-shell" [ngClass]="progressLayout !== 'merged' ? 'pw-shell--progress-' + progressLayout : ''" *ngIf="facade.state$ | async as s">
162
187
  <!-- Root progress — persistent top-level bar visible during sub-paths -->
163
- <div class="pw-shell__root-progress" *ngIf="!hideProgress && s.rootProgress && progressLayout !== 'activeOnly'">
188
+ <div class="pw-shell__root-progress" *ngIf="!effectiveHideProgress && s.rootProgress && progressLayout !== 'activeOnly'">
164
189
  <div class="pw-shell__steps">
165
190
  <div
166
191
  *ngFor="let step of s.rootProgress!.steps; let i = index"
@@ -181,7 +206,7 @@ export class PathShellFooterDirective {
181
206
  <ng-container *ngTemplateOutlet="customHeader.templateRef; context: { $implicit: s }"></ng-container>
182
207
  </ng-container>
183
208
  <ng-template #defaultHeader>
184
- <div class="pw-shell__header" *ngIf="!hideProgress && (s.stepCount > 1 || s.nestingLevel > 0) && progressLayout !== 'rootOnly'">
209
+ <div class="pw-shell__header" *ngIf="!effectiveHideProgress && (s.stepCount > 1 || s.nestingLevel > 0) && progressLayout !== 'rootOnly'">
185
210
  <div class="pw-shell__steps">
186
211
  <div
187
212
  *ngFor="let step of s.steps; let i = index"
@@ -198,75 +223,91 @@ export class PathShellFooterDirective {
198
223
  </div>
199
224
  </ng-template>
200
225
 
201
- <!-- Bodystep content -->
202
- <div class="pw-shell__body">
203
- <ng-container *ngFor="let stepDir of stepDirectives">
204
- <!-- Match by formId first (inner step of a StepChoice), then stepId -->
205
- <ng-container *ngIf="stepDir.stepId === (s.formId ?? s.stepId)">
206
- <ng-container *ngTemplateOutlet="stepDir.templateRef; injector: shellInjector"></ng-container>
207
- </ng-container>
226
+ <!-- Completion panel shown when path finishes with stayOnFinal -->
227
+ <ng-container *ngIf="s.status === 'completed'; else activeContent">
228
+ <ng-container *ngIf="customCompletion; else defaultCompletion">
229
+ <ng-container *ngTemplateOutlet="customCompletion.templateRef; context: { $implicit: s }"></ng-container>
208
230
  </ng-container>
209
- </div>
231
+ <ng-template #defaultCompletion>
232
+ <div class="pw-shell__completion">
233
+ <p class="pw-shell__completion-message">All done.</p>
234
+ <button type="button" class="pw-shell__completion-restart" (click)="facade.restart()">Start over</button>
235
+ </div>
236
+ </ng-template>
237
+ </ng-container>
210
238
 
211
- <!-- Validation messages suppressed when validationDisplay="inline" -->
212
- <ul class="pw-shell__validation" *ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && fieldEntries(s).length > 0">
213
- <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
214
- <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
215
- </li>
216
- </ul>
217
-
218
- <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
219
- <ul class="pw-shell__warnings" *ngIf="validationDisplay !== 'inline' && warningEntries(s).length > 0">
220
- <li *ngFor="let entry of warningEntries(s)" class="pw-shell__warnings-item">
221
- <span *ngIf="entry[0] !== '_'" class="pw-shell__warnings-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
222
- </li>
223
- </ul>
224
-
225
- <!-- Blocking error — guard returned { allowed: false, reason } -->
226
- <p class="pw-shell__blocking-error"
227
- *ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && s.blockingError">
228
- {{ s.blockingError }}
229
- </p>
230
-
231
- <!-- Error panel — replaces footer when an async operation has failed -->
232
- <div class="pw-shell__error" *ngIf="s.status === 'error' && s.error; else footerOrCustom">
233
- <div class="pw-shell__error-title">{{ s.error!.retryCount >= 2 ? 'Still having trouble.' : 'Something went wrong.' }}</div>
234
- <div class="pw-shell__error-message">{{ errorPhaseMessage(s.error!.phase) }}{{ s.error!.message ? ' ' + s.error!.message : '' }}</div>
235
- <div class="pw-shell__error-actions">
236
- <button
237
- *ngIf="s.error!.retryCount < 2"
238
- type="button"
239
- class="pw-shell__btn pw-shell__btn--retry"
240
- (click)="facade.retry()"
241
- >Try again</button>
242
- <button
243
- *ngIf="s.hasPersistence"
244
- type="button"
245
- [class]="'pw-shell__btn ' + (s.error!.retryCount >= 2 ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend')"
246
- (click)="facade.suspend()"
247
- >Save and come back later</button>
248
- <button
249
- *ngIf="s.error!.retryCount >= 2 && !s.hasPersistence"
250
- type="button"
251
- class="pw-shell__btn pw-shell__btn--retry"
252
- (click)="facade.retry()"
253
- >Try again</button>
239
+ <!-- Active step content -->
240
+ <ng-template #activeContent>
241
+ <!-- Body step content -->
242
+ <div class="pw-shell__body">
243
+ <ng-container *ngFor="let stepDir of stepDirectives">
244
+ <!-- Match by formId first (inner step of a StepChoice), then stepId -->
245
+ <ng-container *ngIf="stepDir.stepId === (s.formId ?? s.stepId)">
246
+ <ng-container *ngTemplateOutlet="stepDir.templateRef; injector: shellInjector"></ng-container>
247
+ </ng-container>
248
+ </ng-container>
254
249
  </div>
255
- </div>
256
- <!-- Footercustom or default navigation buttons -->
257
- <ng-template #footerOrCustom>
258
- <ng-container *ngIf="!hideFooter">
259
- <ng-container *ngIf="customFooter; else defaultFooter">
260
- <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
250
+
251
+ <!-- Validation messages suppressed when validationDisplay="inline" -->
252
+ <ul class="pw-shell__validation" *ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && fieldEntries(s).length > 0">
253
+ <li *ngFor="let entry of fieldEntries(s)" class="pw-shell__validation-item">
254
+ <span *ngIf="entry[0] !== '_'" class="pw-shell__validation-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
255
+ </li>
256
+ </ul>
257
+
258
+ <!-- Warning messages — non-blocking, shown immediately (no hasAttemptedNext gate) -->
259
+ <ul class="pw-shell__warnings" *ngIf="validationDisplay !== 'inline' && warningEntries(s).length > 0">
260
+ <li *ngFor="let entry of warningEntries(s)" class="pw-shell__warnings-item">
261
+ <span *ngIf="entry[0] !== '_'" class="pw-shell__warnings-label">{{ formatFieldKey(entry[0]) }}</span>{{ entry[1] }}
262
+ </li>
263
+ </ul>
264
+
265
+ <!-- Blocking error — guard returned { allowed: false, reason } -->
266
+ <p class="pw-shell__blocking-error"
267
+ *ngIf="validationDisplay !== 'inline' && (s.hasAttemptedNext || s.hasValidated) && s.blockingError">
268
+ {{ s.blockingError }}
269
+ </p>
270
+
271
+ <!-- Error panel — replaces footer when an async operation has failed -->
272
+ <div class="pw-shell__error" *ngIf="s.status === 'error' && s.error; else footerOrCustom">
273
+ <div class="pw-shell__error-title">{{ s.error!.retryCount >= 2 ? 'Still having trouble.' : 'Something went wrong.' }}</div>
274
+ <div class="pw-shell__error-message">{{ errorPhaseMessage(s.error!.phase) }}{{ s.error!.message ? ' ' + s.error!.message : '' }}</div>
275
+ <div class="pw-shell__error-actions">
276
+ <button
277
+ *ngIf="s.error!.retryCount < 2"
278
+ type="button"
279
+ class="pw-shell__btn pw-shell__btn--retry"
280
+ (click)="facade.retry()"
281
+ >Try again</button>
282
+ <button
283
+ *ngIf="s.hasPersistence"
284
+ type="button"
285
+ [class]="'pw-shell__btn ' + (s.error!.retryCount >= 2 ? 'pw-shell__btn--retry' : 'pw-shell__btn--suspend')"
286
+ (click)="facade.suspend()"
287
+ >Save and come back later</button>
288
+ <button
289
+ *ngIf="s.error!.retryCount >= 2 && !s.hasPersistence"
290
+ type="button"
291
+ class="pw-shell__btn pw-shell__btn--retry"
292
+ (click)="facade.retry()"
293
+ >Try again</button>
294
+ </div>
295
+ </div>
296
+ <!-- Footer — custom or default navigation buttons -->
297
+ <ng-template #footerOrCustom>
298
+ <ng-container *ngIf="!effectiveHideFooter">
299
+ <ng-container *ngIf="customFooter; else defaultFooter">
300
+ <ng-container *ngTemplateOutlet="customFooter.templateRef; context: { $implicit: s, actions: shellActions }"></ng-container>
301
+ </ng-container>
261
302
  </ng-container>
262
- </ng-container>
303
+ </ng-template>
263
304
  </ng-template>
264
305
  <ng-template #defaultFooter>
265
306
  <div class="pw-shell__footer">
266
307
  <div class="pw-shell__footer-left">
267
308
  <!-- Form mode: Cancel on the left -->
268
309
  <button
269
- *ngIf="getResolvedFooterLayout(s) === 'form' && !hideCancel"
310
+ *ngIf="getResolvedLayout(s) === 'form' && !hideCancel"
270
311
  type="button"
271
312
  class="pw-shell__btn pw-shell__btn--cancel"
272
313
  [disabled]="s.status !== 'idle'"
@@ -274,7 +315,7 @@ export class PathShellFooterDirective {
274
315
  >{{ cancelLabel }}</button>
275
316
  <!-- Wizard mode: Back on the left -->
276
317
  <button
277
- *ngIf="getResolvedFooterLayout(s) === 'wizard' && !s.isFirstStep"
318
+ *ngIf="getResolvedLayout(s) === 'wizard' && !s.isFirstStep"
278
319
  type="button"
279
320
  class="pw-shell__btn pw-shell__btn--back"
280
321
  [disabled]="s.status !== 'idle' || !s.canMovePrevious"
@@ -284,7 +325,7 @@ export class PathShellFooterDirective {
284
325
  <div class="pw-shell__footer-right">
285
326
  <!-- Wizard mode: Cancel on the right -->
286
327
  <button
287
- *ngIf="getResolvedFooterLayout(s) === 'wizard' && !hideCancel"
328
+ *ngIf="getResolvedLayout(s) === 'wizard' && !hideCancel"
288
329
  type="button"
289
330
  class="pw-shell__btn pw-shell__btn--cancel"
290
331
  [disabled]="s.status !== 'idle'"
@@ -322,8 +363,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
322
363
  * ```
323
364
  */
324
365
  @Input() engine?: PathEngine;
325
- /** Initial data merged into the path engine on start. */
366
+ /** Initial data merged into the path engine on start. Used on first visit; overridden by stored snapshot when `restoreKey` is set. */
326
367
  @Input() initialData: PathData = {};
368
+ /**
369
+ * When set, this shell automatically saves its state into the nearest outer `pw-shell`'s
370
+ * data under this key on every change, and restores from that stored state on remount.
371
+ * No-op when used on a top-level shell with no outer `pw-shell` ancestor.
372
+ */
373
+ @Input() restoreKey?: string;
327
374
  /** Start the path automatically on ngOnInit. Set to false to call doStart() manually. */
328
375
  @Input() autoStart = true;
329
376
  /** Label for the Back navigation button. */
@@ -345,12 +392,19 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
345
392
  /** When true, calls `validate()` on the facade so all steps show inline errors simultaneously. Useful when this shell is nested inside a step of an outer shell: bind to the outer snapshot's `hasAttemptedNext`. */
346
393
  @Input() validateWhen = false;
347
394
  /**
348
- * Footer layout mode:
395
+ * Arbitrary services object made available to all step components via
396
+ * `usePathContext<TData, TServices>().services`. Pass API clients, feature
397
+ * flags, or any shared dependency without prop-drilling through each step.
398
+ */
399
+ @Input() services: unknown = null;
400
+ /**
401
+ * Shell layout mode:
349
402
  * - "auto" (default): Uses "form" for single-step top-level paths, "wizard" otherwise.
350
- * - "wizard": Back button on left, Cancel and Submit together on right.
351
- * - "form": Cancel on left, Submit alone on right. Back button never shown.
403
+ * - "wizard": Progress header + Back button on left, Cancel and Submit together on right.
404
+ * - "form": Progress header + Cancel on left, Submit alone on right. Back button never shown.
405
+ * - "tabs": No progress header, no footer. Use for tabbed interfaces with a custom tab bar inside the step body.
352
406
  */
353
- @Input() footerLayout: "wizard" | "form" | "auto" = "auto";
407
+ @Input() layout: "wizard" | "form" | "auto" | "tabs" = "auto";
354
408
  /**
355
409
  * Controls whether the shell renders its auto-generated field-error summary box.
356
410
  * - `"summary"` (default): Shell renders the labeled error list below the step body.
@@ -374,11 +428,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
374
428
  @ContentChildren(PathStepDirective) stepDirectives!: QueryList<PathStepDirective>;
375
429
  @ContentChild(PathShellHeaderDirective) customHeader?: PathShellHeaderDirective;
376
430
  @ContentChild(PathShellFooterDirective) customFooter?: PathShellFooterDirective;
431
+ @ContentChild(PathShellCompletionDirective) customCompletion?: PathShellCompletionDirective;
377
432
 
378
433
  public readonly facade = inject(PathFacade);
379
434
  /** The shell's own component-level injector. Passed to ngTemplateOutlet so that
380
435
  * step components can resolve PathFacade (provided by this shell) via inject(). */
381
436
  protected readonly shellInjector = inject(Injector);
437
+ /** Outer shell's PathFacade — present when this shell is nested inside another pw-shell. */
438
+ private readonly outerFacade = inject(PathFacade, { skipSelf: true, optional: true });
382
439
  public started = false;
383
440
 
384
441
  /** Navigation actions passed to custom `pwShellFooter` templates. */
@@ -386,8 +443,8 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
386
443
  next: () => this.facade.next(),
387
444
  previous: () => this.facade.previous(),
388
445
  cancel: () => this.facade.cancel(),
389
- goToStep: (id) => this.facade.goToStep(id),
390
- goToStepChecked: (id) => this.facade.goToStepChecked(id),
446
+ goToStep: (id, options) => this.facade.goToStep(id, options),
447
+ goToStepChecked: (id, options) => this.facade.goToStepChecked(id, options),
391
448
  setData: (key, value) => this.facade.setData(key, value as never),
392
449
  restart: () => this.facade.restart(),
393
450
  retry: () => this.facade.retry(),
@@ -403,13 +460,20 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
403
460
  if (changes['validateWhen'] && this.validateWhen) {
404
461
  this.facade.validate();
405
462
  }
463
+ if (changes['services']) {
464
+ this.facade.services = this.services;
465
+ }
406
466
  }
407
467
 
408
468
  public ngOnInit(): void {
469
+ this.facade.services = this.services;
409
470
  this.facade.events$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
410
471
  this.event.emit(event);
411
472
  if (event.type === "completed") this.complete.emit(event.data);
412
473
  if (event.type === "cancelled") this.cancel.emit(event.data);
474
+ if (this.restoreKey && this.outerFacade && event.type === "stateChanged") {
475
+ this.outerFacade.setData(this.restoreKey as any, (event as any).snapshot as any);
476
+ }
413
477
  });
414
478
 
415
479
  if (this.autoStart && !this.engine) {
@@ -425,7 +489,18 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
425
489
  public doStart(): void {
426
490
  if (!this.path) throw new Error('[pw-shell] [path] is required when no [engine] is provided');
427
491
  this.started = true;
428
- this.facade.start(this.path, this.initialData);
492
+ let startData: PathData = this.initialData;
493
+ let restoreStepId: string | undefined;
494
+ if (this.restoreKey && this.outerFacade) {
495
+ const stored = this.outerFacade.snapshot()?.data[this.restoreKey] as PathSnapshot | undefined;
496
+ if (stored != null && typeof stored === "object" && "stepId" in stored) {
497
+ startData = stored.data as PathData;
498
+ if (stored.stepIndex > 0) restoreStepId = stored.stepId as string;
499
+ }
500
+ }
501
+ this.facade.start(this.path, startData).then(() => {
502
+ if (restoreStepId) this.facade.goToStep(restoreStepId!);
503
+ });
429
504
  }
430
505
 
431
506
  /**
@@ -451,11 +526,14 @@ export class PathShellComponent implements OnInit, OnChanges, OnDestroy {
451
526
  return Object.entries(s.fieldWarnings) as [string, string][];
452
527
  }
453
528
 
454
- /** Resolves "auto" footerLayout based on snapshot. Single-step top-level "form", otherwise → "wizard". */
455
- protected getResolvedFooterLayout(s: PathSnapshot): "wizard" | "form" {
456
- return this.footerLayout === "auto"
529
+ get effectiveHideProgress(): boolean { return this.hideProgress || this.layout === "tabs"; }
530
+ get effectiveHideFooter(): boolean { return this.hideFooter || this.layout === "tabs"; }
531
+
532
+ /** Resolves "auto"/"tabs" layout to "wizard" or "form" for footer button arrangement. */
533
+ protected getResolvedLayout(s: PathSnapshot): "wizard" | "form" {
534
+ return this.layout === "auto" || this.layout === "tabs"
457
535
  ? (s.stepCount === 1 && s.nestingLevel === 0 ? "form" : "wizard")
458
- : this.footerLayout;
536
+ : this.layout;
459
537
  }
460
538
 
461
539
  protected errorPhaseMessage = errorPhaseMessage;