<!-- GENERATED by scripts/build-llms.mjs from llms/forms.md — do not edit this file. -->

# `lr-rubric-form`

- **Import** `import '@aceshooting/lyra-ui/components/lr-rubric-form.js';` (stable tag alias; registers the tag)
- **Class** `LyraRubricForm`, also available unregistered from `@aceshooting/lyra-ui/components/forms/rubric-form/rubric-form.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 20 parts, 15 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-rubric-form`

A configurable annotation rubric (LangSmith annotation-queue style): score, category, and
freeform-comment keys with a submit-and-next flow for working through an eval queue. Each
`RubricKey.type` routes to an existing sibling control: `score` renders `<lr-segmented>` or
`<lr-slider>`; `category` renders `<lr-select>` or `<lr-checkbox-group>` (`multiple`); `comment`
renders `<lr-textarea>`. The `base` part is an accessible `role="group"`: aggregate label, hint,
and error chrome is linked to that same-shadow role. A host `aria-label` wins by attribute presence
(including an explicitly empty value), while each key retains its field-level name.
Both score branches format visible numeric labels with the effective locale (including non-Latin
digits); segmented item values and submitted rubric values remain stable raw numbers/strings.

Host `aria-describedby` references resolve in the host's root and precede local hint/error guidance
on that aggregate group. Target replacement, removal, reinsertion, reconnection and document
adoption keep the relationship current. Aggregate guidance is not copied to every child field; each
retains its own description/error ownership. Removing `label` or `hint` safely removes the copy
while retaining native attribute-removal property readback, including `null`; explicit empty and
later replacement strings remain supported.

Replacing `value` after a multiple-category user edit reconciles the actual checkbox checked states,
group value and submitted JSON, including when replacement happens in the same task. These parent
writes use live child state, leave child reset-default/dirty semantics intact, and emit no user-edit
events.

**Properties:** `keys: readonly RubricKey[] = []` (attribute: false), where the exported immutable
discriminated union is `ScoreRubricKey | CategoryRubricKey | CommentRubricKey`. Keys use nonblank
first-wins identity and retained valid spelling is not rewritten. Shared fields are
`key`, `label?`, `description?`, and `required?`; only scores expose `min?`/`max?`/`step?`, only
categories expose readonly `RubricKeyOption[]` plus `multiple?`, and only comments expose
`placeholder?`. Runtime schema normalization retains the first occurrence of each nonempty key and
rejects malformed rows. `value: RubricValue = {}` is a defensive readonly snapshot and
`defaultValue: RubricValue = {}` is its explicit form-reset baseline (both attribute: false).
`itemId: string = ''`
(attribute `item-id`, reflected), `hasNext: boolean = false` (attribute `has-next`), `skippable:
boolean = false`, aggregate `label: string = ''`, `hint: string = ''`, `errorText: string = ''` (attribute
`error-text`), SSR presence hints `withLabel: boolean = false` / `withHint: boolean = false`
(attributes `with-label` / `with-hint`), and the shared form properties `name` and `disabled`.
`errors: Readonly<Record<string, string>>` is a frozen effective validation-message snapshot:
intrinsic messages use their rubric key and a consumer-owned whole-form rejection uses `base`.
`customError: string | null` reflects through `custom-error` for that rejection.

Every value path — direct writes, child edits, schema changes, state restoration, reset, events,
rendering, validity, and FormData — uses one canonical object. Finite scores clamp and snap to the
current range/step; a wide slider score without an explicit default records its rendered, snapped
midpoint in both `defaultValue` and the pristine live value, while segmented scores remain
unselected. Nonfinite scores are absent; categories retain only current option values (and their
available occurrence counts); comments must be strings; undeclared keys are dropped. This prevents
a rendered value, public readout, validity result, and submitted JSON from disagreeing.
`form.reset()` restores a fresh clone of `defaultValue`, clears touched/error-reveal state, and
preserves any consumer `setCustomValidity()` message. Changing `defaultValue` updates a pristine
live value but never overwrites a dirty edit.

