@lo-tek/interfaces 0.40.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 (82) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/agent/astro.md +9 -7
  3. package/agent/bundles.md +8 -6
  4. package/agent/datastar.md +53 -51
  5. package/agent/html.md +55 -53
  6. package/agent/kotlin.md +622 -0
  7. package/agent/react.md +8 -6
  8. package/agent/templates.md +60 -63
  9. package/core/lotek-core.wasm +0 -0
  10. package/core/lotek.wasm +0 -0
  11. package/dist/components/css/steps/steps.d.ts +23 -0
  12. package/dist/components/css/steps/steps.d.ts.map +1 -0
  13. package/dist/components/css/steps/steps.js +21 -0
  14. package/dist/components/css/steps/steps.js.map +1 -0
  15. package/dist/components/events.d.ts +3 -1
  16. package/dist/components/events.d.ts.map +1 -1
  17. package/dist/components/frame/error-summary/lt-error-summary.js +1 -1
  18. package/dist/components/frame/popover/lt-popover.d.ts.map +1 -1
  19. package/dist/components/frame/popover/lt-popover.js +4 -19
  20. package/dist/components/frame/popover/lt-popover.js.map +1 -1
  21. package/dist/components/frame/term/lt-term.d.ts +114 -0
  22. package/dist/components/frame/term/lt-term.d.ts.map +1 -0
  23. package/dist/components/frame/term/lt-term.js +530 -0
  24. package/dist/components/frame/term/lt-term.js.map +1 -0
  25. package/dist/components/frame/term/term.d.ts +55 -0
  26. package/dist/components/frame/term/term.d.ts.map +1 -0
  27. package/dist/components/frame/term/term.js +55 -0
  28. package/dist/components/frame/term/term.js.map +1 -0
  29. package/dist/components/host-element.d.ts +19 -0
  30. package/dist/components/host-element.d.ts.map +1 -1
  31. package/dist/components/host-element.js +37 -0
  32. package/dist/components/host-element.js.map +1 -1
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/jsx/react.d.ts +3 -1
  38. package/dist/jsx/react.d.ts.map +1 -1
  39. package/dist/lotek.css +351 -1
  40. package/dist/lt.d.ts +8 -1
  41. package/dist/lt.d.ts.map +1 -1
  42. package/dist/lt.js +9 -1
  43. package/dist/lt.js.map +1 -1
  44. package/dist/react.d.ts +19 -0
  45. package/dist/react.d.ts.map +1 -1
  46. package/dist/react.js +14 -0
  47. package/dist/react.js.map +1 -1
  48. package/dist/register.d.ts.map +1 -1
  49. package/dist/register.js +2 -0
  50. package/dist/register.js.map +1 -1
  51. package/dist/vocabulary/classes.d.ts.map +1 -1
  52. package/dist/vocabulary/classes.js +60 -6
  53. package/dist/vocabulary/classes.js.map +1 -1
  54. package/dist/vocabulary/elements.d.ts.map +1 -1
  55. package/dist/vocabulary/elements.js +15 -2
  56. package/dist/vocabulary/elements.js.map +1 -1
  57. package/dist/vocabulary/types.d.ts +8 -3
  58. package/dist/vocabulary/types.d.ts.map +1 -1
  59. package/live-templates-kotlin.xml +15 -10
  60. package/live-templates.xml +12 -6
  61. package/manifest/files.json +18 -0
  62. package/manifest/manifest.json +145 -10
  63. package/package.json +15 -5
  64. package/src/components/css/steps/steps.css +240 -0
  65. package/src/components/css/steps/steps.ts +26 -0
  66. package/src/components/events.ts +3 -1
  67. package/src/components/frame/error-summary/lt-error-summary.ts +1 -1
  68. package/src/components/frame/popover/lt-popover.ts +4 -24
  69. package/src/components/frame/term/lt-term.ts +573 -0
  70. package/src/components/frame/term/term.css +177 -0
  71. package/src/components/frame/term/term.ts +87 -0
  72. package/src/components/host-element.ts +46 -0
  73. package/src/index.ts +4 -0
  74. package/src/jsx/react.ts +4 -0
  75. package/src/lt.ts +9 -1
  76. package/src/react.ts +15 -0
  77. package/src/register.ts +2 -0
  78. package/src/tokens/tokens.css +4 -0
  79. package/src/vocabulary/classes.ts +60 -6
  80. package/src/vocabulary/elements.ts +15 -2
  81. package/src/vocabulary/types.ts +8 -3
  82. package/web-types.json +31 -10
