@mlola-ui/engine 1.0.2 → 1.0.4
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.json +419 -0
- package/generated/agents.md +90 -6
- package/generated/assets.json +809 -124
- 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/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
|