refactor(soma)!: fase C2b — renderProps() muere; .props es la única bolsa

Tras C2a las dos computaban lo mismo. Sin shim, por doctrina: los 262 call
sites en 62 ficheros migran a `.props`, el método y su tipo desaparecen del
runtime, y las ~200 menciones en comentarios (la plantilla «lets
renderProps() emit…» de los morfos incluida) pasan a nombrar la bolsa de
render, para no dejar deriva doc↔código en el propio código.

Verificación: svelte-check con src/ a 0 (71 errores = exactamente el
ledger de web/) · suites de soma tocadas en verde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent 9dffa541ef
commit 243122393d

@ -163,7 +163,7 @@ en las cuatro variantes, 0 diffs** contra el commit anterior.
| Doble-tap-seek | **diferir** | Necesita debounce click/dblclick sobre el media; el tap-to-play cubre el gesto principal. |
| Thumbnail preview en el scrubber | **diferir** | **Gap del Slider**: no hay pista de preview por hover; requiere extender Slider o una capa eidos-only. |
| Captions surface custom (`Captions` part) | **implementar → hecho** | El player PINTA las cues (ver §Captions abajo). El render nativo se retiró: caía en la franja de la barra de controles y llegaba ilegible. |
| `renderProps` en los botones/partes | **diferir** | Los providers emiten attrs directamente en `props` (pragmático, consistente con el resto del componente); migrar a `renderProps` + `propRef` en el barrido de remediación global. |
| La bolsa de render en los botones/partes | **diferir** | Los providers emiten attrs directamente en `props` (pragmático, consistente con el resto del componente); migrar a la bolsa de render (`.props`) + `propRef` en el barrido de remediación global. |
## Captions — el player pinta las cues (2026-08-04)

