@praxisui/stepper 1.0.0-beta.48 → 1.0.0-beta.53

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/README.md CHANGED
@@ -1,3 +1,45 @@
1
+ ---
2
+ title: "Stepper"
3
+ slug: "stepper-overview"
4
+ description: "Visao geral do @praxisui/stepper com fluxo multi-etapas, persistencia por stepperId, integracao com dynamic-form e editor embutido."
5
+ doc_type: "reference"
6
+ document_kind: "component-overview"
7
+ component: "stepper"
8
+ category: "components"
9
+ audience:
10
+ - "frontend"
11
+ - "host"
12
+ - "architect"
13
+ level: "intermediate"
14
+ status: "active"
15
+ owner: "praxis-ui"
16
+ tags:
17
+ - "stepper"
18
+ - "wizard"
19
+ - "dynamic-form"
20
+ - "settings-panel"
21
+ - "metadata"
22
+ order: 36
23
+ icon: "list-todo"
24
+ toc: true
25
+ sidebar: true
26
+ search_boost: 1.0
27
+ reading_time: 12
28
+ estimated_setup_time: 20
29
+ version: "1.0"
30
+ related_docs:
31
+ - "stepper-api-reference"
32
+ - "host-stepper-integration"
33
+ - "consumer-integration-quickstart"
34
+ - "host-integration-guide"
35
+ keywords:
36
+ - "praxis stepper"
37
+ - "wizard form"
38
+ - "stepper metadata"
39
+ - "stepperId persistence"
40
+ last_updated: "2026-03-07"
41
+ ---
42
+
1
43
  # @praxisui/stepper
2
44
 
3
45
  ## 🔰 Exemplos / Quickstart
@@ -112,7 +154,7 @@ export class CadastroComponent {
112
154
  {
113
155
  id: 'endereco',
114
156
  label: 'Endereço',
115
- form: { resourcePath: 'enderecos', mode: 'create' },
157
+ form: { resourcePath: 'human-resources/enderecos', mode: 'create' },
116
158
  },
117
159
  ],
118
160
  };
