<!-- Generated by npm run gen:skill from tools/skill/extend/component.md - do not edit. -->

# A custom component

In `<application>` after `<about>`; hrefs relative to the app file:

```xml
<components>
  <component name="Stars" tag="stars" href="ui/CA_Stars.js" module="true">
    <style href="ui/stars.css"/>
    <skin href="ui/stars.xsl"/>
  </component>
</components>
```

```js
export default class CA_Stars extends Component {
  knownAttrs() { return ["value", "enabled"]; }
  init() {
    this.dom = this.getElement();
    this.warnUnknownAttrs();
    this.attachEvent(this.dom, "keydown", (e) => this.onKey(e));
    this.value = Number(this.ui.getAttribute("value")) || 0;
    this[this.ui.getAttribute("enabled") === "false" ? "disable" : "enable"]();
    this.render();
    for( const k in this.state ) this.setProperty(k, this.state[k]);
  }
  enable() { this.enabled = true; this.dom.tabIndex = 0; this.dom.removeAttribute("aria-disabled"); }
  disable() { this.enabled = false; this.dom.tabIndex = -1; this.dom.setAttribute("aria-disabled", "true"); }
  getValue() { return String(this.value); }
  setValue(v) { this.value = Number(v) || 0; this.render(); }
  update(evt) {
    if( this.xlink && this.isForeignUpdate(evt) ) this.setValue(this.xlink.selectSingleText(this.xpath));
  }
  commit(v, winEvt) {
    if( !this.enabled || v === this.value ) return;
    this.setValue(v);
    if( this.xlink ) this.xlink.replaceText(String(v), this.xpath, "", this);
    this.setProperty("value", String(v), winEvt, true, true);
    this.fireEvent("Change", winEvt);
  }
}
```

- `Change` for a value the user changed (as input), `Select` for an activation. A second same-type event in one tick is dropped.
- Keys on its own element; `preventDefault()`, `stopPropagation()`.

## Skin and CSS

In an `<xsl:stylesheet version="1.0">`:

```xml
<xsl:template match="stars">
  <div id="{@name}" class="xios-stars {@class}" role="slider" tabindex="0">
    <xsl:if test="@label != ''"><xsl:attribute name="aria-labelledby"><xsl:value-of select="@name"/>!label</xsl:attribute></xsl:if>
    <xsl:attribute name="style"><xsl:call-template name="styleDimensions"/><xsl:value-of select="@style"/></xsl:attribute>
  </div>
</xsl:template>
```

- A role and its state (`aria-valuenow`); `tabindex` 0, -1 when disabled. The parent panel draws `label=` (id `{@name}!label`); without it, `aria-label` from `@title`.
- CSS: layout outside `:where()`, paint in `:where()`; colors `--xios-color-primary`, `-on-primary`, `-surface`, `-on-surface`, knobs `--xc-stars-*`; `:focus-visible`; targets 24px+. Draw inside its box: no `overflow: visible`, no fixed size.

## Test

Start the app from its file (not `sys.startView`); `view.comps.<name>`; events in `view.doc.defaultView`; Change via a trigger.

## In xios-core

`ui/<Dir>/`; entry in `config/system.xml` and `config.xml` (`xios://core/ui/<Dir>/...`, no `tier`); eslint's module list; new-format `doc.xml` with a `<code play="inline">`; `tests/specimen.xml`, `tests/ui/<tag>.xml`; `npm run gen:view-xsd`.
