@mlola-ui/engine 1.0.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/LICENSE +21 -0
- package/README.md +51 -0
- package/build.mjs +123 -0
- package/generated/agents.md +369 -0
- package/generated/assets.json +729 -0
- package/generated/contract.json +1234 -0
- package/generated/foundations.css +154 -0
- package/generated/manifest.json +5 -0
- package/generated/materials.css +55 -0
- package/generated/mlola.css +10 -0
- package/generated/motion.css +93 -0
- package/generated/recipes.css +6504 -0
- package/generated/theme-spec.schema.json +199 -0
- package/generated/theme.css +1 -0
- package/generated/themes.json +258 -0
- package/generated/tokens.css +812 -0
- package/generated/tokens.json +4751 -0
- package/package.json +56 -0
- package/src/behavior-spec.mjs +291 -0
- package/src/color.mjs +150 -0
- package/src/config.mjs +170 -0
- package/src/contract.mjs +103 -0
- package/src/contrast.mjs +68 -0
- package/src/declarations.mjs +131 -0
- package/src/deprecations.mjs +29 -0
- package/src/library-recipes.mjs +105 -0
- package/src/palette.mjs +286 -0
- package/src/render.mjs +190 -0
- package/src/spec.mjs +318 -0
- package/src/spring.mjs +46 -0
- package/src/theme-css.mjs +69 -0
- package/src/theme-distance.mjs +107 -0
- package/src/theme.mjs +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mlola
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @mlola-ui/engine
|
|
2
|
+
|
|
3
|
+
Generated CSS and machine-readable metadata for Mlola UI. Framework-free: it
|
|
4
|
+
emits plain CSS and JSON, so it runs with React, plain HTML, Rails, Go, or a
|
|
5
|
+
static file, and has no runtime dependency.
|
|
6
|
+
|
|
7
|
+
The engine is the reason a model cannot hallucinate this library. Its
|
|
8
|
+
[`contract.json`](generated/contract.json) is derived from the stylesheet itself
|
|
9
|
+
and lists every class and the `data-*` / `aria-*` attributes the CSS reacts to,
|
|
10
|
+
and [`agents.md`](generated/agents.md) is generated from the same source. An
|
|
11
|
+
assistant reading them cannot invent a class that does not exist.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @mlola-ui/engine
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
@import "@mlola-ui/engine";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<html data-theme="atelier" data-mode="light">
|
|
27
|
+
<button class="ml-button" data-variant="primary">Save</button>
|
|
28
|
+
</html>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Exports
|
|
32
|
+
|
|
33
|
+
| Path | What it is |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `@mlola-ui/engine` | the ordered cascade: tokens, foundations, materials, recipes, motion |
|
|
36
|
+
| `@mlola-ui/engine/tokens.css` | theme + semantic tokens, light and dark |
|
|
37
|
+
| `@mlola-ui/engine/recipes.css` | component recipes |
|
|
38
|
+
| `@mlola-ui/engine/motion.css` | keyframes, spring easings, reduced-motion collapse |
|
|
39
|
+
| `@mlola-ui/engine/contract.json` | every class and the attributes it reacts to |
|
|
40
|
+
| `@mlola-ui/engine/tokens.json` | DTCG token document |
|
|
41
|
+
|
|
42
|
+
## Build
|
|
43
|
+
|
|
44
|
+
The engine is generated by a Node 20 script, so the output is reproducible:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node build.mjs
|
|
48
|
+
node build.mjs --check # fails when generated output is stale
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
MIT licensed. Part of [Mlola UI](https://ui.mlola.com).
|
package/build.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { profiles } from "./src/config.mjs";
|
|
5
|
+
import { renderContract, renderProContract } from "./src/contract.mjs";
|
|
6
|
+
import { renderCatalogCss } from "./src/library-recipes.mjs";
|
|
7
|
+
import { renderSpecSchema } from "./src/spec.mjs";
|
|
8
|
+
import { renderThemeCss, renderThemeManifest } from "./src/theme.mjs";
|
|
9
|
+
import {
|
|
10
|
+
renderDtcg,
|
|
11
|
+
renderFoundationsCss,
|
|
12
|
+
renderMaterialsCss,
|
|
13
|
+
renderMotionCss,
|
|
14
|
+
renderRecipesCss,
|
|
15
|
+
renderTokensCss,
|
|
16
|
+
} from "./src/render.mjs";
|
|
17
|
+
|
|
18
|
+
const engineDirectory = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const packagesDirectory = dirname(engineDirectory);
|
|
20
|
+
const repositoryDirectory = dirname(packagesDirectory);
|
|
21
|
+
const generatedDirectory = join(engineDirectory, "generated");
|
|
22
|
+
const tokenGeneratedDirectory = join(packagesDirectory, "tokens", "generated");
|
|
23
|
+
const check = process.argv.includes("--check");
|
|
24
|
+
|
|
25
|
+
const layers = {
|
|
26
|
+
"tokens.css": renderTokensCss(),
|
|
27
|
+
"foundations.css": renderFoundationsCss(),
|
|
28
|
+
"materials.css": renderMaterialsCss(),
|
|
29
|
+
"recipes.css": renderRecipesCss(),
|
|
30
|
+
"motion.css": renderMotionCss(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const layerOrder = "mlola.tokens, mlola.foundations, mlola.materials, mlola.recipes, mlola.motion, mlola.accessibility";
|
|
34
|
+
const aggregate = `/* Generated by @mlola-ui/engine. React and Tailwind are not required. */
|
|
35
|
+
@layer ${layerOrder};
|
|
36
|
+
@import "./tokens.css" layer(mlola.tokens);
|
|
37
|
+
/* The project theme is a token source too, imported last within the same
|
|
38
|
+
layer so a brand wins over every canonical theme. */
|
|
39
|
+
@import "./theme.css" layer(mlola.tokens);
|
|
40
|
+
@import "./foundations.css" layer(mlola.foundations);
|
|
41
|
+
@import "./materials.css" layer(mlola.materials);
|
|
42
|
+
@import "./recipes.css" layer(mlola.recipes);
|
|
43
|
+
@import "./motion.css" layer(mlola.motion);
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const dtcg = `${JSON.stringify(renderDtcg(), null, 2)}\n`;
|
|
47
|
+
const manifest = `${JSON.stringify({
|
|
48
|
+
generatedAt: "deterministic",
|
|
49
|
+
generator: "@mlola-ui/engine",
|
|
50
|
+
profileCount: Object.keys(profiles).length,
|
|
51
|
+
}, null, 2)}\n`;
|
|
52
|
+
|
|
53
|
+
const outputs = new Map();
|
|
54
|
+
for (const [name, contents] of Object.entries(layers)) {
|
|
55
|
+
outputs.set(join(generatedDirectory, name), `${contents.trim()}\n`);
|
|
56
|
+
outputs.set(join(tokenGeneratedDirectory, name), `${contents.trim()}\n`);
|
|
57
|
+
}
|
|
58
|
+
outputs.set(join(generatedDirectory, "mlola.css"), aggregate);
|
|
59
|
+
outputs.set(join(tokenGeneratedDirectory, "mlola.css"), aggregate);
|
|
60
|
+
// A project theme is optional. When present it is compiled alongside the
|
|
61
|
+
// canonical themes and selects on the same data-theme contract.
|
|
62
|
+
const themeFile = join(repositoryDirectory, "mlola.theme.json");
|
|
63
|
+
let projectTheme = "";
|
|
64
|
+
try {
|
|
65
|
+
projectTheme = renderThemeCss(JSON.parse(await readFile(themeFile, "utf8")));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error?.code !== "ENOENT") {
|
|
68
|
+
throw new Error(`mlola.theme.json is present but unusable: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
outputs.set(join(generatedDirectory, "theme.css"), projectTheme || "/* No mlola.theme.json in this project. */\n");
|
|
72
|
+
|
|
73
|
+
const themeManifest = `${JSON.stringify(
|
|
74
|
+
{
|
|
75
|
+
version: 1,
|
|
76
|
+
description:
|
|
77
|
+
"Themes compiled into this build. A project theme from mlola.theme.json appears here alongside the canonical ones.",
|
|
78
|
+
themes: renderThemeManifest(
|
|
79
|
+
projectTheme ? JSON.parse(await readFile(themeFile, "utf8")) : null,
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2,
|
|
84
|
+
)}\n`;
|
|
85
|
+
outputs.set(join(generatedDirectory, "themes.json"), themeManifest);
|
|
86
|
+
|
|
87
|
+
// The spec schema is what editors, the CLI, and models validate a theme against.
|
|
88
|
+
outputs.set(join(generatedDirectory, "theme-spec.schema.json"), `${JSON.stringify(renderSpecSchema(), null, 2)}\n`);
|
|
89
|
+
|
|
90
|
+
// The commercial catalog's styles are assembled here but written outside the
|
|
91
|
+
// engine package, so publishing the engine can never ship them.
|
|
92
|
+
outputs.set(join(packagesDirectory, "registry", "catalog", "generated", "catalog.css"), renderCatalogCss());
|
|
93
|
+
|
|
94
|
+
const contract = `${JSON.stringify(renderContract(), null, 2)}\n`;
|
|
95
|
+
outputs.set(join(generatedDirectory, "contract.json"), contract);
|
|
96
|
+
// Pro's half of the contract stays beside the Pro catalog, out of the published engine.
|
|
97
|
+
outputs.set(join(packagesDirectory, "registry", "catalog", "generated", "contract.json"), `${JSON.stringify(renderProContract(), null, 2)}\n`);
|
|
98
|
+
outputs.set(join(generatedDirectory, "tokens.json"), dtcg);
|
|
99
|
+
outputs.set(join(packagesDirectory, "tokens", "tokens.json"), dtcg);
|
|
100
|
+
outputs.set(join(generatedDirectory, "manifest.json"), manifest);
|
|
101
|
+
|
|
102
|
+
const changed = [];
|
|
103
|
+
for (const [path, contents] of outputs) {
|
|
104
|
+
let previous = null;
|
|
105
|
+
try {
|
|
106
|
+
previous = await readFile(path, "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
// Missing output is a change.
|
|
109
|
+
}
|
|
110
|
+
if (previous === contents) continue;
|
|
111
|
+
changed.push(path.replace(`${repositoryDirectory}/`, ""));
|
|
112
|
+
if (!check) {
|
|
113
|
+
await mkdir(dirname(path), { recursive: true });
|
|
114
|
+
await writeFile(path, contents);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (check && changed.length) {
|
|
119
|
+
console.error(`Generated output is stale:\n${changed.map((path) => `- ${path}`).join("\n")}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
} else {
|
|
122
|
+
console.log(check ? "Generated output is current." : `Generated ${outputs.size} files.`);
|
|
123
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Mlola UI for code generation
|
|
2
|
+
|
|
3
|
+
Generated from the source of truth. Do not edit by hand.
|
|
4
|
+
|
|
5
|
+
## Read this first
|
|
6
|
+
|
|
7
|
+
- **Do not import a styling framework.** The library ships its own CSS. There
|
|
8
|
+
is no Tailwind, no CSS-in-JS and no class name utility to install.
|
|
9
|
+
- **Style through the classes below, never through invented ones.** A class
|
|
10
|
+
that is not in this document does not exist. (Mlola Pro's classes are in the
|
|
11
|
+
guide that comes with Pro source.)
|
|
12
|
+
- **Behaviour is optional and framework-free.** `@mlola-ui/behavior` attaches
|
|
13
|
+
to markup you already rendered. It never renders anything itself, so it works
|
|
14
|
+
with React, Svelte, Vue, Rails, a Go template or a static file.
|
|
15
|
+
- **Native controls stay native.** Checkbox and radio are real `<input>`
|
|
16
|
+
elements. Do not reimplement them.
|
|
17
|
+
|
|
18
|
+
## The shortest correct example
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<link rel="stylesheet" href="@mlola-ui/engine" />
|
|
22
|
+
|
|
23
|
+
<div data-theme="graphite" data-mode="light">
|
|
24
|
+
<button class="ml-button" data-variant="primary">Save</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<script type="module">
|
|
28
|
+
import { observe } from "@mlola-ui/behavior";
|
|
29
|
+
observe();
|
|
30
|
+
</script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Themes
|
|
34
|
+
|
|
35
|
+
Set `data-theme` and `data-mode` on any ancestor. Nothing else changes.
|
|
36
|
+
|
|
37
|
+
| id | name |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `graphite` | Graphite |
|
|
40
|
+
| `atelier` | Atelier Umami |
|
|
41
|
+
| `machined` | Machined Titanium |
|
|
42
|
+
| `aerogel` | Aerogel Glass |
|
|
43
|
+
| `nordic` | Nordic Earth |
|
|
44
|
+
|
|
45
|
+
`data-mode` is `light` or `dark`.
|
|
46
|
+
|
|
47
|
+
A project defines its own theme in one file, `mlola.theme.json`, which
|
|
48
|
+
overrides fonts, colours, geometry, the spacing and type scale, and any
|
|
49
|
+
custom property through `extend`. See `mlola.theme.example.json`.
|
|
50
|
+
|
|
51
|
+
## Elements and their attributes
|
|
52
|
+
|
|
53
|
+
Emit these classes and attributes from any language and the visuals are
|
|
54
|
+
correct. This table is read out of the stylesheet, so it is never stale.
|
|
55
|
+
|
|
56
|
+
| class | attribute | allowed values |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `.ml-accordion-item` | `data-state` | `open` |
|
|
59
|
+
| `.ml-alert` | `data-state` | `closing` |
|
|
60
|
+
| `.ml-alert` | `data-tone` | `danger`, `info`, `neutral`, `success`, `warning` |
|
|
61
|
+
| `.ml-alert` | `data-variant` | `soft` |
|
|
62
|
+
| `.ml-app-shell` | `data-narrow` | _presence only_ |
|
|
63
|
+
| `.ml-avatar-root` | `data-size` | `lg`, `sm`, `xl`, `xs` |
|
|
64
|
+
| `.ml-avatar-status` | `data-status` | `away`, `busy`, `online` |
|
|
65
|
+
| `.ml-badge` | `data-size` | `lg`, `sm` |
|
|
66
|
+
| `.ml-badge` | `data-tone` | `danger`, `info`, `primary`, `success`, `warning` |
|
|
67
|
+
| `.ml-badge` | `data-variant` | `outline`, `solid` |
|
|
68
|
+
| `.ml-breadcrumb-item` | `data-collapse-indicator` | _presence only_ |
|
|
69
|
+
| `.ml-breadcrumb-item` | `data-collapsible` | _presence only_ |
|
|
70
|
+
| `.ml-button` | `aria-disabled` | `true` |
|
|
71
|
+
| `.ml-button` | `aria-pressed` | `true` |
|
|
72
|
+
| `.ml-button` | `data-loading` | _presence only_ |
|
|
73
|
+
| `.ml-button` | `data-size` | `icon`, `lg`, `sm` |
|
|
74
|
+
| `.ml-button` | `data-state` | `success` |
|
|
75
|
+
| `.ml-button` | `data-variant` | `danger`, `link`, `outline`, `primary`, `secondary`, `subtle` |
|
|
76
|
+
| `.ml-calendar-cell` | `data-preview` | _presence only_ |
|
|
77
|
+
| `.ml-calendar-cell` | `data-range` | `end`, `start` |
|
|
78
|
+
| `.ml-calendar-day` | `aria-disabled` | `true` |
|
|
79
|
+
| `.ml-calendar-day` | `data-outside` | _presence only_ |
|
|
80
|
+
| `.ml-calendar-day` | `data-selected` | _presence only_ |
|
|
81
|
+
| `.ml-calendar-day` | `data-today` | _presence only_ |
|
|
82
|
+
| `.ml-calendar-day` | `data-unreachable` | _presence only_ |
|
|
83
|
+
| `.ml-card` | `data-interactive` | _presence only_ |
|
|
84
|
+
| `.ml-card` | `data-variant` | `elevated`, `glass`, `specular` |
|
|
85
|
+
| `.ml-carousel` | `data-playing` | _presence only_ |
|
|
86
|
+
| `.ml-carousel-arrow` | `data-side` | `next`, `previous` |
|
|
87
|
+
| `.ml-carousel-dot` | `aria-current` | `true` |
|
|
88
|
+
| `.ml-chart-bar` | `data-highlighted` | `true` |
|
|
89
|
+
| `.ml-checkbox-field` | `data-state` | `checked`, `indeterminate` |
|
|
90
|
+
| `.ml-circular-progress` | `data-size` | `lg`, `sm` |
|
|
91
|
+
| `.ml-circular-progress` | `data-state` | `complete`, `indeterminate` |
|
|
92
|
+
| `.ml-circular-progress` | `data-tone` | `danger`, `info`, `success`, `warning` |
|
|
93
|
+
| `.ml-color-picker-preset` | `aria-pressed` | `true` |
|
|
94
|
+
| `.ml-combobox-control` | `data-open` | _presence only_ |
|
|
95
|
+
| `.ml-combobox-input` | `data-clearable` | _presence only_ |
|
|
96
|
+
| `.ml-combobox-input` | `data-leading` | _presence only_ |
|
|
97
|
+
| `.ml-combobox-option` | `aria-disabled` | `true` |
|
|
98
|
+
| `.ml-combobox-option` | `aria-selected` | `true` |
|
|
99
|
+
| `.ml-combobox-option` | `data-create` | _presence only_ |
|
|
100
|
+
| `.ml-combobox-option` | `data-highlighted` | _presence only_ |
|
|
101
|
+
| `.ml-combobox-popover` | `data-side` | `top` |
|
|
102
|
+
| `.ml-command-item` | `aria-disabled` | _presence only_ |
|
|
103
|
+
| `.ml-command-item` | `aria-selected` | `true` |
|
|
104
|
+
| `.ml-context-menu-item` | `aria-disabled` | `true` |
|
|
105
|
+
| `.ml-context-menu-item` | `data-danger` | _presence only_ |
|
|
106
|
+
| `.ml-context-menu-item` | `data-highlighted` | _presence only_ |
|
|
107
|
+
| `.ml-date-picker-panel` | `data-presets` | _presence only_ |
|
|
108
|
+
| `.ml-date-picker-trigger` | `data-empty` | _presence only_ |
|
|
109
|
+
| `.ml-dropdown-item` | `data-danger` | _presence only_ |
|
|
110
|
+
| `.ml-dropdown-item` | `data-highlighted` | _presence only_ |
|
|
111
|
+
| `.ml-dropdown-menu` | `data-align` | `end` |
|
|
112
|
+
| `.ml-dropdown-trigger` | `aria-expanded` | `true` |
|
|
113
|
+
| `.ml-dropzone` | `aria-disabled` | _presence only_ |
|
|
114
|
+
| `.ml-dropzone` | `data-dragging` | _presence only_ |
|
|
115
|
+
| `.ml-dropzone-file` | `data-status` | `error` |
|
|
116
|
+
| `.ml-empty` | `data-size` | `page` |
|
|
117
|
+
| `.ml-filter-chip` | `aria-pressed` | `true` |
|
|
118
|
+
| `.ml-filter-chip` | `data-state` | `active` |
|
|
119
|
+
| `.ml-form-message` | `data-tone` | `danger`, `success` |
|
|
120
|
+
| `.ml-grid` | `data-columns` | `1`, `2`, `3`, `4`, `6` |
|
|
121
|
+
| `.ml-grid` | `data-layout` | `3-col`, `4-col`, `list` |
|
|
122
|
+
| `.ml-heatmap` | `data-tone` | `info`, `success`, `warning` |
|
|
123
|
+
| `.ml-heatmap-cell` | `data-active` | _presence only_ |
|
|
124
|
+
| `.ml-heatmap-cell` | `data-level` | `1`, `2`, `3`, `4` |
|
|
125
|
+
| `.ml-heatmap-cell` | `data-outside` | _presence only_ |
|
|
126
|
+
| `.ml-input` | `aria-invalid` | `true` |
|
|
127
|
+
| `.ml-input` | `data-size` | `lg`, `sm` |
|
|
128
|
+
| `.ml-input` | `data-variant` | `filled`, `subtle` |
|
|
129
|
+
| `.ml-input-control` | `data-leading` | _presence only_ |
|
|
130
|
+
| `.ml-input-control` | `data-trailing` | _presence only_ |
|
|
131
|
+
| `.ml-input-field` | `data-invalid` | _presence only_ |
|
|
132
|
+
| `.ml-modal` | `data-size` | `full`, `lg`, `sm`, `xl` |
|
|
133
|
+
| `.ml-number-input-control` | `data-disabled` | _presence only_ |
|
|
134
|
+
| `.ml-number-input-control` | `data-invalid` | _presence only_ |
|
|
135
|
+
| `.ml-otp` | `data-invalid` | _presence only_ |
|
|
136
|
+
| `.ml-pagination-button` | `aria-current` | `page` |
|
|
137
|
+
| `.ml-pagination-button` | `data-state` | `active` |
|
|
138
|
+
| `.ml-popover` | `data-side` | `bottom`, `left`, `right`, `top` |
|
|
139
|
+
| `.ml-priority-icon` | `data-priority` | `high`, `none`, `urgent` |
|
|
140
|
+
| `.ml-progress-root` | `data-active` | _presence only_ |
|
|
141
|
+
| `.ml-progress-root` | `data-size` | `lg`, `sm` |
|
|
142
|
+
| `.ml-progress-root` | `data-state` | `complete`, `indeterminate` |
|
|
143
|
+
| `.ml-progress-root` | `data-tone` | `danger`, `info`, `success`, `warning` |
|
|
144
|
+
| `.ml-radio-group` | `data-orientation` | `horizontal` |
|
|
145
|
+
| `.ml-radio-item` | `data-state` | `checked` |
|
|
146
|
+
| `.ml-resizable` | `data-anchor` | `second` |
|
|
147
|
+
| `.ml-resizable` | `data-direction` | `horizontal`, `vertical` |
|
|
148
|
+
| `.ml-resizable-pane` | `data-folded` | _presence only_ |
|
|
149
|
+
| `.ml-segmented-control-item` | `aria-pressed` | `true` |
|
|
150
|
+
| `.ml-segmented-control-item` | `data-state` | `active` |
|
|
151
|
+
| `.ml-select` | `data-size` | `lg`, `sm` |
|
|
152
|
+
| `.ml-select` | `data-state` | `open` |
|
|
153
|
+
| `.ml-select-option` | `data-disabled` | _presence only_ |
|
|
154
|
+
| `.ml-select-option` | `data-highlighted` | _presence only_ |
|
|
155
|
+
| `.ml-select-option` | `data-state` | `checked` |
|
|
156
|
+
| `.ml-select-popover` | `data-side` | `top` |
|
|
157
|
+
| `.ml-select-root` | `data-invalid` | _presence only_ |
|
|
158
|
+
| `.ml-select-value` | `data-placeholder` | _presence only_ |
|
|
159
|
+
| `.ml-sheet-panel` | `data-side` | `bottom`, `left`, `right`, `top` |
|
|
160
|
+
| `.ml-sheet-panel` | `data-size` | `lg`, `sm` |
|
|
161
|
+
| `.ml-sidebar-item` | `aria-current` | `page` |
|
|
162
|
+
| `.ml-sidebar-title` | `aria-expanded` | `false` |
|
|
163
|
+
| `.ml-sidebar-title` | `data-collapsible` | _presence only_ |
|
|
164
|
+
| `.ml-skeleton` | `data-rounded` | `none` |
|
|
165
|
+
| `.ml-slider-field` | `data-size` | `sm` |
|
|
166
|
+
| `.ml-status-icon` | `data-status` | `backlog`, `canceled`, `done`, `in-progress`, `in-review` |
|
|
167
|
+
| `.ml-stepper` | `data-orientation` | `horizontal`, `vertical` |
|
|
168
|
+
| `.ml-stepper-step` | `data-status` | `complete`, `current`, `error`, `upcoming` |
|
|
169
|
+
| `.ml-table` | `data-size` | `sm` |
|
|
170
|
+
| `.ml-table` | `data-stripe` | `alternate` |
|
|
171
|
+
| `.ml-table` | `data-striped` | _presence only_ |
|
|
172
|
+
| `.ml-table-row` | `data-state` | `selected` |
|
|
173
|
+
| `.ml-tabs` | `data-orientation` | `vertical` |
|
|
174
|
+
| `.ml-tabs` | `data-variant` | `enclosed`, `pills` |
|
|
175
|
+
| `.ml-tabs-trigger` | `aria-selected` | `true` |
|
|
176
|
+
| `.ml-tabs-trigger` | `data-state` | `active` |
|
|
177
|
+
| `.ml-tag-input-control` | `data-disabled` | _presence only_ |
|
|
178
|
+
| `.ml-tag-input-control` | `data-invalid` | _presence only_ |
|
|
179
|
+
| `.ml-tag-input-tag` | `data-armed` | _presence only_ |
|
|
180
|
+
| `.ml-textarea-count` | `data-full` | _presence only_ |
|
|
181
|
+
| `.ml-time-picker-control` | `data-disabled` | _presence only_ |
|
|
182
|
+
| `.ml-time-picker-control` | `data-unavailable` | _presence only_ |
|
|
183
|
+
| `.ml-time-picker-option` | `aria-selected` | `true` |
|
|
184
|
+
| `.ml-time-picker-segment` | `data-empty` | _presence only_ |
|
|
185
|
+
| `.ml-time-picker-segment` | `data-period` | _presence only_ |
|
|
186
|
+
| `.ml-timeline-item` | `data-status` | `complete`, `current`, `upcoming` |
|
|
187
|
+
| `.ml-timeline-marker` | `data-status` | `complete`, `current`, `upcoming` |
|
|
188
|
+
| `.ml-timeline-marker` | `data-tone` | `danger`, `info`, `primary`, `success`, `warning` |
|
|
189
|
+
| `.ml-toast` | `data-has-description` | _presence only_ |
|
|
190
|
+
| `.ml-toast` | `data-swiping` | _presence only_ |
|
|
191
|
+
| `.ml-toast` | `data-tone` | `danger`, `info`, `success`, `warning` |
|
|
192
|
+
| `.ml-toast-slot` | `data-state` | `closed` |
|
|
193
|
+
| `.ml-toaster` | `data-position` | `bottom-center`, `bottom-right`, `top-center`, `top-right` |
|
|
194
|
+
| `.ml-toggle` | `aria-checked` | `true` |
|
|
195
|
+
| `.ml-toggle` | `data-size` | `lg`, `sm` |
|
|
196
|
+
| `.ml-toggle` | `data-state` | `checked` |
|
|
197
|
+
| `.ml-toggle-button` | `aria-pressed` | `true` |
|
|
198
|
+
| `.ml-toggle-button` | `data-state` | `on` |
|
|
199
|
+
| `.ml-tooltip` | `data-side` | `bottom`, `left`, `right`, `top` |
|
|
200
|
+
| `.ml-tour-button` | `data-primary` | _presence only_ |
|
|
201
|
+
| `.ml-tour-card` | `data-centred` | _presence only_ |
|
|
202
|
+
| `.ml-tour-dot` | `data-active` | _presence only_ |
|
|
203
|
+
| `.ml-tour-scrim` | `data-spotlight` | _presence only_ |
|
|
204
|
+
|
|
205
|
+
## Behaviour
|
|
206
|
+
|
|
207
|
+
### accordion
|
|
208
|
+
|
|
209
|
+
Disclosure list. One panel open at a time, or several.
|
|
210
|
+
|
|
211
|
+
- Root: `.ml-accordion`
|
|
212
|
+
- Parts: item `.ml-accordion-item`, trigger `.ml-accordion-trigger`, panel `.ml-accordion-panel`
|
|
213
|
+
- ARIA on trigger: `role` — button (a real <button> is preferred); `aria-expanded` — true when the item is open; `aria-controls` — id of the panel
|
|
214
|
+
- ARIA on panel: `role` — region; `aria-labelledby` — id of the trigger
|
|
215
|
+
- Keyboard:
|
|
216
|
+
- <kbd>Enter</kbd> / <kbd>Space</kbd>: Toggle the focused item.
|
|
217
|
+
- <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd>: Move focus between triggers.
|
|
218
|
+
- <kbd>Home</kbd> / <kbd>End</kbd>: Focus the first or last trigger.
|
|
219
|
+
- State changes:
|
|
220
|
+
- On click or activate a trigger, set data-state on the item, trigger and panel to open, or closed when it was open and collapsing is allowed. Also: hide the closed panel from the accessibility tree.
|
|
221
|
+
|
|
222
|
+
### tabs
|
|
223
|
+
|
|
224
|
+
One panel visible at a time, selected by a tab strip.
|
|
225
|
+
|
|
226
|
+
- Root: `.ml-tabs`
|
|
227
|
+
- Parts: list `.ml-tabs-list`, trigger `.ml-tabs-trigger`, content `.ml-tabs-content`
|
|
228
|
+
- ARIA on list: `role` — tablist; `aria-orientation` — matches data-orientation
|
|
229
|
+
- ARIA on trigger: `role` — tab; `aria-selected` — true on the active tab; `aria-controls` — id of the panel; `tabindex` — 0 on the active tab, -1 on the rest
|
|
230
|
+
- ARIA on content: `role` — tabpanel; `aria-labelledby` — id of the tab
|
|
231
|
+
- Keyboard:
|
|
232
|
+
- <kbd>ArrowRight</kbd> / <kbd>ArrowLeft</kbd>: Move to the next or previous enabled tab when horizontal, wrapping around.
|
|
233
|
+
- <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd>: The same when vertical.
|
|
234
|
+
- <kbd>Home</kbd> / <kbd>End</kbd>: Move to the first or last enabled tab.
|
|
235
|
+
- State changes:
|
|
236
|
+
- On select a tab by pointer or key, set data-state and aria-selected on triggers, data-state on panels to active for the chosen pair, inactive for the rest. Also: move focus to the newly selected tab.
|
|
237
|
+
- Note: Selection follows focus. Disabled tabs are skipped, never focused.
|
|
238
|
+
|
|
239
|
+
### dropdown-menu
|
|
240
|
+
|
|
241
|
+
A menu anchored to a trigger.
|
|
242
|
+
|
|
243
|
+
- Root: `.ml-dropdown`
|
|
244
|
+
- Parts: trigger `.ml-dropdown-trigger`, menu `.ml-dropdown-menu`, item `.ml-dropdown-item`
|
|
245
|
+
- ARIA on trigger: `aria-haspopup` — menu; `aria-expanded` — true while open
|
|
246
|
+
- ARIA on menu: `role` — menu
|
|
247
|
+
- ARIA on item: `role` — menuitem; `data-highlighted` — present on the active item
|
|
248
|
+
- Keyboard:
|
|
249
|
+
- <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd>: Open the menu, then move the highlight.
|
|
250
|
+
- <kbd>Enter</kbd> / <kbd>Space</kbd>: Activate the highlighted item.
|
|
251
|
+
- <kbd>Escape</kbd>: Close and return focus to the trigger.
|
|
252
|
+
- <kbd>Tab</kbd>: Close without activating.
|
|
253
|
+
- State changes:
|
|
254
|
+
- On click the trigger, set data-state to open or closed.
|
|
255
|
+
- On pointer over an item, set data-highlighted to that item only.
|
|
256
|
+
- On pointer down outside the root, set data-state to closed.
|
|
257
|
+
- Note: Disabled items are skipped by the highlight and cannot be activated.
|
|
258
|
+
|
|
259
|
+
### select
|
|
260
|
+
|
|
261
|
+
A listbox behind a combobox trigger.
|
|
262
|
+
|
|
263
|
+
- Root: `.ml-select-root`
|
|
264
|
+
- Parts: trigger `.ml-select`, popover `.ml-select-popover`, list `.ml-select-list`, option `.ml-select-option`, search `.ml-select-search`
|
|
265
|
+
- ARIA on trigger: `role` — combobox; `aria-expanded` — true while open; `aria-controls` — id of the listbox; `aria-activedescendant` — id of the highlighted option while open
|
|
266
|
+
- ARIA on list: `role` — listbox; `aria-multiselectable` — true when multiple
|
|
267
|
+
- ARIA on option: `role` — option; `aria-selected` — true when chosen; `data-highlighted` — present on the active option
|
|
268
|
+
- Keyboard:
|
|
269
|
+
- <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd>: Open, then move the highlight past disabled options.
|
|
270
|
+
- <kbd>Enter</kbd> / <kbd>Space</kbd>: Choose the highlighted option. Space types when a search field has focus.
|
|
271
|
+
- <kbd>Home</kbd> / <kbd>End</kbd>: Highlight the first or last enabled option.
|
|
272
|
+
- <kbd>Escape</kbd>: Close and return focus to the trigger.
|
|
273
|
+
- <kbd>Tab</kbd>: Close without choosing.
|
|
274
|
+
- State changes:
|
|
275
|
+
- On choose an option, set aria-selected and data-state on options to checked for the chosen option. Also: single select closes and restores focus; multiple select stays open.
|
|
276
|
+
|
|
277
|
+
### modal
|
|
278
|
+
|
|
279
|
+
A dialog over the page that owns focus while open.
|
|
280
|
+
|
|
281
|
+
- Root: `.ml-modal`
|
|
282
|
+
- Parts: overlay `.ml-modal-overlay`, close `.ml-modal-close`
|
|
283
|
+
- ARIA on root: `role` — dialog; `aria-modal` — true; `aria-labelledby` — id of the title, or aria-label
|
|
284
|
+
- Keyboard:
|
|
285
|
+
- <kbd>Escape</kbd>: Close, unless closing on Escape is disabled.
|
|
286
|
+
- <kbd>Tab</kbd> / <kbd>Shift+Tab</kbd>: Cycle focus inside the dialog and never leave it.
|
|
287
|
+
- State changes:
|
|
288
|
+
- On open, set focus to the first focusable element inside. Also: lock page scroll.
|
|
289
|
+
- On close, set focus to the element that opened the dialog. Also: release page scroll.
|
|
290
|
+
- On pointer down on the backdrop, set closed to unless closing on backdrop is disabled.
|
|
291
|
+
|
|
292
|
+
### sheet
|
|
293
|
+
|
|
294
|
+
A dialog anchored to one edge of the viewport.
|
|
295
|
+
|
|
296
|
+
- Root: `.ml-sheet-panel`
|
|
297
|
+
- Parts: overlay `.ml-sheet-overlay`, close `.ml-sheet-close`
|
|
298
|
+
- ARIA on root: `role` — dialog; `aria-modal` — true
|
|
299
|
+
- Keyboard:
|
|
300
|
+
- <kbd>Escape</kbd>: Close, unless closing on Escape is disabled.
|
|
301
|
+
- <kbd>Tab</kbd> / <kbd>Shift+Tab</kbd>: Cycle focus inside the panel.
|
|
302
|
+
- State changes:
|
|
303
|
+
- On open, set focus to inside the panel. Also: lock page scroll.
|
|
304
|
+
- On close, set focus to the trigger. Also: release page scroll.
|
|
305
|
+
- Note: Identical to modal apart from which edge it is anchored to.
|
|
306
|
+
|
|
307
|
+
### tooltip
|
|
308
|
+
|
|
309
|
+
A short label shown on hover or focus.
|
|
310
|
+
|
|
311
|
+
- Root: `.ml-tooltip-root`
|
|
312
|
+
- Parts: tooltip `.ml-tooltip`, arrow `.ml-tooltip-arrow`
|
|
313
|
+
- ARIA on tooltip: `role` — tooltip
|
|
314
|
+
- ARIA on trigger: `aria-describedby` — id of the tooltip while it is open
|
|
315
|
+
- Keyboard:
|
|
316
|
+
- <kbd>Escape</kbd>: Hide the tooltip.
|
|
317
|
+
- State changes:
|
|
318
|
+
- On pointer enter or focus the trigger, set visible to after the delay.
|
|
319
|
+
- On pointer leave or blur, set hidden to immediately, cancelling any pending delay.
|
|
320
|
+
- Note: Never put essential information or interactive content in a tooltip.
|
|
321
|
+
|
|
322
|
+
### toast
|
|
323
|
+
|
|
324
|
+
Transient messages in a live region.
|
|
325
|
+
|
|
326
|
+
- Root: `.ml-toaster`
|
|
327
|
+
- Parts: toast `.ml-toast`, action `.ml-toast-action`, close `.ml-toast-close`
|
|
328
|
+
- ARIA on root: `role` — region; `aria-label` — distinct per region, so several are distinguishable
|
|
329
|
+
- ARIA on toast: `role` — status for ordinary messages, alert for danger and warning; `aria-live` — polite, or assertive for danger and warning; `aria-busy` — true while it waits on work (a loading toast)
|
|
330
|
+
- Keyboard:
|
|
331
|
+
- <kbd>Tab</kbd>: Reach the action and dismiss controls.
|
|
332
|
+
- State changes:
|
|
333
|
+
- On push, set a toast into the region matching its position to visible.
|
|
334
|
+
- On duration elapsed, set removed to unless the duration is zero.
|
|
335
|
+
|
|
336
|
+
### switch
|
|
337
|
+
|
|
338
|
+
An on/off control that is not a native checkbox.
|
|
339
|
+
|
|
340
|
+
- Root: `.ml-toggle`
|
|
341
|
+
- Parts: thumb `.ml-toggle-thumb`
|
|
342
|
+
- ARIA on root: `role` — switch; `aria-checked` — true or false
|
|
343
|
+
- Keyboard:
|
|
344
|
+
- <kbd>Enter</kbd> / <kbd>Space</kbd>: Toggle.
|
|
345
|
+
- State changes:
|
|
346
|
+
- On activate, set aria-checked and data-state to the opposite value.
|
|
347
|
+
- Note: Prefer a native checkbox unless the control genuinely reads as a switch.
|
|
348
|
+
|
|
349
|
+
### slider
|
|
350
|
+
|
|
351
|
+
A single value chosen from a range.
|
|
352
|
+
|
|
353
|
+
- Root: `.ml-slider-field`
|
|
354
|
+
- Parts: control `.ml-slider`, track `.ml-slider-track`, range `.ml-slider-range`, thumb `.ml-slider-thumb`
|
|
355
|
+
- ARIA on thumb: `role` — slider; `aria-valuenow` — current value; `aria-valuemin` — minimum; `aria-valuemax` — maximum; `tabindex` — 0 unless disabled
|
|
356
|
+
- Keyboard:
|
|
357
|
+
- <kbd>ArrowRight</kbd> / <kbd>ArrowUp</kbd>: Increase by one step.
|
|
358
|
+
- <kbd>ArrowLeft</kbd> / <kbd>ArrowDown</kbd>: Decrease by one step.
|
|
359
|
+
- <kbd>Home</kbd> / <kbd>End</kbd>: Jump to the minimum or maximum.
|
|
360
|
+
- <kbd>PageUp</kbd> / <kbd>PageDown</kbd>: Move by a larger step.
|
|
361
|
+
- State changes:
|
|
362
|
+
- On pointer down on the track, or drag the thumb, set the value from the pointer position, snapped to the step to within min and max.
|
|
363
|
+
|
|
364
|
+
## If you are unsure
|
|
365
|
+
|
|
366
|
+
Prefer emitting less. A plain `<button class="ml-button">` is correct; a
|
|
367
|
+
button with a variant this document does not list is not. When a component
|
|
368
|
+
needs behaviour, mark its root with `data-ml="<behaviour name>"` and let the
|
|
369
|
+
runtime attach, rather than writing event handlers that guess at the contract.
|