@@ -121,6 +163,9 @@ export class CadastroComponent {
121
163
 
122
164
  Validação: em modo `linear`, o avanço bloqueia quando o formulário da etapa atual é inválido. Para validação remota, forneça `(serverValidate)` ao componente.
123
165
 
166
+ Observação sobre `resourcePath`:
167
+ - use o caminho base canônico do recurso, sem `/api`, `/filter` ou `/{id}`. Ex.: `human-resources/funcionarios`.
168
+
124
169
  ```html
125
170
  <praxis-stepper stepperId="stepper-demo"
126
171
  [config]="config"
@@ -148,7 +193,7 @@ validateStep = async ({ step, formGroup, formData }) => {
148
193
  O `PraxisWizardFormComponent` é um wrapper experimental que traduz um JSON de wizard em `StepperMetadata` + `FormConfig` e aplica um layout inspirado no Financial Times.
149
194
 
150
195
  ```ts
151
- import { PraxisWizardFormComponent, FT_WIZARD_CONFIG } from '@praxisui/stepper/experimental';
196
+ import { PraxisWizardFormComponent, FT_WIZARD_CONFIG } from '@praxisui/stepper';
152
197
 
153
198
  @Component({
154
199
  selector: 'app-ft-wizard',
@@ -168,6 +213,8 @@ Notas:
168
213
  - Cada bloco pode definir `id` (string) para gerar `blockId` estável em runtime; sem `id`, o fallback usa índice posicional.
169
214
  - `blocks` e `zones` são mutuamente exclusivos no contrato. Se ambos forem enviados, `zones` tem prioridade no runtime.
170
215
  - O wizard faz validação runtime (campos essenciais e exclusividade de `blocks`/`zones`). Em `devMode`, lança erro para facilitar o debug; em produção, emite `console.warn`.
216
+ - A CTA primária reaproveita a validação do `PraxisStepper` interno; em `submit`, a etapa atual também é validada antes da emissão.
217
+ - `cta.action = 'custom'` emite `(customAction)` no wizard para orquestração externa do host.
171
218
  - Preset visual recomendado para FT-like: `appearance.preset = 'ft-light'` (aplicado automaticamente no wizard).
172
219
 
173
220
  ## Tokens de tema (Wizard)
@@ -195,8 +242,8 @@ Tokens disponíveis:
195
242
  - A persistência de progresso e preferências usa `CONFIG_STORAGE` (localStorage por padrão).
196
243
  - JSON Schema: `projects/praxis-stepper/src/lib/wizard/praxis-wizard-form.schema.json`.
197
244
  - Personalização de cor do stepper (sem hardcode): ajuste `--praxis-wizard-accent` no host ou em `:root` para controlar o estado ativo/concluído.
245
+ - `appearance.tokens` aplica variáveis CSS diretamente no host do stepper; use nomes com ou sem prefixo `--`.
198
246
  - Para evitar `::ng-deep`, use `stepperClass` e `contentClass` no metadata e aplique estilos em um stylesheet global do app (ou em presets internos da lib).
199
- - O import `@praxisui/stepper/experimental` é resolvido via `tsconfig` no workspace e não faz parte do pacote público publicado.
200
247
 
201
248
  ```scss
202
249
  :root {
@@ -1,7 +1,7 @@
1
1
  import * as i1$1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { Inject, Component, inject, signal, EventEmitter, computed, TemplateRef, Output, Input, ContentChild, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER, isDevMode, ElementRef, Renderer2, effect, ViewChild } from '@angular/core';
4
+ import { Inject, Component, inject, signal, ElementRef, Renderer2, EventEmitter, computed, TemplateRef, Output, Input, ContentChild, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER, isDevMode, effect, ViewChild } from '@angular/core';
5
5
  import { ActivatedRoute } from '@angular/router';
6
6
  import * as i2$1 from '@angular/material/stepper';
7
7
  import { MatStepperModule } from '@angular/material/stepper';
@@ -2923,6 +2923,10 @@ class PraxisStepper {
2923
2923
  _selectedIndex = signal(0, ...(ngDevMode ? [{ debugName: "_selectedIndex" }] : []));
2924
2924
  _formGroups = new Map();
2925
2925
  _formValidity = new Map();
2926
+ hostElement = inject((ElementRef));
2927
+ renderer = inject(Renderer2);
2928
+ appliedHeaderClasses = [];
2929
+ appliedTokenNames = [];
2926
2930
  // CTA models for empty state
2927
2931
  primaryCta = { label: 'Configurar Stepper', icon: 'tune', color: 'primary', action: () => this.openEditor() };
2928
2932
  secondaryCtas = [{ label: 'Adicionar etapa inicial', icon: 'add', action: () => this.addFirstStep() }];
@@ -2946,6 +2950,7 @@ class PraxisStepper {
2946
2950
  if (next?.selectedIndex != null)
2947
2951
  this._selectedIndex.set(next.selectedIndex);
2948
2952
  this.persistConfig(next);
2953
+ this.scheduleDomSync();
2949
2954
  }
2950
2955
  set selectedIndexInput(i) {
2951
2956
  if (i != null)
@@ -3010,7 +3015,6 @@ class PraxisStepper {
3010
3015
  return [
3011
3016
  'praxis-stepper',
3012
3017
  cfg?.stepperClass,
3013
- cfg?.headerClass,
3014
3018
  cfg?.appearance?.themeClass,
3015
3019
  cfg?.appearance?.preset === 'ft-light' ? 'pdx-stepper-ft-light' : null,
3016
3020
  ].filter(Boolean).join(' ');
@@ -3138,6 +3142,7 @@ class PraxisStepper {
3138
3142
  const sel = typeof cloned.selectedIndex === 'number' ? cloned.selectedIndex : this._selectedIndex();
3139
3143
  this._selectedIndex.set(this.clampIndex(sel));
3140
3144
  this.persistConfig(cloned);
3145
+ this.scheduleDomSync();
3141
3146
  }
3142
3147
  };
3143
3148
  ref.applied$.subscribe(apply);
@@ -3149,6 +3154,7 @@ class PraxisStepper {
3149
3154
  const sel = typeof cloned.selectedIndex === 'number' ? cloned.selectedIndex : this._selectedIndex();
3150
3155
  this._selectedIndex.set(this.clampIndex(sel));
3151
3156
  this.persistConfig(cloned);
3157
+ this.scheduleDomSync();
3152
3158
  }
3153
3159
  // CTA helpers
3154
3160
  addFirstStep() {
@@ -3165,9 +3171,13 @@ class PraxisStepper {
3165
3171
  // Seleciona a primeira etapa
3166
3172
  this._selectedIndex.set(0);
3167
3173
  this.persistConfig(next);
3174
+ this.scheduleDomSync();
3168
3175
  }
3169
3176
  // Navegação com validação remota opcional
3170
3177
  async onNext(i) {
3178
+ await this.nextWithValidation(i);
3179
+ }
3180
+ async validateStep(i = this.selectedIndexComputed()) {
3171
3181
  // Em modo linear, bloquear quando inválido
3172
3182
  if (this.linear() && this._formValidity.get(i) === false) {
3173
3183
  const fg = this._formGroups.get(i);
@@ -3175,9 +3185,12 @@ class PraxisStepper {
3175
3185
  fg.markAllAsTouched();
3176
3186
  fg.updateValueAndValidity({ emitEvent: true });
3177
3187
  }
3178
- return;
3188
+ return false;
3179
3189
  }
3180
3190
  const step = this.steps()[i];
3191
+ if (!step) {
3192
+ return true;
3193
+ }
3181
3194
  const fg = this._formGroups.get(i);
3182
3195
  const data = fg?.getRawValue();
3183
3196
  if (this.serverValidate) {
@@ -3187,10 +3200,18 @@ class PraxisStepper {
3187
3200
  // Atualizar erro visual
3188
3201
  step.errorMessage = res.errorMessage || (res.formErrors && res.formErrors[0]) || 'Erro de validação';
3189
3202
  this._formValidity.set(i, false);
3190
- return;
3203
+ return false;
3191
3204
  }
3192
3205
  }
3206
+ return true;
3207
+ }
3208
+ async nextWithValidation(i = this.selectedIndexComputed()) {
3209
+ const isValid = await this.validateStep(i);
3210
+ if (!isValid) {
3211
+ return false;
3212
+ }
3193
3213
  this.next();
3214
+ return true;
3194
3215
  }
3195
3216
  onPrev() { this.prev(); }
3196
3217
  onAnimationDone() { this.animationDone.emit(); }
@@ -3263,8 +3284,51 @@ class PraxisStepper {
3263
3284
  this._config.set(cloned);
3264
3285
  const sel = typeof cloned.selectedIndex === 'number' ? cloned.selectedIndex : this._selectedIndex();
3265
3286
  this._selectedIndex.set(this.clampIndex(sel));
3287
+ this.scheduleDomSync();
3288
+ });
3289
+ }
3290
+ ngAfterViewInit() {
3291
+ this.scheduleDomSync();
3292
+ }
3293
+ scheduleDomSync() {
3294
+ queueMicrotask(() => {
3295
+ this.syncAppearanceTokens();
3296
+ this.syncHeaderClasses();
3266
3297
  });
3267
3298
  }
3299
+ syncAppearanceTokens() {
3300
+ const host = this.hostElement.nativeElement;
3301
+ const nextTokens = this._config()?.appearance?.tokens ?? {};
3302
+ const nextTokenNames = new Set(Object.keys(nextTokens).map((name) => this.resolveCssTokenName(name)));
3303
+ this.appliedTokenNames
3304
+ .filter((name) => !nextTokenNames.has(name))
3305
+ .forEach((name) => this.renderer.removeStyle(host, name));
3306
+ Object.entries(nextTokens).forEach(([name, value]) => {
3307
+ this.renderer.setStyle(host, this.resolveCssTokenName(name), value);
3308
+ });
3309
+ this.appliedTokenNames = [...nextTokenNames];
3310
+ }
3311
+ syncHeaderClasses() {
3312
+ const host = this.hostElement.nativeElement;
3313
+ const nextClasses = this.parseClassList(this._config()?.headerClass);
3314
+ const targets = host.querySelectorAll('.mat-step-header, .mat-horizontal-stepper-header-container');
3315
+ targets.forEach((target) => {
3316
+ this.appliedHeaderClasses
3317
+ .filter((className) => !nextClasses.includes(className))
3318
+ .forEach((className) => this.renderer.removeClass(target, className));
3319
+ nextClasses.forEach((className) => this.renderer.addClass(target, className));
3320
+ });
3321
+ this.appliedHeaderClasses = nextClasses;
3322
+ }
3323
+ parseClassList(value) {
3324
+ return (value ?? '')
3325
+ .split(/\s+/)
3326
+ .map((className) => className.trim())
3327
+ .filter(Boolean);
3328
+ }
3329
+ resolveCssTokenName(name) {
3330
+ return name.startsWith('--') ? name : `--${name}`;
3331
+ }
3268
3332
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisStepper, deps: [], target: i0.ɵɵFactoryTarget.Component });
3269
3333
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisStepper, isStandalone: true, selector: "praxis-stepper", inputs: { stepperId: "stepperId", componentInstanceId: "componentInstanceId", config: "config", selectedIndexInput: "selectedIndexInput", selectedIndex: "selectedIndex", disableRippleInput: "disableRippleInput", editModeEnabled: "editModeEnabled", labelPosition: "labelPosition", color: "color", serverValidate: "serverValidate", stepperContext: "stepperContext" }, outputs: { selectedIndexChange: "selectedIndexChange", widgetEvent: "widgetEvent", stepFormReady: "stepFormReady", stepFormValueChange: "stepFormValueChange", animationDone: "animationDone", selectionChange: "selectionChange" }, host: { properties: { "class": "densityClass()" } }, queries: [{ propertyName: "stepLabelTpl", first: true, predicate: ["stepLabelTpl"], descendants: true, read: TemplateRef }], ngImport: i0, template: `
3270
3334
  <div class="stepper-ai-assistant" *ngIf="editModeEnabled">
@@ -3272,6 +3336,7 @@ class PraxisStepper {
3272
3336
  </div>
3273
3337
  <ng-container *ngIf="steps().length > 0; else emptyState">
3274
3338
  <mat-stepper
3339
+ #stepperRoot
3275
3340
  [linear]="linear()"
3276
3341
  [orientation]="orientation()"
3277
3342
  [headerPosition]="headerPosition()"
@@ -3429,6 +3494,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3429
3494
  </div>
3430
3495
  <ng-container *ngIf="steps().length > 0; else emptyState">
3431
3496
  <mat-stepper
3497
+ #stepperRoot
3432
3498
  [linear]="linear()"
3433
3499
  [orientation]="orientation()"
3434
3500
  [headerPosition]="headerPosition()"
@@ -3797,22 +3863,20 @@ const PRAXIS_STEPPER_COMPONENT_METADATA = {
3797
3863
  inputs: [
3798
3864
  { name: 'stepperId', type: 'string', label: 'ID do Stepper', description: 'Identificador para persistência (obrigatório)' },
3799
3865
  { name: 'componentInstanceId', type: 'string', label: 'ID da instância', description: 'Identificador opcional para múltiplas instâncias na mesma rota' },
3800
- { name: 'config', type: 'StepperMetadata', label: 'Configuração', description: 'Configuração JSON com passos e opções' },
3866
+ {
3867
+ name: 'config',
3868
+ type: 'StepperMetadata',
3869
+ label: 'Configuração',
3870
+ description: 'Configuração JSON com passos, aparência, densidade, classes CSS e navegação',
3871
+ },
3801
3872
  { name: 'selectedIndex', type: 'number', label: '[(selectedIndex)] Índice selecionado' },
3802
3873
  { name: 'selectedIndexInput', type: 'number', label: 'Índice selecionado (legacy)' },
3803
3874
  { name: 'disableRippleInput', type: 'boolean', default: false, label: 'Desabilitar ripple' },
3804
3875
  { name: 'editModeEnabled', type: 'boolean', default: false, label: 'Modo de edição', description: 'Exibe controles de edição e assistente AI no runtime' },
3805
3876
  { name: 'labelPosition', type: "'bottom' | 'end'", label: 'Posição do rótulo (horizontal)' },
3806
3877
  { name: 'color', type: 'ThemePalette', label: 'Cor (M2)' },
3807
- { name: 'appearance', type: '{ themeClass?: string; tokens?: Record<string,string>; icons?: { number?: string; done?: string; edit?: string; error?: string }; iconsSet?: string }', label: 'Aparência (tokens, themeClass, ícones, conjunto)' },
3808
3878
  { name: 'serverValidate', type: '(args) => Promise<{ ok: boolean; fieldErrors?: Record<string,string[]>; formErrors?: string[] }>', label: 'Validação remota (opt-in)' },
3809
- { name: 'density', type: "'default' | 'comfortable' | 'compact'", label: 'Densidade' },
3810
- { name: 'stepperClass', type: 'string', label: 'Classe CSS (stepper)' },
3811
- { name: 'headerClass', type: 'string', label: 'Classe CSS (header)' },
3812
- { name: 'contentClass', type: 'string', label: 'Classe CSS (conteúdo)' },
3813
3879
  { name: 'stepperContext', type: 'Record<string, any>', label: 'Contexto', description: 'Contexto compartilhado para widgets internos' },
3814
- { name: 'navigation', type: "{ visible?: boolean; prevLabel?: string; nextLabel?: string; prevIcon?: string; nextIcon?: string; variant?: 'basic'|'flat'|'stroked'|'raised'; color?: ThemePalette; align?: 'start'|'center'|'end'|'space-between' }", label: 'Navegação (botões)'
3815
- },
3816
3880
  ],
3817
3881
  outputs: [
3818
3882
  { name: 'selectionChange', type: 'any', label: 'Troca de seleção' },
@@ -4534,6 +4598,7 @@ class PraxisWizardFormComponent {
4534
4598
  this._config.set(validateWizardConfig(cfg));
4535
4599
  }
4536
4600
  submit = new EventEmitter();
4601
+ customAction = new EventEmitter();
4537
4602
  _config = signal(null, ...(ngDevMode ? [{ debugName: "_config" }] : []));
4538
4603
  persistedState = signal(null, ...(ngDevMode ? [{ debugName: "persistedState" }] : []));
4539
4604
  selectedIndex = signal(0, ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
@@ -4581,17 +4646,29 @@ class PraxisWizardFormComponent {
4581
4646
  return !!this.stepper?.isNextDisabled(this.selectedIndex());
4582
4647
  return false;
4583
4648
  }
4584
- onPrimaryAction() {
4649
+ async onPrimaryAction() {
4585
4650
  this.focusPrimaryAction();
4586
4651
  const action = this.primaryAction();
4587
4652
  if (action === 'next') {
4588
- this.stepper?.next();
4653
+ await this.stepper?.nextWithValidation(this.selectedIndex());
4589
4654
  return;
4590
4655
  }
4591
4656
  if (action === 'submit') {
4657
+ const isValid = await this.stepper?.validateStep(this.selectedIndex());
4658
+ if (isValid === false) {
4659
+ return;
4660
+ }
4592
4661
  this.submit.emit({ wizardId: this.wizardId, values: this.collectValues() });
4593
4662
  return;
4594
4663
  }
4664
+ if (action === 'custom') {
4665
+ this.customAction.emit({
4666
+ wizardId: this.wizardId,
4667
+ stepId: this.activeStep()?.id,
4668
+ stepIndex: this.selectedIndex(),
4669
+ values: this.collectValues(),
4670
+ });
4671
+ }
4595
4672
  }
4596
4673
  onSecondaryAction() {
4597
4674
  this.focusSecondaryAction();
@@ -4680,7 +4757,7 @@ class PraxisWizardFormComponent {
4680
4757
  el?.focus();
4681
4758
  }
4682
4759
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisWizardFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4683
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisWizardFormComponent, isStandalone: true, selector: "praxis-wizard-form", inputs: { wizardId: "wizardId", editModeEnabled: "editModeEnabled", config: "config" }, outputs: { submit: "submit" }, viewQueries: [{ propertyName: "stepper", first: true, predicate: ["stepper"], descendants: true, static: true }], ngImport: i0, template: `
4760
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisWizardFormComponent, isStandalone: true, selector: "praxis-wizard-form", inputs: { wizardId: "wizardId", editModeEnabled: "editModeEnabled", config: "config" }, outputs: { submit: "submit", customAction: "customAction" }, viewQueries: [{ propertyName: "stepper", first: true, predicate: ["stepper"], descendants: true, static: true }], ngImport: i0, template: `
4684
4761
  <section class="ft-wizard-shell" [attr.data-wizard-id]="wizardId">
4685
4762
  <div class="ft-wizard-header">
4686
4763
  <div class="ft-brand">{{ wizardBrand() }}</div>
@@ -4773,6 +4850,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4773
4850
  type: Input
4774
4851
  }], submit: [{
4775
4852
  type: Output
4853
+ }], customAction: [{
4854
+ type: Output
4776
4855
  }] } });
4777
4856
  function pickPreferenceValues(value, fields) {
4778
4857
  if (!value || !fields || fields.length === 0)