@ -93,7 +93,7 @@ export const calendarMorfo = {
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -270,7 +270,7 @@ export const calendarMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-readonly',
value: v.propRef('readonly'),
@ -350,7 +350,7 @@ export const calendarMorfo = {
],
aria: [
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-selected',
value: v.propRef('selected'),
@ -391,7 +391,7 @@ export const calendarMorfo = {
aria: [
{ attr: 'aria-label', value: v.propRef('dateLabel'), severity: 'recommended' },
// Provider emitted `boolToStr(disabled)` (always "true"/"false");
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
// No aria-selected here: it is not supported on `role='button'`
// (checkpoint C4 fix, 2026-07-07) — the selection state lives on the
// Cell (`role='gridcell'`), which legitimately carries it.

@ -84,7 +84,7 @@ export const dateFieldMorfo = {
{
// Provider stringified this as `boolToStr(disabled)` ("true"/
// "false" ALWAYS), so it models a boolean state, not presence.
// `propRef` + `ariaBoolean` lets renderProps() emit the same
// `propRef` + `ariaBoolean` lets the render bag emit the same
// "true"/"false" the provider used to.
attr: 'aria-disabled',
value: v.propRef('disabled'),

@ -59,7 +59,7 @@ export const dateRangePickerMorfo = {
aria: [
// Provider stringified these as `boolToStr(...)` ("true"/"false"
// ALWAYS), so they model a boolean state, not presence. `propRef` +
// `ariaBoolean` lets renderProps() emit the same "true"/"false".
// `ariaBoolean` lets the render bag emit the same "true"/"false".
{
attr: 'aria-disabled',
value: v.propRef('disabled'),

@ -127,7 +127,7 @@ export const fileUploadMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -157,7 +157,7 @@ export const fileUploadMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),

@ -106,7 +106,7 @@ export const formMorfo = {
{
// Provider stringified this as `boolToStr(isPending)` ("true"/
// "false" ALWAYS), so it models a boolean state, not presence.
// `propRef` + `ariaBoolean` lets renderProps() emit the same
// `propRef` + `ariaBoolean` lets the render bag emit the same
// "true"/"false" the provider used to.
attr: 'aria-busy',
value: v.propRef('isPending'),

@ -68,7 +68,7 @@ export const knobMorfo = {
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value') },
// The provider overrides this with a locale-formatted string via
// renderProps() — soma genuinely owns the *value* (formatting).
// the render bag — soma genuinely owns the *value* (formatting).
{ attr: 'aria-valuetext', value: v.propRef('value'), severity: 'recommended' },
{
attr: 'aria-labelledby',

@ -62,7 +62,7 @@ export const monthGridMorfo = {
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -120,7 +120,7 @@ export const monthGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -146,7 +146,7 @@ export const monthGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -171,7 +171,7 @@ export const monthGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-readonly',
value: v.propRef('readonly'),
@ -204,7 +204,7 @@ export const monthGridMorfo = {
],
aria: [
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-selected',
value: v.propRef('selected'),

@ -177,7 +177,7 @@ export const passwordFieldMorfo = {
{
// Provider stringified this as `boolToStr(isInvalid)` ("true"/
// "false" ALWAYS), so it models a boolean state, not presence.
// `propRef` + `ariaBoolean` lets renderProps() emit the same
// `propRef` + `ariaBoolean` lets the render bag emit the same
// "true"/"false" the provider used to.
attr: 'aria-invalid',
value: v.propRef('invalid'),

@ -74,7 +74,7 @@ export const timeFieldMorfo = {
{
// Provider stringified these as `boolToStr(...)` ("true"/"false"
// ALWAYS), so they model a boolean state, not presence. `propRef`
// + `ariaBoolean` lets renderProps() emit the same "true"/"false".
// + `ariaBoolean` lets the render bag emit the same "true"/"false".
attr: 'aria-disabled',
value: v.propRef('disabled'),
severity: 'optional',
@ -89,7 +89,7 @@ export const timeFieldMorfo = {
{
// Provider emitted `boolToTrueOrUndef(invalid)` (present-when-true,
// absent otherwise), so literal + `prop-truthy` reproduces that
// exactly — renderProps() emits "true" only when invalid.
// exactly — the render bag emits "true" only when invalid.
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',

@ -61,7 +61,7 @@ export const yearGridMorfo = {
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -119,7 +119,7 @@ export const yearGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -145,7 +145,7 @@ export const yearGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(disabled)` (always "true"/"false"), so
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
@ -170,7 +170,7 @@ export const yearGridMorfo = {
severity: 'recommended'
},
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-readonly',
value: v.propRef('readonly'),
@ -203,7 +203,7 @@ export const yearGridMorfo = {
],
aria: [
// Provider emitted `boolToStr(...)` (always "true"/"false") for both;
// `propRef` + `ariaBoolean` lets renderProps() emit the same string.
// `propRef` + `ariaBoolean` lets the render bag emit the same string.
{
attr: 'aria-selected',
value: v.propRef('selected'),

@ -26,7 +26,7 @@ export type FieldRole = keyof typeof FIELD_ROLE_ATTR;
* own state attrs and adds its specifics.
*
* Mirrors the existing css-field ↔ spin-field adoption (`data-spin-field*` via
* `v.literal('')`): a static presence attr auto-emitted through `renderProps()`,
* `v.literal('')`): a static presence attr auto-emitted through the render bag,
* so no per-provider soma change is needed. Used as a plain `data[]` element (not
* spread) so the morfo stays an `as const satisfies Morfo` literal — the kebab (and
* thus `AttrsOf`) is untouched.

@ -283,7 +283,7 @@ export interface MorfoAriaEntry {
* Render the value as an ARIA boolean (`"true"`/`"false"` string) instead of
* passing it through raw. A `stateRef` value already compiles to aria-boolean;
* set this for a `propRef` boolean (e.g. `aria-required` ← `required` prop) so
* `renderProps()` stringifies it and the provider need not re-declare it.
* the render bag stringifies it and the provider need not re-declare it.
*/
ariaBoolean?: boolean;
}

@ -166,7 +166,7 @@ export class CalendarProvider implements CalendarNav {
owner: this,
context: CalendarProvider.ctx,
// Source for the morfo's `aria-disabled` (propRef('disabled')) so
// renderProps() resolves it instead of the provider re-declaring it.
// the render bag resolves it instead of the provider re-declaring it.
props: { disabled: () => opts.disabled.current }
});
@ -583,8 +583,8 @@ export class CalendarProvider implements CalendarNav {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// role + aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
// Computed accessible name (calendarLabel + heading) — overrides the
// morfo's raw `ariaLabel` propRef (which the provider doesn't source).
'aria-label': this.fullCalendarLabel,
@ -751,8 +751,8 @@ abstract class CalendarPaginationButtonProvider {
const disabled = this.disabled();
return this.runtimePart.assert({
// type, aria-label (commonRef calendar.prev/next-month) and
// aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(disabled),
disabled,
onclick: this.onclick
@ -860,8 +860,8 @@ export class CalendarMonthSelectProvider {
const disabled = this.isDisabled;
return this.runtimePart.assert({
// aria-label (commonRef calendar.month-select) and aria-disabled are
// morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// morfo-declared, resolved via the render bag.
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(disabled),
disabled,
value: String(this.selectedMonthItem.value),
@ -940,8 +940,8 @@ export class CalendarYearSelectProvider {
const disabled = this.isDisabled;
return this.runtimePart.assert({
// aria-label (commonRef calendar.year-select) and aria-disabled are
// morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// morfo-declared, resolved via the render bag.
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(disabled),
disabled,
value: String(this.selectedYearItem.value),
@ -976,7 +976,7 @@ export class CalendarGridProvider {
ref: opts.ref,
owner: this,
// Sources for the morfo's `aria-readonly`/`aria-disabled`
// (propRef('readonly')/propRef('disabled')) so renderProps resolves them.
// (propRef('readonly')/propRef('disabled')) so the render bag resolves them.
props: {
readonly: () => this.provider.opts.readonly.current,
disabled: () => this.provider.opts.disabled.current
@ -988,8 +988,8 @@ export class CalendarGridProvider {
const headingId = this.opts.labelledBy.current ?? this.provider.headingId.current;
return this.runtimePart.assert({
// role, aria-readonly and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Computed: honours the `labelledBy` prop override + the registered
// heading id (overrides the morfo's raw partRef('heading')).
'aria-labelledby': headingId || undefined
@ -1144,8 +1144,8 @@ export class CalendarCellProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role, aria-selected and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Soma-owned data values (the morfo declares these attrs without a value).
'data-selected': boolToEmptyStrOrUndef(this.selected),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -1212,8 +1212,8 @@ export class CalendarDayProvider {
const s = this.cell.snippetProps;
const tabindex = this.focused && !s.disabled ? 0 : -1;
return this.runtimePart.assert({
// role and aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// role and aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
tabindex,
// Soma-owned data values (the morfo declares these attrs without a value).
'data-selected': boolToEmptyStrOrUndef(s.selected),

@ -443,16 +443,16 @@ export class CarouselProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-roledescription are morfo-declared, resolved by renderProps.
// role + aria-roledescription are morfo-declared, resolved by the render bag.
// aria-label rides the render bag as the naming DEFAULT (consumer-first,
// A-85): the consumer's own `aria-label` stays in restProps and wins by
// merge policy — the old resolver chain routed it through here only to
// hand it back unchanged.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.isDisabled ? -1 : 0,
lang: this.opts.locale.current || undefined,
// data-* below are contract-only in the morfo (declared with `values`,
// no resolvable `value`), so renderProps doesn't emit them — soma owns them.
// no resolvable `value`), so the render bag doesn't emit them — soma owns them.
'data-orientation': this.opts.orientation.current,
'data-dir': this.resolvedDir,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -607,8 +607,8 @@ export class CarouselItemProvider {
// gap is the default `0px`.
const basis = spv === 1 ? '100%' : `calc((100% - ${spv - 1} * ${gap}) / ${spv})`;
return this.runtimePart.assert({
// role + aria-roledescription are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// role + aria-roledescription are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// aria-label: soma override — the morfo declares propRef('index') (the raw
// slide number); soma supplies the localizable "Slide N of M" phrasing.
'aria-label': `Slide ${this.opts.index.current + 1} of ${this.provider.slideCount}`,
@ -657,10 +657,10 @@ export class CarouselPrevTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type + role are morfo-declared, resolved by renderProps. aria-label is the
// type + role are morfo-declared, resolved by the render bag. aria-label is the
// naming DEFAULT declared in the morfo (consumer-first, A-85) and rides the
// render bag; the consumer's own attr wins in `mergeProps`.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onclick: this.onclick
@ -700,10 +700,10 @@ export class CarouselNextTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type + role are morfo-declared, resolved by renderProps. aria-label is the
// type + role are morfo-declared, resolved by the render bag. aria-label is the
// naming DEFAULT declared in the morfo (consumer-first, A-85) and rides the
// render bag; the consumer's own attr wins in `mergeProps`.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onclick: this.onclick
@ -731,7 +731,7 @@ export class CarouselIndicatorGroupProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// `orientation` source so renderProps resolves the morfo's
// `orientation` source so the render bag resolves the morfo's
// aria-orientation (propRef('orientation')).
props: { orientation: () => this.provider.opts.orientation.current }
});
@ -739,8 +739,8 @@ export class CarouselIndicatorGroupProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-orientation are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// role + aria-orientation are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// data-orientation is contract-only in the morfo (no resolvable `value`) — soma owns it.
'data-orientation': this.provider.opts.orientation.current
} as const)
@ -787,8 +787,8 @@ export class CarouselIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type + role are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// type + role are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// aria-selected: soma override — the morfo declares stateRef('active') which
// stringifies to 'true'/'false'; soma supplies the real boolean (the active
// flag is computed here, not registered as a runtime state source).

@ -1433,8 +1433,8 @@ export class ChronosProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-label + aria-disabled are morfo-declared (renderProps).
...this.runtimePart.renderProps(),
// role + aria-label + aria-disabled are morfo-declared (the render bag).
...this.runtimePart.props,
'data-view': this.view,
'data-readonly': boolToEmptyStrOrUndef(this.opts.readonly.current),
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
@ -1530,7 +1530,7 @@ export class ChronosRegionProvider {
if (part === 'heading') this.provider.headingId = opts.id.current;
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
/**
@ -1572,7 +1572,7 @@ export class ChronosAgendaItemProvider {
});
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
// ── Event chip ───────────────────────────────────────────────────────────────
@ -1588,7 +1588,7 @@ export class ChronosAgendaItemProvider {
* `role` / aria never reached the DOM (the template re-typed them by hand), and
* nothing could resolve a chip element back to its instance — which is why the
* four events it carries had to smuggle an element through `targetOverride`.
* Registering per instance closes both: `renderProps()` applies the declared
* Registering per instance closes both: `the render bag` applies the declared
* contract, and {@link SomaRuntimePart.trigger} anchors the stamp on the chip
* the user actually touched.
*/
@ -1659,7 +1659,7 @@ export class ChronosEventChipProvider {
this.runtimePart.assert({
// Morfo-declared: role=button, aria-label, data-event-id, data-color —
// resolved against THIS chip's sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: 0,
onkeydown: this.onkeydown,
// Soma-owned: the morfo declares these attrs without a value source.
@ -1703,7 +1703,7 @@ export class ChronosResizeHandleProvider {
readonly props = $derived.by(() =>
// Morfo-declared: data-edge (propRef) + aria-hidden.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -1752,7 +1752,7 @@ export class ChronosDayCellProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=gridcell, data-date, aria-label, aria-selected.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: the roving tab stop and the value-less state markers.
tabindex: this.opts.focused.current ? 0 : -1,
'data-today': boolToEmptyStrOrUndef(this.opts.today.current),
@ -1801,7 +1801,7 @@ export class ChronosActionProvider {
}
// Morfo-declared: role, type, the translated aria-label.
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
// ── "+N more" overflow link ──────────────────────────────────────────────────
@ -1838,7 +1838,7 @@ export class ChronosMoreLinkProvider {
}
// Morfo-declared: role, type, the translated aria-label, data-count.
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
// ── Navigation buttons ───────────────────────────────────────────────────────
@ -1881,7 +1881,7 @@ export class ChronosNavButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role, type, the translated aria-label, aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
} as const)
);

@ -162,7 +162,7 @@ export class ColorFieldProvider {
context: ColorFieldProvider.ctx,
// Per-part source for the morfo's `aria-invalid` (literal 'true' gated
// on `prop-truthy: invalid`). `isInvalid` merges validation + Field —
// richer than `opts.invalid`, so renderProps resolves the gate against it.
// richer than `opts.invalid`, so the render bag resolves the gate against it.
props: { invalid: () => this.isInvalid }
});
@ -419,7 +419,7 @@ export class ColorFieldProvider {
}
readonly sharedSegmentAttrs = {
// `role: 'spinbutton'` comes from the morfo via renderProps() on each
// `role: 'spinbutton'` comes from the morfo via the render bag() on each
// segment — not re-declared here.
contenteditable: 'true' as const,
tabindex: 0,
@ -435,7 +435,7 @@ export class ColorFieldProvider {
this.runtimePart.assert({
// Morfo supplies `aria-invalid` (literal 'true' when `invalid` truthy),
// resolved against the per-part `invalid` source registered above.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned data-* — the morfo declares these without a value source.
'data-format': this.opts.format.current,
'data-invalid': boolToEmptyStrOrUndef(this.isInvalid),
@ -515,7 +515,7 @@ export class ColorFieldInputProvider {
ref: opts.ref,
owner: this,
onRefChange: (v) => this.provider.setFieldNode(v),
// Per-part sources for the morfo's value-bound aria. renderProps then
// Per-part sources for the morfo's value-bound aria. the render bag then
// resolves aria-disabled/readonly (ariaBoolean) + aria-invalid against
// the merged disabled/readonly/invalid flags instead of the provider
// re-declaring them. (aria-label/aria-labelledby stay soma overrides —
@ -549,7 +549,7 @@ export class ColorFieldInputProvider {
// Morfo supplies role=group + aria-disabled/readonly/invalid (resolved
// against the registered flags). aria-label/aria-labelledby below
// override the morfo defaults (explicit prop + labelNode fallback).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label': this.opts.ariaLabel.current,
'aria-labelledby': labelledBy,
// Soma-owned data-* — the morfo declares these without a value source.
@ -780,7 +780,7 @@ abstract class BaseColorSegmentProvider {
// aria-invalid (resolved against the registered flags). The computed
// aria below (value/min/max/label/labelledby) overrides the morfo's raw
// propRef defaults; aria-readonly is a per-segment soma override.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
...this.provider.sharedSegmentAttrs,
// Soma-owned data-* — the morfo declares these without a value source.
'data-segment': this.part,
@ -941,7 +941,7 @@ export class ColorFieldFormatSelectProvider {
// Morfo supplies the default aria-label (translationRef → format-select).
// The soma aria-label below overrides it to honour an explicit ariaLabel
// prop (falling back to the same default label).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.provider.isDisabled || undefined,
'aria-label': this.opts.ariaLabel.current ?? this.provider.getFormatSelectLabel(),
// Soma-owned data-* — the morfo declares these without a value source.

@ -481,8 +481,8 @@ export class ColorPickerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared `data-state` (← stateRef `open`) is resolved by
// renderProps against the component-level `open` state source.
...this.runtimePart.renderProps(),
// the render bag against the component-level `open` state source.
...this.runtimePart.props,
'aria-invalid': this.isInvalid || undefined,
// The morfo declares these data-* WITHOUT a value (presence /
// soma-owned), so soma still supplies them.
@ -615,7 +615,7 @@ export class ColorPickerTriggerProvider {
// Morfo supplies `type`, `aria-haspopup`, `aria-expanded` (← state
// `open`) and the default translated `aria-label`. aria-controls is
// dropped (Content registers on the Popover runtime, not ours).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma override: honour an explicit `ariaLabel` prop over the morfo
// default.
'aria-label': this.resolvedLabel,
@ -669,8 +669,8 @@ export class ColorPickerValueSwatchProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden; renderProps supplies it.
...this.runtimePart.renderProps(),
// Morfo declares aria-hidden; the render bag supplies it.
...this.runtimePart.props,
style: this.backgroundStyle
} as const)
);
@ -949,8 +949,8 @@ export class ColorPickerAreaBackgroundProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden; renderProps supplies it.
...this.runtimePart.renderProps(),
// Morfo declares aria-hidden; the render bag supplies it.
...this.runtimePart.props,
style: this.backgroundStyle
} as const)
);
@ -1046,9 +1046,9 @@ export class ColorPickerAreaThumbProvider {
const ariaText = this.ariaValueText;
return this.runtimePart.assert({
// Morfo supplies role=slider. aria-label / aria-valuenow are
// propRefs over unregistered sources, so renderProps drops them and
// propRefs over unregistered sources, so the render bag drops them and
// soma supplies the computed 2D aria-label / aria-valuetext below.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.provider.isDisabled ? -1 : 0,
'aria-label': `${this.area.xChannel}, ${this.area.yChannel}`,
'aria-valuetext': ariaText,
@ -1173,8 +1173,8 @@ export class ColorPickerTransparencyGridProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden; renderProps supplies it.
...this.runtimePart.renderProps(),
// Morfo declares aria-hidden; the render bag supplies it.
...this.runtimePart.props,
style: this.style
} as const)
);
@ -1215,7 +1215,7 @@ export class ColorPickerSwatchGroupProvider {
this.runtimePart.assert({
// Morfo supplies role=radiogroup. aria-label is soma-owned (not
// morfo-declared) — honours an explicit prop / falls back to langs.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label': this.resolvedLabel,
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled)
} as const)
@ -1240,7 +1240,7 @@ export class ColorPickerSwatchTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Per-part source so renderProps resolves the morfo aria-checked
// Per-part source so the render bag resolves the morfo aria-checked
// (ariaBoolean over propRef `checked`) — always emits true/false.
props: { checked: () => this.isChecked }
});
@ -1273,7 +1273,7 @@ export class ColorPickerSwatchTriggerProvider {
// Morfo supplies type=button, role=radio and aria-checked (← propRef
// `checked`, registered above). aria-label is propRef over an
// unregistered source, so soma supplies the colour string below.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label': this.opts.color.current,
'data-checked': boolToEmptyStrOrUndef(this.isChecked),
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
@ -1309,8 +1309,8 @@ export class ColorPickerSwatchProvider {
readonly props = $derived.by(() => {
const c = this.effectiveColor;
return this.runtimePart.assert({
// Morfo declares aria-hidden; renderProps supplies it.
...this.runtimePart.renderProps(),
// Morfo declares aria-hidden; the render bag supplies it.
...this.runtimePart.props,
style: c ? `background: ${c};` : undefined
} as const);
});
@ -1337,8 +1337,8 @@ export class ColorPickerSwatchIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden; renderProps supplies it.
...this.runtimePart.renderProps()
// Morfo declares aria-hidden; the render bag supplies it.
...this.runtimePart.props
} as const)
);
}
@ -1412,7 +1412,7 @@ export class ColorPickerEyeDropperProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo supplies type=button + the default translated aria-label.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma override: honour an explicit `ariaLabel` prop over the default.
'aria-label': this.resolvedLabel,
'data-unsupported': boolToEmptyStrOrUndef(!this.isSupported),

@ -523,7 +523,7 @@ export class CommandInputProvider {
const listId = this.provider.listId.current;
return this.runtimePart.assert({
// Morfo-declared: role, aria-autocomplete, aria-expanded.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
type: 'text' as const,
autocomplete: 'off' as const,
autocorrect: 'off' as const,
@ -574,7 +574,7 @@ export class CommandListProvider {
// Morfo-declared: role. The morfo also declares aria-label via
// translationRef (the default); soma overrides to honour an explicit
// `label` prop, falling back to the same translated string.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label':
this.provider.opts.label.current ?? this.provider.soma.langs.ts(COMMAND_LANGS.LIST)
} as const)
@ -654,7 +654,7 @@ export class CommandItemProvider {
ref: opts.ref,
owner: this,
// Per-part sources for the morfo's aria-selected/aria-disabled — the
// component-level runtime carries no sources. With these, renderProps()
// component-level runtime carries no sources. With these, the render bag
// resolves those aria attrs from the morfo instead of re-declaring them.
props: {
selected: () => this.isSelected,
@ -748,7 +748,7 @@ export class CommandItemProvider {
this.runtimePart.assert({
// Morfo-declared: role, aria-selected, aria-disabled (resolved from this
// part's registered selected/disabled sources).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: composed aria id (overrides the registration id) + the
// data-* attrs the morfo declares WITHOUT a value (it owns the value).
id: this.idForAria,
@ -872,7 +872,7 @@ export class CommandLinkItemProvider {
this.runtimePart.assert({
// Morfo-declared: role, aria-selected, aria-disabled (resolved from this
// part's registered selected/disabled sources).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: composed aria id, the native href the morfo doesn't model,
// and the data-* attrs the morfo declares WITHOUT a value.
id: this.idForAria,
@ -942,7 +942,7 @@ export class CommandGroupProvider {
// partRef('group-heading'), but that can't resolve without a registered
// parts source — soma supplies the registered heading id. data-empty is
// declared without a value (soma owns it).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-labelledby': this.headingId.current || undefined,
'data-empty': boolToEmptyStrOrUndef(!this.hasVisibleItems)
} as const)
@ -975,7 +975,7 @@ export class CommandGroupHeadingProvider {
readonly props = $derived.by(() =>
// Morfo-declared: aria-hidden. No soma-specific extras.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -1002,7 +1002,7 @@ export class CommandGroupItemsProvider {
readonly props = $derived.by(() =>
// Morfo-declared: role. No soma-specific extras.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -1033,7 +1033,7 @@ export class CommandEmptyProvider {
readonly props = $derived.by(() =>
// Morfo-declared: role, aria-live. No soma-specific extras.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -1071,7 +1071,7 @@ export class CommandLoadingProvider {
// Morfo-declared: role, aria-valuemin/max, aria-valuenow (from the
// registered percent source). data-value is declared without a value —
// soma owns it.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-value': this.percent.current
} as const)
);
@ -1105,6 +1105,6 @@ export class CommandSeparatorProvider {
readonly props = $derived.by(() =>
// Morfo-declared: role, aria-orientation. No soma-specific extras.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}

@ -180,7 +180,7 @@ export class ContextMenuTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Per-part sources so renderProps resolves the morfo's state-/part-
// Per-part sources so the render bag resolves the morfo's state-/part-
// bound aria: aria-expanded ← open, aria-controls ← content id.
states: { open: () => this.provider.opts.open.current },
parts: { content: () => this.provider.contentId.current }
@ -199,7 +199,7 @@ export class ContextMenuTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: aria-haspopup, aria-expanded, aria-controls.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: data-state (morfo declares it without a value) + the
// native disabled marker + the handler.
'data-state': getDataOpenClosed(this.provider.opts.open.current),
@ -262,7 +262,7 @@ export class ContextMenuContentProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// aria-labelledby ← trigger id (renderProps resolves the partRef).
// aria-labelledby ← trigger id (the render bag resolves the partRef).
parts: { trigger: () => this.provider.triggerId.current },
onRefChange: (el) => {
this.provider.setContentRef(el);
@ -382,7 +382,7 @@ export class ContextMenuContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=menu, aria-labelledby, aria-orientation.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned data-state (morfo declares it without a value) + layers.
'data-state': getDataOpenClosed(this.provider.opts.open.current),
...this.floating.props,
@ -478,7 +478,7 @@ export class ContextMenuItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=menuitem, aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: tabindex + the no-value data-* markers + handlers.
tabindex: this.opts.disabled.current ? undefined : -1,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
@ -518,14 +518,14 @@ export class ContextMenuGroupProvider {
ref: opts.ref,
owner: this,
context: ContextMenuGroupProvider.ctx,
// aria-labelledby ← group-heading id (renderProps resolves the partRef).
// aria-labelledby ← group-heading id (the render bag resolves the partRef).
parts: { 'group-heading': () => this.headingId.current }
});
}
readonly props = $derived.by(() =>
// Morfo-declared: role=group, aria-labelledby.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -582,7 +582,7 @@ export class ContextMenuSeparatorProvider {
readonly props = $derived.by(() =>
// Morfo-declared: role=separator, aria-orientation=horizontal.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -643,7 +643,7 @@ export class ContextMenuCheckboxItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=menuitemcheckbox, aria-checked, aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: tabindex + the no-value data-* markers + handlers.
tabindex: this.opts.disabled.current ? undefined : -1,
'data-state': this.opts.checked.current ? 'checked' : 'unchecked',
@ -687,7 +687,7 @@ export class ContextMenuRadioGroupProvider {
readonly props = $derived.by(() =>
// Morfo-declared: role=group.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -753,7 +753,7 @@ export class ContextMenuRadioItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=menuitemradio, aria-checked, aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: tabindex + the no-value data-* markers + handlers.
tabindex: this.opts.disabled.current ? undefined : -1,
'data-state': this.isChecked ? 'checked' : 'unchecked',
@ -878,7 +878,7 @@ export class ContextMenuSubTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// renderProps resolves the morfo's state-/part-/prop-bound aria:
// the render bag resolves the morfo's state-/part-/prop-bound aria:
// aria-expanded ← open, aria-controls ← sub-content id,
// aria-disabled ← disabled prop.
states: { open: () => this.sub.opts.open.current },
@ -952,7 +952,7 @@ export class ContextMenuSubTriggerProvider {
this.runtimePart.assert({
// Morfo-declared: role=menuitem, aria-haspopup, aria-expanded,
// aria-controls, aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: tabindex + the no-value data-* markers + handlers.
tabindex: this.opts.disabled.current ? undefined : -1,
'data-state': getDataOpenClosed(this.sub.opts.open.current),
@ -1001,7 +1001,7 @@ export class ContextMenuSubContentProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// aria-labelledby ← sub-trigger id (renderProps resolves the partRef).
// aria-labelledby ← sub-trigger id (the render bag resolves the partRef).
parts: { 'sub-trigger': () => this.sub.subTriggerId.current },
onRefChange: (el) => {
this.sub.setContentRef(el);
@ -1083,7 +1083,7 @@ export class ContextMenuSubContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=menu, aria-labelledby, aria-orientation.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned data-state (morfo declares it without a value) + layers.
'data-state': getDataOpenClosed(this.sub.opts.open.current),
...this.floating.props,

@ -546,7 +546,7 @@ export class CssFieldInputProvider {
// Morfo-declared: role, data-spin-field-input, aria-required/disabled/
// readonly/invalid (the last four via ariaBoolean). Soma adds only the
// CSS-value-specific aria below + handlers/native.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
inputmode: 'text' as const,
autocomplete: 'off' as const,
autocorrect: 'off' as const,
@ -604,7 +604,7 @@ export class CssFieldIncrementTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// trigger-local disabled (includes atMax) → renderProps resolves aria-disabled
// trigger-local disabled (includes atMax) → the render bag resolves aria-disabled
props: { disabled: () => this.isDisabled }
});
}
@ -629,7 +629,7 @@ export class CssFieldIncrementTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, data-spin-field, aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: -1,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -670,7 +670,7 @@ export class CssFieldDecrementTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// trigger-local disabled (includes atMin) → renderProps resolves aria-disabled
// trigger-local disabled (includes atMin) → the render bag resolves aria-disabled
props: { disabled: () => this.isDisabled }
});
}
@ -695,7 +695,7 @@ export class CssFieldDecrementTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, data-spin-field, aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: -1,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -818,7 +818,7 @@ export class CssFieldScrubberProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role, aria-hidden, data-spin-field-scrubber are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-scrubbing': boolToEmptyStrOrUndef(this.provider.scrubbing),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
style: {

@ -933,7 +933,7 @@ export class DateFieldInputProvider {
owner: this,
onRefChange: (v) => this.provider.setFieldNode(v),
// Per-part source for the morfo's `aria-label` ← `ariaLabel` prop, so
// `renderProps()` resolves it instead of the provider re-declaring it.
// `the render bag` resolves it instead of the provider re-declaring it.
// (`disabled`/`invalid` come from the component-level sources.)
props: { ariaLabel: () => this.opts.ariaLabel.current }
});
@ -971,7 +971,7 @@ export class DateFieldInputProvider {
// Morfo-declared attrs (role=group, aria-label, aria-disabled, and
// aria-labelledby from the Label part), resolved against this part's
// registered sources — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: aria-labelledby honours an explicit `ariaLabelledBy`
// prop (the morfo only knows the Label part); aria-describedby is the
// computed hidden-description element id (not the morfo's prop);

@ -291,7 +291,7 @@ export class DatePickerProvider {
// invalid, aria-disabled/readonly/required), resolved against the
// component-level `open` state + disabled/readonly/required/invalid
// prop sources — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Raw, so no assertion leaves the root inheriting. The in-place half reads
// it: the root IS the field, and its segment row (`[data-field-control]`,
// `flex-direction: row`) reverses in RTL. The portalled calendar reads the

@ -296,7 +296,7 @@ export class DateRangePickerProvider {
// invalid, aria-disabled/readonly/required), resolved against the
// component-level sources (`states.open` + `props.disabled/readonly/
// required/invalid`) — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Raw, so no assertion leaves the root inheriting. The root itself is the
// in-place paint: `[data-date-range-picker]` is a wrapping flex ROW, so the
// endpoint + trigger order reverses in RTL (each endpoint's own composed

@ -164,7 +164,7 @@ describe('DragDropProvider', () => {
expect(result.draggable.props).toMatchObject({
role: 'button',
tabindex: 0,
// renderProps() emits the morfo's literal ARIA string, not a JS boolean.
// the render bag emits the morfo's literal ARIA string, not a JS boolean.
'aria-grabbed': 'true',
'data-dragging': '',
'data-value': 'card-a'
@ -177,7 +177,7 @@ describe('DragDropProvider', () => {
'data-accepting': ''
});
expect(result.preview.props).toMatchObject({
// renderProps() emits the morfo's literal ARIA string, not a JS boolean.
// the render bag emits the morfo's literal ARIA string, not a JS boolean.
'aria-hidden': 'true',
'data-active': ''
});

@ -357,7 +357,7 @@ export class DragDropProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares data-dragging without a value (soma-owned); supply it.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-dragging': boolToEmptyStrOrUndef(this.active !== null)
} as const)
);
@ -401,7 +401,7 @@ export class DraggableProvider {
owner: this,
// Per-part sources for the morfo's conditional aria (aria-disabled ←
// prop-truthy `disabled`, aria-grabbed ← prop-truthy `active`). With
// these, `renderProps()` resolves them instead of the provider
// these, `the render bag` resolves them instead of the provider
// re-declaring role/aria.
props: {
disabled: () => this.opts.disabled.current,
@ -501,7 +501,7 @@ export class DraggableProvider {
// role, aria-disabled (literal+condition), aria-grabbed (literal+
// condition) are morfo-declared and resolved against this part's
// registered `disabled`/`active` sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.opts.disabled.current ? -1 : 0,
// aria-roledescription: the morfo declares the `draggable` label key
// ("Draggable"); soma overrides with the dedicated role-description key
@ -548,7 +548,7 @@ export class DroppableProvider {
ref: opts.ref,
owner: this,
// Per-part source for the morfo's conditional aria-disabled
// (literal 'true' ← prop-truthy `disabled`). `renderProps()` resolves
// (literal 'true' ← prop-truthy `disabled`). `the render bag` resolves
// it instead of the provider re-declaring role/aria.
props: {
disabled: () => this.opts.disabled.current
@ -603,7 +603,7 @@ export class DroppableProvider {
this.runtimePart.assert({
// role, aria-disabled (literal+condition) are morfo-declared and
// resolved against this part's registered `disabled` source.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.provider.active && this.isAccepting ? 0 : -1,
// aria-roledescription: morfo declares the `droppable` label key
// ("Drop zone"); soma overrides with the dedicated role-description key
@ -676,7 +676,7 @@ export class DragPreviewProvider {
this.runtimePart.assert({
// aria-hidden is morfo-declared (literal 'true'). data-active is
// declared without a value (soma-owned); supply it.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-active': boolToEmptyStrOrUndef(this.provider.active !== null),
style: this.style
} as const)

@ -225,7 +225,7 @@ export class MenuTriggerProvider {
owner: this,
// Per-part sources for the morfo's value-bound attrs: aria-expanded
// (stateRef 'open') and aria-controls (partRef 'content', emitted only
// when content is present). With these, `renderProps()` resolves them
// when content is present). With these, `the render bag` resolves them
// from the morfo instead of the provider re-declaring them.
states: { open: () => this.provider.opts.open.current },
parts: { content: () => this.provider.contentId.current || undefined }
@ -257,8 +257,8 @@ export class MenuTriggerProvider {
this.runtimePart.assert({
// Morfo-declared (type, aria-haspopup, aria-controls) resolved against
// this part's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
// aria-expanded resolves from renderProps (morfo stateRef → string);
...this.runtimePart.props,
// aria-expanded resolves from the render bag (morfo stateRef → string);
// data-state has no morfo value source so soma derives it.
'data-state': getDataOpenClosed(this.provider.opts.open.current),
// The root renders no element, so the trigger IS the in-place half and
@ -329,7 +329,7 @@ export class MenuContentProvider {
this.provider.setContentRef(el);
},
// aria-labelledby (partRef 'trigger', emitted only when the trigger is
// present) resolves from this source via renderProps.
// present) resolves from this source via the render bag.
parts: { trigger: () => this.provider.triggerId.current || undefined }
});
this.provider.contentId.current = opts.id.current;
@ -497,7 +497,7 @@ export class MenuContentProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-labelledby, aria-orientation) resolved
// against this part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// data-state has no morfo value source — soma derives it.
'data-state': getDataOpenClosed(this.provider.opts.open.current),
...this.floating.props,
@ -567,7 +567,7 @@ export class MenuItemProvider {
ref: opts.ref,
owner: this,
// aria-disabled (literal 'true', emitted only when `disabled` is
// truthy) resolves from this source via renderProps.
// truthy) resolves from this source via the render bag.
props: { disabled: () => this.opts.disabled.current }
});
}
@ -597,7 +597,7 @@ export class MenuItemProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-disabled) resolved against this part's
// registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma owns: tabindex (native, computed from disabled +
// accessibleWhenDisabled), data-disabled/data-text-value (morfo
// declares them without a value source), handlers.
@ -643,7 +643,7 @@ export class MenuGroupProvider {
owner: this,
context: MenuGroupProvider.ctx,
// aria-labelledby (partRef 'group-heading', emitted only when the
// heading is present) resolves from this source via renderProps.
// heading is present) resolves from this source via the render bag.
parts: { 'group-heading': () => this.headingId.current || undefined }
});
}
@ -652,7 +652,7 @@ export class MenuGroupProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-labelledby) resolved against this part's
// registered sources.
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}
@ -712,7 +712,7 @@ export class MenuSeparatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (role, aria-orientation) — both literals.
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}
@ -785,7 +785,7 @@ export class MenuCheckboxItemProvider {
owner: this,
// Per-part sources: aria-checked (stateRef 'checked') resolves from the
// EFFECTIVE checked state (group membership when grouped), aria-disabled
// (literal 'true' when `disabled` truthy) — both via renderProps.
// (literal 'true' when `disabled` truthy) — both via the render bag.
states: { checked: () => this.effectiveChecked },
props: { disabled: () => this.opts.disabled.current }
});
@ -827,8 +827,8 @@ export class MenuCheckboxItemProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-disabled) resolved against this part's
// registered sources.
...this.runtimePart.renderProps(),
// aria-checked resolves from renderProps (morfo stateRef → string).
...this.runtimePart.props,
// aria-checked resolves from the render bag (morfo stateRef → string).
// Soma owns: data-state (no morfo value source), tabindex/
// data-disabled/data-text-value (native or no-value), handlers.
tabindex:
@ -877,7 +877,7 @@ export class MenuRadioGroupProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (role) — literal.
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}
@ -914,7 +914,7 @@ export class MenuRadioItemProvider {
owner: this,
// Per-part sources: aria-checked (stateRef 'checked') resolves from the
// computed isChecked (value === group value), aria-disabled (literal
// 'true' when `disabled` truthy) — both via renderProps.
// 'true' when `disabled` truthy) — both via the render bag.
states: { checked: () => this.isChecked },
props: { disabled: () => this.opts.disabled.current }
});
@ -947,8 +947,8 @@ export class MenuRadioItemProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-disabled) resolved against this part's
// registered sources.
...this.runtimePart.renderProps(),
// aria-checked resolves from renderProps (morfo stateRef → string).
...this.runtimePart.props,
// aria-checked resolves from the render bag (morfo stateRef → string).
// Soma owns: data-state (no morfo value source), tabindex/
// data-disabled/data-text-value (native or no-value), handlers.
tabindex:
@ -1184,8 +1184,8 @@ export class MenuSubTriggerProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-haspopup, aria-controls, aria-disabled)
// resolved against this part's registered sources.
...this.runtimePart.renderProps(),
// aria-expanded resolves from renderProps (morfo stateRef → string).
...this.runtimePart.props,
// aria-expanded resolves from the render bag (morfo stateRef → string).
// Soma owns: data-state (no morfo value source), tabindex/
// data-disabled/data-text-value (native or no-value), handlers.
tabindex:
@ -1242,7 +1242,7 @@ export class MenuSubContentProvider {
this.sub.setContentRef(el);
},
// aria-labelledby (partRef 'sub-trigger', emitted only when present)
// resolves from this source via renderProps.
// resolves from this source via the render bag.
parts: { 'sub-trigger': () => this.sub.subTriggerId.current || undefined }
});
this.sub.contentId.current = opts.id.current;
@ -1341,7 +1341,7 @@ export class MenuSubContentProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-labelledby, aria-orientation) resolved
// against this part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// data-state has no morfo value source — soma derives it.
'data-state': getDataOpenClosed(this.sub.opts.open.current),
// Programmatically focusable: keyboard open lands focus on the panel

@ -281,7 +281,7 @@ describe('EditableProvider', () => {
expect(opts.editing.current).toBe(true);
expect(result.edit.props.disabled).toBe(true);
// `data-editing` is morfo-declared and reaches props through the runtime's
// renderProps() spread — present at runtime, erased from the static type,
// render-bag spread — present at runtime, erased from the static type,
// hence the indexed read through a cast.
expect((result.edit.props as Record<string, unknown>)['data-editing']).toBe('');
expect(result.submit.props).toMatchObject({

@ -275,7 +275,7 @@ export class EditablePreviewProvider {
this.runtimePart.assert({
// Morfo-declared: role=textbox, aria-readonly=true. data-placeholder-
// shown/disabled/readonly are declared without a value (soma-owned).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma override: aria-label is the placeholder, removed when empty
// (the morfo's raw propRef would emit '' instead of absent).
'aria-label': this.provider.opts.placeholder.current || undefined,
@ -360,7 +360,7 @@ export class EditableInputProvider {
// Morfo-declared: aria-disabled/invalid (literal 'true' present-when-
// prop-truthy). data-disabled/readonly/invalid are declared without a
// value (soma-owned). Resolved against the component-level sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
hidden: !this.provider.isEditing || undefined,
value: this.provider.draft,
placeholder: this.provider.opts.placeholder.current || undefined,
@ -420,8 +420,8 @@ export class EditableEditTriggerProvider {
this.runtimePart.assert({
// Morfo-declared: type=button, data-editing ← editing. data-disabled/
// readonly are declared without a value (soma-owned).
...this.runtimePart.renderProps(),
// Naming default ships in `renderProps()` (consumerWins, A-85) — the
...this.runtimePart.props,
// Naming default ships in `the render bag` (consumerWins, A-85) — the
// morfo's commonRef resolves the same `#?common.buttons.*` id.
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),
@ -468,8 +468,8 @@ export class EditableSubmitTriggerProvider {
this.runtimePart.assert({
// Morfo-declared: type=button, data-editing ← editing. data-disabled/
// readonly are declared without a value (soma-owned).
...this.runtimePart.renderProps(),
// Naming default ships in `renderProps()` (consumerWins, A-85) — the
...this.runtimePart.props,
// Naming default ships in `the render bag` (consumerWins, A-85) — the
// morfo's commonRef resolves the same `#?common.buttons.*` id.
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),
@ -506,7 +506,7 @@ export class EditableHiddenInputProvider {
this.runtimePart.assert({
// Morfo-declared: aria-hidden=true. Native form attrs the morfo does
// not model stay soma-owned.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
type: 'hidden' as const,
name: this.provider.opts.name.current || undefined,
value: this.provider.opts.value.current,
@ -554,8 +554,8 @@ export class EditableCancelTriggerProvider {
this.runtimePart.assert({
// Morfo-declared: type=button, data-editing ← editing. data-disabled/
// readonly are declared without a value (soma-owned).
...this.runtimePart.renderProps(),
// Naming default ships in `renderProps()` (consumerWins, A-85) — the
...this.runtimePart.props,
// Naming default ships in `the render bag` (consumerWins, A-85) — the
// morfo's commonRef resolves the same `#?common.buttons.*` id.
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(this.provider.opts.readonly.current),

@ -184,7 +184,7 @@ export class EmojiPickerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current)
} as const)
);
@ -223,7 +223,7 @@ export class EmojiPickerTriggerProvider {
// Morfo supplies type=button, aria-haspopup=dialog, aria-expanded (←open)
// and the default translated aria-label. `data-state` comes from the
// composed PopoverTrigger via mergeProps in the wrapper.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label': this.resolvedLabel,
disabled: this.provider.opts.disabled.current || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.provider.opts.disabled.current)
@ -256,7 +256,7 @@ export class EmojiPickerContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// The morfo declares data-state without a stateRef — soma supplies it.
'data-state': this.provider.opts.open.current ? 'open' : 'closed'
} as const)
@ -374,6 +374,6 @@ export class EmojiPickerPreviewProvider {
}
readonly props = $derived.by(() =>
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}

@ -333,7 +333,7 @@ export class FieldInputProvider {
ref: opts.ref,
owner: this,
// Per-part source for the morfo's `type` aria entry (the component-level
// sources don't carry it). With this, `renderProps()` resolves `type`
// sources don't carry it). With this, `the render bag` resolves `type`
// from the morfo instead of the provider re-declaring it.
props: { type: () => this.opts.type.current }
});
@ -382,7 +382,7 @@ export class FieldInputProvider {
// Morfo-declared attrs (type, data-state, aria-labelledby, aria-invalid,
// aria-busy, aria-required), resolved against this part's registered
// sources — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: native form attrs + the value the component owns.
value: this.opts.value.current,
name: this.opts.name.current,
@ -500,8 +500,8 @@ export class FieldPrefixProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden — resolved by renderProps, not re-declared.
...this.runtimePart.renderProps()
// Morfo declares aria-hidden — resolved by the render bag, not re-declared.
...this.runtimePart.props
} as const)
);
}
@ -530,8 +530,8 @@ export class FieldSuffixProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares aria-hidden — resolved by renderProps, not re-declared.
...this.runtimePart.renderProps()
// Morfo declares aria-hidden — resolved by the render bag, not re-declared.
...this.runtimePart.props
} as const)
);
}

@ -394,7 +394,7 @@ export class FileUploadDropzoneProvider {
this.runtimePart.assert({
// role, aria-label, aria-disabled are morfo-declared (resolved against
// the part's `disabled` source) — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.provider.isDisabled ? -1 : 0,
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
'data-dragging': boolToEmptyStrOrUndef(this.dragging),
@ -445,7 +445,7 @@ export class FileUploadTriggerProvider {
this.runtimePart.assert({
// type, aria-label, aria-disabled are morfo-declared (resolved against
// the part's `disabled` source) — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.provider.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
onclick: this.onclick
@ -536,7 +536,7 @@ export class FileUploadFileListProvider {
this.runtimePart.assert({
// role + aria-label (morfo's `list` translationRef resolves to the same
// idlangref FILE_UPLOAD_LANGS.LIST) are morfo-declared.
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}
@ -582,7 +582,7 @@ export class FileUploadItemProvider {
this.runtimePart.assert({
// role is morfo-declared. data-file-name/type are morfo-declared WITHOUT
// a value (soma owns the values).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-file-name': this.opts.file.current.name,
'data-file-type': this.opts.file.current.type || undefined
} as const)
@ -738,7 +738,7 @@ export class FileUploadItemRemoveProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type is morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.item.provider.isDisabled || undefined,
// Soma override: the morfo's aria-label is the bare "Remove file" text;
// the provider appends the file name for a per-item accessible label.
@ -790,7 +790,7 @@ export class FileUploadItemProgressProvider {
this.runtimePart.assert({
// role, aria-valuenow (← percent source), aria-valuemin/max are
// morfo-declared. data-value is morfo-declared WITHOUT a value.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma override: the morfo's aria-label is the bare file name; the
// provider annotates it with the current percentage.
'aria-label': `${this.item.opts.file.current.name}: ${this.percent}%`,
@ -833,7 +833,7 @@ export class FileUploadClearTriggerProvider {
this.runtimePart.assert({
// type + aria-label (morfo's `buttons.clear` commonRef resolves to the
// same idlangref FILE_UPLOAD_LANGS.CLEAR) are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onclick: this.onclick

@ -200,7 +200,7 @@ export class FormSubmitProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-busy (← isPending, ariaBoolean) are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-label': this.resolvedLabel,
disabled: this.provider.isPending || undefined,
// Morfo declares data-pending as presence-only (no value); soma owns
@ -258,7 +258,7 @@ export class FormResetProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type is morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// aria-label honours the explicit `ariaLabel` prop (falls back to the
// same `buttons.reset` translation the morfo declares).
'aria-label': this.resolvedLabel,
@ -325,7 +325,7 @@ export class FormErrorSummaryProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=alert, aria-live=polite are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// hidden is a native attr the morfo does not model.
hidden: this.isHidden || undefined,
// Morfo declares data-hidden as presence-only (no value); soma owns

@ -165,7 +165,7 @@ export class GradientPickerProvider {
}
get props() {
return this.runtimePart.renderProps();
return this.runtimePart.props;
}
}
@ -199,7 +199,7 @@ export class GradientPickerTriggerProvider {
get props() {
const ariaLabel = this.opts.ariaLabel.current;
return {
...this.runtimePart.renderProps(),
...this.runtimePart.props,
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
disabled: this.provider.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
@ -232,6 +232,6 @@ export class GradientPickerValueSwatchProvider {
);
get props() {
return { ...this.runtimePart.renderProps(), style: this.backgroundStyle };
return { ...this.runtimePart.props, style: this.backgroundStyle };
}
}

@ -327,7 +327,7 @@ describe('GridListProvider', () => {
});
// `role` is morfo-declared and reaches props through the runtime's
// renderProps() spread — present at runtime, erased from the static type,
// render-bag spread — present at runtime, erased from the static type,
// hence the indexed read through a cast.
expect((result.cell.props as Record<string, unknown>).role).toBe('gridcell');
expect(result.checkbox.props).toMatchObject({

@ -75,7 +75,7 @@ export class GridListProvider {
context: GridListProvider.ctx,
// Per-part sources for the morfo's conditional aria booleans
// (aria-multiselectable/disabled/readonly/invalid/required). They
// are field-aware here, so renderProps() resolves the conditions
// are field-aware here, so the render bag resolves the conditions
// against the same flags the provider previously hardcoded.
props: {
multiple: () => this.opts.selectionMode.current === 'multiple',
@ -447,12 +447,12 @@ export class GridListProvider {
this.runtimePart.assert({
// Morfo-declared: role=grid + aria-multiselectable/disabled/readonly/
// invalid/required (conditional, resolved from this part's sources).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Raw, never `resolvedDir`: the rows are flex ROWS and the trailing cell is
// pushed out with `margin-inline-start: auto` — both read this root's direction.
// Soma-owned: aria-labelledby + the data-* flags are not value-backed
// in the morfo, so soma supplies them. (aria-label is the morfo's
// naming default and rides `renderProps()` — consumer-first, A-85.)
// naming default and rides `the render bag` — consumer-first, A-85.)
'aria-labelledby': this.opts.ariaLabelledby.current,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(this.isReadonly),
@ -491,7 +491,7 @@ export class GridListRowProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// row-local disabled (includes provider disabled) → renderProps
// row-local disabled (includes provider disabled) → the render bag
// resolves the morfo's conditional aria-disabled.
props: { disabled: () => this.isDisabled }
});
@ -520,7 +520,7 @@ export class GridListRowProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=row + conditional aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: roving tabindex; aria-selected is removed in 'none'
// mode (overrides the morfo's stateRef); the data-* are not
// value-backed in the morfo, so soma supplies them.
@ -573,7 +573,7 @@ export class GridListCellProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared: role=gridcell.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
onkeydown: this.onkeydown
} as const)
);
@ -603,7 +603,7 @@ export class GridListSelectionCheckboxProvider {
owner: this,
// The morfo's conditional aria-disabled reads prop `disabled`; the
// checkbox is non-interactive when the grid is disabled OR readonly,
// so feed the combined flag → renderProps resolves the condition.
// so feed the combined flag → the render bag resolves the condition.
props: { disabled: () => this.provider.isDisabled || this.provider.isReadonly }
});
}
@ -635,10 +635,10 @@ export class GridListSelectionCheckboxProvider {
const checked = this.rowValue ? this.provider.isSelected(this.rowValue) : false;
return this.runtimePart.assert({
// Morfo-declared: role=checkbox + conditional aria-disabled.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: native type=button; aria-checked is computed from the
// enclosing row association; data-* are not value-backed in the morfo.
// (aria-label is the morfo's naming default and rides `renderProps()`
// (aria-label is the morfo's naming default and rides `the render bag`
// — consumer-first, A-85.)
type: 'button' as const,
'aria-checked': checked,

@ -299,7 +299,7 @@ export class KnobControlProvider {
id: opts.id,
ref: opts.ref,
owner: this
// no syncAttrs: renderProps() path — we override aria-valuetext.
// no syncAttrs: render-bag path — we override aria-valuetext.
});
}
@ -350,7 +350,7 @@ export class KnobControlProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(), // role=slider, aria-value*, data-*
...this.runtimePart.props, // role=slider, aria-value*, data-*
tabindex: this.provider.opts.disabled.current ? -1 : 0,
'aria-valuetext': this.formatValue(), // soma owns the *value* (formatting)
'data-dragging': boolToEmptyStrOrUndef(this.provider.dragging),

@ -201,7 +201,7 @@ describe('LinkPreviewProvider', () => {
expect(result.content.isPresent).toBe(true);
expect(result.content.props).toMatchObject({
id: 'link-preview-content',
// renderProps() emits the morfo-declared ARIA literal as the canonical
// the render bag emits the morfo-declared ARIA literal as the canonical
// ARIA STRING "true" (DOM-identical to the old boolean shape).
'aria-hidden': 'true',
'data-state': 'open',

@ -235,7 +235,7 @@ export class LinkPreviewTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares `data-state` value-less (soma owns the open/closed value).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-state': getDataOpenClosed(this.provider.opts.open.current),
onpointerenter: this.onpointerenter,
onpointerleave: this.onpointerleave
@ -358,7 +358,7 @@ export class LinkPreviewContentProvider {
// LinkPreview is intentionally hidden from assistive tech (study §2.3).
// No `role` → the preview is skipped entirely by screen readers instead
// of announcing a phantom dialog that is then suppressed by aria-hidden.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-state': getDataOpenClosed(this.provider.opts.open.current),
...this.floating.props,
...this.provider.contentPresence.transitionAttrs,
@ -396,7 +396,7 @@ export class LinkPreviewArrowProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
...this.floatingArrow.props
} as const)
);

@ -79,7 +79,7 @@ export class ListboxProvider {
ref: opts.ref,
owner: this,
context: ListboxProvider.ctx,
// Per-part sources so `renderProps()` resolves the morfo's role +
// Per-part sources so `the render bag` resolves the morfo's role +
// aria-orientation/multiselectable/disabled/readonly/required/invalid
// instead of the provider re-declaring them. `multiple` maps the
// morfo's boolean prop to this component's `selectionMode`.
@ -347,9 +347,9 @@ export class ListboxProvider {
// Morfo-declared (role, aria-orientation/multiselectable/disabled/
// readonly/required/invalid, default aria-label) resolved against this
// part's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned: aria-labelledby is not morfo-modeled. (aria-label is the
// morfo's naming default and rides `renderProps()` — consumer-first,
// morfo's naming default and rides `the render bag` — consumer-first,
// A-85 — suppressed by the morfo when this one is set.)
'aria-labelledby': this.opts.ariaLabelledby.current,
// Soma-owned data-* (morfo declares them without a value source).
@ -399,7 +399,7 @@ export class ListboxItemProvider {
ref: opts.ref,
owner: this,
context: ListboxItemProvider.ctx,
// Per-part sources so `renderProps()` resolves the morfo's aria-selected
// Per-part sources so `the render bag` resolves the morfo's aria-selected
// (stateRef 'selected') and aria-disabled (prop-truthy 'disabled').
states: { selected: () => this.isSelected },
props: { disabled: () => this.isDisabled }
@ -451,7 +451,7 @@ export class ListboxItemProvider {
this.runtimePart.assert({
// Morfo-declared (role, aria-selected, aria-disabled) resolved against
// this item's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.isRovingTarget ? 0 : -1,
// Soma-owned data-* (morfo declares them without a value source).
'data-state': this.isSelected ? 'selected' : 'unselected',
@ -499,8 +499,8 @@ export class ListboxItemIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared aria-hidden resolved from renderProps.
...this.runtimePart.renderProps(),
// Morfo-declared aria-hidden resolved from the render bag.
...this.runtimePart.props,
// Soma-owned data-* (morfo declares no data on this part).
'data-state': this.item.isSelected ? 'selected' : 'unselected'
} as const)
@ -537,7 +537,7 @@ export class ListboxGroupProvider {
ref: opts.ref,
owner: this,
context: ListboxGroupProvider.ctx,
// Per-part source so `renderProps()` resolves the morfo's
// Per-part source so `the render bag` resolves the morfo's
// aria-labelledby (partRef 'group-label' + part-present condition)
// against the registered GroupLabel id.
parts: { 'group-label': () => this.labelId || undefined }
@ -546,8 +546,8 @@ export class ListboxGroupProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared role + aria-labelledby resolved from renderProps.
...this.runtimePart.renderProps()
// Morfo-declared role + aria-labelledby resolved from the render bag.
...this.runtimePart.props
} as const)
);
}

@ -343,7 +343,7 @@ export class MaskFieldInputProvider {
// Morfo-declared attrs (role=textbox, data-disabled/readonly/invalid,
// aria-invalid/required as booleans, aria-label default) resolved
// against the component-level sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
value: p.opts.value.current,
inputmode: p.inputMode,
placeholder: p.opts.placeholder.current,

@ -502,7 +502,7 @@ describe('MediaPlayerProvider', () => {
fake.emit('play', { paused: false });
fake.emit('volumechange', { muted: true });
// These attrs are now resolved by renderProps (from the registered label
// These attrs are now resolved by the render bag (from the registered label
// prop + `on` state sources), so they aren't statically-typed keys on the
// props bag — assert via toMatchObject like the initial snapshot above.
expect(result.play.props).toMatchObject({ 'data-state': 'playing', 'aria-label': 'Pause' });

@ -805,7 +805,7 @@ export class MediaPlayerProvider {
// the chrome inherits. Without it the `dir` PROP reached only the composed
// Sliders and the player laid its own controls out the other way round.
// data-dir is contract-only in the morfo (declared with `values`, no
// resolvable `value`), so renderProps doesn't emit it — soma owns it.
// resolvable `value`), so the render bag doesn't emit it — soma owns it.
'data-dir': this.resolvedDir,
onkeydown: this.onkeydown
} as const)
@ -843,9 +843,9 @@ export class MediaPlayerMediaProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// data-media is morfo-declared (propRef('media')), resolved by renderProps
// data-media is morfo-declared (propRef('media')), resolved by the render bag
// against the component-level `media` source.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Tap-to-toggle-play gesture on the video surface (YouTube convention);
// the audio element is 0-height, so this only fires for video.
onclick: () => this.provider.togglePlay()
@ -877,8 +877,8 @@ export class MediaPlayerPosterProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// aria-hidden is morfo-declared (literal), resolved by renderProps.
...this.runtimePart.renderProps(),
// aria-hidden is morfo-declared (literal), resolved by the render bag.
...this.runtimePart.props,
// data-hidden is contract-only in the morfo (presence flag) — soma owns it.
'data-hidden': boolToEmptyStrOrUndef(this.provider.started)
} as const)
@ -909,8 +909,8 @@ export class MediaPlayerBufferingIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// aria-hidden is morfo-declared (literal), resolved by renderProps.
...this.runtimePart.renderProps()
// aria-hidden is morfo-declared (literal), resolved by the render bag.
...this.runtimePart.props
} as const)
);
}
@ -963,8 +963,8 @@ export class MediaPlayerCaptionsProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// aria-hidden is morfo-declared (literal), resolved by renderProps.
...this.runtimePart.renderProps(),
// aria-hidden is morfo-declared (literal), resolved by the render bag.
...this.runtimePart.props,
// data-captions is contract-only in the morfo (presence flag) — soma
// owns it. Present only while there is something to read.
'data-captions': boolToEmptyStrOrUndef(this.provider.captionsOn)
@ -1021,7 +1021,7 @@ export class MediaPlayerPlayButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Source so renderProps resolves the morfo's aria-label (propRef('label')).
// Source so the render bag resolves the morfo's aria-label (propRef('label')).
props: { label: () => this.label }
});
}
@ -1032,8 +1032,8 @@ export class MediaPlayerPlayButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-keyshortcuts are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// type/role/aria-label/aria-keyshortcuts are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// data-state is contract-only in the morfo (no resolvable value) — soma owns it.
'data-state': this.provider.paused ? 'paused' : 'playing',
onclick: () => this.provider.togglePlay()
@ -1061,7 +1061,7 @@ export class MediaPlayerSeekButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and
// Sources so the render bag resolves aria-label (propRef('label')) and
// data-direction (propRef('direction')).
props: {
label: () => this.label,
@ -1083,8 +1083,8 @@ export class MediaPlayerSeekButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/data-direction are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// type/role/aria-label/data-direction are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
onclick: this.onclick
} as const)
);
@ -1118,7 +1118,7 @@ export class MediaPlayerRestartButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
disabled: this.opts.disabled.current || undefined,
// Seeking to 0 also clears the skip flag through `seek` — a restart is
@ -1173,7 +1173,7 @@ export class MediaPlayerSkipButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(this.unavailable),
disabled: this.unavailable || undefined,
onclick: () => {
@ -1204,7 +1204,7 @@ export class MediaPlayerMuteButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and the
// Sources so the render bag resolves aria-label (propRef('label')) and the
// on-state pair (data-state + aria-pressed via stateRef('on')).
props: { label: () => this.label },
states: { on: () => this.provider.muted }
@ -1233,8 +1233,8 @@ export class MediaPlayerMuteButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-keyshortcuts/aria-pressed/data-state are
// morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// data-level is contract-only in the morfo (an enum with no resolvable
// source) — soma owns the value.
'data-level': this.level,
@ -1262,7 +1262,7 @@ export class MediaPlayerFullscreenButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and the
// Sources so the render bag resolves aria-label (propRef('label')) and the
// on-state pair (data-state + aria-pressed via stateRef('on')).
props: { label: () => this.label },
states: { on: () => this.provider.fullscreen }
@ -1278,8 +1278,8 @@ export class MediaPlayerFullscreenButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-keyshortcuts/aria-pressed/data-state are
// morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// morfo-declared, resolved by the render bag.
...this.runtimePart.props,
onclick: () => void this.provider.toggleFullscreen()
} as const)
);
@ -1305,7 +1305,7 @@ export class MediaPlayerTimeProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Source so renderProps resolves the morfo's data-type (propRef('type')).
// Source so the render bag resolves the morfo's data-type (propRef('type')).
props: { type: () => this.opts.type.current }
});
}
@ -1323,8 +1323,8 @@ export class MediaPlayerTimeProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// data-type is morfo-declared (propRef('type')), resolved by renderProps.
...this.runtimePart.renderProps()
// data-type is morfo-declared (propRef('type')), resolved by the render bag.
...this.runtimePart.props
} as const)
);
}
@ -1421,7 +1421,7 @@ export class MediaPlayerPipButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and the
// Sources so the render bag resolves aria-label (propRef('label')) and the
// on-state pair (data-state + aria-pressed via stateRef('on')).
props: { label: () => this.label },
states: { on: () => this.provider.pip }
@ -1435,8 +1435,8 @@ export class MediaPlayerPipButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-pressed/data-state are morfo-declared,
// resolved by renderProps.
...this.runtimePart.renderProps(),
// resolved by the render bag.
...this.runtimePart.props,
onclick: () => void this.provider.togglePip()
} as const)
);
@ -1462,7 +1462,7 @@ export class MediaPlayerCaptionButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')), the
// Sources so the render bag resolves aria-label (propRef('label')), the
// on-state pair (data-state + aria-pressed via stateRef('on')) and the
// prop-falsy gate that sheds aria-pressed on a menu-trigger composition.
props: { label: () => this.label, menuTrigger: () => this.opts.menuTrigger.current },
@ -1477,8 +1477,8 @@ export class MediaPlayerCaptionButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/role/aria-label/aria-keyshortcuts/aria-pressed/data-state are
// morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// morfo-declared, resolved by the render bag.
...this.runtimePart.props,
// data-disabled is contract-only in the morfo (presence flag) — soma owns
// it; `disabled` is the native attr. Disabled until the media exposes at
// least one caption/subtitle track.
@ -1641,7 +1641,7 @@ export class MediaPlayerRateButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and
// Sources so the render bag resolves aria-label (propRef('label')) and
// data-rate (propRef('rate')).
props: {
label: () => this.label,
@ -1654,8 +1654,8 @@ export class MediaPlayerRateButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type/aria-label/data-rate are morfo-declared, resolved by renderProps.
...this.runtimePart.renderProps(),
// type/aria-label/data-rate are morfo-declared, resolved by the render bag.
...this.runtimePart.props,
onclick: () => this.provider.cycleRate(1)
} as const)
);
@ -1686,8 +1686,8 @@ export class MediaPlayerLiveIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// aria-hidden is morfo-declared (literal), resolved by renderProps.
...this.runtimePart.renderProps(),
// aria-hidden is morfo-declared (literal), resolved by the render bag.
...this.runtimePart.props,
// data-live is contract-only in the morfo (presence flag) — soma owns it.
'data-live': boolToEmptyStrOrUndef(this.provider.live)
} as const)

@ -135,7 +135,7 @@ describe('MenubarProvider', () => {
type: 'button',
role: 'menuitem',
'aria-haspopup': 'menu',
// renderProps() resolves aria-expanded (stateRef 'open') as the ARIA
// the render bag resolves aria-expanded (stateRef 'open') as the ARIA
// STRING, not a JS boolean — DOM-identical, correct ARIA shape.
'aria-expanded': 'false',
'data-state': 'closed',
@ -148,7 +148,7 @@ describe('MenubarProvider', () => {
// (composition IS the declaration); sibling menu entries keep "menu".
result.edit.menu.surface = 'panel';
flushSync();
// aria-haspopup is a morfo-declared attr (resolved at runtime via renderProps),
// aria-haspopup is a morfo-declared attr (resolved at runtime via the render bag),
// so it isn't a static key of the props type — index through a record cast.
expect((result.edit.trigger.props as Record<string, unknown>)['aria-haspopup']).toBe('dialog');
expect((result.file.trigger.props as Record<string, unknown>)['aria-haspopup']).toBe('menu');
@ -161,7 +161,7 @@ describe('MenubarProvider', () => {
expect(opts.value.current).toBe('file');
expect(result.file.openChange).toHaveBeenCalledWith(true);
expect(result.file.trigger.props).toMatchObject({
// renderProps() resolves aria-expanded (stateRef 'open') as the ARIA
// the render bag resolves aria-expanded (stateRef 'open') as the ARIA
// STRING, not a JS boolean — DOM-identical, correct ARIA shape.
'aria-expanded': 'true',
'aria-controls': 'menubar-file-content',

@ -217,8 +217,8 @@ export class MenubarProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-orientation are morfo-declared (resolved by renderProps).
...this.runtimePart.renderProps(),
// role + aria-orientation are morfo-declared (resolved by the render bag).
...this.runtimePart.props,
// Soma-only: writing direction the morfo does not model.
} as const)
);
@ -386,7 +386,7 @@ export class MenubarTriggerProvider {
ref: opts.ref,
owner: this,
// Per-part state source for the morfo's `aria-expanded` (stateRef 'open').
// With it, `renderProps()` resolves aria-expanded instead of the
// With it, `the render bag` resolves aria-expanded instead of the
// provider re-declaring it.
states: { open: () => this.isOpen },
// The morfo's `aria-haspopup` is a propRef: 'menu' for a Content
@ -490,11 +490,11 @@ export class MenubarTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (resolved by renderProps): type, role, aria-haspopup,
// Morfo-declared (resolved by the render bag): type, role, aria-haspopup,
// aria-expanded (← 'open' state). data-state / data-disabled /
// data-highlighted / data-menubar-value are declared WITHOUT a value
// source — soma owns them, so they stay below the spread.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: aria-controls (not modelled by the morfo) + the value-less
// data-* markers + native form attr + handlers.
'aria-controls': this.isOpen ? this.menu.menu.contentId.current || undefined : undefined,
@ -719,8 +719,8 @@ export class MenubarPanelContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (role=dialog, aria-labelledby) via renderProps.
...this.runtimePart.renderProps(),
// Morfo-declared (role=dialog, aria-labelledby) via the render bag.
...this.runtimePart.props,
// data-state has no morfo value source — soma derives it.
'data-state': getDataOpenClosed(this.menu.isOpen),
// Mega-panel marker — the recipe caps the intrinsic size from it.

@ -108,7 +108,7 @@ export class MonthGridProvider {
owner: this,
context: MonthGridProvider.ctx,
// Source for the morfo's `aria-disabled` (propRef('disabled')) so
// renderProps() resolves it instead of the provider re-declaring it.
// the render bag resolves it instead of the provider re-declaring it.
props: { disabled: () => opts.disabled.current }
});
// Initial focused month follows selection or today.
@ -302,8 +302,8 @@ export class MonthGridProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// role + aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
// Computed accessible name (ariaLabel + heading year) — overrides the
// morfo's raw `ariaLabel` propRef (which the provider doesn't source).
'aria-label': this.fullLabel,
@ -394,8 +394,8 @@ abstract class MonthGridPaginationButtonProvider {
const disabled = this.disabled();
return this.runtimePart.assert({
// type, aria-label (commonRef month-grid.prev/next-year) and
// aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(disabled),
disabled,
onclick: this.onclick
@ -458,7 +458,7 @@ export class MonthGridGridProvider {
ref: opts.ref,
owner: this,
// Sources for the morfo's `aria-readonly`/`aria-disabled`
// (propRef('readonly')/propRef('disabled')) so renderProps resolves them.
// (propRef('readonly')/propRef('disabled')) so the render bag resolves them.
props: {
readonly: () => this.provider.opts.readonly.current,
disabled: () => this.provider.opts.disabled.current
@ -470,8 +470,8 @@ export class MonthGridGridProvider {
const headingId = this.opts.labelledBy.current ?? this.provider.headingId.current;
return this.runtimePart.assert({
// role, aria-readonly and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Computed: honours the `labelledBy` prop override + the registered
// heading id (overrides the morfo's raw partRef('heading')).
'aria-labelledby': headingId || undefined
@ -545,8 +545,8 @@ export class MonthGridCellProvider {
const tabindex = this.focused && !this.disabled ? 0 : -1;
return this.runtimePart.assert({
// role, aria-selected and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Native button type (the morfo doesn't model it on the cell).
type: 'button' as const,
tabindex,

@ -245,7 +245,7 @@ export class NaturalTimePickerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// The in-place half paints from this: the trigger's `text-align: start`
// and its icon's `margin-inline-start: auto`, plus the inline Panel's own
// auto inline-start margins. Raw, per the note on `resolvedDir` above —

@ -211,7 +211,7 @@ export class NavTreeProvider {
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
} as const)
);
readonly listProps = $derived.by(() => this.listPart.renderProps());
readonly listProps = $derived.by(() => this.listPart.props);
}
// ── Item (per node) ────────────────────────────────────────────────────────────
@ -333,9 +333,9 @@ export class NavTreeItemProvider {
void this.runtime.trigger(this.expanded ? 'emerge-collapse' : 'emerge-expand');
}
readonly itemProps = $derived.by(() => this.itemPart.renderProps());
readonly itemProps = $derived.by(() => this.itemPart.props);
readonly linkProps = $derived.by(() => ({
...this.linkPart.renderProps(),
...this.linkPart.props,
// A disabled node keeps its row but loses the destination: no `href` means
// nothing to follow by pointer, keyboard or context menu (a CSS-only
// `pointer-events: none` would still let Enter and «open in new tab»
@ -343,12 +343,12 @@ export class NavTreeItemProvider {
href: this.disabled ? undefined : this.opts.item.current.node.href
}));
readonly triggerProps = $derived.by(() => ({
...this.triggerPart.renderProps(),
...this.triggerPart.props,
onclick: () => this.toggle()
}));
readonly badgeProps = $derived.by(() => this.badgePart.renderProps());
readonly badgeProps = $derived.by(() => this.badgePart.props);
readonly groupProps = $derived.by(() => ({
...this.groupPart.renderProps(),
...this.groupPart.props,
hidden: this.expanded ? undefined : true
}));

@ -363,7 +363,7 @@ export class NavigationMenuProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// dir is soma-resolved (prop → prefs → 'ltr'); the morfo doesn't model
// it. data-orientation is a morfo enum without a value source, so soma
// supplies the value.
@ -393,7 +393,7 @@ export class NavigationMenuListProvider {
ref: opts.ref,
owner: this,
// Per-part source for the morfo's `aria-orientation`
// (propRef:orientation). renderProps resolves it instead of the
// (propRef:orientation). the render bag resolves it instead of the
// provider re-declaring the attr.
props: {
orientation: () => this.provider.opts.orientation.current
@ -406,7 +406,7 @@ export class NavigationMenuListProvider {
// aria-orientation (propRef:orientation) is morfo-declared, resolved
// against the registered source above. data-orientation is a morfo
// enum without a value source, so soma supplies it.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// No naming here: the accessible name is the LANDMARK's and lives on
// the `<nav>` (A-111). The list carries structure only.
'data-orientation': this.provider.opts.orientation.current
@ -457,7 +457,7 @@ export class NavigationMenuItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// data-state / data-value are morfo declarations without a value
// source (enum + bare data), so soma supplies the values.
'data-state': this.isOpen ? 'open' : 'closed',
@ -579,7 +579,7 @@ export class NavigationMenuTriggerProvider {
// type (literal button) comes from the morfo. `aria-label` does NOT:
// the trigger declares no default, so the consumer's own attr rides
// restProps straight to the button (A-85).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// aria-expanded: morfo declares it via stateRef('open') (stringified
// to 'true'/'false'); soma keeps the typed boolean it owns.
'aria-expanded': this.isOpen,
@ -655,7 +655,7 @@ export class NavigationMenuContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// aria-labelledby: morfo declares partRef('trigger') gated on a
// trigger being present; soma uses the per-item trigger id (the
// morfo partRef resolves last-write-wins across triggers, not the
@ -692,7 +692,7 @@ export class NavigationMenuLinkProvider {
ref: opts.ref,
owner: this,
// Per-part source for the morfo's aria-current (literal 'page' gated
// on prop-truthy:active). renderProps resolves it via the condition.
// on prop-truthy:active). the render bag resolves it via the condition.
props: { active: () => this.opts.active.current }
});
}
@ -736,7 +736,7 @@ export class NavigationMenuLinkProvider {
this.runtimePart.assert({
// aria-current (literal 'page' gated on prop-truthy:active) is
// morfo-declared, resolved against the registered source above.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// data-active is bare morfo data without a value source — soma
// supplies the presence value.
'data-active': boolToEmptyStrOrUndef(this.opts.active.current),
@ -819,7 +819,7 @@ export class NavigationMenuIndicatorProvider {
// its own CSS. Forcing `width`/`height` here would cover the trigger's
// text when the consumer renders an underline just 2px tall.
return this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// data-state is a morfo enum without a value source — soma supplies
// it. style is soma-owned (computed indicator position).
'data-state': this.isOpen ? 'open' : 'closed',

@ -582,7 +582,7 @@ export class NumberFieldInputProvider {
owner: this,
// Per-part sources for the morfo's value-bound aria (the component-level
// sources only carry disabled/readonly/required/invalid). With these,
// `renderProps()` resolves aria-valuenow/min/max from the morfo instead
// `the render bag` resolves aria-valuenow/min/max from the morfo instead
// of the provider re-declaring them.
props: {
value: () => this.provider.opts.value.current,
@ -612,7 +612,7 @@ export class NumberFieldInputProvider {
// Morfo-declared attrs (role, data-spin-field-input, aria-valuenow/min/
// max/required/disabled/readonly/invalid), resolved against this part's
// registered sources — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: what the morfo can't express.
inputmode: 'decimal' as const,
maxlength: this.provider.opts.maxLength.current,
@ -674,7 +674,7 @@ export class NumberFieldIncrementTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// trigger-local disabled (includes atMax) → renderProps resolves aria-disabled
// trigger-local disabled (includes atMax) → the render bag resolves aria-disabled
props: { disabled: () => this.isDisabled }
});
}
@ -699,7 +699,7 @@ export class NumberFieldIncrementTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, data-spin-field, aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: -1,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -740,7 +740,7 @@ export class NumberFieldDecrementTriggerProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// trigger-local disabled (includes atMin) → renderProps resolves aria-disabled
// trigger-local disabled (includes atMin) → the render bag resolves aria-disabled
props: { disabled: () => this.isDisabled }
});
}
@ -765,7 +765,7 @@ export class NumberFieldDecrementTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, data-spin-field, aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: -1,
disabled: this.isDisabled || undefined,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
@ -891,7 +891,7 @@ export class NumberFieldScrubberProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role, aria-hidden, data-spin-field-scrubber are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-scrubbing': boolToEmptyStrOrUndef(this.provider.scrubbing),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
style: {

@ -193,7 +193,7 @@ describe('PaginationProvider', () => {
expect(root.getAttribute('role')).toBe('navigation');
expect(ellipsisEl.getAttribute('data-position')).toBe('end');
expect(ellipsisEl.getAttribute('aria-hidden')).toBe('true');
// Triggers + item now compose their morfo attrs through `renderProps()`
// Triggers + item now compose their morfo attrs through the render bag
// (no `syncAttrs`); a Svelte component spreads `props` onto the element in
// production, but this test never renders one — assert the props object.
expect(result.first.props).toMatchObject({ type: 'button', 'aria-label': 'First page' });
@ -258,7 +258,7 @@ describe('PaginationProvider', () => {
await tick();
// Root keeps `syncAttrs: true` → still applied to the bare element.
expect(root.getAttribute('data-disabled')).toBe('');
// next-trigger + item now project data-disabled via `renderProps()`
// next-trigger + item now project data-disabled via `the render bag`
// (no `syncAttrs`); assert the props object instead of the bare element.
expect(result.next.props).toMatchObject({ 'data-disabled': '' });
expect(result.item.props).toMatchObject({ 'data-disabled': '' });

@ -231,7 +231,7 @@ export class PaginationFirstTriggerProvider {
this.runtimePart = this.provider.runtime.part('first-trigger', {
id: opts.id,
ref: opts.ref,
// trigger-local disabled (includes hasPrevPage) → renderProps resolves
// trigger-local disabled (includes hasPrevPage) → the render bag resolves
// data-disabled; aria-label comes from the morfo's translationRef.
props: {
disabled: () => this.isDisabled
@ -255,7 +255,7 @@ export class PaginationFirstTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, role, data-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
} as const)
@ -286,7 +286,7 @@ export class PaginationPrevTriggerProvider {
this.runtimePart = this.provider.runtime.part('prev-trigger', {
id: opts.id,
ref: opts.ref,
// trigger-local disabled (includes hasPrevPage) → renderProps resolves
// trigger-local disabled (includes hasPrevPage) → the render bag resolves
// data-disabled; aria-label comes from the morfo's translationRef.
props: {
disabled: () => this.isDisabled
@ -310,7 +310,7 @@ export class PaginationPrevTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, role, data-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
} as const)
@ -341,7 +341,7 @@ export class PaginationNextTriggerProvider {
this.runtimePart = this.provider.runtime.part('next-trigger', {
id: opts.id,
ref: opts.ref,
// trigger-local disabled (includes hasNextPage) → renderProps resolves
// trigger-local disabled (includes hasNextPage) → the render bag resolves
// data-disabled; aria-label comes from the morfo's translationRef.
props: {
disabled: () => this.isDisabled
@ -365,7 +365,7 @@ export class PaginationNextTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, role, data-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
} as const)
@ -396,7 +396,7 @@ export class PaginationLastTriggerProvider {
this.runtimePart = this.provider.runtime.part('last-trigger', {
id: opts.id,
ref: opts.ref,
// trigger-local disabled (includes hasNextPage) → renderProps resolves
// trigger-local disabled (includes hasNextPage) → the render bag resolves
// data-disabled; aria-label comes from the morfo's translationRef.
props: {
disabled: () => this.isDisabled
@ -420,7 +420,7 @@ export class PaginationLastTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type, aria-label, role, data-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
} as const)
@ -453,7 +453,7 @@ export class PaginationItemProvider {
ref: opts.ref,
// Per-part sources for the morfo's data-/aria- (data-selected, data-
// disabled, data-value, aria-current, aria-label). With these,
// renderProps() resolves them from the morfo instead of the provider
// the render bag resolves them from the morfo instead of the provider
// re-declaring them. `ariaLabel` is the soma-computed page label that
// the morfo's aria-label propRef reads.
props: {
@ -488,7 +488,7 @@ export class PaginationItemProvider {
this.runtimePart.assert({
// type, role, aria-current, aria-label, data-selected/disabled/value
// are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
disabled: this.isDisabled || undefined,
onclick: this.onclick
} as const)

@ -275,10 +275,10 @@ export class PasswordFieldControlProvider {
}
// data-password-field-control (kebab) + data-field-control (fieldRole) +
// data-disabled / data-invalid are morfo-declared and resolved by renderProps.
// data-disabled / data-invalid are morfo-declared and resolved by the render bag.
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}
@ -354,7 +354,7 @@ export class PasswordFieldInputProvider {
// Morfo-declared attrs (role=textbox, aria-invalid/required as
// booleans, data-disabled/readonly/invalid) resolved against the
// component-level sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
type: p.opts.visible.current ? ('text' as const) : ('password' as const),
value: p.opts.value.current,
autocomplete: p.autocomplete,
@ -427,8 +427,8 @@ export class PasswordFieldVisibilityTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type=button, role=button, aria-pressed, data-visible, data-disabled
// are morfo-declared and resolved by renderProps.
...this.runtimePart.renderProps(),
// are morfo-declared and resolved by the render bag.
...this.runtimePart.props,
tabindex: -1,
disabled: this.provider.isDisabled || undefined,
// Label flips Show ↔ Hide with visibility — the morfo's static
@ -469,8 +469,8 @@ export class PasswordFieldCapsLockIndicatorProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=status, aria-live=polite and data-active (presence) are
// morfo-declared and resolved by renderProps.
...this.runtimePart.renderProps()
// morfo-declared and resolved by the render bag.
...this.runtimePart.props
} as const)
);
}
@ -529,8 +529,8 @@ export class PasswordFieldStrengthMeterProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=meter, aria-valuemin=0, aria-valuemax=4 come from renderProps.
...this.runtimePart.renderProps(),
// role=meter, aria-valuemin=0, aria-valuemax=4 come from the render bag.
...this.runtimePart.props,
// data-strength / aria-valuenow carry the CLAMPED, derived-or-explicit
// score (clampScore over the meter's own opt or the provider's
// derivedStrength) — not the raw `strength` propRef the morfo

@ -460,7 +460,7 @@ export class PathTraceProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-state': this.dataState
} as const)
);
@ -515,7 +515,7 @@ export class PathTraceTokenProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-roledescription': this.provider.soma.langs.ts(PATH_TRACE_LANGS.TOKEN_ROLE),
tabindex: this.provider.host.locked ? -1 : 0,
'data-tracing': boolToEmptyStrOrUndef(this.provider.tracing),
@ -576,7 +576,7 @@ export class PathTraceTrackProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-off-path': boolToEmptyStrOrUndef(this.provider.offPath)
} as const)
);

@ -210,7 +210,7 @@ export class PickerProvider<TValue> {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps()
...this.runtimePart.props
} as const)
);
}

@ -297,9 +297,9 @@ export class PinInputProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo declares the data-* presence attrs WITHOUT values (soma owns
// the boolean→empty-string projection); renderProps() supplies static
// the boolean→empty-string projection); the render bag supplies static
// identity, the provider fills the data-* below.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-readonly': boolToEmptyStrOrUndef(this.isReadonly),
'data-invalid': boolToEmptyStrOrUndef(this.isInvalid),
@ -347,7 +347,7 @@ export class PinInputInputProvider {
// Morfo-declared attrs (type=text, conditional aria-invalid, the
// data-* presence markers) resolved against this part's sources —
// no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
inputmode: this.provider.resolvedInputmode,
autocomplete: this.provider.resolvedAutocomplete as 'one-time-code',
autocapitalize: 'none' as const,
@ -360,7 +360,7 @@ export class PinInputInputProvider {
disabled: this.provider.isDisabled || undefined,
readonly: this.provider.isReadonly || undefined,
// No 'aria-label' line: the morfo's translationRef DEFAULT already
// rides `renderProps()` (naming attr — consumer-first, A-85). The
// rides `the render bag` (naming attr — consumer-first, A-85). The
// consumer's own label is merged over this bag by the wrapper, which
// is where the hidden input is rendered.
'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled),
@ -405,7 +405,7 @@ export class PinInputCellProvider {
// role=presentation + aria-hidden are morfo-declared; the data-*
// presence markers are declared without values (soma owns the
// boolean→empty-string projection below).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-active': boolToEmptyStrOrUndef(cell.isActive),
'data-filled': boolToEmptyStrOrUndef(cell.isFilled),
'data-placeholder': boolToEmptyStrOrUndef(!cell.isFilled && !cell.isActive),

@ -153,7 +153,7 @@ export class ProofOfHumanProvider implements ProofOfHumanChallengeHost {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Localize the morfo's translation-backed ARIA (the resolver would emit
// the raw idlangref otherwise — same pattern as drag-drop).
'aria-roledescription': this.soma.langs.ts(PROOF_OF_HUMAN_LANGS.ROLEDESC),

@ -155,7 +155,7 @@ export class RangeCalendarProvider implements CalendarNav {
ref: opts.ref,
owner: this,
context: RangeCalendarProvider.ctx,
// Per-part source so `renderProps()` resolves the morfo's
// Per-part source so `the render bag` resolves the morfo's
// `aria-disabled` (propRef('disabled'), ariaBoolean) — the component
// -level sources are empty.
props: { disabled: () => this.opts.disabled.current }
@ -661,8 +661,8 @@ export class RangeCalendarProvider implements CalendarNav {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-disabled come from the morfo via renderProps.
...this.runtimePart.renderProps(),
// role + aria-disabled come from the morfo via the render bag.
...this.runtimePart.props,
// Computed label (base + heading) — overrides the morfo's raw
// propRef('ariaLabel') default.
'aria-label': this.fullCalendarLabel,
@ -792,7 +792,7 @@ abstract class RangeCalendarPaginationButtonProvider {
const disabled = this.disabled();
return this.runtimePart.assert({
// type + aria-label (per-part commonRef) are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// aria-disabled is not modelled by the morfo for these buttons.
'aria-disabled': boolToStr(disabled),
'data-disabled': boolToEmptyStrOrUndef(disabled),
@ -1006,7 +1006,7 @@ export class RangeCalendarGridProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-labelledby + aria-readonly + aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Not modelled by the morfo.
'aria-multiselectable': true as const
} as const)
@ -1175,7 +1175,7 @@ export class RangeCalendarCellProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-selected + aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-owned data-* (morfo declares them valueless).
'data-selected': boolToEmptyStrOrUndef(this.selected),
'data-range-start': boolToEmptyStrOrUndef(this.rangeStart),
@ -1256,7 +1256,7 @@ export class RangeCalendarDayProvider {
const tabindex = this.focused && !s.disabled ? 0 : -1;
return this.runtimePart.assert({
// role + aria-disabled are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Native attr the morfo doesn't model.
tabindex,
// Soma-owned data-* (morfo declares them valueless).

@ -438,7 +438,7 @@ export class RotateAlignProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-state': this.dataState
} as const)
);
@ -493,7 +493,7 @@ export class RotateAlignNeedleProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-roledescription': this.provider.soma.langs.ts(ROTATE_ALIGN_LANGS.NEEDLE_ROLE),
tabindex: this.provider.host.locked ? -1 : 0,
'data-rotating': boolToEmptyStrOrUndef(this.provider.rotating),
@ -550,7 +550,7 @@ export class RotateAlignDialProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'data-aligned': boolToEmptyStrOrUndef(this.provider.aligned)
} as const)
);

@ -299,7 +299,7 @@ export class ScrollAreaScrollbarProvider {
owner: this,
context: ScrollAreaScrollbarProvider.ctx,
// Per-part sources for the morfo's value-bound aria. The component-level
// `soma.runtime(...)` carries no sources, so `renderProps()` resolves
// `soma.runtime(...)` carries no sources, so `the render bag` resolves
// aria-valuenow/orientation/controls from these instead of the provider
// re-declaring them.
props: {
@ -486,7 +486,7 @@ export class ScrollAreaScrollbarProvider {
// Morfo-declared attrs (role, aria-controls ← viewport id, aria-valuenow
// ← scrollPercent, aria-valuemin/max literals, aria-orientation), resolved
// against this part's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: the morfo's aria-label is the vertical default; soma supplies
// the orientation-specific text.
'aria-label': this.isVertical

@ -121,7 +121,7 @@ describe('SearchFieldProvider', () => {
placeholder: 'Find',
required: true,
'aria-label': 'Search',
// renderProps() emits the morfo's literal ARIA string (DOM-identical to a boolean true).
// the render bag emits the morfo's literal ARIA string (DOM-identical to a boolean true).
'aria-invalid': 'true'
});
expect(result.clear.props).toMatchObject({
@ -250,7 +250,7 @@ describe('SearchFieldProvider', () => {
disabled: true,
required: true,
'aria-labelledby': 'field-label',
// renderProps() emits the morfo's literal ARIA string (DOM-identical to a boolean true).
// the render bag emits the morfo's literal ARIA string (DOM-identical to a boolean true).
'aria-invalid': 'true',
'aria-describedby': 'field-helper field-error',
'data-disabled': ''

@ -312,7 +312,7 @@ export class SearchFieldInputProvider {
// Morfo-declared attrs (role=searchbox, type=search, data-disabled/
// readonly, aria-invalid via literal+prop-truthy) resolved against this
// part's registered sources — no longer re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: what the morfo can't express.
value: this.provider.opts.value.current,
name: this.provider.opts.name.current,
@ -381,7 +381,7 @@ export class SearchFieldClearTriggerProvider {
// A-85): the consumer's own `aria-label` stays in restProps and beats
// it in `mergeProps`. The provider used to re-resolve that chain and
// hand the result back — one consumer surface, not two.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Native form attr the morfo does not model.
disabled:
this.provider.isDisabled || this.provider.isReadonly || this.provider.isEmpty || undefined,

@ -409,7 +409,7 @@ export class SelectTriggerProvider {
// Morfo-declared (type, role, aria-haspopup, aria-expanded,
// aria-controls, aria-activedescendant, aria-required) resolved against
// this part's sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: aria-invalid is not on the morfo's Trigger; the data-*
// below are declared by the morfo WITHOUT a value (soma owns them);
// `disabled` is a native attr the morfo doesn't model.
@ -589,7 +589,7 @@ export class SelectContentProvider {
// Morfo-declared (role, aria-labelledby ← trigger part, aria-
// multiselectable ← multiple prop) resolved against the component
// sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: data-state is declared by the morfo WITHOUT a value;
// floating / presence / dismissal props + tabindex the morfo can't model.
'data-state': getDataOpenClosed(this.provider.opts.open.current),
@ -659,7 +659,7 @@ export class SelectItemProvider {
// Per-instance sources for the morfo's value-bound aria: `aria-selected`
// ← `checked` state, `aria-disabled` ← `disabled` prop (the component-
// level sources only carry the root's open/disabled/etc., not per-item
// selection). With these, renderProps() resolves both from the morfo.
// selection). With these, the render bag resolves both from the morfo.
states: { checked: () => this.isSelected },
props: { disabled: () => this.opts.disabled.current }
});
@ -697,7 +697,7 @@ export class SelectItemProvider {
// Morfo-declared (role, aria-selected ← checked state, aria-disabled
// ← disabled prop) resolved against this item's sources — no longer
// re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: tabindex + the data-* below (all declared by the morfo
// WITHOUT a value, so soma owns them).
tabindex: this.opts.disabled.current ? undefined : -1,
@ -776,7 +776,7 @@ export class SelectGroupProvider {
readonly props = $derived.by(() =>
// Morfo-declared (role, aria-labelledby ← group-heading part) resolved
// against this group's sources — no longer re-declared here.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -836,7 +836,7 @@ export class SelectSeparatorProvider {
readonly props = $derived.by(() =>
// Morfo-declared (role) resolved here — no longer re-declared.
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}

@ -211,7 +211,7 @@ export class SidebarProvider {
readonly providerProps = $derived.by(() =>
this.providerPart.assert({
...this.providerPart.renderProps()
...this.providerPart.props
// The recipe selects with `:dir(rtl)`, which reads the RESOLVED direction,
// so an asserted `dir` must reach the DOM or the paint would lag the prop.
// Raw, not `resolvedDir`: no assertion means no attribute (direction contract).
@ -249,7 +249,7 @@ export class SidebarPanelProvider {
() => this.opts.ariaLabel.current || this.provider.resolvedLabel.current
);
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
/** Shared behaviour of the two disclosure controls (Trigger and Rail). */
@ -267,7 +267,7 @@ class SidebarToggleProvider {
}
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Per-instance target id — see the note in SidebarPanelProvider.
'aria-controls': this.provider.panelId.current || undefined,
disabled: this.provider.toggleDisabled || undefined,
@ -318,7 +318,7 @@ export class SidebarRegionProvider {
this.runtimePart = this.provider.runtime.part(part, { id: opts.id, ref: opts.ref });
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
/** The scrolling body — the `<nav>` landmark, named apart from the panel. */
@ -345,7 +345,7 @@ export class SidebarContentProvider {
() => this.opts.ariaLabel.current || this.provider.resolvedNavLabel.current
);
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
// ── Group · GroupLabel · Menu ────────────────────────────────────────────────
@ -375,7 +375,7 @@ export class SidebarGroupProvider {
});
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
export class SidebarGroupLabelProvider {
@ -393,7 +393,7 @@ export class SidebarGroupLabelProvider {
if (group) group.labelId.current = opts.id.current;
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
export class SidebarMenuProvider {
@ -412,7 +412,7 @@ export class SidebarMenuProvider {
}
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// The morfo declares `aria-labelledby: partRef('group-label')`, but a
// partRef resolves last-write-wins across instances — with three groups
// every menu would name the LAST label. Soma supplies its own group's id.
@ -532,7 +532,7 @@ export class SidebarMenuItemProvider {
}
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Both entry paths need their own exit. Focus: a keyboard user opens the
// flyout by focusing the row, tabs THROUGH its links, and tabbing past
// them must not strand an open surface. Pointer: the flyout opens on
@ -613,7 +613,7 @@ export class SidebarMenuButtonProvider {
readonly disabled = $derived.by(() => !!this.opts.disabled.current && !!this.opts.href.current);
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
href: this.disabled ? undefined : this.opts.href.current,
onpointerenter: () => this.item?.openSub(),
onfocusin: () => this.item?.openSub(),
@ -665,7 +665,7 @@ export class SidebarMenuActionProvider {
});
}
readonly props = $derived.by(() => this.runtimePart.renderProps());
readonly props = $derived.by(() => this.runtimePart.props);
}
// ── MenuSub · MenuSubButton ──────────────────────────────────────────────────
@ -751,7 +751,7 @@ export class SidebarMenuSubProvider {
readonly open = $derived.by(() => this.item?.subOpen ?? true);
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
...(this.isFloating && this.floating ? this.floating.props : {})
// The morfo declares `Escape → close-sub` on this part, and the item
// handles it for the whole subtree (focus normally sits on the ROW, not
@ -787,7 +787,7 @@ export class SidebarMenuSubButtonProvider {
readonly disabled = $derived.by(() => !!this.opts.disabled.current && !!this.opts.href.current);
readonly props = $derived.by(() => ({
...this.runtimePart.renderProps(),
...this.runtimePart.props,
href: this.disabled ? undefined : this.opts.href.current,
// Same contract as MenuButton — see `emitRowPress`.
onclick: () => emitRowPress(this.runtimePart, this.provider, this.opts)

@ -535,7 +535,7 @@ export class SliderThumbProvider {
owner: this,
// Per-part sources for the morfo's value-bound aria. The component-level
// sources only carry orientation/disabled; the thumb resolves its OWN
// scalar value/label (indexed by `index`) plus min/max so `renderProps()`
// scalar value/label (indexed by `index`) plus min/max so `the render bag`
// emits aria-valuenow/min/max/label/orientation/disabled from the morfo
// instead of the provider re-declaring them.
props: {
@ -616,9 +616,9 @@ export class SliderThumbProvider {
this.runtimePart.assert({
// Morfo-declared attrs (role=slider, aria-valuenow/min/max from this
// part's scalar sources, aria-label from the indexed thumbLabel,
// aria-orientation, aria-disabled) — resolved via renderProps, no
// aria-orientation, aria-disabled) — resolved via the render bag, no
// longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: native focusability + data the morfo declares without a
// value source + handler + positioning.
tabindex: this.provider.isDisabled ? undefined : 0,

@ -179,7 +179,7 @@ export class StepperListProvider {
this.runtimePart.assert({
// role + aria-orientation are morfo-declared (aria-orientation reads
// the component-level `orientation` source).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma override: honours an explicit `ariaLabel` prop, falling back to
// the morfo's translated default (same key).
'aria-label': this.opts.ariaLabel.current || this.provider.soma.langs.ts(STEPPER_LANGS.LIST),
@ -335,7 +335,7 @@ export class StepperTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role='tab' + type='button' are morfo-declared (static identity).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma overrides — computed values the morfo can't resolve from this
// part's (unregistered) sources:
// aria-controls ← the Item-registered content id (morfo partRef
@ -559,7 +559,7 @@ export class StepperPrevTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role='button' + type='button' are morfo-declared (static identity).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Native disabled + handler + soma-owned data-disabled (morfo declares
// it without a value source).
disabled: this.isDisabled || undefined,
@ -607,7 +607,7 @@ export class StepperNextTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role='button' + type='button' are morfo-declared (static identity).
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Native disabled + handler + soma-owned data-disabled (morfo declares
// it without a value source).
disabled: this.isDisabled || undefined,

@ -156,8 +156,8 @@ export class TableColumnHeaderProvider {
: 'none';
return this.runtimePart.assert({
// role=columnheader is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// role=columnheader is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
// aria-sort is the computed ARIA token (ascending/descending/none),
// gated on sortability — soma owns the value the morfo's propRef
// can't compute.
@ -258,8 +258,8 @@ export class TableSortTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type=button is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// type=button is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
'data-sortable': boolToEmptyStrOrUndef(this.canSort),
'data-sorted': this.sorted || undefined,
onclick: this.onclick,
@ -340,8 +340,8 @@ export class TableRowProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=row is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// role=row is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
// aria-rowindex is 1-based (morfo's `index` propRef is the raw 0-based
// value); aria-selected is gated on selection mode — soma owns both
// computed values.
@ -392,8 +392,8 @@ export class TableCellProvider {
const cell = this.opts.cell.current;
const pinned = this.table.getIsColumnPinned(cell.column.id);
return this.runtimePart.assert({
// role=gridcell is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// role=gridcell is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
// aria-colindex is 1-based (morfo's `colIndex` propRef is raw 0-based) —
// soma owns the computed value.
'aria-colindex': this.opts.colIndex.current + 1,
@ -533,8 +533,8 @@ export class TableRowDetailProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=row is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// role=row is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
// Deterministic DOM id derived from row.id (so the sibling Trigger's
// aria-controls can target it across the <tr> boundary) — overrides
// the part's generated id.
@ -628,8 +628,8 @@ export class TableRowDetailTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type=button is morfo-declared; renderProps supplies it.
...this.runtimePart.renderProps(),
// type=button is morfo-declared; the render bag supplies it.
...this.runtimePart.props,
// aria-expanded is the live boolean (morfo's stateRef('open') has no
// registered state source here); aria-controls targets the sibling
// RowDetail by its deterministic id (not a registered part-ref across

@ -322,7 +322,7 @@ export class TagGroupItemProvider {
ref: opts.ref,
owner: this,
context: TagGroupItemProvider.ctx,
// Per-part sources so `renderProps()` resolves the morfo's
// Per-part sources so `the render bag` resolves the morfo's
// `aria-selected` (stateRef 'selected') and `aria-disabled`
// (literal+prop-truthy) against THIS item's state instead of the
// provider re-declaring them.
@ -360,7 +360,7 @@ export class TagGroupItemProvider {
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved against this
// part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.isDisabled ? -1 : this.isRovingTarget ? 0 : -1,
// Soma owns the value: `none` selection mode removes aria-selected
// entirely (the morfo's stateRef can't express that branch), and we
@ -414,7 +414,7 @@ export class TagGroupLinkProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Per-part source so `renderProps()` resolves the morfo's
// Per-part source so `the render bag` resolves the morfo's
// `aria-disabled` (literal+prop-truthy) against this link's state.
props: { disabled: () => this.isDisabled }
});
@ -439,7 +439,7 @@ export class TagGroupLinkProvider {
return this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved against this
// part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// We also emit the `item` part attr so the Provider's DOM queries
// find Links alongside Items — both participate in the roving set.
[attrs.item]: '',
@ -499,7 +499,7 @@ export class TagGroupRemoveButtonProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type is morfo-declared (literal 'button').
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma owns the value: the contextual `Remove {tag}` label (honouring
// an explicit ariaLabel prop) overrides the morfo's default
// translationRef.

@ -360,7 +360,7 @@ export class TagsInputRowProvider {
});
}
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.renderProps()));
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.props));
}
// ── InputCell ────────────────────────────────────────────────────────────────
@ -387,7 +387,7 @@ export class TagsInputInputCellProvider {
});
}
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.renderProps()));
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.props));
}
// ── Input ────────────────────────────────────────────────────────────────────
@ -482,8 +482,8 @@ export class TagsInputInputProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (aria-invalid via the provider-level `invalid`
// source) comes from renderProps.
...this.runtimePart.renderProps(),
// source) comes from the render bag.
...this.runtimePart.props,
// Soma-only: native input attrs the morfo doesn't model +
// state-derived data-* (morfo declares them valueless) + handlers.
type: 'text' as const,
@ -542,11 +542,11 @@ export class TagsInputItemProvider {
context: TagsInputItemProvider.ctx,
// Per-part source: the item's effective disabled (item-local OR
// provider-level) so the morfo's `aria-disabled` condition resolves
// via renderProps instead of being re-declared here.
// via the render bag instead of being re-declared here.
props: { disabled: () => this.isDisabled },
// Per-part state source for the morfo's `aria-selected` (stateRef
// 'active', FIRMA 2026-08-24). The roving highlight is per-item, so it
// is registered per-instance here → `renderProps()` resolves
// is registered per-instance here → `the render bag` resolves
// aria-selected from the morfo instead of soma re-declaring it.
states: { active: () => this.isHighlighted }
});
@ -595,8 +595,8 @@ export class TagsInputItemProvider {
this.runtimePart.assert({
// role=option, aria-selected (from the per-part `active` state) and
// aria-disabled (from the per-part `disabled` source) are
// morfo-declared → renderProps.
...this.runtimePart.renderProps(),
// morfo-declared → the render bag.
...this.runtimePart.props,
// Soma-only: roving tabindex + state-derived data-* (morfo declares
// data-value/data-state/data-highlighted/data-disabled valueless) +
// handlers.
@ -681,8 +681,8 @@ export class TagsInputItemDeleteTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type=button + aria-label (morfo translationRef → same idlangref)
// come from renderProps.
...this.runtimePart.renderProps(),
// come from the render bag.
...this.runtimePart.props,
// Soma-only: non-roving tabindex, native disabled (incl. readonly),
// state-derived data-disabled, handler.
tabindex: -1,
@ -724,8 +724,8 @@ export class TagsInputClearTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// type=button + aria-label (morfo translationRef → same idlangref)
// come from renderProps.
...this.runtimePart.renderProps(),
// come from the render bag.
...this.runtimePart.props,
// Soma-only: visibility + native disabled + handler.
hidden:
this.provider.isEmpty ||

@ -363,7 +363,7 @@ export class TextAreaInputProvider {
// aria-required, data-disabled/readonly/invalid), resolved against the
// component-level disabled/readonly/required/invalid sources — no longer
// re-declared in the provider.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
value: p.opts.value.current,
rows: p.opts.autosize.current ? p.opts.minRows.current : p.opts.rows.current,
placeholder: p.opts.placeholder.current,

@ -537,7 +537,7 @@ export class TimeFieldInputProvider {
owner: this,
onRefChange: (v) => this.provider.setFieldNode(v),
// Per-part source for the morfo's `aria-label` ← `ariaLabel` prop, so
// `renderProps()` resolves it instead of the provider re-declaring it.
// `the render bag` resolves it instead of the provider re-declaring it.
// (`disabled`/`readonly`/`invalid` come from the component-level sources.)
props: { ariaLabel: () => this.opts.ariaLabel.current }
});
@ -573,7 +573,7 @@ export class TimeFieldInputProvider {
// Morfo-declared attrs (role=group, aria-label, aria-disabled/readonly,
// aria-invalid, aria-labelledby from the Label part), resolved against
// this part's registered sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Soma-only: aria-labelledby honours an explicit `ariaLabelledBy` prop
// (the morfo only knows the Label part); aria-describedby is the computed
// hidden-description element id (not the morfo's prop); data-invalid/
@ -920,7 +920,7 @@ export abstract class BaseTimeNumericSegmentProvider {
return this.runtimePart.assert({
// Morfo-declared attrs (role=spinbutton, aria-disabled, aria-invalid),
// resolved against the component-level sources — no longer re-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
...this.provider.sharedSegmentAttrs,
// Soma-only: data-* are morfo-declared WITHOUT a value; the value-aria
// (`...p`), per-segment aria-readonly and aria-labelledby are computed

@ -37,10 +37,10 @@
});
// Morfo-driven attrs: data-state and the `aria-label` all come from the
// Clock's morfo declaration via `renderProps()` (which also carries the marker
// Clock's morfo declaration via `the render bag` (which also carries the marker
// + ref attachment). No hardcoded role/data-state, and the morfo's aria-label
// is honoured instead of dropped.
const mergedProps = $derived(mergeProps(restProps, clockPart.renderProps()));
const mergedProps = $derived(mergeProps(restProps, clockPart.props));
</script>
{#if child}

@ -266,7 +266,7 @@ export class TimePickerProvider {
// Morfo-declared attrs (data-state, data-disabled/readonly/required/
// invalid, aria-disabled/readonly/required), resolved against this
// part's registered component-level sources — no longer re-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Raw, so no assertion leaves the root inheriting. The in-place half reads
// it: the root IS the field, its segment row (`[data-field-control]`,
// `flex-direction: row`) reverses in RTL, and the embedded trigger rides

@ -38,10 +38,10 @@
});
// Morfo-driven attrs: role, data-state and the `aria-label` all come from the
// Clock's morfo declaration via `renderProps()` (which also carries the marker
// Clock's morfo declaration via `the render bag` (which also carries the marker
// + ref attachment). No hardcoded role/data-state, and the morfo's aria-label
// is honoured instead of dropped.
const mergedProps = $derived(mergeProps(restProps, clockPart.renderProps()));
const mergedProps = $derived(mergeProps(restProps, clockPart.props));
</script>
{#if child}

@ -317,7 +317,7 @@ export class TimeRangePickerProvider {
// Morfo-declared attrs (data-state, data-disabled/readonly/required,
// aria-disabled/readonly/required) resolved against this part's
// registered states/props sources — no longer re-declared here.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
// Raw, so no assertion leaves the root inheriting. The root itself is the
// in-place paint: `[data-time-range-picker]` is a wrapping flex ROW, so the
// endpoint + trigger order reverses in RTL, and the embedded trigger rides

@ -90,7 +90,7 @@ describe('TimelineProvider', () => {
expect(result.last.effectiveStatus).toBe('upcoming');
expect(result.current.isCurrent).toBe(true);
// renderProps resolves the morfo attrs into the props object the wrapper
// the render bag resolves the morfo attrs into the props object the wrapper
// spreads onto the element; assert the object (no wrapper in this harness).
expect(result.provider.props).toMatchObject({
id: 'timeline-root',

@ -91,8 +91,8 @@ export class TimelineProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// data-timeline, role='list' and data-live are morfo-declared;
// renderProps resolves them from the registered `live` source.
...this.runtimePart.renderProps(),
// the render bag resolves them from the registered `live` source.
...this.runtimePart.props,
// Soma override — honour an explicit `ariaLabel`, falling back to the
// morfo's translated default (same key).
'aria-label': this.opts.ariaLabel.current || this.soma.langs.ts(TIMELINE_LANGS.LABEL)
@ -176,8 +176,8 @@ export class TimelineItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// data-timeline-item, data-status and data-intent are morfo-declared;
// renderProps resolves them from the registered status/intent sources.
...this.runtimePart.renderProps(),
// the render bag resolves them from the registered status/intent sources.
...this.runtimePart.props,
// Soma override — aria-current reads the derived `current` status
// (the morfo condition would need a registered state source).
'aria-current': this.isCurrent ? ('step' as const) : undefined

@ -77,7 +77,7 @@ export class TreeGridProvider {
owner: this,
context: TreeGridProvider.ctx,
// Per-part sources for the morfo's conditional aria (aria-multiselectable/
// disabled/readonly). With these, `renderProps()` resolves them from the
// disabled/readonly). With these, `the render bag` resolves them from the
// morfo instead of the provider re-declaring the literals.
props: {
multiple: () => this.opts.selectionMode.current === 'multiple',
@ -456,7 +456,7 @@ export class TreeGridProvider {
this.runtimePart.assert({
// role, aria-multiselectable/disabled/readonly are morfo-declared and
// resolved from this part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
'aria-labelledby': this.opts.ariaLabelledby.current,
// Soma-owned data-* (morfo declares them without a value).
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
@ -494,7 +494,7 @@ export class TreeGridHeaderProvider {
readonly props = $derived.by(() =>
// role is morfo-declared (role: 'row').
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -521,7 +521,7 @@ export class TreeGridColumnHeaderProvider {
readonly props = $derived.by(() =>
// role is morfo-declared (role: 'columnheader').
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -609,7 +609,7 @@ export class TreeGridRowProvider {
return this.runtimePart.assert({
// role, aria-level, aria-expanded and aria-disabled are morfo-declared
// and resolved from this part's registered sources.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: this.isDisabled ? -1 : this.isRovingTarget ? 0 : -1,
// Soma override: aria-selected is suppressed entirely when the grid is
// not selectable (selectionMode === 'none'); the morfo's stateRef would
@ -658,7 +658,7 @@ export class TreeGridRowChildrenProvider {
readonly props = $derived.by(() =>
// role is morfo-declared (role: 'group').
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -685,7 +685,7 @@ export class TreeGridCellProvider {
readonly props = $derived.by(() =>
// role is morfo-declared (role: 'gridcell').
this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const)
this.runtimePart.assert({ ...this.runtimePart.props } as const)
);
}
@ -729,7 +729,7 @@ export class TreeGridExpandTriggerProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role, type and aria-hidden are morfo-declared.
...this.runtimePart.renderProps(),
...this.runtimePart.props,
tabindex: -1,
// Soma override: aria-label reflects expand/collapse state and honours
// the explicit `ariaLabel` prop over the morfo's translation default.

@ -317,8 +317,8 @@ export class TreeViewProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// `role` is morfo-declared (static `tree`), resolved by renderProps().
...this.runtimePart.renderProps(),
// `role` is morfo-declared (static `tree`), resolved by the render bag().
...this.runtimePart.props,
// Soma override: provider models `multiple` via `selectionMode` and emits
// the boolean attribute (morfo declares the default `aria-multiselectable`
// keyed on a `multiple` prop the component-level sources don't register).
@ -382,8 +382,8 @@ export class TreeViewBranchProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// `role` is morfo-declared (static `treeitem`), resolved by renderProps().
...this.runtimePart.renderProps(),
// `role` is morfo-declared (static `treeitem`), resolved by the render bag().
...this.runtimePart.props,
// Soma overrides — boolean ARIA state the morfo expresses as 'true'/'false'
// strings (aria-boolean) or present/absent; this provider emits the raw
// boolean, and `aria-level` is the computed depth+1 (morfo carries raw depth).
@ -485,8 +485,8 @@ export class TreeViewBranchContentProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// `role` is morfo-declared (static `group`), resolved by renderProps().
...this.runtimePart.renderProps(),
// `role` is morfo-declared (static `group`), resolved by the render bag().
...this.runtimePart.props,
// Soma-owned data-* (the morfo declares these without a value source).
'data-state': this.branch.isExpanded ? 'open' : 'closed',
'data-depth': this.branch.opts.depth.current
@ -607,8 +607,8 @@ export class TreeViewItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// `role` is morfo-declared (static `treeitem`), resolved by renderProps().
...this.runtimePart.renderProps(),
// `role` is morfo-declared (static `treeitem`), resolved by the render bag().
...this.runtimePart.props,
// Soma overrides — boolean ARIA state the morfo expresses as a 'true'
// string / present-absent; this provider emits the raw boolean, and
// `aria-level` is the computed depth+1 (morfo carries raw depth).

@ -165,8 +165,8 @@ export class WaveformWaveProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// aria-hidden (literal) + data-shape are morfo-declared, resolved by
// renderProps against the component-level `shape` source.
...this.runtimePart.renderProps()
// the render bag against the component-level `shape` source.
...this.runtimePart.props
} as const)
);
}

@ -102,7 +102,7 @@ export class YearGridProvider {
owner: this,
context: YearGridProvider.ctx,
// Source for the morfo's `aria-disabled` (propRef('disabled')) so
// renderProps() resolves it instead of the provider re-declaring it.
// the render bag resolves it instead of the provider re-declaring it.
props: { disabled: () => opts.disabled.current }
});
// Initial focused year follows selection or placeholder.
@ -284,8 +284,8 @@ export class YearGridProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role + aria-disabled are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// role + aria-disabled are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
// Computed accessible name (ariaLabel + heading) — overrides the morfo's
// raw `ariaLabel` propRef (which the provider doesn't source).
'aria-label': this.fullLabel,
@ -376,8 +376,8 @@ abstract class YearGridPaginationButtonProvider {
const disabled = this.disabled();
return this.runtimePart.assert({
// type, aria-label (commonRef year-grid.prev/next-page) and aria-disabled
// are morfo-declared, resolved via renderProps.
...this.runtimePart.renderProps(),
// are morfo-declared, resolved via the render bag.
...this.runtimePart.props,
'data-disabled': boolToEmptyStrOrUndef(disabled),
disabled,
onclick: this.onclick
@ -440,7 +440,7 @@ export class YearGridGridProvider {
ref: opts.ref,
owner: this,
// Sources for the morfo's `aria-readonly`/`aria-disabled`
// (propRef('readonly')/propRef('disabled')) so renderProps resolves them.
// (propRef('readonly')/propRef('disabled')) so the render bag resolves them.
props: {
readonly: () => this.provider.opts.readonly.current,
disabled: () => this.provider.opts.disabled.current
@ -452,8 +452,8 @@ export class YearGridGridProvider {
const headingId = this.opts.labelledBy.current ?? this.provider.headingId.current;
return this.runtimePart.assert({
// role, aria-readonly and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Computed: honours the `labelledBy` prop override + the registered
// heading id (overrides the morfo's raw partRef('heading')).
'aria-labelledby': headingId || undefined
@ -527,8 +527,8 @@ export class YearGridCellProvider {
const tabindex = this.focused && !this.disabled ? 0 : -1;
return this.runtimePart.assert({
// role, aria-selected and aria-disabled are morfo-declared, resolved
// via renderProps.
...this.runtimePart.renderProps(),
// via the render bag.
...this.runtimePart.props,
// Native button type the morfo doesn't model.
type: 'button' as const,
tabindex,

@ -233,17 +233,17 @@ export interface SomaRuntimePartOpts<Owner = unknown> extends SomaRuntimePartBas
*/
export interface SomaRuntimePart<M extends Morfo, K extends PartKebabOf<M>> {
readonly attachment: RefAttachment | undefined;
/** Static render identity: id, marker attr, archetype and ref attachment. */
/**
* The render bag (P0 fase C, audit 2026-08-26): static identity (id,
* marker attr, archetype, ref attachment, dir) plus every morfo-declared
* attr resolved against THIS part's registered sources. Spread it in a
* part's `props` getter, then add only the soma-specific extras (handlers,
* formatted overrides, native attrs). The single attr pipeline — it
* absorbed the old `the render bag`.
*/
readonly props: Record<string, unknown> & { readonly dir?: Direction };
/** Resolve morfo static/dynamic attrs for render-time legacy/manual props. */
resolveProps(bindings?: MorfoBindings): Record<string, unknown>;
/**
* Static identity + every morfo-declared attr resolved against THIS part's
* registered sources. Spread this in a part's `props` getter, then add only
* the soma-specific extras (handlers, formatted overrides, native attrs).
* The canonical alternative to hardcoding role/aria/data in the provider.
*/
renderProps(): Record<string, unknown>;
/** Validate an authored prop bag against the morfo data contract. */
assert<P extends Record<string, unknown>>(props: P): P;
/**
@ -688,15 +688,6 @@ export function createSomaRuntime<M extends Morfo>(
}
return props;
},
renderProps() {
// Since the fase-C unification (P0, audit 2026-08-26) the render
// bag IS the full contract — `partPropsForRegistration` resolves
// static identity + every morfo-declared static/dynamic attr
// against THIS part's registered sources. `renderProps()` and
// `props` are now the same computation; this name survives until
// the ~70 call sites migrate to `props` (fase C2b), then it goes.
return partPropsForRegistration(partName, reg);
},
assert<P extends Record<string, unknown>>(props: P): P {
assertContract(compiled.kebab, partName, props, sources.logger);
return props;
@ -738,7 +729,7 @@ export function createSomaRuntime<M extends Morfo>(
if (reg.attachment) Object.assign(props, reg.attachment);
// The `dir` stamp. Ships here — the one bag BOTH `props` and
// `renderProps()` include — because it was the last thing every provider
// `the render bag` include — because it was the last thing every provider
// still wrote by hand: 49 copies of the same line, and 20 of 55
// components had simply forgotten it. The morfo DECLARES the direction
// (and which parts carry it); the type computed from that declaration

@ -5,7 +5,7 @@ import { loadMorfos, auditComponent } from '../../../scripts/soma-attr-audit';
/**
* Regression guard for the morfo↔soma attr de-duplication migration.
*
* A migrated provider sources its role/aria/data from `runtimePart.renderProps()`
* A migrated provider sources its role/aria/data from `runtimePart.props`
* and must not re-declare them. The only hits that may remain are LEGITIMATE
* value overrides the morfo can't express (a locale-formatted `aria-valuetext`,
* a parsed `aria-valuenow`). This test pins each migrated component to its
@ -43,7 +43,7 @@ const MIGRATED_CEILINGS: Record<string, number> = {
// Batch 2 (workflow-migrated, centrally verified — tests + check green).
// date-field/time-field keep the `segment` part hardcoded on purpose: it is
// multi-archetype (editable segments are spinbuttons, literal separators must
// NOT be) so a single renderProps would inject role into the literals. The
// NOT be) so a single render bag would inject role into the literals. The
// `input` composites (aria-labelledby/describedby join) and the computed
// aria-labels (color-picker/calendar — include value/month) are legit overrides.
field: 1, // input: composite aria-describedby
@ -76,7 +76,7 @@ const MIGRATED_CEILINGS: Record<string, number> = {
combobox: 0,
command: 4, // aria-activedescendant/controls (dynamic) + computed labels
// Menu siblings — aria-expanded/aria-checked come from the morfo's stateRef
// (aria-boolean string) via renderProps; tests assert the string. 0 residue.
// (aria-boolean string) via the render bag; tests assert the string. 0 residue.
'dropdown-menu': 0,
'context-menu': 0,
// F3 census pins (SOM-4, clean-room 2026-07-10 → verified 2026-07-11):
@ -85,7 +85,7 @@ const MIGRATED_CEILINGS: Record<string, number> = {
'mask-field': 1, // input aria-label — computed, yields to Field.Label's labelledby
timeline: 2, // provider aria-label (prop-or-localized) + item aria-current (derived from root `active`)
// Batch 4 (SOM-4 close, 2026-07-12): media-player fully migrated. Every button
// sources its label (propRef) + on-state (stateRef) so renderProps emits
// sources its label (propRef) + on-state (stateRef) so the render bag emits
// type/role/aria-label/aria-keyshortcuts/aria-pressed/data-state; media/poster/
// buffering/time resolve their data-*/aria-hidden the same way. 0 residue —
// contract-only attrs (play-button data-state, *-disabled presence, slider

Loading…
Cancel
Save

Powered by TurnKey Linux.