# LO-TEK in templates and server-written HTML

The rulebook for LO-TEK where the server produces the markup: Go templates,
Razor, PHP, Blade, Twig, Jinja, Django, ERB, Liquid, Handlebars, Nunjucks and
Edge, and HTML built as strings in Kotlin, Java, C# or Python.

This is the environment LO-TEK is made for. The components are CSS classes and
web components, that is, things the browser understands directly, so a Go
template gets the same components as a React app without taking in any
JavaScript framework.

It is also the environment with the smallest safety net. No compiler looks at
the attributes in a template, and no type stops `data-variant="outlined"`.
That is why item 5 applies more strictly here than anywhere else: the check is
the only thing that reads your markup.

This is @lo-tek/interfaces 0.36.0. The file is generated by the package and
follows its version, so it can never say anything other than the code next to
it.

## Short version

1. **One `<link>` to `lotek.css` in the page template,** once for the whole
   app. Without it, the components look unstyled. The answer is then to add
   the link, never to write your own CSS to make them look right.
2. **Use only the classes and elements in the tables below.** `lt-modal`,
   `lt-datepicker` and `data-variant="outlined"` exist in other design
   systems, not in LO-TEK. If you are unsure whether something exists, it is
   listed here or it does not exist.
3. **No hard-coded colours or pixels.** `var(--lt-color-…)` and
   `var(--lt-spacing-…)`.
4. **Web components are registered once with `defineLt*()`** in the page
   template, and a boolean attribute is true as long as it is there. In a
   template that means a condition around the whole attribute,
   `{{if .Locked}}disabled{{end}}`, not `disabled="{{.Locked}}"`.
5. **Run the check on what you have written:**
   `npx @lo-tek/interfaces check templates/*.html`. It knows every class,
   every element, every attribute and every allowed value, prints
   `file:line:column: error: message`, or `warning:`, and exits with an error
   code if it finds anything. With the app running, also check every page you
   have changed: `npx @lo-tek/interfaces check http://localhost:8080/page`.
   That also requires every `for` and `aria-describedby` to point to an id
   that exists, and every field to have a label. You are done when both answer
   "The markup matches LO-TEK".

## 1. The stylesheet

