@mlola-ui/engine 1.0.2 → 1.0.3
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/build.mjs +3 -0
- package/generated/accessibility.css +9 -0
- package/generated/agents.md +90 -6
- package/generated/assets.json +2 -2
- package/generated/contract.json +2 -2
- package/generated/foundations.css +19 -2
- package/generated/mlola.css +1 -0
- package/generated/recipes.css +47 -29
- package/generated/theme-spec.schema.json +1 -1
- package/generated/tokens.css +50 -0
- package/package.json +2 -1
- package/src/behavior-spec.mjs +2 -2
- package/src/color.mjs +7 -7
- package/src/config.mjs +1 -1
- package/src/contract.mjs +1 -1
- package/src/contrast.mjs +2 -2
- package/src/declarations.mjs +15 -1
- package/src/library-recipes.mjs +1 -1
- package/src/palette.mjs +10 -10
- package/src/render.mjs +4 -0
- package/src/spec.mjs +6 -6
- package/src/theme-css.mjs +1 -1
package/build.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { renderCatalogCss } from "./src/library-recipes.mjs";
|
|
|
7
7
|
import { renderSpecSchema } from "./src/spec.mjs";
|
|
8
8
|
import { renderThemeCss, renderThemeManifest } from "./src/theme.mjs";
|
|
9
9
|
import {
|
|
10
|
+
renderAccessibilityCss,
|
|
10
11
|
renderDtcg,
|
|
11
12
|
renderFoundationsCss,
|
|
12
13
|
renderMaterialsCss,
|
|
@@ -28,6 +29,7 @@ const layers = {
|
|
|
28
29
|
"materials.css": renderMaterialsCss(),
|
|
29
30
|
"recipes.css": renderRecipesCss(),
|
|
30
31
|
"motion.css": renderMotionCss(),
|
|
32
|
+
"accessibility.css": renderAccessibilityCss(),
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const layerOrder = "mlola.tokens, mlola.foundations, mlola.materials, mlola.recipes, mlola.motion, mlola.accessibility";
|
|
@@ -41,6 +43,7 @@ const aggregate = `/* Generated by @mlola-ui/engine. React and Tailwind are not
|
|
|
41
43
|
@import "./materials.css" layer(mlola.materials);
|
|
42
44
|
@import "./recipes.css" layer(mlola.recipes);
|
|
43
45
|
@import "./motion.css" layer(mlola.motion);
|
|
46
|
+
@import "./accessibility.css" layer(mlola.accessibility);
|
|
44
47
|
`;
|
|
45
48
|
|
|
46
49
|
const dtcg = `${JSON.stringify(renderDtcg(), null, 2)}\n`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Accessibility layer: last in the cascade, so these guarantees hold over any recipe. */
|
|
2
|
+
/* On a touch screen, a text field smaller than 16px makes iOS zoom the page
|
|
3
|
+
when it takes focus, and the page stays zoomed after. Every field that
|
|
4
|
+
takes typing is at least 16px there; everything else keeps its size. */
|
|
5
|
+
@media (pointer: coarse) {
|
|
6
|
+
:is(input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"], [type="submit"], [type="button"], [type="reset"], [type="image"], [type="hidden"]), textarea, select, [contenteditable]:not([contenteditable="false"])) {
|
|
7
|
+
font-size: max(1rem, 1em);
|
|
8
|
+
}
|
|
9
|
+
}
|
package/generated/agents.md
CHANGED
|
@@ -9,7 +9,7 @@ Generated from the source of truth. Do not edit by hand.
|
|
|
9
9
|
- **Style through the classes below, never through invented ones.** A class
|
|
10
10
|
that is not in this document does not exist. (Mlola Pro's classes are in the
|
|
11
11
|
guide that comes with Pro source.)
|
|
12
|
-
- **
|
|
12
|
+
- **Behavior is optional and framework-free.** `@mlola-ui/behavior` attaches
|
|
13
13
|
to markup you already rendered. It never renders anything itself, so it works
|
|
14
14
|
with React, Svelte, Vue, Rails, a Go template or a static file.
|
|
15
15
|
- **Native controls stay native.** Checkbox and radio are real `<input>`
|
|
@@ -45,9 +45,93 @@ Set `data-theme` and `data-mode` on any ancestor. Nothing else changes.
|
|
|
45
45
|
`data-mode` is `light` or `dark`.
|
|
46
46
|
|
|
47
47
|
A project defines its own theme in one file, `mlola.theme.json`, which
|
|
48
|
-
overrides fonts,
|
|
48
|
+
overrides fonts, colors, geometry, the spacing and type scale, and any
|
|
49
49
|
custom property through `extend`. See `mlola.theme.example.json`.
|
|
50
50
|
|
|
51
|
+
## Tokens
|
|
52
|
+
|
|
53
|
+
Every value a design needs is a token. Read them with `var()`; never write
|
|
54
|
+
a color, a size from outside the scales, a shadow or a duration by hand.
|
|
55
|
+
Values change with the theme and the mode, the names never do.
|
|
56
|
+
|
|
57
|
+
- **Planes and ink.** Backgrounds, surfaces, the three levels of text, borders.
|
|
58
|
+
`--ml-background` `--ml-background-subtle` `--ml-surface` `--ml-surface-elevated` `--ml-text` `--ml-text-muted` `--ml-text-faint` `--ml-border` `--ml-border-subtle`
|
|
59
|
+
- **Color roles.** Each role is a fill with its `-foreground`, a `-text` for text and marks on the page, and (primary) a `-subtle` tint.
|
|
60
|
+
`--ml-primary` `--ml-primary-foreground` `--ml-primary-text` `--ml-primary-subtle` `--ml-success` `--ml-success-foreground` `--ml-success-text` `--ml-warning` `--ml-warning-foreground` `--ml-warning-text` `--ml-danger` `--ml-danger-foreground` `--ml-danger-text` `--ml-info` `--ml-info-foreground` `--ml-info-text`
|
|
61
|
+
- **Charts.** A categorical palette for series, 3:1 on the surface. Never a status.
|
|
62
|
+
`--ml-chart-1` `--ml-chart-2` `--ml-chart-3` `--ml-chart-4` `--ml-chart-5` `--ml-chart-6`
|
|
63
|
+
- **Interaction and light.** Focus, hover and pressed fills, tracks, the veil behind overlays, light and knobs.
|
|
64
|
+
`--ml-focus` `--ml-primary-hover` `--ml-fill-hover` `--ml-fill-active` `--ml-track` `--ml-control-border` `--ml-ring` `--ml-scrim` `--ml-highlight` `--ml-knob` `--ml-knob-shadow` `--ml-sheen`
|
|
65
|
+
- **Spacing.** The only spacing: gaps, padding, margins, offsets.
|
|
66
|
+
`--ml-space-px` `--ml-space-0-5` `--ml-space-1` `--ml-space-1-5` `--ml-space-2` `--ml-space-2-5` `--ml-space-3` `--ml-space-3-5` `--ml-space-4` `--ml-space-4-5` `--ml-space-5` `--ml-space-6` `--ml-space-7` `--ml-space-8` `--ml-space-9` `--ml-space-10` `--ml-space-12` `--ml-space-14` `--ml-space-16`
|
|
67
|
+
- **Type.** The only type sizes, line heights, families and weights.
|
|
68
|
+
`--ml-type-2xs` `--ml-type-xs` `--ml-type-sm` `--ml-type-base` `--ml-type-md` `--ml-type-lg` `--ml-type-xl` `--ml-type-2xl` `--ml-leading-tight` `--ml-leading-snug` `--ml-leading-normal` `--ml-font-sans` `--ml-font-display` `--ml-font-mono` `--ml-display-weight` `--ml-body-leading` `--ml-tracking`
|
|
69
|
+
- **Density.** Control heights, panel padding, the touch target.
|
|
70
|
+
`--ml-control-sm` `--ml-control-md` `--ml-control-lg` `--ml-panel-padding` `--ml-target-min`
|
|
71
|
+
- **Shape.** Corner radii by the size of the thing, and the border weight.
|
|
72
|
+
`--ml-radius-xs` `--ml-radius-sm` `--ml-radius-md` `--ml-radius-lg` `--ml-radius-pill` `--ml-border-width`
|
|
73
|
+
- **Depth and material.** Elevation, and the material a floating surface is made of.
|
|
74
|
+
`--ml-texture-opacity` `--ml-material` `--ml-surface-alpha` `--ml-surface-blur` `--ml-surface-grain` `--ml-surface-highlight` `--ml-shadow-xs` `--ml-shadow-sm` `--ml-shadow-md` `--ml-shadow-lg` `--ml-shadow-xl` `--ml-shadow-tint`
|
|
75
|
+
- **Motion.** Every transition and entrance.
|
|
76
|
+
`--ml-duration-fast` `--ml-duration-normal` `--ml-duration-slow` `--ml-duration-reveal` `--ml-ease-standard` `--ml-ease-spring` `--ml-ease-bounce`
|
|
77
|
+
- **Layers.** The one stacking order.
|
|
78
|
+
`--ml-layer-raised` `--ml-layer-sticky` `--ml-layer-header` `--ml-layer-dropdown` `--ml-layer-overlay` `--ml-layer-modal` `--ml-layer-popover` `--ml-layer-toast` `--ml-layer-tooltip` `--ml-layer-top`
|
|
79
|
+
- **Icons.** The theme's icon channel.
|
|
80
|
+
`--ml-icon-stroke`
|
|
81
|
+
|
|
82
|
+
## Designing new UI in the Mlola language
|
|
83
|
+
|
|
84
|
+
When the elements above do not cover what you need, build it the way they are
|
|
85
|
+
built, and it will look like it belongs.
|
|
86
|
+
|
|
87
|
+
1. **Compose first.** Reach for a component, then a layout primitive, and
|
|
88
|
+
write CSS only for what neither covers. Put it in a layer of your own,
|
|
89
|
+
declared before `mlola.accessibility` so the accessibility guarantees
|
|
90
|
+
still win:
|
|
91
|
+
`@layer mlola.tokens, mlola.foundations, mlola.materials, mlola.recipes, mlola.motion, app, mlola.accessibility;`
|
|
92
|
+
2. **Name what it is, not how it looks.** One class per element role, with
|
|
93
|
+
your own prefix (not `ml-`, so a later Mlola element never collides).
|
|
94
|
+
State and variant go in `data-*` and `aria-*`, never in a second class.
|
|
95
|
+
Reuse the shared words: `data-tone` is `neutral primary info success
|
|
96
|
+
warning danger`; `data-size` is `xs sm md lg xl`; work that went
|
|
97
|
+
wrong is `error`.
|
|
98
|
+
3. **Color by role, never by value.** Planes are `background`,
|
|
99
|
+
`background-subtle`, `surface`, `surface-elevated`. Ink is `text`,
|
|
100
|
+
`text-muted`, `text-faint`. A colored fill (`primary`, `danger`, …)
|
|
101
|
+
always carries its `-foreground`. Colored text, icons, lines and status
|
|
102
|
+
marks on the page use the `-text` role: fills are only kept 1.5:1 from the
|
|
103
|
+
page, enough for an area, not for meaning. Series use `chart-1`…`chart-6`.
|
|
104
|
+
4. **Measure with the scales.** Spacing from `--ml-space-*`, type from
|
|
105
|
+
`--ml-type-*` with `--ml-leading-*`, control heights from
|
|
106
|
+
`--ml-control-*`, panel padding from `--ml-panel-padding`. A `clamp()`
|
|
107
|
+
between two steps is fine; a value invented between them is not.
|
|
108
|
+
5. **Shape and depth come from the theme.** Radii by the size of the thing
|
|
109
|
+
(`xs` a tag, `sm` a small control, `md` a control or card, `lg` a panel
|
|
110
|
+
or dialog, `pill`). Elevation from `--ml-shadow-*`; a floating surface
|
|
111
|
+
also takes the material: `--ml-surface-alpha`, `--ml-surface-blur`,
|
|
112
|
+
`--ml-surface-highlight`.
|
|
113
|
+
6. **Move with the theme.** Durations from `--ml-duration-*`, easing from
|
|
114
|
+
`--ml-ease-*`. Reduced motion is handled by the engine.
|
|
115
|
+
7. **Stack with the layers.** `--ml-layer-*`, never a raw z-index above 9.
|
|
116
|
+
8. **Keep it usable by hand.** Never remove an outline without a
|
|
117
|
+
`:focus-visible` style in its place. Targets are at least 24px; a smaller
|
|
118
|
+
control adds `data-hit="expand"` for touch. Text is never under
|
|
119
|
+
`--ml-type-2xs`.
|
|
120
|
+
9. **Leave the theme alone.** `data-theme` and `data-mode` belong to the
|
|
121
|
+
engine; never set them for a component's own meaning, and never style a
|
|
122
|
+
theme or mode by name. If something must differ by theme, it is a token.
|
|
123
|
+
|
|
124
|
+
## Composition primitives
|
|
125
|
+
|
|
126
|
+
Compose pages from these before writing layout CSS: sections, stacks,
|
|
127
|
+
clusters, grids, headings, forms, stats. They read the same tokens as every
|
|
128
|
+
component, so a page built from them themes with the rest.
|
|
129
|
+
|
|
130
|
+
`.ml-actions` `.ml-brand` `.ml-brand-mark` `.ml-brand-name` `.ml-chart` `.ml-chart-bar` `.ml-chart-bars` `.ml-chart-heading` `.ml-cluster` `.ml-definition-list` `.ml-display` `.ml-divider` `.ml-empty-state` `.ml-eyebrow` `.ml-filter-chip` `.ml-filter-group` `.ml-fine-print` `.ml-form` `.ml-form-message` `.ml-form-options` `.ml-grid` `.ml-heading` `.ml-icon-chip` `.ml-inline-form` `.ml-inline-form-field` `.ml-label` `.ml-lede` `.ml-link` `.ml-page-shell` `.ml-person` `.ml-person-copy` `.ml-positive` `.ml-price` `.ml-required-mark` `.ml-section` `.ml-section-description` `.ml-section-header` `.ml-section-header-centered` `.ml-section-muted` `.ml-section-shell` `.ml-stack` `.ml-stat` `.ml-stat-card` `.ml-stat-grid` `.ml-stat-list` `.ml-stat-meta` `.ml-stat-value` `.ml-text-primary` `.ml-value`
|
|
131
|
+
|
|
132
|
+
- `.ml-link` — A glyph inside a link flows with the text instead of breaking the line.
|
|
133
|
+
- `.ml-page-shell` — A full page: header, main and footer stacked on the page background.
|
|
134
|
+
|
|
51
135
|
## Elements and their attributes
|
|
52
136
|
|
|
53
137
|
Emit these classes and attributes from any language and the visuals are
|
|
@@ -198,11 +282,11 @@ correct. This table is read out of the stylesheet, so it is never stale.
|
|
|
198
282
|
| `.ml-toggle-button` | `data-state` | `on` |
|
|
199
283
|
| `.ml-tooltip` | `data-side` | `bottom`, `left`, `right`, `top` |
|
|
200
284
|
| `.ml-tour-button` | `data-primary` | _presence only_ |
|
|
201
|
-
| `.ml-tour-card` | `data-
|
|
285
|
+
| `.ml-tour-card` | `data-centered` | _presence only_ |
|
|
202
286
|
| `.ml-tour-dot` | `data-active` | _presence only_ |
|
|
203
287
|
| `.ml-tour-scrim` | `data-spotlight` | _presence only_ |
|
|
204
288
|
|
|
205
|
-
##
|
|
289
|
+
## Behavior
|
|
206
290
|
|
|
207
291
|
### accordion
|
|
208
292
|
|
|
@@ -316,7 +400,7 @@ A short label shown on hover or focus.
|
|
|
316
400
|
- <kbd>Escape</kbd>: Hide the tooltip.
|
|
317
401
|
- State changes:
|
|
318
402
|
- On pointer enter or focus the trigger, set visible to after the delay.
|
|
319
|
-
- On pointer leave or blur, set hidden to immediately,
|
|
403
|
+
- On pointer leave or blur, set hidden to immediately, canceling any pending delay.
|
|
320
404
|
- Note: Never put essential information or interactive content in a tooltip.
|
|
321
405
|
|
|
322
406
|
### toast
|
|
@@ -365,5 +449,5 @@ A single value chosen from a range.
|
|
|
365
449
|
|
|
366
450
|
Prefer emitting less. A plain `<button class="ml-button">` is correct; a
|
|
367
451
|
button with a variant this document does not list is not. When a component
|
|
368
|
-
needs
|
|
452
|
+
needs behavior, mark its root with `data-ml="<behavior name>"` and let the
|
|
369
453
|
runtime attach, rather than writing event handlers that guess at the contract.
|
package/generated/assets.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"summary": "currentColor SVG glyphs. Size and stroke follow the active theme unless a prop overrides them.",
|
|
9
9
|
"props": {
|
|
10
10
|
"size": "number or CSS length. Defaults to var(--mlola-glyph-size, 1.125rem).",
|
|
11
|
-
"strokeWidth": "number or CSS length. Defaults to
|
|
11
|
+
"strokeWidth": "number or CSS length. Defaults to the theme's --ml-icon-stroke; --mlola-glyph-stroke overrides it.",
|
|
12
12
|
"opticalSize": "small | default | large",
|
|
13
13
|
"animate": "boolean, honours prefers-reduced-motion",
|
|
14
14
|
"title": "gives the glyph an accessible name; without it the glyph is decorative"
|
|
@@ -1059,7 +1059,7 @@
|
|
|
1059
1059
|
{
|
|
1060
1060
|
"id": "3d",
|
|
1061
1061
|
"label": "3D assets",
|
|
1062
|
-
"summary": "glTF models with a poster still. Materials are named by theme role, so a Mlola viewer paints them in the active theme; any other viewer shows their own
|
|
1062
|
+
"summary": "glTF models with a poster still. Materials are named by theme role, so a Mlola viewer paints them in the active theme; any other viewer shows their own colors.",
|
|
1063
1063
|
"items": [
|
|
1064
1064
|
{
|
|
1065
1065
|
"id": "blocks",
|
package/generated/contract.json
CHANGED
|
@@ -591,7 +591,7 @@
|
|
|
591
591
|
"data-primary": []
|
|
592
592
|
},
|
|
593
593
|
"ml-tour-card": {
|
|
594
|
-
"data-
|
|
594
|
+
"data-centered": []
|
|
595
595
|
},
|
|
596
596
|
"ml-tour-dot": {
|
|
597
597
|
"data-active": []
|
|
@@ -1067,7 +1067,7 @@
|
|
|
1067
1067
|
{
|
|
1068
1068
|
"on": "pointer leave or blur",
|
|
1069
1069
|
"set": "hidden",
|
|
1070
|
-
"to": "immediately,
|
|
1070
|
+
"to": "immediately, canceling any pending delay"
|
|
1071
1071
|
}
|
|
1072
1072
|
],
|
|
1073
1073
|
"notes": "Never put essential information or interactive content in a tooltip."
|
|
@@ -113,10 +113,27 @@ h3 {
|
|
|
113
113
|
opacity: .5;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/* Room for a fingertip on text fields, which grow with their content.
|
|
117
|
+
Buttons keep the shape their recipe draws: stretching a round or square
|
|
118
|
+
control only distorts it. Small ones widen their target invisibly instead
|
|
119
|
+
(see [data-hit] below), and every other button already clears the 24px
|
|
120
|
+
minimum of WCAG 2.5.8. */
|
|
116
121
|
@media (pointer:coarse) {
|
|
117
|
-
:where(
|
|
122
|
+
:where(input:not([type="hidden"],[type="checkbox"],[type="radio"],[type="range"],[type="color"],[type="file"],[type="submit"],[type="button"],[type="reset"],[type="image"]),select,textarea):where(:not([data-shape="inline"])) {
|
|
118
123
|
min-height: var(--ml-target-min);
|
|
119
124
|
}
|
|
125
|
+
|
|
126
|
+
/* A control drawn smaller than a fingertip keeps its look and gains an
|
|
127
|
+
invisible target around it. */
|
|
128
|
+
:where([data-hit="expand"]) {
|
|
129
|
+
position: relative;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:where([data-hit="expand"])::after {
|
|
133
|
+
position: absolute;
|
|
134
|
+
inset: min(-0.375rem, calc((var(--ml-target-min) - 100%) / -2));
|
|
135
|
+
content: "";
|
|
136
|
+
}
|
|
120
137
|
}
|
|
121
138
|
|
|
122
139
|
@media (prefers-reduced-transparency:reduce) {
|
|
@@ -139,7 +156,7 @@ h3 {
|
|
|
139
156
|
forced-color-adjust: auto;
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
/* Box-shadow rings vanish in forced
|
|
159
|
+
/* Box-shadow rings vanish in forced colors; the system highlight stands in. */
|
|
143
160
|
:focus-visible {
|
|
144
161
|
outline: 2px solid Highlight!important;
|
|
145
162
|
outline-offset: 2px!important;
|
package/generated/mlola.css
CHANGED
package/generated/recipes.css
CHANGED
|
@@ -559,7 +559,7 @@
|
|
|
559
559
|
border-radius: var(--ml-radius-xs);
|
|
560
560
|
background: transparent;
|
|
561
561
|
color: var(--ml-text);
|
|
562
|
-
text-align:
|
|
562
|
+
text-align: start;
|
|
563
563
|
font-size: var(--ml-type-base);
|
|
564
564
|
font-weight: 500;
|
|
565
565
|
cursor: pointer;
|
|
@@ -822,7 +822,7 @@
|
|
|
822
822
|
transition: opacity var(--ml-duration-fast) var(--ml-ease-standard), transform var(--ml-duration-normal) var(--ml-ease-standard);
|
|
823
823
|
}
|
|
824
824
|
|
|
825
|
-
/* A solid mark in the tone,
|
|
825
|
+
/* A solid mark in the tone, centered on the first line of text. */
|
|
826
826
|
.ml-alert-icon {
|
|
827
827
|
position: relative;
|
|
828
828
|
display: inline-grid;
|
|
@@ -1302,7 +1302,7 @@
|
|
|
1302
1302
|
font-size: var(--ml-type-sm);
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
|
-
/* Tone picks the
|
|
1305
|
+
/* Tone picks the color role; variant decides how it is worn. */
|
|
1306
1306
|
.ml-badge[data-tone="primary"] {
|
|
1307
1307
|
--ml-badge-tone: var(--ml-primary);
|
|
1308
1308
|
--ml-badge-text: var(--ml-primary-text);
|
|
@@ -1492,7 +1492,7 @@
|
|
|
1492
1492
|
}
|
|
1493
1493
|
|
|
1494
1494
|
/* A toggle button that is on reads as filled ink, so its state is visible
|
|
1495
|
-
without relying on
|
|
1495
|
+
without relying on color alone. */
|
|
1496
1496
|
.ml-button[data-variant="secondary"][aria-pressed="true"],
|
|
1497
1497
|
.ml-button[data-variant="subtle"][aria-pressed="true"],
|
|
1498
1498
|
.ml-button[data-variant="outline"][aria-pressed="true"] {
|
|
@@ -1579,7 +1579,7 @@
|
|
|
1579
1579
|
width: .875rem;
|
|
1580
1580
|
height: .875rem;
|
|
1581
1581
|
border: 1.5px solid currentColor;
|
|
1582
|
-
border-
|
|
1582
|
+
border-inline-end-color: transparent;
|
|
1583
1583
|
border-radius: 50%;
|
|
1584
1584
|
animation: ml-spin .7s linear infinite;
|
|
1585
1585
|
}
|
|
@@ -1596,7 +1596,7 @@
|
|
|
1596
1596
|
}
|
|
1597
1597
|
|
|
1598
1598
|
/* A shortcut inside a filled button takes the button's own ink: an outline
|
|
1599
|
-
of the text
|
|
1599
|
+
of the text color, no fill of its own, so it reads on any variant. */
|
|
1600
1600
|
.ml-button .ml-kbd {
|
|
1601
1601
|
border-color: color-mix(in oklab, currentColor 40%, transparent);
|
|
1602
1602
|
background: transparent;
|
|
@@ -1608,6 +1608,9 @@
|
|
|
1608
1608
|
.ml-calendar {
|
|
1609
1609
|
--ml-calendar-day: 2.25rem;
|
|
1610
1610
|
display: inline-grid;
|
|
1611
|
+
/* One column no wider than the calendar, so two months wrap into a stack
|
|
1612
|
+
when the space is narrower than both side by side. */
|
|
1613
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1611
1614
|
width: max-content;
|
|
1612
1615
|
max-width: 100%;
|
|
1613
1616
|
color: var(--ml-text);
|
|
@@ -1835,7 +1838,7 @@
|
|
|
1835
1838
|
position: absolute;
|
|
1836
1839
|
inset: 0;
|
|
1837
1840
|
pointer-events: none;
|
|
1838
|
-
background:
|
|
1841
|
+
background: var(--ml-sheen);
|
|
1839
1842
|
}
|
|
1840
1843
|
|
|
1841
1844
|
.ml-card-header {
|
|
@@ -2188,6 +2191,7 @@
|
|
|
2188
2191
|
position: relative;
|
|
2189
2192
|
height: 9.5rem;
|
|
2190
2193
|
border-radius: var(--ml-radius-md);
|
|
2194
|
+
/* audit-allow: color.literal — this is the color model itself: value falls to black, saturation to white. */
|
|
2191
2195
|
background: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent), var(--ml-color-hue);
|
|
2192
2196
|
cursor: crosshair;
|
|
2193
2197
|
outline: none;
|
|
@@ -2235,16 +2239,16 @@
|
|
|
2235
2239
|
position: absolute;
|
|
2236
2240
|
width: 1rem;
|
|
2237
2241
|
height: 1rem;
|
|
2238
|
-
border: 2.5px solid
|
|
2242
|
+
border: 2.5px solid var(--ml-knob);
|
|
2239
2243
|
border-radius: 50%;
|
|
2240
|
-
box-shadow:
|
|
2244
|
+
box-shadow: var(--ml-knob-shadow);
|
|
2241
2245
|
pointer-events: none;
|
|
2242
2246
|
translate: -50% -50%;
|
|
2243
2247
|
}
|
|
2244
2248
|
|
|
2245
2249
|
.ml-color-picker-area:focus-visible > .ml-color-picker-thumb,
|
|
2246
2250
|
.ml-color-picker-track:focus-visible > .ml-color-picker-thumb {
|
|
2247
|
-
box-shadow: 0 0 0 2px var(--ml-focus), 0 1px 4px
|
|
2251
|
+
box-shadow: 0 0 0 2px var(--ml-focus), 0 1px 4px var(--ml-shadow-tint);
|
|
2248
2252
|
}
|
|
2249
2253
|
|
|
2250
2254
|
.ml-color-picker-row {
|
|
@@ -2455,7 +2459,8 @@
|
|
|
2455
2459
|
.ml-combobox-list {
|
|
2456
2460
|
display: grid;
|
|
2457
2461
|
gap: var(--ml-space-0-5);
|
|
2458
|
-
|
|
2462
|
+
/* Never taller than the visible room beside the field (set when it opens). */
|
|
2463
|
+
max-height: min(16rem, var(--ml-combobox-room, 16rem));
|
|
2459
2464
|
margin: 0;
|
|
2460
2465
|
padding: 0;
|
|
2461
2466
|
overflow-y: auto;
|
|
@@ -2569,6 +2574,7 @@
|
|
|
2569
2574
|
flex: 1;
|
|
2570
2575
|
min-width: 0;
|
|
2571
2576
|
border: 0;
|
|
2577
|
+
/* audit-allow: focus.removed — the search field is the open dialog's only field; its caret shows focus. */
|
|
2572
2578
|
outline: none;
|
|
2573
2579
|
background: transparent;
|
|
2574
2580
|
color: var(--ml-text);
|
|
@@ -2688,6 +2694,7 @@
|
|
|
2688
2694
|
border-radius: var(--ml-radius-lg);
|
|
2689
2695
|
box-shadow: var(--ml-surface-highlight), var(--ml-shadow-lg);
|
|
2690
2696
|
color: var(--ml-text);
|
|
2697
|
+
/* audit-allow: focus.removed — a menu container, focused only by script (tabindex -1); its items carry the focus style. */
|
|
2691
2698
|
outline: none;
|
|
2692
2699
|
background-color: color-mix(in oklab, var(--ml-surface-elevated) var(--ml-surface-alpha), transparent);
|
|
2693
2700
|
background-image: var(--ml-surface-grain);
|
|
@@ -2981,7 +2988,7 @@
|
|
|
2981
2988
|
color: var(--ml-text);
|
|
2982
2989
|
font-size: var(--ml-type-sm);
|
|
2983
2990
|
font-weight: 400;
|
|
2984
|
-
text-align:
|
|
2991
|
+
text-align: start;
|
|
2985
2992
|
text-decoration: none;
|
|
2986
2993
|
cursor: pointer;
|
|
2987
2994
|
}
|
|
@@ -3271,6 +3278,7 @@
|
|
|
3271
3278
|
/* components/input */
|
|
3272
3279
|
.ml-input-field {
|
|
3273
3280
|
display: grid;
|
|
3281
|
+
grid-template-columns: minmax(0, 1fr);
|
|
3274
3282
|
gap: var(--ml-space-1-5);
|
|
3275
3283
|
width: 100%;
|
|
3276
3284
|
}
|
|
@@ -3646,12 +3654,16 @@
|
|
|
3646
3654
|
/* components/otp-input */
|
|
3647
3655
|
.ml-otp {
|
|
3648
3656
|
display: flex;
|
|
3657
|
+
max-width: 100%;
|
|
3649
3658
|
align-items: center;
|
|
3650
3659
|
gap: var(--ml-space-2);
|
|
3651
3660
|
}
|
|
3652
3661
|
|
|
3662
|
+
/* Boxes give a little way on the narrowest phones instead of running past the edge. */
|
|
3653
3663
|
.ml-otp-box {
|
|
3664
|
+
flex: 0 1 var(--ml-control-lg);
|
|
3654
3665
|
width: var(--ml-control-lg);
|
|
3666
|
+
min-width: 2rem;
|
|
3655
3667
|
height: calc(var(--ml-control-lg) * 1.15);
|
|
3656
3668
|
padding: 0;
|
|
3657
3669
|
border: var(--ml-border-width) solid var(--ml-border);
|
|
@@ -3682,6 +3694,7 @@
|
|
|
3682
3694
|
}
|
|
3683
3695
|
|
|
3684
3696
|
.ml-otp-separator {
|
|
3697
|
+
flex: none;
|
|
3685
3698
|
width: var(--ml-space-2);
|
|
3686
3699
|
height: 2px;
|
|
3687
3700
|
border-radius: var(--ml-radius-pill);
|
|
@@ -3816,7 +3829,7 @@
|
|
|
3816
3829
|
}
|
|
3817
3830
|
|
|
3818
3831
|
.ml-priority-icon[data-priority="urgent"] {
|
|
3819
|
-
color: var(--ml-danger);
|
|
3832
|
+
color: var(--ml-danger-text);
|
|
3820
3833
|
}
|
|
3821
3834
|
|
|
3822
3835
|
.ml-priority-icon[data-priority="high"] {
|
|
@@ -4404,7 +4417,7 @@
|
|
|
4404
4417
|
border-radius: var(--ml-radius-md);
|
|
4405
4418
|
box-shadow: var(--ml-shadow-xs);
|
|
4406
4419
|
cursor: pointer;
|
|
4407
|
-
text-align:
|
|
4420
|
+
text-align: start;
|
|
4408
4421
|
transition: border-color var(--ml-duration-fast),box-shadow var(--ml-duration-fast);
|
|
4409
4422
|
}
|
|
4410
4423
|
|
|
@@ -4439,7 +4452,7 @@
|
|
|
4439
4452
|
overflow: hidden;
|
|
4440
4453
|
text-overflow: ellipsis;
|
|
4441
4454
|
white-space: nowrap;
|
|
4442
|
-
text-align:
|
|
4455
|
+
text-align: start;
|
|
4443
4456
|
}
|
|
4444
4457
|
|
|
4445
4458
|
.ml-select-chips {
|
|
@@ -4528,6 +4541,7 @@
|
|
|
4528
4541
|
border-radius: var(--ml-radius-sm);
|
|
4529
4542
|
background: transparent;
|
|
4530
4543
|
font-size: var(--ml-type-sm);
|
|
4544
|
+
/* audit-allow: focus.removed — the search field inside an open listbox; its caret shows focus. */
|
|
4531
4545
|
outline: none;
|
|
4532
4546
|
}
|
|
4533
4547
|
|
|
@@ -4567,7 +4581,7 @@
|
|
|
4567
4581
|
background: transparent;
|
|
4568
4582
|
color: var(--ml-text);
|
|
4569
4583
|
font-size: var(--ml-type-sm);
|
|
4570
|
-
text-align:
|
|
4584
|
+
text-align: start;
|
|
4571
4585
|
cursor: pointer;
|
|
4572
4586
|
}
|
|
4573
4587
|
|
|
@@ -4822,7 +4836,7 @@
|
|
|
4822
4836
|
}
|
|
4823
4837
|
|
|
4824
4838
|
/* Each line is one line box of the surrounding text: a bar about the height
|
|
4825
|
-
of lowercase letters,
|
|
4839
|
+
of lowercase letters, centered in 1lh. */
|
|
4826
4840
|
.ml-skeleton-line {
|
|
4827
4841
|
height: 0.62em;
|
|
4828
4842
|
margin-block: calc((1lh - 0.62em) / 2);
|
|
@@ -4948,7 +4962,7 @@
|
|
|
4948
4962
|
height: var(--ml-space-4);
|
|
4949
4963
|
border: var(--ml-border-width) solid color-mix(in oklab,var(--ml-primary) 45%,var(--ml-border));
|
|
4950
4964
|
border-radius: 50%;
|
|
4951
|
-
background:
|
|
4965
|
+
background: var(--ml-knob);
|
|
4952
4966
|
box-shadow: var(--ml-shadow-sm);
|
|
4953
4967
|
transform: translate(-50%,-50%);
|
|
4954
4968
|
cursor: grab;
|
|
@@ -4968,7 +4982,7 @@
|
|
|
4968
4982
|
}
|
|
4969
4983
|
|
|
4970
4984
|
/* components/status-icon */
|
|
4971
|
-
/*
|
|
4985
|
+
/* Color carries the stage; the glyph carries it without color too. */
|
|
4972
4986
|
.ml-status-icon {
|
|
4973
4987
|
--ml-status-mark: var(--ml-surface);
|
|
4974
4988
|
flex: none;
|
|
@@ -4981,14 +4995,16 @@
|
|
|
4981
4995
|
}
|
|
4982
4996
|
|
|
4983
4997
|
.ml-status-icon[data-status="in-progress"] {
|
|
4984
|
-
color: var(--ml-warning);
|
|
4998
|
+
color: var(--ml-warning-text);
|
|
4985
4999
|
}
|
|
4986
5000
|
|
|
4987
5001
|
.ml-status-icon[data-status="in-review"] {
|
|
4988
|
-
color: var(--ml-success);
|
|
5002
|
+
color: var(--ml-success-text);
|
|
4989
5003
|
}
|
|
4990
5004
|
|
|
5005
|
+
/* Done is a filled disc carrying its check in the foreground: a fill and its pair. */
|
|
4991
5006
|
.ml-status-icon[data-status="done"] {
|
|
5007
|
+
/* audit-allow: color.role — the disc is a fill; its mark takes --ml-primary-foreground. */
|
|
4992
5008
|
color: var(--ml-primary);
|
|
4993
5009
|
--ml-status-mark: var(--ml-primary-foreground);
|
|
4994
5010
|
}
|
|
@@ -5029,7 +5045,7 @@
|
|
|
5029
5045
|
}
|
|
5030
5046
|
|
|
5031
5047
|
/* Horizontal: the connector is a flex item after the step, level with the
|
|
5032
|
-
marker's
|
|
5048
|
+
marker's center, and fills once the step is done. */
|
|
5033
5049
|
.ml-stepper[data-orientation="horizontal"] .ml-stepper-step:not(:last-child)::after {
|
|
5034
5050
|
min-width: var(--ml-space-4);
|
|
5035
5051
|
height: 2px;
|
|
@@ -5216,7 +5232,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5216
5232
|
.ml-table {
|
|
5217
5233
|
width: 100%;
|
|
5218
5234
|
border-collapse: collapse;
|
|
5219
|
-
text-align:
|
|
5235
|
+
text-align: start;
|
|
5220
5236
|
font-size: var(--ml-type-sm);
|
|
5221
5237
|
}
|
|
5222
5238
|
|
|
@@ -5256,7 +5272,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5256
5272
|
.ml-table [data-align="right"],
|
|
5257
5273
|
.ml-table [data-align="end"],
|
|
5258
5274
|
.ml-table [data-numeric] {
|
|
5259
|
-
text-align:
|
|
5275
|
+
text-align: end;
|
|
5260
5276
|
font-variant-numeric: tabular-nums;
|
|
5261
5277
|
}
|
|
5262
5278
|
|
|
@@ -5315,7 +5331,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5315
5331
|
padding: var(--ml-space-3) var(--ml-space-4);
|
|
5316
5332
|
color: var(--ml-text-muted);
|
|
5317
5333
|
font-size: var(--ml-type-xs);
|
|
5318
|
-
text-align:
|
|
5334
|
+
text-align: start;
|
|
5319
5335
|
}
|
|
5320
5336
|
|
|
5321
5337
|
.ml-table-footer {
|
|
@@ -5351,6 +5367,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5351
5367
|
/* components/tabs */
|
|
5352
5368
|
.ml-tabs {
|
|
5353
5369
|
display: grid;
|
|
5370
|
+
grid-template-columns: minmax(0, 1fr);
|
|
5354
5371
|
gap: var(--ml-space-4);
|
|
5355
5372
|
width: 100%;
|
|
5356
5373
|
}
|
|
@@ -5745,7 +5762,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5745
5762
|
|
|
5746
5763
|
/* components/timeline */
|
|
5747
5764
|
/* A rail of markers and a column of text. Each item draws the segment of
|
|
5748
|
-
rail below its own marker, so the line always runs through the
|
|
5765
|
+
rail below its own marker, so the line always runs through the centers,
|
|
5749
5766
|
whatever each item holds, and fills in as the sequence progresses. */
|
|
5750
5767
|
.ml-timeline {
|
|
5751
5768
|
--ml-timeline-marker: 1.5rem;
|
|
@@ -5874,7 +5891,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
5874
5891
|
min-width: 0;
|
|
5875
5892
|
}
|
|
5876
5893
|
|
|
5877
|
-
/* The heading row is as tall as the marker, so a one-line title sits on its
|
|
5894
|
+
/* The heading row is as tall as the marker, so a one-line title sits on its center. */
|
|
5878
5895
|
.ml-timeline-heading {
|
|
5879
5896
|
display: flex;
|
|
5880
5897
|
flex-wrap: wrap;
|
|
@@ -6208,7 +6225,7 @@ button.ml-stepper-target:focus-visible {
|
|
|
6208
6225
|
width: 1rem;
|
|
6209
6226
|
height: 1rem;
|
|
6210
6227
|
border-radius: 50%;
|
|
6211
|
-
background:
|
|
6228
|
+
background: var(--ml-knob);
|
|
6212
6229
|
box-shadow: var(--ml-shadow-sm);
|
|
6213
6230
|
pointer-events: none;
|
|
6214
6231
|
transition: transform var(--ml-duration-normal) var(--ml-ease-spring);
|
|
@@ -6373,11 +6390,12 @@ button.ml-stepper-target:focus-visible {
|
|
|
6373
6390
|
background: var(--ml-surface-elevated);
|
|
6374
6391
|
box-shadow: var(--ml-shadow-xl);
|
|
6375
6392
|
color: var(--ml-text);
|
|
6393
|
+
/* audit-allow: focus.removed — the tour card is focused by script (tabindex -1); its buttons carry the focus style. */
|
|
6376
6394
|
outline: none;
|
|
6377
6395
|
animation: ml-pop-in var(--ml-duration-fast) var(--ml-ease-spring);
|
|
6378
6396
|
}
|
|
6379
6397
|
|
|
6380
|
-
.ml-tour-card[data-
|
|
6398
|
+
.ml-tour-card[data-centered] {
|
|
6381
6399
|
top: 50%;
|
|
6382
6400
|
left: 50%;
|
|
6383
6401
|
translate: -50% -50%;
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"properties": {
|
|
97
97
|
"primary": {
|
|
98
98
|
"type": "string",
|
|
99
|
-
"description": "Seed for the primary
|
|
99
|
+
"description": "Seed for the primary color, oklch() or hex. Lightness is adjusted to meet contrast."
|
|
100
100
|
},
|
|
101
101
|
"primaryDark": {
|
|
102
102
|
"type": [
|
package/generated/tokens.css
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0304), 0 4.103px 13.32px -2px oklch(0 0 0 / 0.0608);
|
|
92
92
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0266), 0 8.206px 23.68px -4px oklch(0 0 0 / 0.076);
|
|
93
93
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0228), 0 15.044px 41.44px -8px oklch(0 0 0 / 0.095);
|
|
94
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.0608);
|
|
94
95
|
--ml-background: oklch(1 0 0);
|
|
95
96
|
--ml-background-subtle: oklch(0.968 0.006 286);
|
|
96
97
|
--ml-surface: oklch(1 0 0);
|
|
@@ -130,6 +131,10 @@
|
|
|
130
131
|
--ml-control-border: var(--ml-text-faint);
|
|
131
132
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
132
133
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
134
|
+
--ml-highlight: oklch(1 0 0);
|
|
135
|
+
--ml-knob: var(--ml-highlight);
|
|
136
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
137
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
:root[data-mode="dark"],
|
|
@@ -172,6 +177,7 @@
|
|
|
172
177
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.079), 0 4.103px 13.32px -2px oklch(0 0 0 / 0.1581);
|
|
173
178
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0692), 0 8.206px 23.68px -4px oklch(0 0 0 / 0.1976);
|
|
174
179
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0593), 0 15.044px 41.44px -8px oklch(0 0 0 / 0.247);
|
|
180
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.1581);
|
|
175
181
|
--ml-focus: var(--ml-primary-text);
|
|
176
182
|
--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background));
|
|
177
183
|
--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent);
|
|
@@ -180,6 +186,10 @@
|
|
|
180
186
|
--ml-control-border: var(--ml-text-faint);
|
|
181
187
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
182
188
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
189
|
+
--ml-highlight: oklch(1 0 0);
|
|
190
|
+
--ml-knob: var(--ml-highlight);
|
|
191
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
192
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
@supports (transition-timing-function: linear(0, 1)) {
|
|
@@ -239,6 +249,7 @@
|
|
|
239
249
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0485), 0 6.378px 24.692px -2px oklch(0 0 0 / 0.0971);
|
|
240
250
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0425), 0 12.756px 43.898px -4px oklch(0 0 0 / 0.1213);
|
|
241
251
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0364), 0 23.387px 76.821px -8px oklch(0 0 0 / 0.1517);
|
|
252
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.0971);
|
|
242
253
|
--ml-background: oklch(0.972 0.016 80);
|
|
243
254
|
--ml-background-subtle: oklch(0.94 0.02 80);
|
|
244
255
|
--ml-surface: oklch(0.989 0.01 80);
|
|
@@ -278,6 +289,10 @@
|
|
|
278
289
|
--ml-control-border: var(--ml-text-faint);
|
|
279
290
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
280
291
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
292
|
+
--ml-highlight: oklch(1 0 0);
|
|
293
|
+
--ml-knob: var(--ml-highlight);
|
|
294
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
295
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
281
296
|
}
|
|
282
297
|
|
|
283
298
|
:root[data-theme="atelier"][data-mode="dark"],
|
|
@@ -319,6 +334,7 @@
|
|
|
319
334
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.1262), 0 6.378px 24.692px -2px oklch(0 0 0 / 0.2524);
|
|
320
335
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.1104), 0 12.756px 43.898px -4px oklch(0 0 0 / 0.3155);
|
|
321
336
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0946), 0 23.387px 76.821px -8px oklch(0 0 0 / 0.3944);
|
|
337
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.2524);
|
|
322
338
|
--ml-focus: var(--ml-primary-text);
|
|
323
339
|
--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background));
|
|
324
340
|
--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent);
|
|
@@ -327,6 +343,10 @@
|
|
|
327
343
|
--ml-control-border: var(--ml-text-faint);
|
|
328
344
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
329
345
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
346
|
+
--ml-highlight: oklch(1 0 0);
|
|
347
|
+
--ml-knob: var(--ml-highlight);
|
|
348
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
349
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
330
350
|
}
|
|
331
351
|
|
|
332
352
|
@supports (transition-timing-function: linear(0, 1)) {
|
|
@@ -385,6 +405,7 @@
|
|
|
385
405
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0306), 0 3.828px 13.331px -2px oklch(0 0 0 / 0.0612);
|
|
386
406
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0268), 0 7.655px 23.699px -4px oklch(0 0 0 / 0.0764);
|
|
387
407
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0229), 0 14.034px 41.474px -8px oklch(0 0 0 / 0.0956);
|
|
408
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.0612);
|
|
388
409
|
--ml-background: oklch(0.994 0.003 240);
|
|
389
410
|
--ml-background-subtle: oklch(0.962 0.006 240);
|
|
390
411
|
--ml-surface: oklch(0.997 0.001 240);
|
|
@@ -424,6 +445,10 @@
|
|
|
424
445
|
--ml-control-border: var(--ml-text-faint);
|
|
425
446
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
426
447
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
448
|
+
--ml-highlight: oklch(1 0 0);
|
|
449
|
+
--ml-knob: var(--ml-highlight);
|
|
450
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
451
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
427
452
|
}
|
|
428
453
|
|
|
429
454
|
:root[data-theme="machined"][data-mode="dark"],
|
|
@@ -465,6 +490,7 @@
|
|
|
465
490
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0795), 0 3.828px 13.331px -2px oklch(0 0 0 / 0.159);
|
|
466
491
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0696), 0 7.655px 23.699px -4px oklch(0 0 0 / 0.1988);
|
|
467
492
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0596), 0 14.034px 41.474px -8px oklch(0 0 0 / 0.2485);
|
|
493
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.159);
|
|
468
494
|
--ml-focus: var(--ml-primary-text);
|
|
469
495
|
--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background));
|
|
470
496
|
--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent);
|
|
@@ -473,6 +499,10 @@
|
|
|
473
499
|
--ml-control-border: var(--ml-text-faint);
|
|
474
500
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
475
501
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
502
|
+
--ml-highlight: oklch(1 0 0);
|
|
503
|
+
--ml-knob: var(--ml-highlight);
|
|
504
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
505
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
476
506
|
}
|
|
477
507
|
|
|
478
508
|
@supports (transition-timing-function: linear(0, 1)) {
|
|
@@ -531,6 +561,7 @@
|
|
|
531
561
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0593), 0 9.818px 32.195px -2px oklch(0 0 0 / 0.1186);
|
|
532
562
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0519), 0 19.636px 57.235px -4px oklch(0 0 0 / 0.1483);
|
|
533
563
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0445), 0 35.999px 100.162px -8px oklch(0 0 0 / 0.1854);
|
|
564
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.1186);
|
|
534
565
|
--ml-background: oklch(0.986 0.007 260);
|
|
535
566
|
--ml-background-subtle: oklch(0.954 0.022 260);
|
|
536
567
|
--ml-surface: oklch(0.995 0.002 260);
|
|
@@ -570,6 +601,10 @@
|
|
|
570
601
|
--ml-control-border: var(--ml-text-faint);
|
|
571
602
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
572
603
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
604
|
+
--ml-highlight: oklch(1 0 0);
|
|
605
|
+
--ml-knob: var(--ml-highlight);
|
|
606
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
607
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
573
608
|
}
|
|
574
609
|
|
|
575
610
|
:root[data-theme="aerogel"][data-mode="dark"],
|
|
@@ -611,6 +646,7 @@
|
|
|
611
646
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.1542), 0 9.818px 32.195px -2px oklch(0 0 0 / 0.3084);
|
|
612
647
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.1349), 0 19.636px 57.235px -4px oklch(0 0 0 / 0.3855);
|
|
613
648
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.1157), 0 35.999px 100.162px -8px oklch(0 0 0 / 0.4819);
|
|
649
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.3084);
|
|
614
650
|
--ml-focus: var(--ml-primary-text);
|
|
615
651
|
--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background));
|
|
616
652
|
--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent);
|
|
@@ -619,6 +655,10 @@
|
|
|
619
655
|
--ml-control-border: var(--ml-text-faint);
|
|
620
656
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
621
657
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
658
|
+
--ml-highlight: oklch(1 0 0);
|
|
659
|
+
--ml-knob: var(--ml-highlight);
|
|
660
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
661
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
622
662
|
}
|
|
623
663
|
|
|
624
664
|
@supports (transition-timing-function: linear(0, 1)) {
|
|
@@ -677,6 +717,7 @@
|
|
|
677
717
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.0407), 0 4.887px 19.58px -2px oklch(0 0 0 / 0.0815);
|
|
678
718
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0356), 0 9.775px 34.81px -4px oklch(0 0 0 / 0.1018);
|
|
679
719
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0305), 0 17.92px 60.917px -8px oklch(0 0 0 / 0.1273);
|
|
720
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.0815);
|
|
680
721
|
--ml-background: oklch(0.969 0.018 135);
|
|
681
722
|
--ml-background-subtle: oklch(0.937 0.023 135);
|
|
682
723
|
--ml-surface: oklch(0.988 0.011 135);
|
|
@@ -716,6 +757,10 @@
|
|
|
716
757
|
--ml-control-border: var(--ml-text-faint);
|
|
717
758
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
718
759
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
760
|
+
--ml-highlight: oklch(1 0 0);
|
|
761
|
+
--ml-knob: var(--ml-highlight);
|
|
762
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
763
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
719
764
|
}
|
|
720
765
|
|
|
721
766
|
:root[data-theme="nordic"][data-mode="dark"],
|
|
@@ -757,6 +802,7 @@
|
|
|
757
802
|
--ml-shadow-md: 0 1px 2px oklch(0 0 0 / 0.1059), 0 4.887px 19.58px -2px oklch(0 0 0 / 0.2118);
|
|
758
803
|
--ml-shadow-lg: 0 2px 4px oklch(0 0 0 / 0.0927), 0 9.775px 34.81px -4px oklch(0 0 0 / 0.2647);
|
|
759
804
|
--ml-shadow-xl: 0 4px 8px oklch(0 0 0 / 0.0794), 0 17.92px 60.917px -8px oklch(0 0 0 / 0.3309);
|
|
805
|
+
--ml-shadow-tint: oklch(0 0 0 / 0.2118);
|
|
760
806
|
--ml-focus: var(--ml-primary-text);
|
|
761
807
|
--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background));
|
|
762
808
|
--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent);
|
|
@@ -765,6 +811,10 @@
|
|
|
765
811
|
--ml-control-border: var(--ml-text-faint);
|
|
766
812
|
--ml-ring: 0 0 0 3px color-mix(in oklab, var(--ml-focus) 24%, transparent);
|
|
767
813
|
--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28));
|
|
814
|
+
--ml-highlight: oklch(1 0 0);
|
|
815
|
+
--ml-knob: var(--ml-highlight);
|
|
816
|
+
--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3);
|
|
817
|
+
--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05));
|
|
768
818
|
}
|
|
769
819
|
|
|
770
820
|
@supports (transition-timing-function: linear(0, 1)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlola-ui/engine",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Framework-free CSS, spring easings, and the machine-readable contract for Mlola UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./materials.css": "./generated/materials.css",
|
|
20
20
|
"./recipes.css": "./generated/recipes.css",
|
|
21
21
|
"./motion.css": "./generated/motion.css",
|
|
22
|
+
"./accessibility.css": "./generated/accessibility.css",
|
|
22
23
|
"./tokens.json": "./generated/tokens.json",
|
|
23
24
|
"./manifest.json": "./generated/manifest.json"
|
|
24
25
|
},
|
package/src/behavior-spec.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* packages/behavior, and by the audit that proves the two agree.
|
|
8
8
|
*
|
|
9
9
|
* Components built on native form controls are deliberately absent: checkbox
|
|
10
|
-
* and radio are real inputs, so the browser already owns their
|
|
10
|
+
* and radio are real inputs, so the browser already owns their behavior and
|
|
11
11
|
* nothing here can rot. Only the indeterminate flag on a checkbox needs script,
|
|
12
12
|
* because HTML exposes it as a property rather than an attribute.
|
|
13
13
|
*/
|
|
@@ -216,7 +216,7 @@ export const behaviors = {
|
|
|
216
216
|
keyboard: [{ keys: ["Escape"], does: "Hide the tooltip." }],
|
|
217
217
|
transitions: [
|
|
218
218
|
{ on: "pointer enter or focus the trigger", set: "visible", to: "after the delay" },
|
|
219
|
-
{ on: "pointer leave or blur", set: "hidden", to: "immediately,
|
|
219
|
+
{ on: "pointer leave or blur", set: "hidden", to: "immediately, canceling any pending delay" },
|
|
220
220
|
],
|
|
221
221
|
notes: "Never put essential information or interactive content in a tooltip.",
|
|
222
222
|
},
|
package/src/color.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color math the engine can rely on, in the browser or in Node.
|
|
3
3
|
*
|
|
4
|
-
* Everything is OKLCH in and OKLCH out. A
|
|
4
|
+
* Everything is OKLCH in and OKLCH out. A color outside sRGB is brought into
|
|
5
5
|
* gamut by reducing chroma at a fixed lightness and hue, the same strategy CSS
|
|
6
6
|
* Color 4 uses, so the value the engine measures is the value the browser
|
|
7
7
|
* paints. Contrast follows WCAG 2.2.
|
|
@@ -53,7 +53,7 @@ function srgbToOklch([r, g, b]) {
|
|
|
53
53
|
return { L, C, H };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** Linear sRGB for an OKLCH
|
|
56
|
+
/** Linear sRGB for an OKLCH color, not clamped. */
|
|
57
57
|
function toLinear({ L, C, H }) {
|
|
58
58
|
const radians = (H * Math.PI) / 180;
|
|
59
59
|
const a = C * Math.cos(radians);
|
|
@@ -71,7 +71,7 @@ function toLinear({ L, C, H }) {
|
|
|
71
71
|
const EPSILON = 1e-5;
|
|
72
72
|
const inGamut = (color) => toLinear(color).every((channel) => channel >= -EPSILON && channel <= 1 + EPSILON);
|
|
73
73
|
|
|
74
|
-
/** Reduce chroma until the
|
|
74
|
+
/** Reduce chroma until the color fits sRGB. Lightness and hue are kept. */
|
|
75
75
|
export function toGamut(color) {
|
|
76
76
|
const L = clamp(color.L, 0, 1);
|
|
77
77
|
const candidate = { ...color, L };
|
|
@@ -86,7 +86,7 @@ export function toGamut(color) {
|
|
|
86
86
|
return { ...candidate, C: low };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Gamma-encoded sRGB in 0..1, for an in-gamut
|
|
89
|
+
/** Gamma-encoded sRGB in 0..1, for an in-gamut color. */
|
|
90
90
|
export function toSrgb(color) {
|
|
91
91
|
return toLinear(toGamut(color)).map((channel) => clamp(encode(clamp(channel, 0, 1)), 0, 1));
|
|
92
92
|
}
|
|
@@ -106,7 +106,7 @@ function composite(top, bottom) {
|
|
|
106
106
|
return { ...srgbToOklch(mixed), alpha: 1 };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** WCAG 2.2 contrast ratio. Accepts
|
|
109
|
+
/** WCAG 2.2 contrast ratio. Accepts color strings or parsed colors. */
|
|
110
110
|
export function contrast(foreground, background) {
|
|
111
111
|
const fg = typeof foreground === "string" ? parseColor(foreground) : foreground;
|
|
112
112
|
const bg = typeof background === "string" ? parseColor(background) : background;
|
|
@@ -117,7 +117,7 @@ export function contrast(foreground, background) {
|
|
|
117
117
|
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/** Serialise an OKLCH
|
|
120
|
+
/** Serialise an OKLCH color, gamut-mapped, at stable precision. */
|
|
121
121
|
export function formatColor(color) {
|
|
122
122
|
const mapped = toGamut(color);
|
|
123
123
|
const chroma = round(mapped.C, 3);
|
package/src/config.mjs
CHANGED
|
@@ -100,7 +100,7 @@ const canonicalMeta = {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Canonical themes in the shape the rest of the build reads: the
|
|
103
|
+
* Canonical themes in the shape the rest of the build reads: the normalized
|
|
104
104
|
* spec, its vector as an ordered array, and the descriptive copy.
|
|
105
105
|
*/
|
|
106
106
|
export const profiles = Object.fromEntries(
|
package/src/contract.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { behaviors } from "./behavior-spec.mjs";
|
|
|
10
10
|
* library actually renders. Any renderer in any language that emits these
|
|
11
11
|
* classes and attributes gets the correct visuals.
|
|
12
12
|
*
|
|
13
|
-
* The
|
|
13
|
+
* The behavior half — which attribute flips on which event, and the keyboard
|
|
14
14
|
* map — cannot be read out of CSS, so it is authored in behavior-spec.mjs and
|
|
15
15
|
* audited against this derived data.
|
|
16
16
|
*/
|
package/src/contrast.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Color math for the accessibility gate.
|
|
3
3
|
*
|
|
4
4
|
* The palettes are curated by hand, so "meets WCAG AA" has to be a measured
|
|
5
5
|
* fact rather than a comment. This converts the engine's OKLCH tokens to sRGB,
|
|
@@ -44,7 +44,7 @@ export function oklchToSrgb({ L, C, H }) {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Composite a possibly translucent
|
|
47
|
+
/** Composite a possibly translucent color over an opaque backdrop. */
|
|
48
48
|
function composite([top, alpha], [bottom]) {
|
|
49
49
|
return top.map((channel, index) => channel * alpha + bottom[index] * (1 - alpha));
|
|
50
50
|
}
|
package/src/declarations.mjs
CHANGED
|
@@ -35,6 +35,8 @@ export function shadowDeclarations(derived, gain = 1) {
|
|
|
35
35
|
`--ml-shadow-md: 0 1px 2px ${alpha(0.4)}, 0 ${px(y * 0.6)} ${px(blur * 0.9)} -2px ${alpha(0.8)}`,
|
|
36
36
|
`--ml-shadow-lg: 0 2px 4px ${alpha(0.35)}, 0 ${px(y * 1.2)} ${px(blur * 1.6)} -4px ${alpha(1)}`,
|
|
37
37
|
`--ml-shadow-xl: 0 4px 8px ${alpha(0.3)}, 0 ${px(y * 2.2)} ${px(blur * 2.8)} -8px ${alpha(1.25)}`,
|
|
38
|
+
// The contact shadow's color alone, for shapes a box-shadow cannot follow (filter: drop-shadow).
|
|
39
|
+
`--ml-shadow-tint: ${alpha(0.8)}`,
|
|
38
40
|
];
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -113,7 +115,7 @@ export function themeDeclarations(spec) {
|
|
|
113
115
|
*/
|
|
114
116
|
export const PALETTE_DERIVED = [
|
|
115
117
|
"--ml-focus: var(--ml-primary-text)",
|
|
116
|
-
// Hover and pressed fills move a
|
|
118
|
+
// Hover and pressed fills move a color toward the page, so they lighten in
|
|
117
119
|
// light mode and darken in dark mode without a second palette.
|
|
118
120
|
"--ml-primary-hover: color-mix(in oklab, var(--ml-primary) 88%, var(--ml-background))",
|
|
119
121
|
"--ml-fill-hover: color-mix(in oklab, var(--ml-text) 5%, transparent)",
|
|
@@ -128,4 +130,16 @@ export const PALETTE_DERIVED = [
|
|
|
128
130
|
// it carries the theme's tint, and more opaque the darker the page, so it
|
|
129
131
|
// reads as a light veil by day and still separates layers at night.
|
|
130
132
|
"--ml-scrim: oklch(from var(--ml-background) calc(l * 0.22) c h / calc(0.54 - l * 0.28))",
|
|
133
|
+
// Pure light: a specular glint, the lit side of an orb. Light in both modes,
|
|
134
|
+
// as light is.
|
|
135
|
+
"--ml-highlight: oklch(1 0 0)",
|
|
136
|
+
// The thumb of a switch or slider. It stays light on any track and in dark
|
|
137
|
+
// mode, like a physical control; a theme may give it another finish.
|
|
138
|
+
"--ml-knob: var(--ml-highlight)",
|
|
139
|
+
// A knob that must read over any color at all (a color picker's handle)
|
|
140
|
+
// carries its own edge: a hairline and a small drop.
|
|
141
|
+
"--ml-knob-shadow: 0 0 0 1px oklch(0 0 0 / 0.25), 0 1px 4px oklch(0 0 0 / 0.3)",
|
|
142
|
+
// The sheen of a specular surface: light from the top left, a little shade
|
|
143
|
+
// in the far corner.
|
|
144
|
+
"--ml-sheen: linear-gradient(135deg, color-mix(in oklab, var(--ml-highlight) 18%, transparent), transparent 46%, oklch(0 0 0 / 0.05))",
|
|
131
145
|
];
|
package/src/library-recipes.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
* Assemble the library's stylesheets from the CSS files their owners keep.
|
|
7
7
|
*
|
|
8
8
|
* Each component carries its own `<name>.css` next to its source, so a
|
|
9
|
-
* component's markup,
|
|
9
|
+
* component's markup, behavior and styling are reviewed and copied together.
|
|
10
10
|
* The engine only concatenates them, in a stable order, into the published
|
|
11
11
|
* layers:
|
|
12
12
|
*
|
package/src/palette.mjs
CHANGED
|
@@ -4,23 +4,23 @@ import { contrast, formatColor, parseColor, solveLightness } from "./color.mjs";
|
|
|
4
4
|
* Palettes are derived, not curated.
|
|
5
5
|
*
|
|
6
6
|
* A spec gives a primary seed and the tint of the neutrals; this module builds
|
|
7
|
-
* every
|
|
7
|
+
* every color token for light and dark mode from them. Each text or fill
|
|
8
8
|
* pairing is solved to a WCAG target instead of being picked and then tested,
|
|
9
9
|
* so any seed a person or a model supplies produces an accessible palette.
|
|
10
10
|
*
|
|
11
11
|
* Roles, so a token is never asked to do two jobs:
|
|
12
12
|
* - `primary`, `success`, ... are fills; their `-foreground` sits on them.
|
|
13
|
-
* - `primary-text` is the brand
|
|
13
|
+
* - `primary-text` is the brand color solved for text on the page (links,
|
|
14
14
|
* focus rings). A vivid yellow stays a yellow button; its links go darker.
|
|
15
|
-
* - `success-text`, `danger-text`, ... are the status
|
|
16
|
-
* the page, which a fill
|
|
15
|
+
* - `success-text`, `danger-text`, ... are the status colors for text on
|
|
16
|
+
* the page, which a fill color usually cannot be in both modes.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** WCAG thresholds with a small margin, so rounding never drops below them. */
|
|
20
20
|
export const TARGETS = {
|
|
21
21
|
text: 17,
|
|
22
22
|
textDark: 17.5,
|
|
23
|
-
//
|
|
23
|
+
// Colored text is solved with headroom, so it still clears AA on the
|
|
24
24
|
// hover fills and ~15% tints the library draws under it.
|
|
25
25
|
body: 5.3,
|
|
26
26
|
// Every text role is readable text. Faint is the quietest level, not a
|
|
@@ -42,7 +42,7 @@ function hardestBackground(foreground, neutrals) {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** Solve a text
|
|
45
|
+
/** Solve a text color against every page background it may sit on. */
|
|
46
46
|
function solveText(color, neutrals, target, direction) {
|
|
47
47
|
let solved = solveLightness(color, neutrals.background, target, direction);
|
|
48
48
|
solved = solveLightness(solved, hardestBackground(solved, neutrals), target, direction);
|
|
@@ -130,17 +130,17 @@ function primaryFor(spec, mode, neutrals) {
|
|
|
130
130
|
let fill = { L: seed.L, C: seed.C, H: seed.H };
|
|
131
131
|
if (mode === "dark" && !spec.color.primaryDark) {
|
|
132
132
|
// Without an explicit night seed, a near-black brand inverts to near-white
|
|
133
|
-
// and a deep brand
|
|
133
|
+
// and a deep brand color is lifted, so neither sinks into the page.
|
|
134
134
|
fill = isAchromatic(seed)
|
|
135
135
|
? { L: neutrals.text.L, C: seed.C, H: seed.H }
|
|
136
136
|
: { L: Math.max(seed.L, 0.62), C: seed.C, H: seed.H };
|
|
137
137
|
}
|
|
138
|
-
// A fill keeps the brand's
|
|
138
|
+
// A fill keeps the brand's color; it only has to stand apart from the page
|
|
139
139
|
// and carry its own label. Text contrast is primary-text's job.
|
|
140
140
|
if (contrast(fill, neutrals.background) < TARGETS.separation) {
|
|
141
141
|
fill = solveLightness(fill, neutrals.background, TARGETS.separation, mode === "light" ? "darker" : "lighter");
|
|
142
142
|
}
|
|
143
|
-
// WCAG 2's ratio favours dark ink on mid-tone saturated
|
|
143
|
+
// WCAG 2's ratio favours dark ink on mid-tone saturated colors, but eyes read
|
|
144
144
|
// white on a violet, blue or red far better. In light mode a chromatic,
|
|
145
145
|
// non-warm brand darkens a little (at most 0.12 L) so white clears the
|
|
146
146
|
// target; yellows and oranges keep dark ink, as convention expects.
|
|
@@ -172,7 +172,7 @@ function statusFor(name, mode, neutrals) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Six categorical
|
|
175
|
+
* Six categorical colors for charts, apart from the status roles so a series
|
|
176
176
|
* never reads as "good" or "bad" by accident. The first follows the brand's
|
|
177
177
|
* hue (blue for a monochrome brand); the rest are the hues furthest from those
|
|
178
178
|
* already taken, so neighbours never share a family. Each is solved to 3:1
|
package/src/render.mjs
CHANGED
|
@@ -105,6 +105,10 @@ export function renderMotionCss() {
|
|
|
105
105
|
return `/* Motion and accessibility layer. */\n${readEngineCss("motion")}`;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function renderAccessibilityCss() {
|
|
109
|
+
return `/* Accessibility layer: last in the cascade, so these guarantees hold over any recipe. */\n${readEngineCss("accessibility")}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
export function renderDtcg() {
|
|
109
113
|
const dtcgColor = (value) => {
|
|
110
114
|
const match = value.match(/^oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\)$/);
|
package/src/spec.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { parseColor } from "./color.mjs";
|
|
|
11
11
|
*
|
|
12
12
|
* Every field is one atomic decision, so it can be answered by one typed
|
|
13
13
|
* question: a channel is a score on an ordered scale, a material or a font set
|
|
14
|
-
* is a choice among named options, a
|
|
14
|
+
* is a choice among named options, a color is either given or chosen from a
|
|
15
15
|
* hue family.
|
|
16
16
|
*
|
|
17
17
|
* Deliberate choices, because this has to survive a decade:
|
|
@@ -47,7 +47,7 @@ export const MATERIALS = {
|
|
|
47
47
|
anodized: "Opaque surfaces with a machined top highlight.",
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
/** Hue families a
|
|
50
|
+
/** Hue families a color can be chosen from, as OKLCH hue angles. */
|
|
51
51
|
export const HUE_FAMILIES = {
|
|
52
52
|
red: 25,
|
|
53
53
|
orange: 50,
|
|
@@ -202,7 +202,7 @@ export function resolveFonts(fonts) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
|
-
*
|
|
205
|
+
* Normalize any theme input into a complete spec. Never throws: a partial
|
|
206
206
|
* theme inherits the rest from `base`, which defaults to a neutral spec.
|
|
207
207
|
*
|
|
208
208
|
* Also reads the pre-1.0 theme file shape (`theme` for the vector, `inherit`,
|
|
@@ -246,9 +246,9 @@ export function validateSpec(input) {
|
|
|
246
246
|
const value = input.vector?.[channel];
|
|
247
247
|
if (typeof value !== "number" || value < 0 || value > 1) problems.push(`vector.${channel} must be a number from 0 to 1.`);
|
|
248
248
|
}
|
|
249
|
-
if (!parseColor(input.color?.primary)) problems.push("color.primary must be an oklch() or hex
|
|
249
|
+
if (!parseColor(input.color?.primary)) problems.push("color.primary must be an oklch() or hex color.");
|
|
250
250
|
if (input.color?.primaryDark != null && !parseColor(input.color.primaryDark)) {
|
|
251
|
-
problems.push("color.primaryDark must be an oklch() or hex
|
|
251
|
+
problems.push("color.primaryDark must be an oklch() or hex color, or null.");
|
|
252
252
|
}
|
|
253
253
|
const neutral = input.color?.neutral;
|
|
254
254
|
if (!neutral || typeof neutral.hue !== "number" || typeof neutral.chroma !== "number" || neutral.chroma < 0 || neutral.chroma > 0.06) {
|
|
@@ -295,7 +295,7 @@ export function renderSpecSchema() {
|
|
|
295
295
|
color: {
|
|
296
296
|
type: "object",
|
|
297
297
|
properties: {
|
|
298
|
-
primary: { type: "string", description: "Seed for the primary
|
|
298
|
+
primary: { type: "string", description: "Seed for the primary color, oklch() or hex. Lightness is adjusted to meet contrast." },
|
|
299
299
|
primaryDark: { type: ["string", "null"], description: "Optional different seed for dark mode." },
|
|
300
300
|
neutral: { ...tint, description: "Tint of the page and surfaces." },
|
|
301
301
|
ink: { anyOf: [tint, { type: "null" }], description: "Tint of text and dark-mode surfaces. Defaults to neutral." },
|
package/src/theme-css.mjs
CHANGED
|
@@ -27,7 +27,7 @@ function selectorList(spec, { root, aliases }, dark) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Render a
|
|
30
|
+
* Render a normalized spec as a self-contained stylesheet.
|
|
31
31
|
*
|
|
32
32
|
* `root` also applies the theme to `:root`, which only the default theme
|
|
33
33
|
* does. `aliases` are further `data-theme` names for the same theme, such as
|