@uni-design-system/uni-core 7.3.0 → 8.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.
@@ -930,6 +930,25 @@ var BaseIcons = {
930
930
  spinner: "data:image/svg+xml;charset=UTF-8,%3csvg stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3e%3cstyle%3e .oui_spinner %7b transform-origin: center; animation: rotate_360 2s linear infinite;%7d .oui_spinner circle %7b stroke-linecap: round; animation: dash 1.5s ease-in-out infinite;%7d %40keyframes rotate_360 %7b 100%25 %7b transform: rotate(360deg);%7d %7d %40keyframes dash %7b 0%25 %7b stroke-dasharray: 0 150; stroke-dashoffset: 0;%7d 47.5%25 %7b stroke-dasharray: 42 150; stroke-dashoffset: -16;%7d 95%25, 100%25 %7b stroke-dasharray: 42 150; stroke-dashoffset: -59;%7d %7d %3c/style%3e%3cg class='oui_spinner'%3e%3ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3e%3c/circle%3e%3c/g%3e%3c/svg%3e "
931
931
  };
932
932
  //#endregion
933
+ //#region src/concepts/style/selectors.constants.ts
934
+ /**
935
+ * The highlight state for roving-focus composites (menus, and any other widget
936
+ * that moves focus programmatically): pointer hover, or focus the user drove
937
+ * from the keyboard.
938
+ *
939
+ * Why not plain `:focus` — a roving-focus composite calls `.focus()` on an item
940
+ * every time it opens, including pointer opens, and `:focus` would paint that
941
+ * as a highlight the mouse user never asked for, reading as a preselected
942
+ * item. `:focus-visible` excludes programmatic focus that follows a click while
943
+ * still matching keyboard navigation.
944
+ *
945
+ * Shared as a constant because Emotion merges styles by **exact selector
946
+ * text**: a component's base rule and any theme variant restyling that rule
947
+ * must key both with this value, or the variant silently fails to override and
948
+ * reintroduces the phantom highlight.
949
+ */
950
+ var HOVER_OR_KEYBOARD_FOCUS = "&:hover, &:focus-visible";
951
+ //#endregion
933
952
  //#region src/concepts/theme/themes/base.theme.ts