One link in the page template, with all the components:

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css">
```

Or from your own server, if you copy the file into your static assets.
`lotek.css` is 103 kB, 13 kB compressed, has everything flattened without
`@import`, and is the fastest option when you link.

If you want only the stylesheets the pages actually use, `tokens.css` defines
all the variables, and all the other stylesheets build on it. It must be
loaded, and comes first out of habit: the order between it and the others does
not matter. After that, one stylesheet per component, from the tables below.
Without a bundler that means more round trips, and one forgotten link is
enough for something to look unstyled. For a template, one file is nearly
always the right choice.

## 2. What exists

35 CSS components and 9 web components, and this is the whole list. The
classes are `lt-` + kebab-case. Variants are always `data-*` attributes, never
classes of their own: `data-variant="secondary"`, not `lt-button--secondary`.
The default variant has no attribute.

### CSS components (no JavaScript)

| Class | Stylesheet | Attributes |
| --- | --- | --- |
| `lt-accordion`<br>`lt-accordion__content` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/accordion/accordion.css` | `data-variant`: plain |
| `lt-alert`<br>`lt-alert__title` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/alert/alert.css` | `data-color`: info, success, warning, danger |
| `lt-avatar`<br>`lt-avatar-stack` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/avatar/avatar.css` | `data-variant`: square<br>`data-size`: small, large |
| `lt-badge` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/badge/badge.css` | `data-color`: success, warning, danger, neutral |
| `lt-breadcrumbs` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/breadcrumbs/breadcrumbs.css` | none |
| `lt-button` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/button/button.css` | `data-variant`: secondary, ghost, danger |
| `lt-card`<br>`lt-card__title` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/card/card.css` | `data-variant`: filled<br>`data-interactive` (flag) |
| `lt-checkbox`<br>`lt-checkbox-row` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/checkbox/checkbox.css` | `data-state`: invalid, success |
| `lt-divider` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/divider/divider.css` | `data-variant`: subtle, strong |
| `lt-error-text` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/error-text/error-text.css` | `data-variant`: warning |
| `lt-fieldset`<br>`lt-legend` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/fieldset/fieldset.css` | `data-state`: invalid, success<br>`data-required`: symbol, text<br>`data-optional` (flag) |
| `lt-file-upload`<br>`lt-file-upload-list` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/file-upload/file-upload.css` | `data-state`: invalid, success |
| `lt-heading` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/heading/heading.css` | `data-size`: xs, s, m, xl, mega |
| `lt-help-text` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/help-text/help-text.css` | `data-variant`: strong, success, warning |
| `lt-icon` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/icon/icon.css` | none |
| `lt-input` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/input/input.css` | `data-state`: invalid, success<br>`data-variant`: date, datetime-local, time |
| `lt-label` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/label/label.css` | `data-required`: symbol, text<br>`data-optional` (flag) |
| `lt-link` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/link/link.css` | none |
| `lt-list` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/list/list.css` | `data-variant`: plain, divided |
| `lt-pagination`<br>`lt-pagination__gap` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/pagination/pagination.css` | none |
| `lt-paragraph` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/paragraph/paragraph.css` | `data-variant`: lead<br>`data-size`: small, large |
| `lt-progress` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/progress/progress.css` | `data-color`: success, warning, danger |
| `lt-radio`<br>`lt-radio-row` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/radio/radio.css` | `data-state`: invalid, success |
| `lt-search`<br>`lt-search-row` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/search/search.css` | `data-state`: invalid, success |
| `lt-select` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/select/select.css` | `data-state`: invalid, success<br>`data-picker`: styled |
| `lt-skeleton` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/skeleton/skeleton.css` | `data-variant`: text, circle |
| `lt-skip-link` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/skip-link/skip-link.css` | none |
| `lt-spinner` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/spinner/spinner.css` | `data-size`: small, large |
| `lt-sr-only` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/sr-only/sr-only.css` | none |
| `lt-switch`<br>`lt-switch-row` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/switch/switch.css` | none |
| `lt-table`<br>`lt-table__sort`<br>`lt-table-scroll` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/table/table.css` | `data-variant`: striped<br>`data-hoverable` (flag) |
| `lt-tag` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/tag/tag.css` | `data-variant`: filled<br>`data-selectable` (flag) |
| `lt-textarea` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/textarea/textarea.css` | `data-state`: invalid, success |
| `lt-toggle-group`<br>`lt-toggle-group__option` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/toggle-group/toggle-group.css` | none |
| `lt-tooltip`<br>`lt-tooltip__bubble` | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/tooltip/tooltip.css` | none |

There is also `lt-theme-control`, a class on a radio button that lets the user
choose a theme. It is in `tokens.css`, and is described under Tokens.

## 3. Tokens

One layer. A colour is a cell in a matrix of **family**, that is, what it
means, and **role**, that is, what it does, and the name is
`--lt-color-<family>-<role>`. Every family has every role, so
`--lt-color-danger-border` and `--lt-color-success-border` both exist. The
roles are the same whatever the family: `surface` is a tinted surface, `fill`
a filled one, `content` the text on top of `fill`, `border` a border, and
`text` the family's colour as text. Border and text each have a weaker and a
stronger step next to them.

```
Families: `accent`, `visited`, `brand1`, `brand2`, `brand3`, `neutral`, `danger`, `warning`, `success`.

Roles: `surface`, `border-subtle`, `border`, `border-strong`, `fill`, `text`, `text-strong`, `text-subtle`, `content`.

Outside the matrix: `--lt-color-disabled-surface`, `--lt-color-disabled-text`, `--lt-color-neutral-canvas`, `--lt-color-neutral-raised`, `--lt-color-overlay`, `--lt-focus-ring`, `--lt-icon-calendar`, `--lt-icon-check`, `--lt-icon-clock`, `--lt-icon-dash`, `--lt-icon-search`, `--lt-shadow-overlay`.
```

Spacing: `--lt-spacing-0-5`, `--lt-spacing-1`, `--lt-spacing-10`,
`--lt-spacing-12`, `--lt-spacing-16`, `--lt-spacing-2`, `--lt-spacing-3`,
`--lt-spacing-4`, `--lt-spacing-5`, `--lt-spacing-6`, `--lt-spacing-7`,
`--lt-spacing-8`, `--lt-spacing-px`.

