refactor(stepper): THM-4 — consolidate step state on data-state, drop boolean aliases

Every step part (item/trigger/indicator/separator) declared BOTH
`data-state` (enum) AND `data-current`/`data-complete`/`data-incomplete`
(booleans) — the same state twice, used inconsistently (enum on
trigger/indicator, boolean on separator). Consolidate on the canonical
`data-state`:
- morfo: prune the 12 boolean declarations.
- provider: drop the current/complete/incomplete sources
  (item/indicator/separator) + the trigger's explicit boolean writes +
  the now-unused isComplete/isIncomplete getters (isCurrent stays — it
  drives aria-current/tabindex).
- separator.css: [data-complete] -> [data-state='complete'].
- README + provider test updated to the single form.

Verified with the real component (Next advances → complete/current/
incomplete via data-state, boolean attrs gone, complete separator still
paints). stepper unused 30->18; provider test + contracts green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent d9c3d03f7d
commit 75fa94005b

@ -394,18 +394,28 @@ incomplete) **Y** los booleanos `data-current`/`data-complete`/`data-incomplete`
(`propRef`, severity optional). El propio CSS las usa **inconsistentemente**:
enum en trigger/indicator, booleano `data-complete` en el separator. Así que NO
es deuda-sin-consumidor (la forma booleana SÍ se consume en el separator) —
es **redundancia**: cada parte declara ambas formas y usa una. Consolidar en
`data-state` (canónico) + migrar el separator a `data-state='complete'` + podar
los booleanos limpiaría la inconsistencia, PERO es **cambio de API pública**
(un consumidor podría estilizar `[data-complete]`) → **decisión de diseño**, no
poda unilateral.
es **redundancia**: cada parte declara ambas formas y usa una.
**CONSOLIDADO ✅ (decisión de usuario 2026-07-12)** en `data-state` (canónico),
API pública unificada. 5 sitios: morfo (podados 12 declaraciones booleanas de
item/trigger/indicator/separator) · provider (podados los sources
current/complete/incomplete de item/indicator/separator + los writes explícitos
del trigger + los getters `isComplete`/`isIncomplete` — `isCurrent` se queda,
lo usan aria-current/tabindex) · provider-test (assertions booleanas fuera) ·
README (10 filas booleanas fuera) · separator.css (`[data-complete]` →
`[data-state='complete']`). **Verificado real** (demo, botón Next): estados por
`data-state` (complete/current/incomplete), booleanos ausentes, separator
completo pinta `--_stepper-palette-solid` vía `data-state='complete']`
(behavior-preserving). Gates: provider-test + contracts verdes · baseline 9 ·
**stepper unused 30→18** (total catálogo 1152→1140).
### THM-4 — lectura emergente (2 lotes)
media-player (0 deuda) + stepper (0 deuda-real, 1 redundancia de API): los
hotspots son **mayormente legítimos** (composites grandes + superficie
behavioral); la deuda-sin-consumidor genuina es **rara**. Lo accionable son
**decisiones de diseño** (dual-API de stepper), no podas mecánicas. Esto
media-player (0 deuda) + stepper (0 deuda-real, 1 redundancia de API —
**consolidada**): los hotspots son **mayormente legítimos** (composites grandes
+ superficie behavioral); la deuda-sin-consumidor genuina es **rara**. Lo
accionable son **decisiones de diseño** (como el dual-API de stepper que el
usuario decidió consolidar), no podas mecánicas. Esto
CONFIRMA la predicción de la doctrina ("la mayoría son legítimos") y sugiere que
**la tasa de deuda NO justifica una regla de máquina** — THM-4 es sobre todo
adjudicación documentada. Restan color-picker/avatar/trp por adjudicar

@ -146,7 +146,7 @@
min-block-size: var(--stepper-separator-min-size);
}
[data-stepper-separator][data-complete] {
[data-stepper-separator][data-state='complete'] {
background: var(--_stepper-palette-solid);
}

@ -86,10 +86,7 @@ export const stepperMorfo = {
attr: 'data-orientation',
values: ['horizontal', 'vertical'],
value: v.propRef('orientation')
},
{ attr: 'data-current', value: v.propRef('current'), severity: 'optional' },
{ attr: 'data-complete', value: v.propRef('complete'), severity: 'optional' },
{ attr: 'data-incomplete', value: v.propRef('incomplete'), severity: 'optional' }
}
],
aria: []
},
@ -103,10 +100,7 @@ export const stepperMorfo = {
optional: true,
states: ['complete', 'current', 'incomplete'],
data: [
{ attr: 'data-state', values: ['complete', 'current', 'incomplete'] },
{ attr: 'data-current', severity: 'optional' },
{ attr: 'data-complete', severity: 'optional' },
{ attr: 'data-incomplete', severity: 'optional' }
{ attr: 'data-state', values: ['complete', 'current', 'incomplete'] }
],
aria: [
{ attr: 'type', value: v.literal('button') },
@ -151,10 +145,7 @@ export const stepperMorfo = {
attr: 'data-state',
values: ['complete', 'current', 'incomplete'],
value: v.propRef('state')
},
{ attr: 'data-current', value: v.propRef('current'), severity: 'optional' },
{ attr: 'data-complete', value: v.propRef('complete'), severity: 'optional' },
{ attr: 'data-incomplete', value: v.propRef('incomplete'), severity: 'optional' }
}
],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
@ -177,10 +168,7 @@ export const stepperMorfo = {
attr: 'data-orientation',
values: ['horizontal', 'vertical'],
value: v.propRef('orientation')
},
{ attr: 'data-current', value: v.propRef('current'), severity: 'optional' },
{ attr: 'data-complete', value: v.propRef('complete'), severity: 'optional' },
{ attr: 'data-incomplete', value: v.propRef('incomplete'), severity: 'optional' }
}
],
aria: []
},