@@ -0,0 +1,177 @@
1
+ @layer lotek {
2
+ /* The word. It is a link, but reads as part of the sentence: the colour
3
+ is the text's, and the dotted line says it has an explanation. */
4
+ .lt-term {
5
+ color: inherit;
6
+ text-decoration-line: underline;
7
+ text-decoration-style: dotted;
8
+ text-decoration-color: var(
9
+ --lt-term-underline-color,
10
+ var(--lt-color-accent-fill)
11
+ );
12
+ text-decoration-thickness: 0.1em;
13
+ text-underline-offset: 0.2em;
14
+ }
15
+
16
+ /* Only where there is a pointer that hovers. iOS keeps `:hover` on a
17
+ link after a tap, and the line stayed solid until the next tap
18
+ somewhere else. */
19
+ @media (hover: hover) {
20
+ .lt-term:hover {
21
+ text-decoration-style: solid;
22
+ }
23
+ }
24
+
25
+ /* The word whose card is open, so the reader sees what the card belongs
26
+ to. It matters most on a narrow screen, where the card is a sheet at
27
+ the edge of the window and not next to the word. */
28
+ .lt-term[aria-expanded="true"] {
29
+ text-decoration-style: solid;
30
+ background: var(--lt-color-accent-surface);
31
+ border-radius: var(--lt-spacing-0-5);
32
+ }
33
+
34
+ .lt-term:focus-visible {
35
+ outline: var(--lt-focus-ring);
36
+ outline-offset: 2px;
37
+ border-radius: var(--lt-spacing-0-5);
38
+ }
39
+
40
+ /* The card is the glossary entry itself, in the top layer while
41
+ `<lt-term>` has it open. Every rule stands on `:popover-open`, so the
42
+ entry looks as the page made it everywhere else. */
43
+ .lt-term__entry:popover-open {
44
+ /* The position is relative to the viewport. The values are set by the
45
+ web component, which works them out against the word. */
46
+ position: fixed;
47
+ inset: auto;
48
+ inset-block-start: var(--lt-term-top, 0);
49
+ inset-inline-start: var(--lt-term-left, 0);
50
+ margin: 0;
51
+ /* The space between the word and the card. The web component reads it
52
+ from here, where the browser has turned it into pixels, and places the
53
+ card with it. The card is never scrolled to, so the property does
54
+ nothing else. */
55
+ scroll-margin-block-start: var(--lt-term-gap, var(--lt-spacing-2));
56
+ box-sizing: border-box;
57
+ inline-size: max-content;
58
+ max-inline-size: var(
59
+ --lt-term-width,
60
+ min(22rem, calc(100vw - var(--lt-spacing-8)))
61
+ );
62
+ padding: var(--lt-term-padding, var(--lt-spacing-3) var(--lt-spacing-4));
63
+ font-family: inherit;
64
+ font-size: var(--lt-font-size-m);
65
+ line-height: var(--lt-line-height-default);
66
+ color: var(--lt-color-neutral-text-strong);
67
+ background: var(--lt-color-neutral-surface);
68
+ border: 1px solid var(--lt-color-neutral-border-strong);
69
+ border-inline-start: var(--lt-spacing-1) solid
70
+ var(--lt-term-accent-color, var(--lt-color-accent-fill));
71
+ border-radius: var(--lt-term-radius, var(--lt-spacing-1));
72
+ box-shadow: var(--lt-shadow-overlay);
73
+ /* An explanation taller than the window scrolls in the card rather
74
+ than running off the screen. */
75
+ max-block-size: calc(100dvh - var(--lt-spacing-4));
76
+ overflow-y: auto;
77
+ /* Scrolling to the end of the explanation stops there. Otherwise it
78
+ went on to the page behind, the word left the view, and the card
79
+ closed while the reader was still in it. */
80
+ overscroll-behavior: contain;
81
+ }
82
+
83
+ /* `--lt-term-left` is the distance from the left edge of the viewport.
84
+ `inset-inline-start` is the right edge in a right-to-left document, so
85
+ there it is flipped, as on the popover. */
86
+ .lt-term__entry:popover-open:dir(rtl) {
87
+ inset-inline-start: auto;
88
+ inset-inline-end: var(--lt-term-left, 0);
89
+ }
90
+
91
+ /* On a narrow screen the card is a sheet across the window, at the
92
+ bottom, or at the top when the word is down where the sheet would
93
+ cover it. A card by the word covered the lines the reader was about to
94
+ read, and closing it meant reaching for the middle of the screen.
95
+ `--lt-term-sheet` tells the web component which form the card has; the
96
+ stylesheet is the one place that decides. The width is `sm` in the
97
+ breakpoints in `tokens.ts`: a media query cannot read a variable. */
98
+ @media (width < 640px) {
99
+ .lt-term__entry:popover-open {
100
+ --lt-term-sheet: 1;
101
+ inset-inline: 0;
102
+ inline-size: auto;
103
+ max-inline-size: none;
104
+ max-block-size: var(--lt-term-sheet-height, 60dvh);
105
+ padding-block-end: calc(
106
+ var(--lt-spacing-3) +
107
+ env(safe-area-inset-bottom, 0px)
108
+ );
109
+ border-radius: 0;
110
+ border-inline-end: none;
111
+ }
112
+
113
+ /* The direction does not matter across the whole width. Without this
114
+ the rule above for right to left won, and the sheet hung from the
115
+ word. */
116
+ .lt-term__entry:popover-open:dir(rtl) {
117
+ inset-inline: 0;
118
+ }
119
+ }
120
+
121
+ /* The label over the term. CSS cannot import a text, so it is read from a
122
+ variable with the English text as the fallback. */
123
+ .lt-term__entry:popover-open::before {
124
+ content: var(--lt-term-label-text, "Glossary");
125
+ display: block;
126
+ margin-block-end: var(--lt-spacing-1);
127
+ font-family: var(--lt-term-label-font, inherit);
128
+ font-size: var(--lt-font-size-xs);
129
+ line-height: var(--lt-line-height-compact);
130
+ letter-spacing: 0.08em;
131
+ text-transform: uppercase;
132
+ color: var(--lt-color-accent-text);
133
+ }
134
+
135
+ .lt-term__entry:popover-open > dt {
136
+ margin: 0;
137
+ font-weight: var(--lt-font-weight-semibold);
138
+ }
139
+
140
+ .lt-term__entry:popover-open > dd {
141
+ margin: 0;
142
+ }
143
+
144
+ @starting-style {
145
+ .lt-term__entry:popover-open {
146
+ opacity: 0;
147
+ }
148
+ }
149
+
150
+ @media (prefers-reduced-motion: no-preference) {
151
+ .lt-term__entry:popover-open {
152
+ transition: opacity 120ms ease;
153
+ }
154
+ }
155
+
156
+ /* More contrast: the line under the word is solid and in the text's own
157
+ colour, and the card's edge is as strong as the text. */
158
+ @media (prefers-contrast: more) {
159
+ .lt-term {
160
+ text-decoration-style: solid;
161
+ text-decoration-color: currentColor;
162
+ }
163
+
164
+ .lt-term__entry:popover-open {
165
+ border-block-color: var(--lt-color-neutral-text-strong);
166
+ border-inline-end-color: var(--lt-color-neutral-text-strong);
167
+ }
168
+ }
169
+
170
+ /* High contrast: the shadow is removed and the surface is the page's, so
171
+ the edge is what holds the card apart from the text under it. */
172
+ @media (forced-colors: active) {
173
+ .lt-term__entry:popover-open {
174
+ border-color: CanvasText;
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,87 @@
1
+ import { attributes } from "../../css/shared.js"
2
+
3
+ export const TERM_CLASS = "lt-term" as const
4
+ export const TERM_ENTRY_CLASS = "lt-term__entry" as const
5
+
6
+ export type TermOptions = {
7
+ /**
8
+ * Id of the entry in the glossary: the `<div>` around the `<dt>` and
9
+ * `<dd>`. The word links to it, and the web component shows the entry
10
+ * itself as the card. Make it from the term, such as `term-signal`, so it
11
+ * is the same on every render.
12
+ */
13
+ id: string
14
+ }
15
+
16
+ export type TermAttributes = {
17
+ word: {
18
+ class: typeof TERM_CLASS
19
+ href: string
20
+ "aria-details": string
21
+ }
22
+ entry: {
23
+ class: typeof TERM_ENTRY_CLASS
24
+ id: string
25
+ }
26
+ }
27
+
28
+ /**
29
+ * The attributes for a word in the text that is explained in a glossary on
30
+ * the same page.
31
+ *
32
+ * Only what holds without JavaScript: the word is a link to the entry, and
33
+ * `aria-details` says the entry explains it. `<lt-term>` around the link
34
+ * opens the entry as a card by the word, and writes `aria-expanded`.
35
+ *
36
+ * ```ts
37
+ * const signal = lt.term({ id: "term-signal" })
38
+ * ```
39
+ * ```html
40
+ * <p>Datastar keeps <lt-term><a {...signal.word}>signals</a></lt-term> in the browser.</p>
41
+ * <dl>
42
+ * <div {...signal.entry}><dt>Signal</dt><dd>A value Datastar keeps in the browser.</dd></div>
43
+ * </dl>
44
+ * ```
45
+ */
46
+ export function term({ id }: TermOptions): TermAttributes {
47
+ // A made-up id would point at nothing: the entry is in the glossary, and
48
+ // only the template knows its id.
49
+ if (typeof id !== "string" || id.trim() === "") {
50
+ throw new Error(
51
+ "lt.term(): id is required. It is the id of the entry in the glossary, " +
52
+ 'such as "term-signal".',
53
+ )
54
+ }
55
+ return {
56
+ word: attributes({
57
+ class: TERM_CLASS,
58
+ href: `#${id}`,
59
+ "aria-details": id,
60
+ }),
61
+ entry: attributes({
62
+ class: TERM_ENTRY_CLASS,
63
+ id,
64
+ }),
65
+ }
66
+ }
67
+
68
+ /**
69
+ * `lt.term()` with the state `<lt-term>` writes, for `/react`.
70
+ *
71
+ * React reports a mismatch for every attribute a web component writes on an
72
+ * element React has rendered. `aria-expanded="false"` is there from the
73
+ * start, so the web component has nothing to write before React hydrates.
74
+ */
75
+ export function termWithState(options: TermOptions): {
76
+ word: TermAttributes["word"] & { "aria-expanded": "false" }
77
+ entry: TermAttributes["entry"]
78
+ } {
79
+ const built = term(options)
80
+ return {
81
+ word: attributes(
82
+ { ...built.word, "aria-expanded": "false" as const },
83
+ built.word,
84
+ ),
85
+ entry: built.entry,
86
+ }
87
+ }
@@ -587,3 +587,49 @@ export async function whenUpgraded<T extends HTMLElement>(
587
587
  customElements.upgrade(element)
588
588
  return element as T
589
589
  }
590
+
591
+ /** The space between a panel and the window's edge. */
592
+ const PLACE_EDGE = 4
593
+
594
+ /**
595
+ * Where a panel in the top layer goes next to the element that opened it,
596
+ * as distances from the window's top and left edge.
597
+ *
598
+ * `below` asks for the panel under the anchor, otherwise over it, and
599
+ * `alignRight` lines the panel's right edge up with the anchor's. `gap` is
600
+ * the space between the anchor and the panel. The panel must not end up off
601
+ * screen. If it goes out at the side, it is moved in. If there is no room on
602
+ * the side of the anchor that was asked for, it goes on the other side, and
603
+ * is then clamped into the viewport either way.
604
+ *
605
+ * `<lt-popover>` and `<lt-term>` place their panels with it, and write the
606
+ * answer as two variables the stylesheet reads. It lives here so a copy from
607
+ * `lotek takeover` finds it through `@lo-tek/interfaces/host-element`.
608
+ */
609
+ export function place(
610
+ anchor: DOMRect,
611
+ panel: DOMRect,
612
+ below: boolean,
613
+ alignRight: boolean,
614
+ gap = 4,
615
+ ): { top: number; left: number } {
616
+ // The viewport a fixed box lives in, without a classic scrollbar:
617
+ // `innerWidth` counts it, and a panel pushed in from the right edge sat
618
+ // under the scrollbar on Windows and Linux.
619
+ const width = document.documentElement.clientWidth || window.innerWidth
620
+ const height = document.documentElement.clientHeight || window.innerHeight
621
+ let top = below ? anchor.bottom + gap : anchor.top - panel.height - gap
622
+ let left = alignRight ? anchor.right - panel.width : anchor.left
623
+
624
+ // `top-*` lacked the vertical clamp once, and a panel at the top of the
625
+ // page lay entirely off screen, with no way to scroll it into view.
626
+ left = Math.max(PLACE_EDGE, Math.min(left, width - panel.width - PLACE_EDGE))
627
+ if (below && top + panel.height > height) {
628
+ top = anchor.top - panel.height - gap
629
+ } else if (!below && top < PLACE_EDGE) {
630
+ top = anchor.bottom + gap
631
+ }
632
+ top = Math.max(PLACE_EDGE, Math.min(top, height - panel.height - PLACE_EDGE))
633
+
634
+ return { top: Math.round(top), left: Math.round(left) }
635
+ }
package/src/index.ts CHANGED
@@ -155,6 +155,8 @@ export type {
155
155
  export { spinner } from "./components/css/spinner/spinner.js"
156
156
  export type { SrOnlyAttributes } from "./components/css/sr-only/sr-only.js"
157
157
  export { srOnly } from "./components/css/sr-only/sr-only.js"
158
+ export type { StepsAttributes } from "./components/css/steps/steps.js"
159
+ export { steps } from "./components/css/steps/steps.js"
158
160
  export type {
159
161
  SwitchAttributes,
160
162
  SwitchOptions,
@@ -207,6 +209,8 @@ export * from "./components/frame/suggestion/lt-suggestion.js"
207
209
  export * from "./components/frame/suggestion/suggestion.js"
208
210
  export * from "./components/frame/tabs/lt-tabs.js"
209
211
  export * from "./components/frame/tabs/tabs.js"
212
+ export * from "./components/frame/term/lt-term.js"
213
+ export * from "./components/frame/term/term.js"
210
214
  export * from "./components/standalone/connection-status/connection-status.js"
211
215
  export * from "./components/standalone/connection-status/lt-connection-status.js"
212
216
  export * from "./components/standalone/toast/lt-toast.js"
package/src/jsx/react.ts CHANGED
@@ -158,6 +158,8 @@ type LtPopoverAttributes = Host &
158
158
  placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end"
159
159
  }
160
160
 
161
+ type LtTermAttributes = Host
162
+
161
163
  type LtSessionTimeoutAttributes = Host & {
162
164
  /** Seconds without activity before the warning appears. */
163
165
  "warn-at"?: number | string
@@ -192,6 +194,7 @@ declare module "react" {
192
194
  "lt-popover": LtPopoverAttributes
193
195
  "lt-suggestion": LtSuggestionAttributes
194
196
  "lt-tabs": LtTabsAttributes
197
+ "lt-term": LtTermAttributes
195
198
  "lt-session-timeout": LtSessionTimeoutAttributes
196
199
  "lt-connection-status": LtConnectionStatusAttributes
197
200
  "lt-toast": LtToastAttributes
@@ -208,5 +211,6 @@ export type {
208
211
  LtSessionTimeoutAttributes,
209
212
  LtSuggestionAttributes,
210
213
  LtTabsAttributes,
214
+ LtTermAttributes,
211
215
  LtToastAttributes,
212
216
  }
package/src/lt.ts CHANGED
@@ -33,6 +33,7 @@ import { skeleton } from "./components/css/skeleton/skeleton.js"
33
33
  import { skipLink } from "./components/css/skip-link/skip-link.js"
34
34
  import { spinner } from "./components/css/spinner/spinner.js"
35
35
  import { srOnly } from "./components/css/sr-only/sr-only.js"
36
+ import { steps } from "./components/css/steps/steps.js"
36
37
  import { switchControl } from "./components/css/switch/switch.js"
37
38
  import { table, tableSort } from "./components/css/table/table.js"
38
39
  import { tag } from "./components/css/tag/tag.js"
@@ -46,6 +47,7 @@ import { popover } from "./components/frame/popover/popover.js"
46
47
  import { sessionTimeout } from "./components/frame/session-timeout/session-timeout.js"
47
48
  import { suggestion } from "./components/frame/suggestion/suggestion.js"
48
49
  import { tabs } from "./components/frame/tabs/tabs.js"
50
+ import { term } from "./components/frame/term/term.js"
49
51
  import { connectionStatus } from "./components/standalone/connection-status/connection-status.js"
50
52
  import { toast } from "./components/standalone/toast/toast.js"
51
53
  import { setAttributes } from "./dom.js"
@@ -103,7 +105,7 @@ export const lt = {
103
105
  * <label {...field.label}>Fixer's address</label>
104
106
  * <input {...lt.input({ type: "email", state: field.state })} {...field.control} />
105
107
  * <p {...lt.helpText()} {...field.help}>The board writes to the fixer, never to the crew.</p>
106
- * <p {...lt.errorText()} {...field.error}>Enter a valid address.</p>
108
+ * <p {...lt.errorText()} {...field.error}>Enter the fixer's address, for example fixer@relay.example.</p>
107
109
  * ```
108
110
  *
109
111
  * The same calculation `<lt-field>` does in the browser, but as data, for
@@ -143,6 +145,7 @@ export const lt = {
143
145
  spinner,
144
146
  /** Text only screen readers get. Requires `sr-only.css`. */
145
147
  srOnly,
148
+ steps,
146
149
  /**
147
150
  * The field and the suggestion list. `<lt-suggestion>` handles the
148
151
  * keyboard and the filtering, the server writes the markup.
@@ -169,6 +172,11 @@ export const lt = {
169
172
  */
170
173
  tableSort,
171
174
  tag,
175
+ /**
176
+ * A word in the text, linked to its entry in a glossary on the same page.
177
+ * `<lt-term>` around the link shows the entry as a card by the word.
178
+ */
179
+ term,
172
180
  textarea,
173
181
 
174
182
  /**
package/src/react.ts CHANGED
@@ -35,6 +35,7 @@ import { skeleton } from "./components/css/skeleton/skeleton.js"
35
35
  import { skipLink } from "./components/css/skip-link/skip-link.js"
36
36
  import { spinner } from "./components/css/spinner/spinner.js"
37
37
  import { srOnly } from "./components/css/sr-only/sr-only.js"
38
+ import { steps } from "./components/css/steps/steps.js"
38
39
  import { switchControl } from "./components/css/switch/switch.js"
39
40
  import { table, tableSort } from "./components/css/table/table.js"
40
41
  import { tag } from "./components/css/tag/tag.js"
@@ -55,6 +56,7 @@ import {
55
56
  type TabsWithStateOptions,
56
57
  tabsWithState,
57
58
  } from "./components/frame/tabs/tabs.js"
59
+ import { termWithState } from "./components/frame/term/term.js"
58
60
  import { connectionStatus } from "./components/standalone/connection-status/connection-status.js"
59
61
  import { toast } from "./components/standalone/toast/toast.js"
60
62
  import { type Attributes, setAttributes as setHtmlAttributes } from "./dom.js"
@@ -261,6 +263,7 @@ export const lt = {
261
263
  progress: forReact(progress),
262
264
  spinner: forReact(spinner),
263
265
  srOnly: forReact(srOnly),
266
+ steps: forReact(steps),
264
267
  switch: forReact(switchControl),
265
268
  toggleGroup: forReact(toggleGroup),
266
269
  tooltip: forReact(tooltip),
@@ -354,6 +357,18 @@ export const lt = {
354
357
  }
355
358
  },
356
359
 
360
+ /**
361
+ * With `aria-expanded`, which `<lt-term>` writes: React reports a mismatch
362
+ * for an attribute it did not render. See `termWithState`.
363
+ */
364
+ term: (options: Parameters<typeof termWithState>[0]) => {
365
+ const built = termWithState(options)
366
+ return {
367
+ word: toReactAttributes(built.word),
368
+ entry: toReactAttributes(built.entry),
369
+ }
370
+ },
371
+
357
372
  suggestion: (options: Parameters<typeof suggestion>[0]) => {
358
373
  const built = suggestion(options)
359
374
  return {
package/src/register.ts CHANGED
@@ -20,6 +20,7 @@ import { defineLtPopover } from "./components/frame/popover/lt-popover.js"
20
20
  import { defineLtSessionTimeout } from "./components/frame/session-timeout/lt-session-timeout.js"
21
21
  import { defineLtSuggestion } from "./components/frame/suggestion/lt-suggestion.js"
22
22
  import { defineLtTabs } from "./components/frame/tabs/lt-tabs.js"
23
+ import { defineLtTerm } from "./components/frame/term/lt-term.js"
23
24
  import { defineLtConnectionStatus } from "./components/standalone/connection-status/lt-connection-status.js"
24
25
  import { defineLtToast } from "./components/standalone/toast/lt-toast.js"
25
26
 
@@ -40,4 +41,5 @@ export function defineLt(): void {
40
41
  defineLtToast()
41
42
  defineLtSessionTimeout()
42
43
  defineLtConnectionStatus()
44
+ defineLtTerm()
43
45
  }
@@ -527,6 +527,7 @@
527
527
  .lt-skip-link,
528
528
  .lt-spinner,
529
529
  .lt-sr-only,
530
+ .lt-steps,
530
531
  .lt-suggestion__empty,
531
532
  .lt-suggestion__list,
532
533
  .lt-suggestion__option,
@@ -539,6 +540,8 @@
539
540
  .lt-tabs__list,
540
541
  .lt-tabs__panel,
541
542
  .lt-tag,
543
+ .lt-term,
544
+ .lt-term__entry,
542
545
  .lt-textarea,
543
546
  .lt-toast,
544
547
  .lt-toast__close,
@@ -551,6 +554,7 @@
551
554
  lt-tabs,
552
555
  lt-error-summary,
553
556
  lt-popover,
557
+ lt-term,
554
558
  lt-suggestion,
555
559
  lt-dialog,
556
560
  lt-toast,
@@ -757,6 +757,31 @@ export const classes: Classes = {
757
757
  "content": "text",
758
758
  "attributes": {}
759
759
  },
760
+ "lt-steps": {
761
+ "component": "steps",
762
+ "title": "Steps",
763
+ "description": "A step indicator that shows where the user is in a task over several pages. A CSS component with one class and no JavaScript.",
764
+ "link": "https://lo-tek.sobernetics.no/components/steps/",
765
+ "content": "parts",
766
+ "parts": {
767
+ "step": {
768
+ "selector": "li",
769
+ "description": "One step, with its name as text, or a link back to it. The step the user is on has `aria-current=\"step\"`, and the steps before it are drawn as done.",
770
+ "content": "free",
771
+ "multiple": true
772
+ }
773
+ },
774
+ "texts": [
775
+ {
776
+ "variable": "--lt-steps-done-text",
777
+ "text": "Completed ",
778
+ "attribute": "aria-current",
779
+ "value": "step",
780
+ "siblingsBefore": true
781
+ }
782
+ ],
783
+ "attributes": {}
784
+ },
760
785
  "lt-switch": {
761
786
  "component": "switch",
762
787
  "title": "Switch",
@@ -962,7 +987,7 @@ export const classes: Classes = {
962
987
  "lt-dialog": {
963
988
  "component": "dialog",
964
989
  "title": "Dialog",
965
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
990
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
966
991
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
967
992
  "content": "free",
968
993
  "parts": {
@@ -1013,7 +1038,7 @@ export const classes: Classes = {
1013
1038
  "lt-dialog__body": {
1014
1039
  "component": "dialog",
1015
1040
  "title": "Dialog",
1016
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
1041
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
1017
1042
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
1018
1043
  "content": "free",
1019
1044
  "partOf": "lt-dialog",
@@ -1022,7 +1047,7 @@ export const classes: Classes = {
1022
1047
  "lt-dialog__title": {
1023
1048
  "component": "dialog",
1024
1049
  "title": "Dialog",
1025
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
1050
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
1026
1051
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
1027
1052
  "content": "text",
1028
1053
  "partOf": "lt-dialog",
@@ -1031,7 +1056,7 @@ export const classes: Classes = {
1031
1056
  "lt-dialog__footer": {
1032
1057
  "component": "dialog",
1033
1058
  "title": "Dialog",
1034
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
1059
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
1035
1060
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
1036
1061
  "content": "free",
1037
1062
  "partOf": "lt-dialog",
@@ -1040,7 +1065,7 @@ export const classes: Classes = {
1040
1065
  "lt-dialog__header": {
1041
1066
  "component": "dialog",
1042
1067
  "title": "Dialog",
1043
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
1068
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
1044
1069
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
1045
1070
  "content": "parts",
1046
1071
  "partOf": "lt-dialog",
@@ -1062,7 +1087,7 @@ export const classes: Classes = {
1062
1087
  "lt-dialog__subtitle": {
1063
1088
  "component": "dialog",
1064
1089
  "title": "Dialog",
1065
- "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for servers that cannot call showModal().",
1090
+ "description": "Modal dialog built on the native dialog element. Six classes and no JavaScript, with an optional component for when the server decides that the dialog is open.",
1066
1091
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
1067
1092
  "content": "text",
1068
1093
  "partOf": "lt-dialog",
@@ -1305,6 +1330,35 @@ export const classes: Classes = {
1305
1330
  "content": "free",
1306
1331
  "attributes": {}
1307
1332
  },
1333
+ "lt-term": {
1334
+ "component": "term",
1335
+ "title": "Term",
1336
+ "description": "A word in the text that shows its entry from a glossary on the same page, in a card by the word. Frame component.",
1337
+ "link": "https://lo-tek.sobernetics.no/components/term/",
1338
+ "content": "text",
1339
+ "attributes": {}
1340
+ },
1341
+ "lt-term__entry": {
1342
+ "component": "term",
1343
+ "title": "Term",
1344
+ "description": "A word in the text that shows its entry from a glossary on the same page, in a card by the word. Frame component.",
1345
+ "link": "https://lo-tek.sobernetics.no/components/term/",
1346
+ "content": "parts",
1347
+ "parts": {
1348
+ "term": {
1349
+ "selector": "dt",
1350
+ "description": "The term, as it stands in the glossary.",
1351
+ "content": "text",
1352
+ "unique": true
1353
+ },
1354
+ "description": {
1355
+ "selector": "dd",
1356
+ "description": "The explanation. It can hold links and code.",
1357
+ "content": "free"
1358
+ }
1359
+ },
1360
+ "attributes": {}
1361
+ },
1308
1362
  "lt-connection-status": {
1309
1363
  "component": "connection-status",
1310
1364
  "title": "Connection Status",
@@ -184,6 +184,19 @@ export const elements: Elements = {
184
184
  }
185
185
  }
186
186
  },
187
+ "lt-term": {
188
+ "link": "https://lo-tek.sobernetics.no/components/term/",
189
+ "description": "A word in the text that shows its glossary entry in a card by the word. A link to the entry's id is enough; the component sets the class, `aria-details` and `aria-expanded` on the link, and shows the entry itself in the top layer while the card is open. Without JavaScript the word is a link to the glossary.",
190
+ "attributes": {},
191
+ "parts": {
192
+ "word": {
193
+ "selector": "a[href^='#']",
194
+ "description": "The word: a link to the glossary entry, such as `<a href=\"#term-signal\">`. The entry is a `<div>` with that id in a `<dl>`, with a `<dt>` and a `<dd>`, anywhere on the page.",
195
+ "content": "text",
196
+ "unique": true
197
+ }
198
+ }
199
+ },
187
200
  "lt-suggestion": {
188
201
  "link": "https://lo-tek.sobernetics.no/components/suggestion/",
189
202
  "description": "Field with a suggestion list. A `<label for>`, an `<input id>`, an `.lt-suggestion__list` with `<li>` items and a `[role=status]` are enough; the component sets the roles and the wiring where the markup came without them, and gives the list and the options ids. The field never gets an id from the component. Filters as the user types, moves the highlight with the arrow keys and announces the number of matches.",
@@ -310,7 +323,7 @@ export const elements: Elements = {
310
323
  },
311
324
  "lt-dialog": {
312
325
  "link": "https://lo-tek.sobernetics.no/components/dialog/",
313
- "description": "Makes a `<dialog class=\"lt-dialog\">` modal with `showModal()`, for servers that cannot call it themselves. The host around the dialog. Gives the dialog its name from the first heading outside `lt-dialog__body` when neither `aria-labelledby` nor `aria-label` is there.",
326
+ "description": "Makes a `<dialog class=\"lt-dialog\">` the server sends open a real modal, with `showModal()`. The host around the dialog. Gives the dialog its name from the first heading outside `lt-dialog__body` when neither `aria-labelledby` nor `aria-label` is there.",
314
327
  "attributes": {
315
328
  "open": {
316
329
  "type": "flag",
@@ -393,7 +406,7 @@ export const elements: Elements = {
393
406
  },
394
407
  "lt-session-timeout": {
395
408
  "link": "https://lo-tek.sobernetics.no/components/session-timeout/",
396
- "description": "Warns before a signed-in session expires, and counts down. The renderer writes the dialog and the text inside the element: a `<dialog>` with `.lt-session-timeout__count` for the number, `[role=status]` for the announcement and the buttons in a `<form method=\"dialog\">`. Dispatches `session-warn` when the dialog opens, `session-extend` when the user wants to continue, `session-logout` when the user logs out, `session-expired` when time is up, and `session-activity` at most once per `activity-interval` while the user is active, so the app can keep the server session alive.",
409
+ "description": "Warns before a signed-in session expires, and counts down. The renderer writes the dialog and the text inside the element: a `<dialog>` with `.lt-session-timeout__count` for the number, `[role=status]` for the announcement and the buttons in a `<form method=\"dialog\">`. Dispatches `session-warn` when the dialog opens, `session-extend` when the user wants to continue, `session-logout` when the user logs out with a button that closes the dialog, `session-expired` when time is up, and `session-activity` at most once per `activity-interval` while the user is active, so the app can keep the server session alive.",
397
410
  "attributes": {
398
411
  "warn-at": {
399
412
  "type": "number",
@@ -90,9 +90,13 @@ export type Element = {
90
90
  export type DefaultText = { text: string; replacedBy: readonly string[] }
91
91
 
92
92
  /**
93
- * A text the stylesheet shows in `::after` on an element with the class and
94
- * `attribute`, with `value` when the selector requires one, and none of the
95
- * attributes in `unless`. `variable` is the CSS variable that replaces it.
93
+ * A text the stylesheet shows on an element with the class and `attribute`,
94
+ * with `value` when the selector requires one, and none of the attributes in
95
+ * `unless`. With `siblingsBefore`,
96
+ * the attribute is on a child of the element with the class, and the text
97
+ * is shown on the children before it, as "Completed " is on the steps before
98
+ * the one with `aria-current="step"` in `lt-steps`. `variable` is the CSS
99
+ * variable that replaces it.
96
100
  */
97
101
  export type CssText = {
98
102
  variable: string
@@ -100,6 +104,7 @@ export type CssText = {
100
104
  attribute: string
101
105
  value?: string
102
106
  unless?: readonly string[]
107
+ siblingsBefore?: true
103
108
  }
104
109
 
105
110
  /** The content of `elements.ts`: tag to element. */