Font: `--lt-font-size-l`, `--lt-font-size-m`, `--lt-font-size-mega`,
`--lt-font-size-reference`, `--lt-font-size-s`, `--lt-font-size-xl`,
`--lt-font-size-xs`, `--lt-font-size-xxl`, `--lt-font-size-xxs`,
`--lt-font-weight-bold`, `--lt-font-weight-medium`,
`--lt-font-weight-regular`, `--lt-font-weight-semibold`.

Line height: `--lt-line-height-article`, `--lt-line-height-compact`,
`--lt-line-height-default`, `--lt-line-height-heading`.

`disabled` and `neutral` are not the same. `disabled` is for controls that are
turned off, and is exempt from the contrast requirement in WCAG 1.4.3.
`neutral` is for subdued information the user actually has to read or press,
and holds 4.5:1. Never use the `disabled` colours to subdue something that is
meant to be read.

### Light and dark

Without you doing anything, the colours follow the device's setting. A page
that wants to decide for itself sets `data-theme="light"` or
`data-theme="dark"` on `<html>`.

The attribute is a **theme boundary** and works on any element, not only on
the root. One theme can sit inside another, both ways.

This is what an embedded component should use. When you put LO-TEK into a page
you do not control, set the attribute on the component's own root element:

```html
<div data-theme="light">
  <button class="lt-button">Save</button>
</div>
```

The component is then light whatever the device is set to, and the host is
left alone.

**The host's CSS stops at the boundary.** The host often has rules without a
layer, such as `button { background: none; border: none }` or a normalize, and
they beat `@layer lotek`. In that case, set `data-lt-boundary` on the same
root element:

```html
<div data-lt-boundary data-theme="light">
  <button class="lt-button">Save</button>
</div>
```

Below the root element the browser ignores CSS without a layer, within the
specificity below, and LO-TEK's layer applies again. The rule is in
`lotek.css` and in `@lo-tek/interfaces/boundary.css`. Fonts and other
inherited properties still come from the host, and the `--lt-*` variables
work. Three things follow from it:

- Your own CSS inside the boundary must be in a layer after `lotek`. CSS
  without a layer disappears there in the same way as the host's.
- Elements without a LO-TEK class lose the host's CSS without a layer. A layer
  the host has declared before `lotek`, such as Tailwind's Preflight in
  `base`, still applies where LO-TEK sets nothing.
- The boundary has the specificity (0,2,0). A rule from the host with the same
  specificity wins if it is loaded after the boundary, and one with more
  always wins. So do `!important` and layers the host has declared after
  `lotek`.

LO-TEK does **not** set `color-scheme` on `:root`. The property controls the
browser's own surfaces, that is, scrollbars, dropdowns and calendar panels,
and it is inherited. A child can opt out with `color-scheme: normal`, but that
is a counter-rule the host never asked to have to write: with the value on the
root, it applied to the whole document, including where the package is only a
guest. If the whole page should follow the system, write
`color-scheme: light dark` on `<html>` yourself, in the same way as you set
the layer order yourself. The exception is the theme picker below: when you
use it, each option sets `color-scheme` itself.

### Letting the user choose a theme

`lt-theme-control` on a radio button makes `value` a theme choice. It is plain
CSS, without a line of JavaScript:

```html
<fieldset class="lt-toggle-group">
  <legend class="lt-sr-only">Theme</legend>
  <label class="lt-toggle-group__option">
    <input class="lt-theme-control" type="radio" name="theme"
           value="auto" checked /> Follow the system
  </label>
  <label class="lt-toggle-group__option">
    <input class="lt-theme-control" type="radio" name="theme"
           value="light" /> Light
  </label>
  <label class="lt-toggle-group__option">
    <input class="lt-theme-control" type="radio" name="theme"
           value="dark" /> Dark
  </label>
</fieldset>
```

The choice controls two things. LO-TEK's colours come from the tokens, and the
browser's **own** surfaces from `color-scheme`: `light` gives `light`, `dark`
gives `dark`, and `auto` gives `light dark`. `light dark` is not a theme, but
the statement that the page works in both, so the browser can choose by the
system.

"Follow the system" has no theme block, and that is on purpose: a value
without a block matches nothing, and then the media query applies again.