**Slots:** `label` — aggregate rubric label before the fields; `hint` — aggregate supporting text;
`error` — aggregate validation content; `actions` —
extra host controls rendered in the footer beside Submit/Skip.

**Events:** `lr-input` (`detail: { value }`), `lr-validity-change` (frozen
`detail: { valid, errors }`, deduplicated on effective native validity including consumer custom
errors and own/fieldset validation barring), `lr-submit` (`detail: { value, itemId }`), and `lr-skip`
(`detail: { itemId }`, `skippable` only). `lr-invalid` (no detail) is the one bubbling/composed,
cancelable alias emitted when the complete rubric fails a native validity check; preventing it also
suppresses the native event's default validation UI.
Native and prefixed input/change events from the composed child controls are contained; consumers
above the rubric receive only the rubric-owned aggregate event shape.

**Methods:** `getForm()` returns the owning form. `setCustomValidity(message)` sets or clears a
form-level error no per-key rule can
express ("this item was already annotated by someone else"): a non-empty message raises
`customError` and blocks submission, `''` restores the rubric's own computed validity — unanswered
required keys, and any key with an unsupported `type`, still hold it invalid. It is whole-control
state exposed as `errors.base`, rather than being attributed to one rubric key. It survives every
`value`/`keys` write and a form reset. When
`errorText` is empty, the current custom-validity message is rendered in the aggregate error region;
clearing it hides that region unless the `error` slot supplies other content.
`click()` forwards to the active field (the same one a submit-and-next transition auto-focuses),
so the host behaves like a single control under both a `<label>`-driven and a programmatic click.

**CSS parts:** `base` (the outer `role="group"` wrapper), `form-control` (aggregate chrome wrapper),
`aggregate-label` / `form-control-label`, `fields` / `form-control-input`, `aggregate-hint` /
`form-control-help-text`, `aggregate-error` / `form-control-error`, `field` (one key's wrapper),
`label`, `description`, `scale` (the
rendered score/category/comment control's wrapper), `error` (a field-level validation message),
`footer`, `submit`, `skip` (only rendered when `skippable`), `empty` (shown when `keys` has no
entries), and `unsupported` (the fallback note for a key whose `type` is outside the three supported
ones).

Field-level `error` content is ordinary visible validation text, not a shadow live region. Score
controls compose the current message into the semantic control's accessible name; category/comment
controls use their own same-shadow label/error plumbing. `reportValidity()` therefore reveals and
focuses the error once without an additional `role="alert"` announcement.

**The required marker.** Required score and unsupported fields own their visible label, so the
shared marker is rendered there. Required category and comment fields retain the marker from their
own `lr-select`/`lr-checkbox-group` or `lr-textarea` label instead; the rubric never adds a second
glyph. `--lr-form-control-required-content` (a quoted CSS `content` string; `''` suppresses it),
`--lr-form-control-required-color` (default `var(--lr-color-danger)`), and
`--lr-form-control-required-offset` (default `0`) retune the same marker across every required
field.

**Themeable custom properties:** Submit rest uses `--lr-rubric-form-submit-bg`,
`--lr-rubric-form-submit-border-color`, and `--lr-rubric-form-submit-color`; its hover and pressed
paint use `--lr-rubric-form-submit-hover-bg`, `--lr-rubric-form-submit-hover-border-color`,
`--lr-rubric-form-submit-active-bg`, and `--lr-rubric-form-submit-active-border-color`. Skip rest uses
`--lr-rubric-form-skip-bg`, `--lr-rubric-form-skip-border-color`, and
`--lr-rubric-form-skip-color`, with `--lr-rubric-form-skip-hover-bg` and
`--lr-rubric-form-skip-active-bg` for its pointer states. All preserve the existing shared-token
and color-mix treatments as fallbacks. Disabled actions still dim through `--lr-opacity-disabled`,
the same library-wide token every other disabled control reads.
