@mlola-ui/engine 1.0.1 → 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/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 normalised
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 behaviour half — which attribute flips on which event, and the keyboard
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
- * Colour math for the accessibility gate.
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 colour over an opaque backdrop. */
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
  }
@@ -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 colour toward the page, so they lighten in
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
  ];
@@ -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, behaviour and styling are reviewed and copied together.
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 colour token for light and dark mode from them. Each text or fill
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 colour solved for text on the page (links,
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 colours for text on
16
- * the page, which a fill colour usually cannot be in both modes.
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
- // Coloured text is solved with headroom, so it still clears AA on the
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 colour against every page background it may sit on. */
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 colour is lifted, so neither sinks into the page.
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 colour; it only has to stand apart from the page
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 colours, but eyes read
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 colours for charts, apart from the status roles so a series
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 colour is either given or chosen from 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 colour can be chosen from, as OKLCH hue angles. */
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
- * Normalise any theme input into a complete spec. Never throws: a partial
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 colour.");
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 colour, or null.");
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 colour, oklch() or hex. Lightness is adjusted to meet contrast." },
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 normalised spec as a self-contained stylesheet.
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