Do not write `color-scheme` yourself when the picker is in use. Such a rule
outside a layer beats `@layer lotek` and locks the browser's surfaces to the
system while the user has chosen something else.

`light` and `dark` win over `data-theme` on `<html>`, so the server can send
the stored choice while a click still takes effect before the response is
back. `auto` does not: it means "no override from me", so if the server has
written `data-theme`, the server's value stands. For "follow the system" to
work at once, whatever stores the choice must also remove the attribute.

Two things it does not do. It **stores nothing**: submit the group in a form
and store the choice in a cookie the server reads, or in `localStorage`. And
it must be in the **same tree as `<html>`**, since `:has()` does not see out
of its own tree; a control inside a shadow root does not set the theme on the
page.

## 4. Web components

Three rules apply to all of them:

1. **`defineLt*()` runs once** in the page template, not once per component.
   The import has no side effect on its own; it is the `define` call that
   registers the element.
2. **Boolean attributes are true as long as they are present.**
   `hidden="false"`, `disabled="false"` and `open="false"` turn it *on*. To
   turn any of them off, remove the attribute entirely.
3. **You write the markup, the component adds the behaviour.** `<lt-field>`
   does not create your label or your control. You write `for` on the label
   and `id` on the control, or wrap the control in a `<label>`, so the field
   has a name without JavaScript too. The component wires the help text and
   the error text with `aria-describedby` and `aria-invalid`, and never writes
   an id on the control: an id the server did not send makes an update from
   the server replace the field, with the focus and what the user has typed.
   The field is invalid when the error text is there without `hidden`.

| Element | Category | Stylesheet | Registration | Attributes | Events | Classes inside |
| --- | --- | --- | --- | --- | --- | --- |
| `<lt-field>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/field/field.css` | `defineLtField()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/field/lt-field.js` | `disabled` (flag)<br>`optional` (flag)<br>`described-by` (text) | none | none |
| `<lt-tabs>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/tabs/tabs.css` | `defineLtTabs()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/tabs/lt-tabs.js` | `server-controlled` (flag)<br>`activation`: manual, automatic | `tab-select` (index) | `lt-tabs__list`<br>`lt-tabs__panel` |
| `<lt-error-summary>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/error-summary/error-summary.css` | `defineLtErrorSummary()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/error-summary/lt-error-summary.js` | `data-autofocus`: false, true<br>`hidden` (flag) | none | `lt-error-summary`<br>`lt-error-summary__title` |
| `<lt-popover>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/popover/popover.css` | `defineLtPopover()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/popover/lt-popover.js` | `open` (flag)<br>`placement`: bottom-start, bottom-end, top-start, top-end<br>`server-controlled` (flag) | `popover-toggle` (open) | `lt-popover` |
| `<lt-suggestion>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/suggestion/suggestion.css` | `defineLtSuggestion()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/suggestion/lt-suggestion.js` | `prefiltered` (flag)<br>`count-none` (text)<br>`count-zero` (text)<br>`count-one` (text)<br>`count-two` (text)<br>`count-few` (text)<br>`count-many` (text)<br>`count-other` (text)<br>`server-controlled` (flag) | `suggestion-select` (value, label) | `lt-suggestion__list`<br>`lt-suggestion__option`<br>`lt-suggestion__value`<br>`lt-suggestion__empty` |
| `<lt-dialog>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/dialog/dialog.css` | `defineLtDialog()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/dialog/lt-dialog.js` | `open` (flag)<br>`server-controlled` (flag) | `dialog-toggle` (open, returnValue) | `lt-dialog` (`data-color`: brand, info, success, warning, danger)<br>`lt-dialog__body`<br>`lt-dialog__title`<br>`lt-dialog__footer`<br>`lt-dialog__header`<br>`lt-dialog__subtitle` |
| `<lt-toast>` | standalone | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/toast/toast.css` | `defineLtToast()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/toast/lt-toast.js` | `duration` (number)<br>`label` (text)<br>`close-label` (text) | `toast-dismiss` | `lt-toast`<br>`lt-toast__message`<br>`lt-toast__close` |
| `<lt-session-timeout>` | frame | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/frame/session-timeout/session-timeout.css` | `defineLtSessionTimeout()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/session-timeout/lt-session-timeout.js` | `warn-at` (number)<br>`expires-at` (number)<br>`activity-interval` (number) | `session-warn`<br>`session-extend`<br>`session-logout`<br>`session-expired`<br>`session-activity` | `lt-session-timeout`<br>`lt-session-timeout__dialog`<br>`lt-session-timeout__title`<br>`lt-session-timeout__text`<br>`lt-session-timeout__count`<br>`lt-session-timeout__actions` |
| `<lt-connection-status>` | standalone | `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/standalone/connection-status/connection-status.css` | `defineLtConnectionStatus()` from `https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/standalone/connection-status/lt-connection-status.js` | `offline-text` (text)<br>`online-text` (text) | `connection-lost`<br>`connection-restored` | `lt-connection-status`<br>`lt-connection-status__bar` |

