@lo-tek/interfaces 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/agent/astro.md +9 -7
- package/agent/bundles.md +8 -6
- package/agent/datastar.md +60 -51
- package/agent/html.md +55 -53
- package/agent/kotlin.md +622 -0
- package/agent/react.md +8 -6
- package/agent/templates.md +60 -63
- package/core/lotek-core.wasm +0 -0
- package/core/lotek.wasm +0 -0
- package/dist/components/css/steps/steps.d.ts +23 -0
- package/dist/components/css/steps/steps.d.ts.map +1 -0
- package/dist/components/css/steps/steps.js +21 -0
- package/dist/components/css/steps/steps.js.map +1 -0
- package/dist/components/events.d.ts +3 -1
- package/dist/components/events.d.ts.map +1 -1
- package/dist/components/frame/error-summary/lt-error-summary.js +1 -1
- package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
- package/dist/components/frame/popover/lt-popover.js +4 -19
- package/dist/components/frame/popover/lt-popover.js.map +1 -1
- package/dist/components/frame/term/lt-term.d.ts +114 -0
- package/dist/components/frame/term/lt-term.d.ts.map +1 -0
- package/dist/components/frame/term/lt-term.js +530 -0
- package/dist/components/frame/term/lt-term.js.map +1 -0
- package/dist/components/frame/term/term.d.ts +55 -0
- package/dist/components/frame/term/term.d.ts.map +1 -0
- package/dist/components/frame/term/term.js +55 -0
- package/dist/components/frame/term/term.js.map +1 -0
- package/dist/components/host-element.d.ts +19 -0
- package/dist/components/host-element.d.ts.map +1 -1
- package/dist/components/host-element.js +37 -0
- package/dist/components/host-element.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/jsx/react.d.ts +3 -1
- package/dist/jsx/react.d.ts.map +1 -1
- package/dist/lotek.css +351 -1
- package/dist/lt.d.ts +8 -1
- package/dist/lt.d.ts.map +1 -1
- package/dist/lt.js +9 -1
- package/dist/lt.js.map +1 -1
- package/dist/react.d.ts +19 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +14 -0
- package/dist/react.js.map +1 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +2 -0
- package/dist/register.js.map +1 -1
- package/dist/vocabulary/classes.d.ts.map +1 -1
- package/dist/vocabulary/classes.js +60 -6
- package/dist/vocabulary/classes.js.map +1 -1
- package/dist/vocabulary/elements.d.ts.map +1 -1
- package/dist/vocabulary/elements.js +15 -2
- package/dist/vocabulary/elements.js.map +1 -1
- package/dist/vocabulary/types.d.ts +8 -3
- package/dist/vocabulary/types.d.ts.map +1 -1
- package/live-templates-kotlin.xml +15 -12
- package/live-templates.xml +12 -6
- package/manifest/files.json +18 -0
- package/manifest/manifest.json +145 -10
- package/package.json +15 -5
- package/src/components/css/steps/steps.css +240 -0
- package/src/components/css/steps/steps.ts +26 -0
- package/src/components/events.ts +3 -1
- package/src/components/frame/error-summary/lt-error-summary.ts +1 -1
- package/src/components/frame/popover/lt-popover.ts +4 -24
- package/src/components/frame/term/lt-term.ts +573 -0
- package/src/components/frame/term/term.css +177 -0
- package/src/components/frame/term/term.ts +87 -0
- package/src/components/host-element.ts +46 -0
- package/src/index.ts +4 -0
- package/src/jsx/react.ts +4 -0
- package/src/lt.ts +9 -1
- package/src/react.ts +15 -0
- package/src/register.ts +2 -0
- package/src/tokens/tokens.css +4 -0
- package/src/vocabulary/classes.ts +60 -6
- package/src/vocabulary/elements.ts +15 -2
- package/src/vocabulary/types.ts +8 -3
- package/web-types.json +31 -10
package/agent/kotlin.md
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
# LO-TEK in Kotlin and Java
|
|
2
|
+
|
|
3
|
+
The rulebook for LO-TEK on a JVM server: Ktor, Spring, Javalin, http4k or
|
|
4
|
+
anything else that writes HTML from Kotlin or Java, with kotlinx.html, in
|
|
5
|
+
strings, or with a template engine such as JTE, Thymeleaf, Freemarker or
|
|
6
|
+
Pebble.
|
|
7
|
+
|
|
8
|
+
Nothing here needs Node or npm. The Maven package has the builder functions as
|
|
9
|
+
`Lt`, with enums for the allowed values, the check as `assertLotek(html)` for
|
|
10
|
+
your tests, and the CSS and the web components as a WebJar the app serves
|
|
11
|
+
itself.
|
|
12
|
+
|
|
13
|
+
The compiler is the first check: `ButtonVariant.SECUNDARY` does not compile,
|
|
14
|
+
so code that calls the builder functions cannot invent a variant. It does not
|
|
15
|
+
see markup written by hand next to them, or the wiring across a whole page.
|
|
16
|
+
That is what `assertLotek` reads.
|
|
17
|
+
|
|
18
|
+
This is @lo-tek/interfaces 0.42.0. The file is generated by the package and
|
|
19
|
+
follows its version, so it can never say anything other than the code next to
|
|
20
|
+
it.
|
|
21
|
+
|
|
22
|
+
## Short version
|
|
23
|
+
|
|
24
|
+
1. **One `<link>` to `LotekWebJar.CSS` in the page template,** once for the
|
|
25
|
+
whole app, with the WebJar served under `/webjars`. Without it, the
|
|
26
|
+
components look unstyled. The answer is then to add the link, never to
|
|
27
|
+
write your own CSS to make them look right.
|
|
28
|
+
2. **Use only the classes and elements in the tables below.** `lt-modal`,
|
|
29
|
+
`lt-datepicker` and `data-variant="outlined"` exist in other design
|
|
30
|
+
systems, not in LO-TEK. If you are unsure whether something exists, it is
|
|
31
|
+
listed here or it does not exist.
|
|
32
|
+
3. **No hard-coded colours or pixels.** `var(--lt-color-…)` and
|
|
33
|
+
`var(--lt-spacing-…)`.
|
|
34
|
+
4. **Web components are registered once with `defineLt()` from
|
|
35
|
+
`LotekWebJar.REGISTER`** in the page template, and a boolean attribute is
|
|
36
|
+
true as long as it is there. In kotlinx.html a flag property set to `false`
|
|
37
|
+
removes the attribute, while `attributes["hidden"] = "false"` still hides
|
|
38
|
+
the element.
|
|
39
|
+
5. **Check the HTML the server sends, in a test:** `assertLotek(html)` fails
|
|
40
|
+
with every finding as `line:column: error: message`, or `warning:`. It
|
|
41
|
+
knows every class, every element, every attribute and every allowed value,
|
|
42
|
+
and on a whole page it also requires every `for` and `aria-describedby` to
|
|
43
|
+
point to an id that exists, and every field to have a label. With the app
|
|
44
|
+
running, also check every page you have changed:
|
|
45
|
+
`java -jar lotek.jar check http://localhost:8080/page`. You are done when
|
|
46
|
+
the tests are green and the command answers "The markup matches LO-TEK".
|
|
47
|
+
|
|
48
|
+
## 1. The package, the stylesheet and the registration
|
|
49
|
+
|
|
50
|
+
```kotlin
|
|
51
|
+
// build.gradle.kts
|
|
52
|
+
dependencies {
|
|
53
|
+
implementation("io.github.markusaugust:lo-tek-interfaces:0.42.0")
|
|
54
|
+
// Only with kotlinx.html: the extension lt(…) and one function per web component.
|
|
55
|
+
implementation("io.github.markusaugust:lo-tek-kotlinx-html:0.42.0")
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The version is the npm package's, but not every npm version is on Maven
|
|
60
|
+
Central. Use the newest one there, and never write a version you have not seen
|
|
61
|
+
on
|
|
62
|
+
https://central.sonatype.com/artifact/io.github.markusaugust/lo-tek-interfaces.
|
|
63
|
+
The package requires Java 17 or newer.
|
|
64
|
+
|
|
65
|
+
The package is also a WebJar: the files are under
|
|
66
|
+
`META-INF/resources/webjars/lo-tek/<version>/`, with the same paths as in the
|
|
67
|
+
npm package. Spring Boot and servlet containers serve that folder without
|
|
68
|
+
setup. Ktor needs one line in `routing`:
|
|
69
|
+
`staticResources("/webjars", "META-INF/resources/webjars")`.
|
|
70
|
+
|
|
71
|
+
`LotekWebJar` has the paths with the version in them, so the page never points
|
|
72
|
+
to a version other than the one the package has. The page template, once for
|
|
73
|
+
the whole app:
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
head {
|
|
77
|
+
link(rel = "stylesheet", href = LotekWebJar.CSS)
|
|
78
|
+
}
|
|
79
|
+
body {
|
|
80
|
+
// The page.
|
|
81
|
+
script(type = "module") { unsafe { +"import { defineLt } from \"${LotekWebJar.REGISTER}\"; defineLt()" } }
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The same in a string, without kotlinx.html:
|
|
86
|
+
|
|
87
|
+
```kotlin
|
|
88
|
+
fun page(content: String) = """
|
|
89
|
+
<!DOCTYPE html>
|
|
90
|
+
<html lang="en">
|
|
91
|
+
<head><link rel="stylesheet" href="${LotekWebJar.CSS}"></head>
|
|
92
|
+
<body>
|
|
93
|
+
$content
|
|
94
|
+
<script type="module">
|
|
95
|
+
import { defineLt } from "${LotekWebJar.REGISTER}"
|
|
96
|
+
defineLt()
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
100
|
+
"""
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`lotek.css` is 112 kB, 14 kB compressed, has every component, and is nearly
|
|
104
|
+
always the right choice. If you want only the stylesheets the pages use,
|
|
105
|
+
`tokens.css` defines all the variables, and all the other stylesheets build on
|
|
106
|
+
it. It must be loaded, and comes first out of habit: the order between it and
|
|
107
|
+
the others does not matter. After that, one stylesheet per component, from the
|
|
108
|
+
tables below, each under `LotekWebJar.ROOT`.
|
|
109
|
+
|
|
110
|
+
## 2. What exists
|
|
111
|
+
|
|
112
|
+
36 CSS components and 10 web components, and this is the whole list. The
|
|
113
|
+
classes are `lt-` + kebab-case. Variants are always `data-*` attributes, never
|
|
114
|
+
classes of their own: `data-variant="secondary"`, not `lt-button--secondary`.
|
|
115
|
+
The default variant has no attribute.
|
|
116
|
+
|
|
117
|
+
### CSS components (no JavaScript)
|
|
118
|
+
|
|
119
|
+
| Class | Stylesheet | Attributes |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `lt-accordion`<br>`lt-accordion__content` | `${LotekWebJar.ROOT}/src/components/css/accordion/accordion.css` | `data-variant`: plain |
|
|
122
|
+
| `lt-alert`<br>`lt-alert__title` | `${LotekWebJar.ROOT}/src/components/css/alert/alert.css` | `data-color`: info, success, warning, danger |
|
|
123
|
+
| `lt-avatar`<br>`lt-avatar-stack` | `${LotekWebJar.ROOT}/src/components/css/avatar/avatar.css` | `data-variant`: square<br>`data-size`: small, large |
|
|
124
|
+
| `lt-badge` | `${LotekWebJar.ROOT}/src/components/css/badge/badge.css` | `data-color`: success, warning, danger, neutral |
|
|
125
|
+
| `lt-breadcrumbs` | `${LotekWebJar.ROOT}/src/components/css/breadcrumbs/breadcrumbs.css` | none |
|
|
126
|
+
| `lt-button` | `${LotekWebJar.ROOT}/src/components/css/button/button.css` | `data-variant`: secondary, ghost, danger |
|
|
127
|
+
| `lt-card`<br>`lt-card__title` | `${LotekWebJar.ROOT}/src/components/css/card/card.css` | `data-variant`: filled<br>`data-interactive` (flag) |
|
|
128
|
+
| `lt-checkbox`<br>`lt-checkbox-row` | `${LotekWebJar.ROOT}/src/components/css/checkbox/checkbox.css` | `data-state`: invalid, success |
|
|
129
|
+
| `lt-divider` | `${LotekWebJar.ROOT}/src/components/css/divider/divider.css` | `data-variant`: subtle, strong |
|
|
130
|
+
| `lt-error-text` | `${LotekWebJar.ROOT}/src/components/css/error-text/error-text.css` | `data-variant`: warning |
|
|
131
|
+
| `lt-fieldset`<br>`lt-legend` | `${LotekWebJar.ROOT}/src/components/css/fieldset/fieldset.css` | `data-state`: invalid, success<br>`data-required`: symbol, text<br>`data-optional` (flag) |
|
|
132
|
+
| `lt-file-upload`<br>`lt-file-upload-list` | `${LotekWebJar.ROOT}/src/components/css/file-upload/file-upload.css` | `data-state`: invalid, success |
|
|
133
|
+
| `lt-heading` | `${LotekWebJar.ROOT}/src/components/css/heading/heading.css` | `data-size`: xs, s, m, xl, mega |
|
|
134
|
+
| `lt-help-text` | `${LotekWebJar.ROOT}/src/components/css/help-text/help-text.css` | `data-variant`: strong, success, warning |
|
|
135
|
+
| `lt-icon` | `${LotekWebJar.ROOT}/src/components/css/icon/icon.css` | none |
|
|
136
|
+
| `lt-input` | `${LotekWebJar.ROOT}/src/components/css/input/input.css` | `data-state`: invalid, success<br>`data-variant`: date, datetime-local, time |
|
|
137
|
+
| `lt-label` | `${LotekWebJar.ROOT}/src/components/css/label/label.css` | `data-required`: symbol, text<br>`data-optional` (flag) |
|
|
138
|
+
| `lt-link` | `${LotekWebJar.ROOT}/src/components/css/link/link.css` | none |
|
|
139
|
+
| `lt-list` | `${LotekWebJar.ROOT}/src/components/css/list/list.css` | `data-variant`: plain, divided |
|
|
140
|
+
| `lt-pagination`<br>`lt-pagination__gap` | `${LotekWebJar.ROOT}/src/components/css/pagination/pagination.css` | none |
|
|
141
|
+
| `lt-paragraph` | `${LotekWebJar.ROOT}/src/components/css/paragraph/paragraph.css` | `data-variant`: lead<br>`data-size`: small, large |
|
|
142
|
+
| `lt-progress` | `${LotekWebJar.ROOT}/src/components/css/progress/progress.css` | `data-color`: success, warning, danger |
|
|
143
|
+
| `lt-radio`<br>`lt-radio-row` | `${LotekWebJar.ROOT}/src/components/css/radio/radio.css` | `data-state`: invalid, success |
|
|
144
|
+
| `lt-search`<br>`lt-search-row` | `${LotekWebJar.ROOT}/src/components/css/search/search.css` | `data-state`: invalid, success |
|
|
145
|
+
| `lt-select` | `${LotekWebJar.ROOT}/src/components/css/select/select.css` | `data-state`: invalid, success<br>`data-picker`: styled |
|
|
146
|
+
| `lt-skeleton` | `${LotekWebJar.ROOT}/src/components/css/skeleton/skeleton.css` | `data-variant`: text, circle |
|
|
147
|
+
| `lt-skip-link` | `${LotekWebJar.ROOT}/src/components/css/skip-link/skip-link.css` | none |
|
|
148
|
+
| `lt-spinner` | `${LotekWebJar.ROOT}/src/components/css/spinner/spinner.css` | `data-size`: small, large |
|
|
149
|
+
| `lt-sr-only` | `${LotekWebJar.ROOT}/src/components/css/sr-only/sr-only.css` | none |
|
|
150
|
+
| `lt-steps` | `${LotekWebJar.ROOT}/src/components/css/steps/steps.css` | none |
|
|
151
|
+
| `lt-switch`<br>`lt-switch-row` | `${LotekWebJar.ROOT}/src/components/css/switch/switch.css` | none |
|
|
152
|
+
| `lt-table`<br>`lt-table__sort`<br>`lt-table-scroll` | `${LotekWebJar.ROOT}/src/components/css/table/table.css` | `data-variant`: striped<br>`data-hoverable` (flag) |
|
|
153
|
+
| `lt-tag` | `${LotekWebJar.ROOT}/src/components/css/tag/tag.css` | `data-variant`: filled<br>`data-selectable` (flag) |
|
|
154
|
+
| `lt-textarea` | `${LotekWebJar.ROOT}/src/components/css/textarea/textarea.css` | `data-state`: invalid, success |
|
|
155
|
+
| `lt-toggle-group`<br>`lt-toggle-group__option` | `${LotekWebJar.ROOT}/src/components/css/toggle-group/toggle-group.css` | none |
|
|
156
|
+
| `lt-tooltip`<br>`lt-tooltip__bubble` | `${LotekWebJar.ROOT}/src/components/css/tooltip/tooltip.css` | none |
|
|
157
|
+
|
|
158
|
+
There is also `lt-theme-control`, a class on a radio button that lets the user
|
|
159
|
+
choose a theme. It is in `tokens.css`, and is described under Tokens.
|
|
160
|
+
|
|
161
|
+
## 3. Tokens
|
|
162
|
+
|
|
163
|
+
One layer. A colour is a cell in a matrix of **family**, that is, what it
|
|
164
|
+
means, and **role**, that is, what it does, and the name is
|
|
165
|
+
`--lt-color-<family>-<role>`. Every family has every role, so
|
|
166
|
+
`--lt-color-danger-border` and `--lt-color-success-border` both exist. The
|
|
167
|
+
roles are the same whatever the family: `surface` is a tinted surface, `fill`
|
|
168
|
+
a filled one, `content` the text on top of `fill`, `border` a border, and
|
|
169
|
+
`text` the family's colour as text. Border and text each have a weaker and a
|
|
170
|
+
stronger step next to them.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Families: `accent`, `visited`, `brand1`, `brand2`, `brand3`, `neutral`, `danger`, `warning`, `success`.
|
|
174
|
+
|
|
175
|
+
Roles: `surface`, `border-subtle`, `border`, `border-strong`, `fill`, `text`, `text-strong`, `text-subtle`, `content`.
|
|
176
|
+
|
|
177
|
+
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`.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Spacing: `--lt-spacing-0-5`, `--lt-spacing-1`, `--lt-spacing-10`,
|
|
181
|
+
`--lt-spacing-12`, `--lt-spacing-16`, `--lt-spacing-2`, `--lt-spacing-3`,
|
|
182
|
+
`--lt-spacing-4`, `--lt-spacing-5`, `--lt-spacing-6`, `--lt-spacing-7`,
|
|
183
|
+
`--lt-spacing-8`, `--lt-spacing-px`.
|
|
184
|
+
|
|
185
|
+
Font: `--lt-font-size-l`, `--lt-font-size-m`, `--lt-font-size-mega`,
|
|
186
|
+
`--lt-font-size-reference`, `--lt-font-size-s`, `--lt-font-size-xl`,
|
|
187
|
+
`--lt-font-size-xs`, `--lt-font-size-xxl`, `--lt-font-size-xxs`,
|
|
188
|
+
`--lt-font-weight-bold`, `--lt-font-weight-medium`,
|
|
189
|
+
`--lt-font-weight-regular`, `--lt-font-weight-semibold`.
|
|
190
|
+
|
|
191
|
+
Line height: `--lt-line-height-article`, `--lt-line-height-compact`,
|
|
192
|
+
`--lt-line-height-default`, `--lt-line-height-heading`.
|
|
193
|
+
|
|
194
|
+
`disabled` and `neutral` are not the same. `disabled` is for controls that are
|
|
195
|
+
turned off, and is exempt from the contrast requirement in WCAG 1.4.3.
|
|
196
|
+
`neutral` is for subdued information the user actually has to read or press,
|
|
197
|
+
and holds 4.5:1. Never use the `disabled` colours to subdue something that is
|
|
198
|
+
meant to be read.
|
|
199
|
+
|
|
200
|
+
### Light and dark
|
|
201
|
+
|
|
202
|
+
Without you doing anything, the colours follow the device's setting. A page
|
|
203
|
+
that wants to decide for itself sets `data-theme="light"` or
|
|
204
|
+
`data-theme="dark"` on `<html>`.
|
|
205
|
+
|
|
206
|
+
The attribute is a **theme boundary** and works on any element, not only on
|
|
207
|
+
the root. One theme can sit inside another, both ways.
|
|
208
|
+
|
|
209
|
+
This is what an embedded component should use. When you put LO-TEK into a page
|
|
210
|
+
you do not control, set the attribute on the component's own root element:
|
|
211
|
+
|
|
212
|
+
```kotlin
|
|
213
|
+
div {
|
|
214
|
+
attributes["data-theme"] = "light"
|
|
215
|
+
button { lt(Lt.button()); +"Save" }
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The component is then light whatever the device is set to, and the host is
|
|
220
|
+
left alone.
|
|
221
|
+
|
|
222
|
+
**The host's CSS stops at the boundary.** The host often has rules without a
|
|
223
|
+
layer, such as `button { background: none; border: none }` or a normalize, and
|
|
224
|
+
they beat `@layer lotek`. In that case, set `data-lt-boundary` on the same
|
|
225
|
+
root element:
|
|
226
|
+
|
|
227
|
+
```kotlin
|
|
228
|
+
div {
|
|
229
|
+
attributes["data-lt-boundary"] = ""
|
|
230
|
+
attributes["data-theme"] = "light"
|
|
231
|
+
button { lt(Lt.button()); +"Save" }
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Below the root element the browser then ignores the host's CSS without a
|
|
236
|
+
layer, unless a rule of the host's wins on specificity as described below, and
|
|
237
|
+
LO-TEK's layer applies again. The rule is in `lotek.css` and in
|
|
238
|
+
`${LotekWebJar.ROOT}/src/tokens/boundary.css`. Fonts and other inherited
|
|
239
|
+
properties still come from the host, and the `--lt-*` variables work. Three
|
|
240
|
+
things follow from it:
|
|
241
|
+
|
|
242
|
+
- Your own CSS inside the boundary must be in a layer after `lotek`. CSS
|
|
243
|
+
without a layer disappears there in the same way as the host's.
|
|
244
|
+
- Elements without a LO-TEK class lose the host's CSS without a layer. A layer
|
|
245
|
+
the host has declared before `lotek`, such as Tailwind's Preflight in
|
|
246
|
+
`base`, still applies where LO-TEK sets nothing.
|
|
247
|
+
- The boundary has the specificity (0,2,0). A rule from the host with the same
|
|
248
|
+
specificity wins if it is loaded after the boundary, and one with more
|
|
249
|
+
always wins. So do `!important` and layers the host has declared after
|
|
250
|
+
`lotek`.
|
|
251
|
+
|
|
252
|
+
LO-TEK sets `color-scheme` only where someone has chosen a theme: on
|
|
253
|
+
`[data-theme="light"]`, on `[data-theme="dark"]` and on the options of the
|
|
254
|
+
theme picker below. Bare `:root` never gets it. The property controls the
|
|
255
|
+
browser's own surfaces, that is, scrollbars, dropdowns and calendar panels,
|
|
256
|
+
and it is inherited. A child can opt out with `color-scheme: normal`, but that
|
|
257
|
+
is a counter-rule the host never asked to have to write: with the value on the
|
|
258
|
+
root, it applied to the whole document, including where the package is only a
|
|
259
|
+
guest. If the whole page should follow the system, write
|
|
260
|
+
`color-scheme: light dark` on `<html>` yourself, in the same way as you set
|
|
261
|
+
the layer order yourself.
|
|
262
|
+
|
|
263
|
+
### Letting the user choose a theme
|
|
264
|
+
|
|
265
|
+
`lt-theme-control` on a radio button makes `value` a theme choice. It is plain
|
|
266
|
+
CSS, without a line of JavaScript:
|
|
267
|
+
|
|
268
|
+
```kotlin
|
|
269
|
+
fieldSet {
|
|
270
|
+
lt(Lt.toggleGroup())
|
|
271
|
+
legend { lt(Lt.srOnly()); +"Theme" }
|
|
272
|
+
label {
|
|
273
|
+
lt(Lt.toggleGroup.option)
|
|
274
|
+
radioInput(name = "theme", classes = "lt-theme-control") { value = "auto"; checked = true }
|
|
275
|
+
+" Follow the system"
|
|
276
|
+
}
|
|
277
|
+
label {
|
|
278
|
+
lt(Lt.toggleGroup.option)
|
|
279
|
+
radioInput(name = "theme", classes = "lt-theme-control") { value = "light" }
|
|
280
|
+
+" Light"
|
|
281
|
+
}
|
|
282
|
+
label {
|
|
283
|
+
lt(Lt.toggleGroup.option)
|
|
284
|
+
radioInput(name = "theme", classes = "lt-theme-control") { value = "dark" }
|
|
285
|
+
+" Dark"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`lt-theme-control` has no builder function, so it is the one class written as
|
|
291
|
+
a string.
|
|
292
|
+
|
|
293
|
+
The choice controls two things. LO-TEK's colours come from the tokens, and the
|
|
294
|
+
browser's **own** surfaces from `color-scheme`: `light` gives `light`, `dark`
|
|
295
|
+
gives `dark`, and `auto` gives `light dark`. `light dark` is not a theme, but
|
|
296
|
+
the statement that the page works in both, so the browser can choose by the
|
|
297
|
+
system.
|
|
298
|
+
|
|
299
|
+
"Follow the system" has no theme block, and that is on purpose: a value
|
|
300
|
+
without a block matches nothing, and then the media query applies again.
|
|
301
|
+
|
|
302
|
+
Do not write `color-scheme` yourself when the picker is in use. Such a rule
|
|
303
|
+
outside a layer beats `@layer lotek` and locks the browser's surfaces to the
|
|
304
|
+
system while the user has chosen something else.
|
|
305
|
+
|
|
306
|
+
`light` and `dark` win over `data-theme` on `<html>`, so the server can send
|
|
307
|
+
the stored choice while a click still takes effect before the response is
|
|
308
|
+
back. `auto` does not: it means "no override from me", so if the server has
|
|
309
|
+
written `data-theme`, the server's value stands. For "follow the system" to
|
|
310
|
+
work at once, whatever stores the choice must also remove the attribute.
|
|
311
|
+
|
|
312
|
+
Two things it does not do. It **stores nothing**: submit the group in a form
|
|
313
|
+
and store the choice in a cookie the server reads, or in `localStorage`. And
|
|
314
|
+
it must be in the **same tree as `<html>`**, since `:has()` does not see out
|
|
315
|
+
of its own tree; a control inside a shadow root does not set the theme on the
|
|
316
|
+
page.
|
|
317
|
+
|
|
318
|
+
## 4. Web components
|
|
319
|
+
|
|
320
|
+
Three rules apply to all of them:
|
|
321
|
+
|
|
322
|
+
1. **`defineLt()` runs once** in the page template, not once per component.
|
|
323
|
+
The import has no side effect on its own; it is the `define` call that
|
|
324
|
+
registers the element.
|
|
325
|
+
2. **Boolean attributes are true as long as they are present.**
|
|
326
|
+
`hidden="false"`, `disabled="false"` and `open="false"` turn it *on*. To
|
|
327
|
+
turn any of them off, remove the attribute entirely.
|
|
328
|
+
3. **You write the markup, the component adds the behaviour.** `<lt-field>`
|
|
329
|
+
does not create your label or your control. With `Lt.field()` the server
|
|
330
|
+
writes the whole wiring, and `<lt-field>` is not needed; it is for markup
|
|
331
|
+
that cannot call a builder function, such as a JTE template. There you
|
|
332
|
+
write `for` on the label and `id` on the control, or wrap the control in a
|
|
333
|
+
`<label>`, and the component wires the help text and the error text. It
|
|
334
|
+
never writes an id on the control: an id the server did not send makes an
|
|
335
|
+
update from the server replace the field, with the focus and what the user
|
|
336
|
+
has typed. The field is invalid when the error text is there without
|
|
337
|
+
`hidden`.
|
|
338
|
+
|
|
339
|
+
| Element | Category | Stylesheet | Registration | Attributes | Events | Classes inside |
|
|
340
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
341
|
+
| `<lt-field>` | frame | `${LotekWebJar.ROOT}/src/components/frame/field/field.css` | `defineLtField()` from `${LotekWebJar.ROOT}/dist/components/frame/field/lt-field.js` | `disabled` (flag)<br>`optional` (flag)<br>`described-by` (text) | none | none |
|
|
342
|
+
| `<lt-tabs>` | frame | `${LotekWebJar.ROOT}/src/components/frame/tabs/tabs.css` | `defineLtTabs()` from `${LotekWebJar.ROOT}/dist/components/frame/tabs/lt-tabs.js` | `server-controlled` (flag)<br>`activation`: manual, automatic | `tab-select` (index) | `lt-tabs__list`<br>`lt-tabs__panel` |
|
|
343
|
+
| `<lt-error-summary>` | frame | `${LotekWebJar.ROOT}/src/components/frame/error-summary/error-summary.css` | `defineLtErrorSummary()` from `${LotekWebJar.ROOT}/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` |
|
|
344
|
+
| `<lt-popover>` | frame | `${LotekWebJar.ROOT}/src/components/frame/popover/popover.css` | `defineLtPopover()` from `${LotekWebJar.ROOT}/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` |
|
|
345
|
+
| `<lt-term>` | frame | `${LotekWebJar.ROOT}/src/components/frame/term/term.css` | `defineLtTerm()` from `${LotekWebJar.ROOT}/dist/components/frame/term/lt-term.js` | none | none | `lt-term`<br>`lt-term__entry` |
|
|
346
|
+
| `<lt-suggestion>` | frame | `${LotekWebJar.ROOT}/src/components/frame/suggestion/suggestion.css` | `defineLtSuggestion()` from `${LotekWebJar.ROOT}/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` |
|
|
347
|
+
| `<lt-dialog>` | frame | `${LotekWebJar.ROOT}/src/components/frame/dialog/dialog.css` | `defineLtDialog()` from `${LotekWebJar.ROOT}/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` |
|
|
348
|
+
| `<lt-toast>` | standalone | `${LotekWebJar.ROOT}/src/components/standalone/toast/toast.css` | `defineLtToast()` from `${LotekWebJar.ROOT}/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` |
|
|
349
|
+
| `<lt-session-timeout>` | frame | `${LotekWebJar.ROOT}/src/components/frame/session-timeout/session-timeout.css` | `defineLtSessionTimeout()` from `${LotekWebJar.ROOT}/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` |
|
|
350
|
+
| `<lt-connection-status>` | standalone | `${LotekWebJar.ROOT}/src/components/standalone/connection-status/connection-status.css` | `defineLtConnectionStatus()` from `${LotekWebJar.ROOT}/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` |
|
|
351
|
+
|
|
352
|
+
None of them uses shadow DOM. The content is in plain DOM, so `querySelector`,
|
|
353
|
+
`FormData` and ordinary CSS work straight into it.
|
|
354
|
+
|
|
355
|
+
## 5. The builder functions
|
|
356
|
+
|
|
357
|
+
`Lt` has the same builder functions and options as `lt` in TypeScript. Each
|
|
358
|
+
gives the attributes for one element, as an `Attributes`, which is a
|
|
359
|
+
`Map<String, String>`. In kotlinx.html, `lt(…)` puts them on the tag; in a
|
|
360
|
+
string, `toHtml()` writes them out with the values escaped, and `+` combines
|
|
361
|
+
two sets:
|
|
362
|
+
|
|
363
|
+
```kotlin
|
|
364
|
+
val save = Lt.button(variant = ButtonVariant.SECONDARY)
|
|
365
|
+
|
|
366
|
+
button { lt(save); +"Save draft" }
|
|
367
|
+
|
|
368
|
+
val asText = """<button ${save.toHtml()}>Save draft</button>"""
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Both give
|
|
372
|
+
`<button class="lt-button" data-variant="secondary">Save draft</button>`. In
|
|
373
|
+
kotlinx.html several sets are applied in order, a later one wins, and the
|
|
374
|
+
classes are added to those the tag already has.
|
|
375
|
+
|
|
376
|
+
The 48 builder functions:
|
|
377
|
+
|
|
378
|
+
`Lt.accordion()`, `Lt.alert()`, `Lt.avatar()`, `Lt.badge()`,
|
|
379
|
+
`Lt.breadcrumbs()`, `Lt.button()`, `Lt.card()`, `Lt.checkbox()`,
|
|
380
|
+
`Lt.connectionStatus()`, `Lt.dialog()`, `Lt.divider()`, `Lt.errorSummary()`,
|
|
381
|
+
`Lt.errorText()`, `Lt.field()`, `Lt.fieldset()`, `Lt.fileUpload()`,
|
|
382
|
+
`Lt.heading()`, `Lt.helpText()`, `Lt.icon()`, `Lt.input()`, `Lt.label()`,
|
|
383
|
+
`Lt.legend()`, `Lt.link()`, `Lt.list()`, `Lt.pagination()`, `Lt.paragraph()`,
|
|
384
|
+
`Lt.popover()`, `Lt.progress()`, `Lt.radio()`, `Lt.search()`, `Lt.select()`,
|
|
385
|
+
`Lt.sessionTimeout()`, `Lt.skeleton()`, `Lt.skipLink()`, `Lt.spinner()`,
|
|
386
|
+
`Lt.srOnly()`, `Lt.steps()`, `Lt.suggestion()`, `Lt.switch()`, `Lt.table()`,
|
|
387
|
+
`Lt.tableSort()`, `Lt.tabs()`, `Lt.tag()`, `Lt.term()`, `Lt.textarea()`,
|
|
388
|
+
`Lt.toast()`, `Lt.toggleGroup()`, `Lt.tooltip()`
|
|
389
|
+
|
|
390
|
+
A builder function for several elements gives a data class with one field per
|
|
391
|
+
element, such as `label`, `control`, `help` and `error` from `Lt.field()`. A
|
|
392
|
+
part the template writes itself has its class as a set on the builder
|
|
393
|
+
function: `Lt.card.title`, `Lt.toggleGroup.option`.
|
|
394
|
+
|
|
395
|
+
The allowed values are enums. A value the builder function's default already
|
|
396
|
+
gives writes no attribute.
|
|
397
|
+
|
|
398
|
+
| Enum | Constants | Used for |
|
|
399
|
+
| --- | --- | --- |
|
|
400
|
+
| `AccordionVariant` | `DEFAULT`, `PLAIN` | `variant` in `Lt.accordion()` |
|
|
401
|
+
| `AlertColor` | `INFO`, `SUCCESS`, `WARNING`, `DANGER` | `color` in `Lt.alert()` |
|
|
402
|
+
| `AvatarVariant` | `CIRCLE`, `SQUARE` | `variant` in `Lt.avatar()` |
|
|
403
|
+
| `BadgeColor` | `INFO`, `SUCCESS`, `WARNING`, `DANGER`, `NEUTRAL` | `color` in `Lt.badge()` |
|
|
404
|
+
| `ButtonVariant` | `PRIMARY`, `SECONDARY`, `GHOST`, `DANGER` | `variant` in `Lt.button()` |
|
|
405
|
+
| `DialogColor` | `NEUTRAL`, `BRAND`, `INFO`, `SUCCESS`, `WARNING`, `DANGER` | `color` in `Lt.dialog()` |
|
|
406
|
+
| `DividerVariant` | `DEFAULT`, `SUBTLE`, `STRONG` | `variant` in `Lt.divider()` |
|
|
407
|
+
| `ErrorTextVariant` | `ERROR`, `WARNING` | `variant` in `Lt.errorText()` |
|
|
408
|
+
| `FieldState` | `DEFAULT`, `INVALID`, `SUCCESS` | `state` in `Lt.checkbox()`<br>`state` from `Lt.field()`<br>`state` in `Lt.fieldset()`<br>`state` in `Lt.fileUpload()`<br>`state` in `Lt.input()`<br>`state` in `Lt.radio()`<br>`state` in `Lt.search()`<br>`state` in `Lt.select()`<br>`state` from `Lt.suggestion()`<br>`state` in `Lt.textarea()` |
|
|
409
|
+
| `HeadingSize` | `XS`, `S`, `M`, `L`, `XL`, `MEGA` | `size` in `Lt.heading()` |
|
|
410
|
+
| `HelpTextVariant` | `MUTED`, `STRONG`, `SUCCESS`, `WARNING` | `variant` in `Lt.helpText()` |
|
|
411
|
+
| `InputType` | `TEXT`, `PASSWORD`, `EMAIL`, `NUMBER`, `DATE`, `DATETIME_LOCAL`, `WEEK`, `MONTH`, `TEL`, `URL`, `SEARCH`, `TIME` | `type` in `Lt.input()` |
|
|
412
|
+
| `ListVariant` | `DEFAULT`, `PLAIN`, `DIVIDED` | `variant` in `Lt.list()` |
|
|
413
|
+
| `ParagraphVariant` | `DEFAULT`, `LEAD` | `variant` in `Lt.paragraph()` |
|
|
414
|
+
| `PopoverPlacement` | `BOTTOM_START`, `BOTTOM_END`, `TOP_START`, `TOP_END` | `placement` in `Lt.popover()` |
|
|
415
|
+
| `ProgressColor` | `SUCCESS`, `WARNING`, `DANGER` | `color` in `Lt.progress()` |
|
|
416
|
+
| `RequiredMarker` | `SYMBOL`, `TEXT` | `required` in `Lt.field()`<br>`required` in `Lt.label()`<br>`required` in `Lt.legend()`<br>`required` in `Lt.suggestion()` |
|
|
417
|
+
| `SelectPicker` | `AUTO`, `STYLED` | `picker` in `Lt.select()` |
|
|
418
|
+
| `Size` | `SMALL`, `MEDIUM`, `LARGE` | `size` in `Lt.avatar()`<br>`size` in `Lt.paragraph()`<br>`size` in `Lt.spinner()` |
|
|
419
|
+
| `SkeletonVariant` | `BLOCK`, `TEXT`, `CIRCLE` | `variant` in `Lt.skeleton()` |
|
|
420
|
+
| `SurfaceVariant` | `OUTLINE`, `FILLED` | `variant` in `Lt.card()`<br>`variant` in `Lt.tag()` |
|
|
421
|
+
| `TableSortDirection` | `ASCENDING`, `DESCENDING` | `direction` in `Lt.tableSort()` |
|
|
422
|
+
| `TableVariant` | `DEFAULT`, `STRIPED` | `variant` in `Lt.table()` |
|
|
423
|
+
|
|
424
|
+
Every enum has `value`, the text in the HTML, and `of(text)`, which gives the
|
|
425
|
+
constant, or `null` for a text that is not allowed. A value from a request
|
|
426
|
+
goes through `of` with a fallback:
|
|
427
|
+
`ButtonVariant.of(text) ?: ButtonVariant.PRIMARY`, never `!!`, which throws
|
|
428
|
+
without saying what the text was.
|
|
429
|
+
|
|
430
|
+
kotlinx.html has an `InputType` of its own. Import `no.lotek.InputType` by
|
|
431
|
+
name next to `kotlinx.html.*`, as the recipes do, and use kotlinx.html's
|
|
432
|
+
shortcuts, such as `radioInput`, for its own types.
|
|
433
|
+
|
|
434
|
+
From Java the builder functions are static methods on `Lt`, and options can be
|
|
435
|
+
left out from the end:
|
|
436
|
+
|
|
437
|
+
```java
|
|
438
|
+
Attributes save = Lt.button(ButtonVariant.SECONDARY);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## 6. A field
|
|
442
|
+
|
|
443
|
+
`Lt.field()` writes the whole wiring between the label, the control, the help
|
|
444
|
+
text and the error text, every time the server renders the form. The field
|
|
445
|
+
then needs no `<lt-field>` and no script: `<lt-field>` is for markup that
|
|
446
|
+
cannot call a builder function, such as a JTE template. Never use both on one
|
|
447
|
+
field. This is the recipe from the field page, compiled and compared with its
|
|
448
|
+
HTML on every build:
|
|
449
|
+
|
|
450
|
+
```kotlin
|
|
451
|
+
import kotlinx.html.*
|
|
452
|
+
import no.lotek.*
|
|
453
|
+
import no.lotek.InputType
|
|
454
|
+
import no.lotek.html.lt
|
|
455
|
+
|
|
456
|
+
fun FlowContent.orderForm(codename: String = "", invalid: Boolean = false) {
|
|
457
|
+
val field = Lt.field(id = "codename", required = RequiredMarker.SYMBOL, error = true, invalid = invalid)
|
|
458
|
+
form(action = "/jobs", method = FormMethod.post) {
|
|
459
|
+
id = "order-form"
|
|
460
|
+
attributes["novalidate"] = ""
|
|
461
|
+
label { lt(field.label); +"Codename" }
|
|
462
|
+
input {
|
|
463
|
+
lt(Lt.input(type = InputType.TEXT), field.control)
|
|
464
|
+
name = "codename"
|
|
465
|
+
required = true
|
|
466
|
+
// What the user typed comes back, so a failed submission loses nothing.
|
|
467
|
+
if (codename.isNotEmpty()) value = codename
|
|
468
|
+
if (invalid) autoFocus = true
|
|
469
|
+
}
|
|
470
|
+
p { lt(Lt.errorText(), field.error); +"Enter a codename." }
|
|
471
|
+
button { lt(Lt.button()); type = ButtonType.submit; +"Order the job" }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
The page template around it:
|
|
477
|
+
|
|
478
|
+
```kotlin
|
|
479
|
+
// Once for the whole app, in the page template. Not once per component.
|
|
480
|
+
head { link(rel = "stylesheet", href = LotekWebJar.CSS) }
|
|
481
|
+
body {
|
|
482
|
+
orderForm()
|
|
483
|
+
script(type = "module") { unsafe { +"import { defineLt } from \"${LotekWebJar.REGISTER}\"; defineLt()" } }
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The handler for `POST` renders the form again with what was typed and
|
|
488
|
+
`invalid = true`, with status 422, and the error text shows. A valid
|
|
489
|
+
submission redirects, so a reload does not post it again.
|
|
490
|
+
|
|
491
|
+
**The id is required, and an empty one throws.** Make it from a key in the
|
|
492
|
+
data, never from the loop index, a counter or a random value, so it is unique
|
|
493
|
+
on the page and the same on every render:
|
|
494
|
+
|
|
495
|
+
```kotlin
|
|
496
|
+
data class JobNote(val code: String, val name: String, val text: String)
|
|
497
|
+
|
|
498
|
+
fun FlowContent.notes(jobs: List<JobNote>) {
|
|
499
|
+
for (job in jobs) {
|
|
500
|
+
val field = Lt.field(id = "note-${job.code}")
|
|
501
|
+
label { lt(field.label); +job.name }
|
|
502
|
+
input { lt(Lt.input(), field.control); name = "note-${job.code}"; value = job.text }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
A morph, as in Datastar or htmx with idiomorph, recognises an element by its
|
|
508
|
+
id, and replaces one whose id changed, along with the focus and what the user
|
|
509
|
+
had typed. With the index the element is kept, but the text ends up in the
|
|
510
|
+
wrong row.
|
|
511
|
+
|
|
512
|
+
## 7. When the server sends new HTML
|
|
513
|
+
|
|
514
|
+
If the page is patched as it runs, with Datastar, htmx or Turbo, the web
|
|
515
|
+
components put back what the user chose when a patch tears it away: the
|
|
516
|
+
selected tab in `<lt-tabs>`, an open `<lt-popover>`. The server needs nothing
|
|
517
|
+
for that. If the server is to move the state, as in "go on to step 2", write
|
|
518
|
+
`server-controlled` on the host. Then every patch decides, and the component
|
|
519
|
+
stops putting the user's choice back. The state is always in the markup the
|
|
520
|
+
server sends: `aria-selected` on the tab and `hidden` on the panel in
|
|
521
|
+
`<lt-tabs>`, and `open` on `<lt-popover>` and `<lt-dialog>`.
|
|
522
|
+
|
|
523
|
+
A `$` that is meant for the page, as in a Datastar expression, is read by
|
|
524
|
+
Kotlin as a template. Write `${'$'}count`, or use a `$$"""…"""` string, where
|
|
525
|
+
only `$$` interpolates.
|
|
526
|
+
|
|
527
|
+
## 8. The check
|
|
528
|
+
|
|
529
|
+
`assertLotek` checks the HTML the server sends, and throws an `AssertionError`
|
|
530
|
+
with the findings, which JUnit, Kotest and TestNG all report:
|
|
531
|
+
|
|
532
|
+
```kotlin
|
|
533
|
+
import kotlinx.html.div
|
|
534
|
+
import kotlinx.html.stream.createHTML
|
|
535
|
+
import no.lotek.assertLotek
|
|
536
|
+
import org.junit.jupiter.api.Test
|
|
537
|
+
|
|
538
|
+
class OrderPageTest {
|
|
539
|
+
@Test
|
|
540
|
+
fun `the order form is correct markup`() {
|
|
541
|
+
assertLotek(page(createHTML().div { orderForm(invalid = true) }))
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@Test
|
|
545
|
+
fun `every note is wired`() {
|
|
546
|
+
assertLotek(page(createHTML().div { notes(listOf(JobNote("222-0481", "Orison", ""))) }))
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
By default it checks a whole page: that every id that is pointed to exists,
|
|
552
|
+
and that every field is wired. `assertLotek(html, fragment = true)` checks a
|
|
553
|
+
piece of a page, such as one component, against the vocabulary only.
|
|
554
|
+
`Lotek.diagnosePage(html)` gives the findings as a list instead.
|
|
555
|
+
`assertLotekRendered { script -> page.evaluate(script) }` checks the page as a
|
|
556
|
+
browser has rendered it, with Playwright for Java, and also fails on a class
|
|
557
|
+
no stylesheet styles.
|
|
558
|
+
|
|
559
|
+
It reads the HTML that comes out, not the code that made it, so it works the
|
|
560
|
+
same for strings, kotlinx.html, JTE, Thymeleaf, Freemarker and Pebble: render
|
|
561
|
+
the template to a text in a test, and give it to `assertLotek`. In the
|
|
562
|
+
template files themselves, write the markup as the component pages show it.
|
|
563
|
+
The rulebook for template syntax is
|
|
564
|
+
https://lo-tek.sobernetics.no/agent/templates.md.
|
|
565
|
+
|
|
566
|
+
The Gradle plugin checks the template files as part of `check`, without a test
|
|
567
|
+
that renders them. `templates` takes a whole folder, with its subfolders:
|
|
568
|
+
`src/main/jte` for JTE, `src/main/resources/templates` for Thymeleaf,
|
|
569
|
+
Freemarker and Pebble.
|
|
570
|
+
|
|
571
|
+
```kotlin
|
|
572
|
+
// build.gradle.kts
|
|
573
|
+
plugins {
|
|
574
|
+
id("io.github.markusaugust.lo-tek") version "0.42.0"
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
lotek {
|
|
578
|
+
templates.from(fileTree("src/main/jte"))
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
`lotek.jar` is the command line, attached to every release on GitHub:
|
|
583
|
+
https://github.com/MarkusAugust/lo-tek/releases/latest. It takes an address,
|
|
584
|
+
or the template files by name, never a folder:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
java -jar lotek.jar check http://localhost:8080/form
|
|
588
|
+
java -jar lotek.jar check src/main/jte/order.kte src/main/jte/jobs.kte
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
A finding you want to keep is suppressed with a comment above the tag:
|
|
592
|
+
|
|
593
|
+
```html
|
|
594
|
+
<!-- lotek-ignore-next unknown-class -->
|
|
595
|
+
<div class="lt-experiment">Testing a new layout</div>
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
In IntelliJ the plugin "LO-TEK Interfaces" from JetBrains Marketplace runs the
|
|
599
|
+
same check while you type, also in HTML in a Kotlin string marked with
|
|
600
|
+
`@Language("HTML")` or `// language=HTML`. In Kotlin it warns about a set on
|
|
601
|
+
an element its part cannot be, such as `label { lt(field.control) }`, about
|
|
602
|
+
`of(…)!!`, and about an id from the loop index.
|
|
603
|
+
|
|
604
|
+
## Known pitfalls
|
|
605
|
+
|
|
606
|
+
| Symptom | Cause |
|
|
607
|
+
| --- | --- |
|
|
608
|
+
| The styles are missing | The `<link>` to `LotekWebJar.CSS` is missing, or `/webjars` is not served and it answers 404 |
|
|
609
|
+
| 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, with two exceptions until they are registered. `<lt-tabs>` shows one panel: of those the server left without `hidden`, the one the URL points to, otherwise the first, and under `server-controlled` always the first. `<lt-suggestion>` hides the list |
|
|
610
|
+
| 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 the nearest `lang`, usually the one on `<html>`. See https://lo-tek.sobernetics.no/translation/ |
|
|
611
|
+
| The error text never shows, and the field counts as valid | `attributes["hidden"] = "false"` on the error text, which hides it all the same. Leave the attribute out, or let `Lt.field(invalid = …)` decide |
|
|
612
|
+
| `Type mismatch` on `InputType` | kotlinx.html has an `InputType` too. Import `no.lotek.InputType` by name |
|
|
613
|
+
| A field loses what the user typed after an update from the server | The id comes from the loop index, a counter or a random value, and changed between two renders |
|
|
614
|
+
| `Unresolved reference` or the wrong value in a Datastar attribute | Kotlin read the `$` as a template. Write `${'$'}` |
|
|
615
|
+
|
|
616
|
+
## When CSS is not enough
|
|
617
|
+
|
|
618
|
+
The components are customised with tokens and `--lt-` variables. A complete
|
|
619
|
+
colour theme from your brand colours is made with `Lotek.buildTheme(recipe)`,
|
|
620
|
+
or with the Gradle plugin's `lotekTheme` task from a `lotek.theme.json`.
|
|
621
|
+
|
|
622
|
+
All of this, with live examples: https://lo-tek.sobernetics.no/kotlin/
|