934
953
  var BaseTypography = {
935
954
  "display-large": {
@@ -1127,6 +1146,25 @@ var buildBorders = (c) => ({
1127
1146
  dark: `1px solid ${c["on-background"]}`,
1128
1147
  dotted: `1px dotted ${c["on-background"]}`
1129
1148
  });
1149
+ /**
1150
+ * One tag colour role across all three tones. `soft` is the resting look (the
1151
+ * container pair), `solid` fills with the role itself, `outline` keeps the
1152
+ * surface and draws the edge — so every role stays consistent and a theme can
1153
+ * still override any single cell.
1154
+ */
1155
+ var tagVariant = (c, role) => ({ [role]: {
1156
+ backgroundColor: c[`${role}-container`],
1157
+ color: c[`on-${role}-container`],
1158
+ "&.tone-solid": {
1159
+ backgroundColor: c[role],
1160
+ color: c[`on-${role}`]
1161
+ },
1162
+ "&.tone-outline": {
1163
+ backgroundColor: "transparent",
1164
+ color: c[role],
1165
+ borderColor: c[`on-${role}-container-border`] ?? c[role]
1166
+ }
1167
+ } });
1130
1168
  var buildComponents = (c) => ({
1131
1169
  alert: { options: {
1132
1170
  topPosition: 40,
@@ -1297,7 +1335,7 @@ var buildComponents = (c) => ({
1297
1335
  },
1298
1336
  variants: { warn: {
1299
1337
  color: c.warn,
1300
- "&:hover, &:focus": {
1338
+ [HOVER_OR_KEYBOARD_FOCUS]: {
1301
1339
  backgroundColor: c["warn-container"],
1302
1340
  color: c["on-warn-container"]
1303
1341
  }
@@ -1351,6 +1389,79 @@ var buildComponents = (c) => ({
1351
1389
  focusOutlineOffset: 2
1352
1390
  } },
1353
1391
  badge: { options: { borderRadius: "xxs" } },
1392
+ tag: {
1393
+ options: {
1394
+ borderRadius: "max",
1395
+ typeface: "tag",
1396
+ gap: "xs",
1397
+ removeIcon: "close",
1398
+ selectedIcon: "check"
1399
+ },
1400
+ fixed: {
1401
+ display: "inline-flex",
1402
+ alignItems: "center",
1403
+ maxWidth: "100%",
1404
+ border: "1px solid transparent",
1405
+ transition: "background-color .2s ease, color .2s ease"
1406
+ },
1407
+ variants: {
1408
+ ...tagVariant(c, "primary"),
1409
+ ...tagVariant(c, "secondary"),
1410
+ ...tagVariant(c, "tertiary"),
1411
+ ...tagVariant(c, "warn"),
1412
+ ...tagVariant(c, "success"),
1413
+ ghost: {
1414
+ backgroundColor: "transparent",
1415
+ color: c["on-background"],
1416
+ "&.tone-solid": {
1417
+ backgroundColor: c["surface-variant"],
1418
+ color: c["on-surface-variant"]
1419
+ },
1420
+ "&.tone-outline": {
1421
+ backgroundColor: "transparent",
1422
+ borderColor: c.outline
1423
+ }
1424
+ },
1425
+ disabled: {
1426
+ backgroundColor: c["disabled-container"],
1427
+ color: c["on-disabled"],
1428
+ "&.tone-solid": {
1429
+ backgroundColor: c.disabled,
1430
+ color: c["on-disabled"]
1431
+ },
1432
+ "&.tone-outline": {
1433
+ backgroundColor: "transparent",
1434
+ borderColor: c.disabled
1435
+ }
1436
+ }
1437
+ },
1438
+ sizes: {
1439
+ sm: {
1440
+ height: 20,
1441
+ fontSize: 12,
1442
+ padding: "0 8px"
1443
+ },
1444
+ md: {
1445
+ height: 24,
1446
+ fontSize: 13,
1447
+ padding: "0 10px"
1448
+ },
1449
+ lg: {
1450
+ height: 32,
1451
+ fontSize: 15,
1452
+ padding: "0 12px"
1453
+ }
1454
+ }
1455
+ },
1456
+ tagInput: { options: {
1457
+ chipGap: "xs",
1458
+ chipSize: "md",
1459
+ minInputWidth: "12ch",
1460
+ listColor: "primary-surface",
1461
+ listShadow: "menu",
1462
+ listBorderRadius: "xs",
1463
+ maxSuggestions: 8
1464
+ } },
1354
1465
  button: {
1355
1466
  options: {
1356
1467
  borderRadius: "max",
@@ -1654,11 +1765,11 @@ var buildComponents = (c) => ({
1654
1765
  typeface: "label"
1655
1766
  } }
1656
1767
  });
1657
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1768
+ var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1658
1769
  var deepMerge = (base, override) => {
1659
1770
  if (!override) return base;
1660
1771
  const out = { ...base };
1661
- for (const [key, value] of Object.entries(override)) out[key] = isRecord(value) && isRecord(out[key]) ? deepMerge(out[key], value) : value;
1772
+ for (const [key, value] of Object.entries(override)) out[key] = isRecord$1(value) && isRecord$1(out[key]) ? deepMerge(out[key], value) : value;
1662
1773
  return out;
1663
1774
  };
1664
1775
  var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows = BaseShadows, borders, components }) => ({
@@ -1678,6 +1789,31 @@ var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows =
1678
1789
  components: deepMerge(buildComponents(colors), components)
1679
1790
  });
1680
1791
  /**
1792
+ * Re-attach the built-in icon set to a theme that traveled without it.
1793
+ *
1794
+ * A serialized theme is ~50 KB, ~71% of which is {@link BaseIcons} — bytes
1795
+ * every uni-core consumer already ships. Transports (MCP tool results, theme
1796
+ * JSON over HTTP) send the {@link dehydrateTheme} form and the receiver
1797
+ * hydrates, applying exactly the `{...BaseIcons, ...icons}` contract
1798
+ * {@link createTheme} applies at construction: the theme's own icons win,
1799
+ * built-ins fill the rest.
1800
+ */
1801
+ var hydrateTheme = (theme) => ({
1802
+ ...theme,
1803
+ icons: {
1804
+ ...BaseIcons,
1805
+ ...theme.icons
1806
+ }
1807
+ });
1808
+ /**
1809
+ * The wire form of a theme: icons identical to the built-in set are dropped,
1810
+ * so only genuine overrides travel. Reverse with {@link hydrateTheme}.
1811
+ */
1812
+ var dehydrateTheme = (theme) => ({
1813
+ ...theme,
1814
+ icons: Object.fromEntries(Object.entries(theme.icons).filter(([name, uri]) => BaseIcons[name] !== uri))
1815
+ });
1816
+ /**
1681
1817
  * Build a full {@link UniTheme} straight from a {@link PaletteConfig} — the
1682
1818
  * one-call path a theme builder uses to turn a brand color (or a seed +
1683
1819
  * scheme + category) into a complete, ready-to-apply theme.
@@ -1825,11 +1961,17 @@ var generateThemes = (input) => {
1825
1961
  };
1826
1962
  };
1827
1963
  /**
1964
+ * One-line human summary of a {@link ContrastReport}. Shared so the generated
1965
+ * theme file's header and the MCP's theme tools report identical wording.
1966
+ */
1967
+ var summarizeContrast = (report) => `${report.checks.length} contrast pairs checked · worst ${report.worstRatio}:1 · ${report.pass ? "all AA" : `${report.checks.filter((check) => !check.pass).length} failing`}`;
1968
+ /**
1828
1969
  * Convenience wrapper: {@link generateThemes} piped through `createTheme()`,
1829
- * returning a registration-ready light/dark {@link UniTheme} pair.
1970
+ * returning a registration-ready light/dark {@link UniTheme} pair alongside
1971
+ * the contrast report the colors were audited against.
1830
1972
  */
1831
1973
  var generateUniThemes = (input) => {
1832
- const { lightColors, darkColors, radii, lightShadows, darkShadows } = generateThemes(input);
1974
+ const { lightColors, darkColors, radii, lightShadows, darkShadows, report } = generateThemes(input);
1833
1975
  const name = input.name ?? "Brand";
1834
1976
  const id = name.replace(/\W+/g, "") || "Brand";
1835
1977
  return {
@@ -1846,7 +1988,8 @@ var generateUniThemes = (input) => {
1846
1988
  colors: darkColors,
1847
1989
  radii,
1848
1990
  shadows: darkShadows
1849
- })
1991
+ }),
1992
+ report
1850
1993
  };
1851
1994
  };
1852
1995
  //#endregion
@@ -1905,7 +2048,7 @@ var emitThemeFile = (input) => {
1905
2048
  input.shape && `--shape=${input.shape}`,
1906
2049
  !darkMode && "--dark-mode=false"
1907
2050
  ].filter(Boolean).join(" ");
1908
- const reportSummary = `${report.checks.length} contrast pairs checked · worst ${report.worstRatio}:1 · ${report.pass ? "all AA" : `${report.checks.filter((c) => !c.pass).length} failing`}`;
2051
+ const reportSummary = summarizeContrast(report);
1909
2052
  const modes = [{
1910
2053
  exportName: `${id}Light`,
1911
2054
  displayName: `${name} Light`,
@@ -2198,6 +2341,235 @@ var UniThemes = {
2198
2341
  };
2199
2342
  var DefaultThemeId = Object.keys(UniThemes)[0];
2200
2343
  //#endregion
2344
+ //#region src/concepts/theme/theme.validation.ts
2345
+ /**
2346
+ * Color tokens components cannot render without: the default variant pairs,
2347
+ * the base surfaces, and the disabled/warn states. Themes may define any
2348
+ * further tokens; these must exist.
2349
+ */
2350
+ var REQUIRED_COLOR_TOKENS = [
2351
+ "primary",
2352
+ "on-primary",
2353
+ "primary-container",
2354
+ "on-primary-container",
2355
+ "primary-surface",
2356
+ "on-primary-surface",
2357
+ "surface",
2358
+ "on-surface",
2359
+ "background",
2360
+ "on-background",
2361
+ "warn",
2362
+ "on-warn",
2363
+ "disabled",
2364
+ "on-disabled",
2365
+ "outline",
2366
+ "transparent"
2367
+ ];
2368
+ /** The canonical type scale; every theme must style each role. */
2369
+ var REQUIRED_TEXT_ROLES = [
2370
+ "display-large",
2371
+ "display-medium",
2372
+ "display-small",
2373
+ "headline-large",
2374
+ "headline-medium",
2375
+ "headline-small",
2376
+ "title-large",
2377
+ "title-medium",
2378
+ "title-small",
2379
+ "body-1-long",
2380
+ "body-1-short",
2381
+ "body-2-long",
2382
+ "body-2-short",
2383
+ "subtitle-1",
2384
+ "subtitle-2",
2385
+ "label",
2386
+ "button",
2387
+ "caption",
2388
+ "overline",
2389
+ "paragraph",
2390
+ "quote",
2391
+ "note"
2392
+ ];
2393
+ var REQUIRED_SPACING_SIZES = [
2394
+ "none",
2395
+ "xxs",
2396
+ "xs",
2397
+ "sm",
2398
+ "md",
2399
+ "lg",
2400
+ "xl"
2401
+ ];
2402
+ var REQUIRED_RADII_SIZES = [
2403
+ "none",
2404
+ "xxs",
2405
+ "xs",
2406
+ "sm",
2407
+ "md",
2408
+ "lg",
2409
+ "max"
2410
+ ];
2411
+ var REQUIRED_SHADOWS = [
2412
+ "raised",
2413
+ "menu",
2414
+ "dialog",
2415
+ "warn"
2416
+ ];
2417
+ var REQUIRED_THICKNESSES = [
2418
+ "thin",
2419
+ "standard",
2420
+ "thick"
2421
+ ];
2422
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2423
+ var isCssValue = (value) => typeof value === "string" || typeof value === "number";
2424
+ /** Nested style expression: string/number leaves, objects for selectors. */
2425
+ var checkStyleExpression = (value, path, issues) => {
2426
+ if (!isRecord(value)) {
2427
+ issues.push({
2428
+ path,
2429
+ message: "must be a style object"
2430
+ });
2431
+ return;
2432
+ }
2433
+ for (const [key, entry] of Object.entries(value)) {
2434
+ if (entry === void 0 || isCssValue(entry)) continue;
2435
+ if (isRecord(entry)) checkStyleExpression(entry, `${path}.${key}`, issues);
2436
+ else issues.push({
2437
+ path: `${path}.${key}`,
2438
+ message: "must be a string, number, or nested style object"
2439
+ });
2440
+ }
2441
+ };
2442
+ var checkStringRecord = (value, path, issues, required = [], allowNumbers = false) => {
2443
+ if (!isRecord(value)) {
2444
+ issues.push({
2445
+ path,
2446
+ message: "must be an object"
2447
+ });
2448
+ return;
2449
+ }
2450
+ for (const key of required) if (!(key in value)) issues.push({
2451
+ path: `${path}.${key}`,
2452
+ message: "is required"
2453
+ });
2454
+ for (const [key, entry] of Object.entries(value)) {
2455
+ if (entry === void 0) continue;
2456
+ if (!(allowNumbers ? isCssValue(entry) : typeof entry === "string")) issues.push({
2457
+ path: `${path}.${key}`,
2458
+ message: allowNumbers ? "must be a string or number" : "must be a string"
2459
+ });
2460
+ }
2461
+ };
2462
+ var checkTypography = (value, issues) => {
2463
+ if (!isRecord(value)) {
2464
+ issues.push({
2465
+ path: "typography",
2466
+ message: "must be an object"
2467
+ });
2468
+ return;
2469
+ }
2470
+ for (const role of REQUIRED_TEXT_ROLES) if (!(role in value)) issues.push({
2471
+ path: `typography.${role}`,
2472
+ message: "is required"
2473
+ });
2474
+ for (const [role, style] of Object.entries(value)) {
2475
+ if (!isRecord(style)) {
2476
+ issues.push({
2477
+ path: `typography.${role}`,
2478
+ message: "must be a TextStyle object"
2479
+ });
2480
+ continue;
2481
+ }
2482
+ if (typeof style["fontFamily"] !== "string") issues.push({
2483
+ path: `typography.${role}.fontFamily`,
2484
+ message: "must be a string"
2485
+ });
2486
+ for (const field of ["fontSize", "lineHeight"]) if (!isCssValue(style[field])) issues.push({
2487
+ path: `typography.${role}.${field}`,
2488
+ message: "must be a string or number"
2489
+ });
2490
+ }
2491
+ };
2492
+ var checkComponents = (value, issues) => {
2493
+ if (!isRecord(value)) {
2494
+ issues.push({
2495
+ path: "components",
2496
+ message: "must be an object"
2497
+ });
2498
+ return;
2499
+ }
2500
+ for (const [name, entry] of Object.entries(value)) {
2501
+ if (entry === void 0) continue;
2502
+ if (!isRecord(entry)) {
2503
+ issues.push({
2504
+ path: `components.${name}`,
2505
+ message: "must be a component theme object"
2506
+ });
2507
+ continue;
2508
+ }
2509
+ if (entry["fixed"] !== void 0) checkStyleExpression(entry["fixed"], `components.${name}.fixed`, issues);
2510
+ for (const section of ["variants", "sizes"]) {
2511
+ const styles = entry[section];
2512
+ if (styles === void 0) continue;
2513
+ if (!isRecord(styles)) {
2514
+ issues.push({
2515
+ path: `components.${name}.${section}`,
2516
+ message: "must be an object"
2517
+ });
2518
+ continue;
2519
+ }
2520
+ for (const [key, style] of Object.entries(styles)) if (style !== void 0) checkStyleExpression(style, `components.${name}.${section}.${key}`, issues);
2521
+ }
2522
+ if (entry["options"] !== void 0 && !isRecord(entry["options"])) issues.push({
2523
+ path: `components.${name}.options`,
2524
+ message: "must be an object"
2525
+ });
2526
+ }
2527
+ };
2528
+ /**
2529
+ * Validate a candidate theme, collecting every structural issue. Never
2530
+ * throws; a failed result carries the complete repair list.
2531
+ */
2532
+ var parseTheme = (input) => {
2533
+ const issues = [];
2534
+ if (!isRecord(input)) return {
2535
+ success: false,
2536
+ issues: [{
2537
+ path: "",
2538
+ message: "theme must be an object"
2539
+ }]
2540
+ };
2541
+ for (const field of ["id", "name"]) if (typeof input[field] !== "string" || input[field] === "") issues.push({
2542
+ path: field,
2543
+ message: "must be a non-empty string"
2544
+ });
2545
+ checkStringRecord(input["colors"], "colors", issues, REQUIRED_COLOR_TOKENS);
2546
+ checkTypography(input["typography"], issues);
2547
+ checkStringRecord(input["borders"], "borders", issues);
2548
+ checkStringRecord(input["radii"], "radii", issues, REQUIRED_RADII_SIZES);
2549
+ checkStringRecord(input["shadows"], "shadows", issues, REQUIRED_SHADOWS);
2550
+ checkStringRecord(input["spacing"], "spacing", issues, REQUIRED_SPACING_SIZES, true);
2551
+ checkStringRecord(input["thicknesses"], "thicknesses", issues, REQUIRED_THICKNESSES, true);
2552
+ checkStringRecord(input["icons"], "icons", issues);
2553
+ checkComponents(input["components"], issues);
2554
+ return issues.length === 0 ? {
2555
+ success: true,
2556
+ theme: input,
2557
+ issues: []
2558
+ } : {
2559
+ success: false,
2560
+ issues
2561
+ };
2562
+ };
2563
+ /** Format issues for logs and error messages. */
2564
+ var formatThemeIssues = (issues) => issues.map(({ path, message }) => path ? `${path} ${message}` : message).join("; ");
2565
+ /** Validate or throw, with every reason in the error message. */
2566
+ var assertTheme = (input) => {
2567
+ const result = parseTheme(input);
2568
+ if (!result.success) throw new Error(`Invalid UniTheme: ${formatThemeIssues(result.issues)}`);
2569
+ return result.theme;
2570
+ };
2571
+ var isUniTheme = (input) => parseTheme(input).success;
2572
+ //#endregion
2201
2573
  //#region src/concepts/typography/typography.records.ts
2202
2574
  var FontWeightMap = {
2203
2575
  thin: 100,
@@ -2261,10 +2633,17 @@ exports.EXPAND_MAX_DURATION = EXPAND_MAX_DURATION;
2261
2633
  exports.EXPAND_MIN_DURATION = EXPAND_MIN_DURATION;
2262
2634
  exports.EXPAND_REFERENCE_HEIGHT = EXPAND_REFERENCE_HEIGHT;
2263
2635
  exports.FontWeightMap = FontWeightMap;
2636
+ exports.HOVER_OR_KEYBOARD_FOCUS = HOVER_OR_KEYBOARD_FOCUS;
2264
2637
  exports.HSLAToString = HSLAToString;
2265
2638
  exports.HSLToHex = HSLToHex;
2266
2639
  exports.HSLToRGB = HSLToRGB;
2267
2640
  exports.LightTheme = LightTheme;
2641
+ exports.REQUIRED_COLOR_TOKENS = REQUIRED_COLOR_TOKENS;
2642
+ exports.REQUIRED_RADII_SIZES = REQUIRED_RADII_SIZES;
2643
+ exports.REQUIRED_SHADOWS = REQUIRED_SHADOWS;
2644
+ exports.REQUIRED_SPACING_SIZES = REQUIRED_SPACING_SIZES;
2645
+ exports.REQUIRED_TEXT_ROLES = REQUIRED_TEXT_ROLES;
2646
+ exports.REQUIRED_THICKNESSES = REQUIRED_THICKNESSES;
2268
2647
  exports.RGBToHSL = RGBToHSL;
2269
2648
  exports.RGBToString = RGBToString;
2270
2649
  exports.RoleHues = RoleHues;
@@ -2273,6 +2652,7 @@ exports.ShadowMap = ShadowMap;
2273
2652
  exports.ShapeRadii = ShapeRadii;
2274
2653
  exports.UniThemes = UniThemes;
2275
2654
  exports.Z_INDEX = Z_INDEX;
2655
+ exports.assertTheme = assertTheme;
2276
2656
  exports.classifyScheme = classifyScheme;
2277
2657
  exports.collapseFadeOut = collapseFadeOut;
2278
2658
  exports.contrastRatio = contrastRatio;
@@ -2281,12 +2661,14 @@ exports.createThemeFromPalette = createThemeFromPalette;
2281
2661
  exports.cycle = cycle;
2282
2662
  exports.darkColors = darkColors;
2283
2663
  exports.debounce = debounce;
2664
+ exports.dehydrateTheme = dehydrateTheme;
2284
2665
  exports.emitDtcgTokens = emitDtcgTokens;
2285
2666
  exports.emitThemeFile = emitThemeFile;
2286
2667
  exports.expandDuration = expandDuration;
2287
2668
  exports.expandFadeIn = expandFadeIn;
2288
2669
  exports.fadeIn = fadeIn;
2289
2670
  exports.fadeOut = fadeOut;
2671
+ exports.formatThemeIssues = formatThemeIssues;
2290
2672
  exports.generateColors = generateColors;
2291
2673
  exports.generatePalette = generatePalette;
2292
2674
  exports.generateShadows = generateShadows;
@@ -2303,14 +2685,18 @@ exports.gradient = gradient;
2303
2685
  exports.hexToHSL = hexToHSL;
2304
2686
  exports.hexToOklch = hexToOklch;
2305
2687
  exports.hexToRgb = hexToRgb;
2688
+ exports.hydrateTheme = hydrateTheme;
2306
2689
  exports.inferCategory = inferCategory;
2690
+ exports.isUniTheme = isUniTheme;
2307
2691
  exports.lightColors = lightColors;
2308
2692
  exports.oklchToHex = oklchToHex;
2693
+ exports.parseTheme = parseTheme;
2309
2694
  exports.randomRangeValue = randomRangeValue;
2310
2695
  exports.relativeLuminance = relativeLuminance;
2311
2696
  exports.removeInputPlatformStyling = removeInputPlatformStyling;
2312
2697
  exports.rgbToHex = rgbToHex;
2313
2698
  exports.schemeHues = schemeHues;
2699
+ exports.summarizeContrast = summarizeContrast;
2314
2700
  exports.svgToIconUri = svgToIconUri;
2315
2701
  exports.toTypeface = toTypeface;
2316
2702
  exports.toTypefaces = toTypefaces;