None of them uses shadow DOM. The content is in plain DOM, so `querySelector`,
`FormData` and ordinary CSS work straight into it.

## 5. A field in a template

```html
<lt-field>
  <label class="lt-label" for="name" data-required="symbol">Full name</label>
  <input class="lt-input" id="name" type="text" name="name" value="{{.Name}}" required />
  {{if .Error}}<p class="lt-error-text">{{.Error}}</p>{{end}}
</lt-field>
```

The error text is rendered only when the field is invalid, and that is enough:
the component counts the field as invalid when the error text is visible.
`value` is always written, so what the user typed is still there when the
server sends the form back.

In a loop the id comes from a key in the data, never from the index or a
counter:

```html
{{range .Municipalities}}
<lt-field>
  <label for="note-{{.Code}}">{{.Name}}</label>
  <input class="lt-input" id="note-{{.Code}}" name="note-{{.Code}}" value="{{.Note}}" />
</lt-field>
{{end}}
```

The registration, once in the page template:

```html
<script type="module">
  import { defineLtField } from
    "https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/components/frame/field/lt-field.js"

  defineLtField()
</script>
```

## 6. When the server sends new HTML

If you patch the page as it runs, with htmx, Turbo, Datastar or your own
`innerHTML` update, the question is what decides the state. By default the web
component does: `<lt-tabs>` remembers the tab the user selected, and puts it
back when a patch tears the selection away. The template needs nothing for
that.

This assumes the patch keeps the elements and only changes the attributes, as
a morph does: Datastar's `outer`, htmx with the idiomorph extension and Turbo
8 with morphing. The selection is tied to the link itself. If the patch
replaces the nodes, as `innerHTML`, htmx's ordinary `innerHTML` and
`outerHTML` swaps and Turbo without morphing do, the tab is a different one,
and the component selects the one the URL points to, otherwise the one the
server hid the other panels for, otherwise the first. If the server wants to
decide, it writes `hidden="until-found"` on the other panels, or decides the
state alone with `server-controlled`.

A morph keeps an element only when the id matches: Datastar and idiomorph do
not morph an element with an id into one without, they replace it. The
components therefore never write an id on what the user touches: the field in
`<lt-field>` and `<lt-suggestion>`, and the tabs in `<lt-tabs>`. Write `for`
on the label and `id` on the field, or wrap the field in the label. The tabs
are links without an id, and the panels have the id the link points to, the
same on every render. The panel in `<lt-popover>` needs an id in the same way,
for `aria-controls` on the button; the component creates none. The list and
the options in `<lt-suggestion>` can be replaced; the component wires them
again and finds the highlighted option again by its text. The component warns
in the console the first time a patch replaces something the user had touched.

If the server should be able to move the state, as in "go on to step 2", say
so with `server-controlled`. Then every patch decides, and the component stops
putting the user's choice back:

```html
<lt-tabs server-controlled>
  <ul class="lt-tabs__list" aria-label="Parts of the case">
    <li><a href="#application">Application</a></li>
    <li><a href="#attachments">Attachments</a></li>
  </ul>
  <section class="lt-tabs__panel" id="application">…</section>
  <section class="lt-tabs__panel" id="attachments" hidden="until-found">…</section>
</lt-tabs>
```

The attribute exists on `<lt-tabs>`, `<lt-popover>`, `<lt-dialog>` and
`<lt-suggestion>`. Without it the server and the component would fight over
the same state. Note that the state is always in the markup the server sent,
that is, `aria-selected` on the tab and `hidden` on the panel, not in an
attribute of its own.