@ -72,24 +72,14 @@ A multi-step wizard with step indicators, navigation controls, and content panel
| List | `data-orientation` | `horizontal` \| `vertical` |
| Item | `data-stepper-item` | Always present |
| Item | `data-state` | `complete` \| `current` \| `incomplete` |
| Item | `data-current` | Present when this is the current step |
| Item | `data-complete` | Present when this step is completed |
| Item | `data-incomplete` | Present when this step is not yet reached |
| Item | `data-orientation` | `horizontal` \| `vertical` |
| Trigger | `data-stepper-trigger` | Always present |
| Trigger | `data-state` | `complete` \| `current` \| `incomplete` |
| Trigger | `data-current` | Present when current |
| Trigger | `data-complete` | Present when completed |
| Trigger | `data-incomplete` | Present when incomplete |
| Trigger | `data-orientation` | `horizontal` \| `vertical` |
| Indicator | `data-stepper-indicator` | Always present |
| Indicator | `data-state` | `complete` \| `current` \| `incomplete` |
| Indicator | `data-current` | Present when current |
| Indicator | `data-complete` | Present when completed |
| Indicator | `data-incomplete` | Present when incomplete |
| Separator | `data-stepper-separator` | Always present |
| Separator | `data-state` | `complete` \| `current` \| `incomplete` |
| Separator | `data-complete` | Present when the step before it is complete |
| Separator | `data-orientation` | `horizontal` \| `vertical` |
| Content | `data-stepper-content` | Always present |
| Content | `data-state` | `complete` \| `current` \| `incomplete` |

@ -151,11 +151,8 @@ describe('StepperProvider', () => {
expect(root.getAttribute('data-orientation')).toBe('horizontal');
expect(root.getAttribute('data-linear')).toBe('');
expect(firstEl.getAttribute('data-state')).toBe('complete');
expect(firstEl.getAttribute('data-complete')).toBe('');
expect(currentEl.getAttribute('data-state')).toBe('current');
expect(currentEl.getAttribute('data-current')).toBe('');
expect(lastEl.getAttribute('data-state')).toBe('incomplete');
expect(lastEl.getAttribute('data-incomplete')).toBe('');
expect(indicatorEl.getAttribute('aria-hidden')).toBe('true');
expect(indicatorEl.getAttribute('data-state')).toBe('current');
expect(separatorEl.getAttribute('role')).toBe('separator');

@ -214,10 +214,7 @@ export class StepperItemProvider {
context: StepperItemProvider.ctx,
props: {
state: () => this.stepState,
orientation: () => this.provider.opts.orientation.current,
current: () => this.isCurrent,
complete: () => this.isComplete,
incomplete: () => this.isIncomplete
orientation: () => this.provider.opts.orientation.current
},
syncAttrs: true
});
@ -232,8 +229,6 @@ export class StepperItemProvider {
readonly stepState = $derived.by(() => this.provider.getStepState(this.opts.index.current));
readonly isCurrent = $derived.by(() => this.stepState === 'current');
readonly isComplete = $derived.by(() => this.stepState === 'complete');
readonly isIncomplete = $derived.by(() => this.stepState === 'incomplete');
readonly props = $derived.by(() =>
this.runtimePart.assert({
@ -343,9 +338,6 @@ export class StepperTriggerProvider {
// Soma-owned data-* — the morfo declares these without a value source
// (documentation only) or not at all (data-orientation):
'data-state': this.item.stepState,
'data-current': boolToEmptyStrOrUndef(this.item.isCurrent),
'data-complete': boolToEmptyStrOrUndef(this.item.isComplete),
'data-incomplete': boolToEmptyStrOrUndef(this.item.isIncomplete),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-orientation': this.provider.opts.orientation.current,
onclick: this.onclick,
@ -375,10 +367,7 @@ export class StepperIndicatorProvider {
ref: opts.ref,
owner: this,
props: {
state: () => this.item.stepState,
current: () => this.item.isCurrent,
complete: () => this.item.isComplete,
incomplete: () => this.item.isIncomplete
state: () => this.item.stepState
},
syncAttrs: true
});
@ -418,10 +407,7 @@ export class StepperSeparatorProvider {
owner: this,
props: {
state: () => this.item.stepState,
orientation: () => this.provider.opts.orientation.current,
current: () => this.item.isCurrent,
complete: () => this.item.isComplete,
incomplete: () => this.item.isIncomplete
orientation: () => this.provider.opts.orientation.current
},
syncAttrs: true
});

Loading…
Cancel
Save

Powered by TurnKey Linux.