@lo-tek/interfaces 0.39.0 → 0.41.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.
Files changed (201) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +1 -1
  3. package/agent/astro.md +16 -14
  4. package/agent/bundles.md +18 -16
  5. package/agent/datastar.md +70 -68
  6. package/agent/html.md +63 -61
  7. package/agent/kotlin.md +622 -0
  8. package/agent/react.md +18 -16
  9. package/agent/templates.md +72 -68
  10. package/core/lotek-core.wasm +0 -0
  11. package/core/lotek.wasm +0 -0
  12. package/dist/components/css/accordion/accordion.d.ts +1 -1
  13. package/dist/components/css/accordion/accordion.js +1 -1
  14. package/dist/components/css/alert/alert.d.ts +1 -1
  15. package/dist/components/css/alert/alert.js +1 -1
  16. package/dist/components/css/avatar/avatar.d.ts +1 -1
  17. package/dist/components/css/avatar/avatar.js +1 -1
  18. package/dist/components/css/badge/badge.d.ts +1 -1
  19. package/dist/components/css/badge/badge.js +1 -1
  20. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +1 -1
  21. package/dist/components/css/breadcrumbs/breadcrumbs.js +1 -1
  22. package/dist/components/css/card/card.d.ts +1 -1
  23. package/dist/components/css/card/card.js +1 -1
  24. package/dist/components/css/file-upload/file-upload.d.ts +1 -1
  25. package/dist/components/css/file-upload/file-upload.js +1 -1
  26. package/dist/components/css/icon/icon.d.ts +1 -1
  27. package/dist/components/css/icon/icon.js +1 -1
  28. package/dist/components/css/label/label.d.ts +1 -1
  29. package/dist/components/css/label/label.js +1 -1
  30. package/dist/components/css/link/link.d.ts +1 -1
  31. package/dist/components/css/link/link.js +1 -1
  32. package/dist/components/css/spinner/spinner.d.ts +7 -5
  33. package/dist/components/css/spinner/spinner.d.ts.map +1 -1
  34. package/dist/components/css/spinner/spinner.js +7 -5
  35. package/dist/components/css/spinner/spinner.js.map +1 -1
  36. package/dist/components/css/steps/steps.d.ts +23 -0
  37. package/dist/components/css/steps/steps.d.ts.map +1 -0
  38. package/dist/components/css/steps/steps.js +21 -0
  39. package/dist/components/css/steps/steps.js.map +1 -0
  40. package/dist/components/css/tag/tag.d.ts +1 -1
  41. package/dist/components/css/tag/tag.js +1 -1
  42. package/dist/components/css/toggle-group/toggle-group.d.ts +1 -1
  43. package/dist/components/css/toggle-group/toggle-group.js +1 -1
  44. package/dist/components/css/tooltip/tooltip.d.ts +2 -2
  45. package/dist/components/css/tooltip/tooltip.js +2 -2
  46. package/dist/components/events.d.ts +3 -1
  47. package/dist/components/events.d.ts.map +1 -1
  48. package/dist/components/frame/dialog/dialog.d.ts +11 -6
  49. package/dist/components/frame/dialog/dialog.d.ts.map +1 -1
  50. package/dist/components/frame/dialog/dialog.js +4 -4
  51. package/dist/components/frame/dialog/dialog.js.map +1 -1
  52. package/dist/components/frame/dialog/lt-dialog.d.ts +1 -1
  53. package/dist/components/frame/dialog/lt-dialog.js +7 -7
  54. package/dist/components/frame/error-summary/error-summary.d.ts +6 -2
  55. package/dist/components/frame/error-summary/error-summary.d.ts.map +1 -1
  56. package/dist/components/frame/error-summary/error-summary.js +1 -1
  57. package/dist/components/frame/error-summary/error-summary.js.map +1 -1
  58. package/dist/components/frame/error-summary/lt-error-summary.d.ts +1 -1
  59. package/dist/components/frame/error-summary/lt-error-summary.js +6 -6
  60. package/dist/components/frame/field/field-core.d.ts +4 -2
  61. package/dist/components/frame/field/field-core.d.ts.map +1 -1
  62. package/dist/components/frame/field/field-core.js.map +1 -1
  63. package/dist/components/frame/field/lt-field.d.ts +5 -4
  64. package/dist/components/frame/field/lt-field.d.ts.map +1 -1
  65. package/dist/components/frame/field/lt-field.js +5 -4
  66. package/dist/components/frame/field/lt-field.js.map +1 -1
  67. package/dist/components/frame/popover/lt-popover.d.ts +3 -3
  68. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
  69. package/dist/components/frame/popover/lt-popover.js +7 -22
  70. package/dist/components/frame/popover/lt-popover.js.map +1 -1
  71. package/dist/components/frame/popover/popover.d.ts +7 -3
  72. package/dist/components/frame/popover/popover.d.ts.map +1 -1
  73. package/dist/components/frame/popover/popover.js +2 -2
  74. package/dist/components/frame/popover/popover.js.map +1 -1
  75. package/dist/components/frame/session-timeout/lt-session-timeout.d.ts +8 -8
  76. package/dist/components/frame/session-timeout/lt-session-timeout.js +7 -7
  77. package/dist/components/frame/session-timeout/session-timeout.d.ts +10 -5
  78. package/dist/components/frame/session-timeout/session-timeout.d.ts.map +1 -1
  79. package/dist/components/frame/session-timeout/session-timeout.js +4 -4
  80. package/dist/components/frame/session-timeout/session-timeout.js.map +1 -1
  81. package/dist/components/frame/suggestion/lt-suggestion.d.ts +12 -11
  82. package/dist/components/frame/suggestion/lt-suggestion.d.ts.map +1 -1
  83. package/dist/components/frame/suggestion/lt-suggestion.js +11 -10
  84. package/dist/components/frame/suggestion/lt-suggestion.js.map +1 -1
  85. package/dist/components/frame/suggestion/suggestion.d.ts +3 -1
  86. package/dist/components/frame/suggestion/suggestion.d.ts.map +1 -1
  87. package/dist/components/frame/suggestion/suggestion.js.map +1 -1
  88. package/dist/components/frame/tabs/lt-tabs.d.ts +3 -3
  89. package/dist/components/frame/tabs/lt-tabs.js +5 -5
  90. package/dist/components/frame/tabs/lt-tabs.js.map +1 -1
  91. package/dist/components/frame/tabs/tabs.d.ts +8 -6
  92. package/dist/components/frame/tabs/tabs.d.ts.map +1 -1
  93. package/dist/components/frame/tabs/tabs.js +4 -4
  94. package/dist/components/frame/tabs/tabs.js.map +1 -1
  95. package/dist/components/frame/term/lt-term.d.ts +114 -0
  96. package/dist/components/frame/term/lt-term.d.ts.map +1 -0
  97. package/dist/components/frame/term/lt-term.js +530 -0
  98. package/dist/components/frame/term/lt-term.js.map +1 -0
  99. package/dist/components/frame/term/term.d.ts +55 -0
  100. package/dist/components/frame/term/term.d.ts.map +1 -0
  101. package/dist/components/frame/term/term.js +55 -0
  102. package/dist/components/frame/term/term.js.map +1 -0
  103. package/dist/components/host-element.d.ts +19 -0
  104. package/dist/components/host-element.d.ts.map +1 -1
  105. package/dist/components/host-element.js +37 -0
  106. package/dist/components/host-element.js.map +1 -1
  107. package/dist/components/standalone/connection-status/lt-connection-status.d.ts +1 -1
  108. package/dist/components/standalone/connection-status/lt-connection-status.js +1 -1
  109. package/dist/components/standalone/toast/lt-toast.d.ts +1 -1
  110. package/dist/components/standalone/toast/lt-toast.js +1 -1
  111. package/dist/diagnostics/index.d.ts +2 -2
  112. package/dist/diagnostics/index.js +2 -2
  113. package/dist/dom.d.ts +2 -1
  114. package/dist/dom.d.ts.map +1 -1
  115. package/dist/dom.js +2 -1
  116. package/dist/dom.js.map +1 -1
  117. package/dist/index.d.ts +4 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/jsx/react.d.ts +3 -1
  122. package/dist/jsx/react.d.ts.map +1 -1
  123. package/dist/lotek.css +370 -1
  124. package/dist/lt.d.ts +13 -6
  125. package/dist/lt.d.ts.map +1 -1
  126. package/dist/lt.js +14 -6
  127. package/dist/lt.js.map +1 -1
  128. package/dist/react.d.ts +19 -0
  129. package/dist/react.d.ts.map +1 -1
  130. package/dist/react.js +14 -0
  131. package/dist/react.js.map +1 -1
  132. package/dist/register.d.ts.map +1 -1
  133. package/dist/register.js +2 -0
  134. package/dist/register.js.map +1 -1
  135. package/dist/vocabulary/classes.d.ts.map +1 -1
  136. package/dist/vocabulary/classes.js +60 -6
  137. package/dist/vocabulary/classes.js.map +1 -1
  138. package/dist/vocabulary/elements.d.ts.map +1 -1
  139. package/dist/vocabulary/elements.js +16 -3
  140. package/dist/vocabulary/elements.js.map +1 -1
  141. package/dist/vocabulary/types.d.ts +28 -3
  142. package/dist/vocabulary/types.d.ts.map +1 -1
  143. package/live-templates-kotlin.xml +69 -0
  144. package/live-templates.xml +29 -23
  145. package/manifest/files.json +18 -0
  146. package/manifest/manifest.json +510 -87
  147. package/manifest/manifest.schema.json +54 -7
  148. package/package.json +16 -5
  149. package/src/components/css/accordion/accordion.ts +1 -1
  150. package/src/components/css/alert/alert.ts +1 -1
  151. package/src/components/css/avatar/avatar.ts +1 -1
  152. package/src/components/css/badge/badge.ts +1 -1
  153. package/src/components/css/breadcrumbs/breadcrumbs.ts +1 -1
  154. package/src/components/css/card/card.ts +1 -1
  155. package/src/components/css/file-upload/file-upload.ts +1 -1
  156. package/src/components/css/icon/icon.ts +1 -1
  157. package/src/components/css/input/input.css +21 -0
  158. package/src/components/css/label/label.ts +1 -1
  159. package/src/components/css/link/link.ts +1 -1
  160. package/src/components/css/spinner/spinner.ts +8 -5
  161. package/src/components/css/steps/steps.css +240 -0
  162. package/src/components/css/steps/steps.ts +26 -0
  163. package/src/components/css/tag/tag.ts +1 -1
  164. package/src/components/css/toggle-group/toggle-group.ts +1 -1
  165. package/src/components/css/tooltip/tooltip.ts +2 -2
  166. package/src/components/events.ts +3 -1
  167. package/src/components/frame/dialog/dialog.ts +11 -6
  168. package/src/components/frame/dialog/lt-dialog.ts +7 -7
  169. package/src/components/frame/error-summary/error-summary.ts +6 -2
  170. package/src/components/frame/error-summary/lt-error-summary.ts +6 -6
  171. package/src/components/frame/field/field-core.ts +4 -2
  172. package/src/components/frame/field/lt-field.ts +5 -4
  173. package/src/components/frame/popover/lt-popover.ts +7 -27
  174. package/src/components/frame/popover/popover.ts +7 -3
  175. package/src/components/frame/session-timeout/lt-session-timeout.ts +8 -8
  176. package/src/components/frame/session-timeout/session-timeout.ts +10 -5
  177. package/src/components/frame/suggestion/lt-suggestion.ts +13 -12
  178. package/src/components/frame/suggestion/suggestion.css +1 -1
  179. package/src/components/frame/suggestion/suggestion.ts +3 -1
  180. package/src/components/frame/tabs/lt-tabs.ts +5 -5
  181. package/src/components/frame/tabs/tabs.css +1 -1
  182. package/src/components/frame/tabs/tabs.ts +8 -6
  183. package/src/components/frame/term/lt-term.ts +573 -0
  184. package/src/components/frame/term/term.css +177 -0
  185. package/src/components/frame/term/term.ts +87 -0
  186. package/src/components/host-element.ts +46 -0
  187. package/src/components/standalone/connection-status/lt-connection-status.ts +1 -1
  188. package/src/components/standalone/toast/lt-toast.ts +1 -1
  189. package/src/diagnostics/index.ts +2 -2
  190. package/src/dom.ts +2 -1
  191. package/src/index.ts +4 -0
  192. package/src/jsx/react.ts +4 -0
  193. package/src/lt.ts +14 -6
  194. package/src/react.ts +15 -0
  195. package/src/register.ts +2 -0
  196. package/src/tokens/boundary.css +4 -0
  197. package/src/tokens/tokens.css +4 -0
  198. package/src/vocabulary/classes.ts +60 -6
  199. package/src/vocabulary/elements.ts +16 -3
  200. package/src/vocabulary/types.ts +25 -3
  201. package/web-types.json +32 -11
@@ -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.41.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.41.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.41.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.41.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/