## 7. The check is the only thing that reads your markup

```bash
npx @lo-tek/interfaces check templates/*.html
```

One finding gives an error code, so it belongs in CI next to the tests. It
catches `lt-buton`, `data-variant="secundary"`, `<lt-modal>` and `<lt-field>`
without a control or a label.

It is made for templates. Comments, `<script>` and `<style>` are skipped, and
`<?…?>` and `<%…%>` inside a tag do not end it early. These are tested and
give no false findings:

```html
<button class="lt-button {{.Extra}}">Send</button>
<button class="lt-button" data-variant="{{.Variant}}">Send</button>
<span class="lt-badge" data-color="{% if x %}danger{% endif %}">1</span>
<button class="lt-button" data-variant="<?= $variant ?>">Send</button>
<button class="lt-button" data-variant="@variant">Send</button>
<button class="lt-button" data-variant="<%= variant %>">Send</button>
```

When the code produces the HTML as strings, there is no file to check. Then
check the page as the server sends it, as a whole page, with the wiring
included:

```bash
npx @lo-tek/interfaces check http://localhost:8080/form
curl -s http://localhost:8080/form | npx @lo-tek/interfaces check --rendered
```

In a test written in JavaScript, `diagnosePage` does the same:

```js
import { diagnosePage } from "@lo-tek/interfaces/diagnostics"

const findings = diagnosePage(html)
// findings is empty when the page is correct
```

The check is also in the editor as you type. The VS Code extension "LO-TEK"
gives completion, explanations, error messages and quick fixes in sixteen
template languages. WebStorm and IntelliJ IDEA Ultimate read `web-types.json`
from the package without any extension, and the plugin "LO-TEK" from JetBrains
Marketplace adds the error messages and completion in HTML in a string marked
with `@Language("HTML")` or `// language=HTML`.

On the JVM without Node there is LO-TEK for Kotlin
(https://lo-tek.sobernetics.no/kotlin/). `lotek.jar` is the command line as
`java -jar lotek.jar check`, and is attached to every release on GitHub:
https://github.com/MarkusAugust/lo-tek/releases/latest. The Maven package
`io.github.markusaugust:lo-tek-interfaces` with `assertLotek(html)` and the
Gradle plugin `io.github.markusaugust.lo-tek` are on Maven Central, with the
same version as the npm package. Not every npm version is on Maven: use the
newest one there, and never write a `version` you have not seen on Central.

## Known pitfalls

| Symptom | Cause |
| --- | --- |
| The styles are missing | `tokens.css` is not loaded. The order between it and the component's stylesheet does not matter |
| An `<lt-toast>` or `<lt-connection-status>` shows nothing, an `<lt-session-timeout>` never warns, or another component does nothing | The `define` function has not run. `<lt-toast>` and `<lt-connection-status>` create their content themselves and are empty without it, and `<lt-session-timeout>` does not count the time. The markup in the others comes from you and is there regardless, except that `<lt-tabs>` shows only one panel, the one the URL points to or the first, and `<lt-suggestion>` hides the list until they are registered |
| A component speaks English on a page in another language | The text in the markup comes from you, and is written with the app's own translation setup. The few default texts the package has are replaced with `label` and `close-label` on `<lt-toast>`, `offline-text` and `online-text` on `<lt-connection-status>`, `count-none`, `count-one` and `count-other` on `<lt-suggestion>`, and `--lt-label-required-text`, `--lt-label-optional-text` and `--lt-label-required-symbol` in CSS. Numbers and durations follow `lang` on `<html>`. See https://lo-tek.sobernetics.no/translation/ |
| The error text never shows, and the field counts as valid | `hidden="false"` is set on the error text, and it hides it all the same. The attribute must be removed, not set to `false` |
| The component does nothing, and nothing says so | The registration runs only on the server, where it does nothing. It must also run in the browser |
| `SyntaxError` in the browser | TypeScript syntax in a `<script type="module">` without a build step |

## When CSS is not enough

The components are customised with tokens and `--lt-` variables. If that is
not enough, `npx @lo-tek/interfaces takeover <component>` copies the source
code of one component into the project, so the code is yours to change. A
complete colour theme from your brand colours is made with
`npx @lo-tek/interfaces theme`.

All of this, with live examples: https://lo-tek.sobernetics.no/
