@uni-design-system/uni-core 7.3.0 → 8.1.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.
Files changed (26) hide show
  1. package/dist/cjs/index.cjs +453 -11
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/esm/index.js +440 -12
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/types/concepts/component/component.types.d.ts +1 -1
  6. package/dist/types/concepts/component/component.types.d.ts.map +1 -1
  7. package/dist/types/concepts/generation/theme-file.emitter.d.ts.map +1 -1
  8. package/dist/types/concepts/generation/theme.generator.d.ts +9 -2
  9. package/dist/types/concepts/generation/theme.generator.d.ts.map +1 -1
  10. package/dist/types/concepts/style/index.d.ts +1 -0
  11. package/dist/types/concepts/style/index.d.ts.map +1 -1
  12. package/dist/types/concepts/style/selectors.constants.d.ts +18 -0
  13. package/dist/types/concepts/style/selectors.constants.d.ts.map +1 -0
  14. package/dist/types/concepts/theme/index.d.ts +1 -0
  15. package/dist/types/concepts/theme/index.d.ts.map +1 -1
  16. package/dist/types/concepts/theme/theme.model.d.ts +2 -1
  17. package/dist/types/concepts/theme/theme.model.d.ts.map +1 -1
  18. package/dist/types/concepts/theme/theme.types.d.ts +7 -0
  19. package/dist/types/concepts/theme/theme.types.d.ts.map +1 -1
  20. package/dist/types/concepts/theme/theme.validation.d.ts +51 -0
  21. package/dist/types/concepts/theme/theme.validation.d.ts.map +1 -0
  22. package/dist/types/concepts/theme/theme.validation.spec.d.ts +2 -0
  23. package/dist/types/concepts/theme/theme.validation.spec.d.ts.map +1 -0
  24. package/dist/types/concepts/theme/themes/base.theme.d.ts +24 -2
  25. package/dist/types/concepts/theme/themes/base.theme.d.ts.map +1 -1
  26. package/package.json +1 -1
package/dist/esm/index.js CHANGED
@@ -929,6 +929,25 @@ var BaseIcons = {
929
929
  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 "
930
930
  };
931
931
  //#endregion
932
+ //#region src/concepts/style/selectors.constants.ts
933
+ /**
934
+ * The highlight state for roving-focus composites (menus, and any other widget
935
+ * that moves focus programmatically): pointer hover, or focus the user drove
936
+ * from the keyboard.
937
+ *
938
+ * Why not plain `:focus` — a roving-focus composite calls `.focus()` on an item
939
+ * every time it opens, including pointer opens, and `:focus` would paint that
940
+ * as a highlight the mouse user never asked for, reading as a preselected
941
+ * item. `:focus-visible` excludes programmatic focus that follows a click while
942
+ * still matching keyboard navigation.
943
+ *
944
+ * Shared as a constant because Emotion merges styles by **exact selector
945
+ * text**: a component's base rule and any theme variant restyling that rule
946
+ * must key both with this value, or the variant silently fails to override and
947
+ * reintroduces the phantom highlight.
948
+ */
949
+ var HOVER_OR_KEYBOARD_FOCUS = "&:hover, &:focus-visible";
950
+ //#endregion
932
951
  //#region src/concepts/theme/themes/base.theme.ts
933
952
  var BaseTypography = {
934
953
  "display-large": {
@@ -1126,6 +1145,25 @@ var buildBorders = (c) => ({
1126
1145
  dark: `1px solid ${c["on-background"]}`,
1127
1146
  dotted: `1px dotted ${c["on-background"]}`
1128
1147
  });
1148
+ /**
1149
+ * One tag colour role across all three tones. `soft` is the resting look (the
1150
+ * container pair), `solid` fills with the role itself, `outline` keeps the
1151
+ * surface and draws the edge — so every role stays consistent and a theme can
1152
+ * still override any single cell.
1153
+ */
1154
+ var tagVariant = (c, role) => ({ [role]: {
1155
+ backgroundColor: c[`${role}-container`],
1156
+ color: c[`on-${role}-container`],
1157
+ "&.tone-solid": {
1158
+ backgroundColor: c[role],
1159
+ color: c[`on-${role}`]
1160
+ },
1161
+ "&.tone-outline": {
1162
+ backgroundColor: "transparent",
1163
+ color: c[role],
1164
+ borderColor: c[`on-${role}-container-border`] ?? c[role]
1165
+ }
1166
+ } });
1129
1167
  var buildComponents = (c) => ({
1130
1168
  alert: { options: {
1131
1169
  topPosition: 40,
@@ -1227,12 +1265,15 @@ var buildComponents = (c) => ({
1227
1265
  } },
1228
1266
  checkbox: { options: {
1229
1267
  size: 20,
1230
- boxColor: "surface"
1268
+ boxColor: "surface",
1269
+ borderRadius: 2,
1270
+ focusRingGap: 2
1231
1271
  } },
1232
1272
  radio: { options: {
1233
1273
  size: 20,
1234
1274
  ringColor: "outline",
1235
- fillColor: "surface"
1275
+ fillColor: "surface",
1276
+ transitionSpeed: .3
1236
1277
  } },
1237
1278
  dialog: { options: {
1238
1279
  borderRadius: "lg",
@@ -1296,7 +1337,7 @@ var buildComponents = (c) => ({
1296
1337
  },
1297
1338
  variants: { warn: {
1298
1339
  color: c.warn,
1299
- "&:hover, &:focus": {
1340
+ [HOVER_OR_KEYBOARD_FOCUS]: {
1300
1341
  backgroundColor: c["warn-container"],
1301
1342
  color: c["on-warn-container"]
1302
1343
  }
@@ -1350,6 +1391,129 @@ var buildComponents = (c) => ({
1350
1391
  focusOutlineOffset: 2
1351
1392
  } },
1352
1393
  badge: { options: { borderRadius: "xxs" } },
1394
+ tag: {
1395
+ options: {
1396
+ borderRadius: "max",
1397
+ typeface: "tag",
1398
+ gap: "xs",
1399
+ removeIcon: "close",
1400
+ selectedIcon: "check"
1401
+ },
1402
+ fixed: {
1403
+ display: "inline-flex",
1404
+ alignItems: "center",
1405
+ maxWidth: "100%",
1406
+ border: "1px solid transparent",
1407
+ transition: "background-color .2s ease, color .2s ease"
1408
+ },
1409
+ variants: {
1410
+ ...tagVariant(c, "primary"),
1411
+ ...tagVariant(c, "secondary"),
1412
+ ...tagVariant(c, "tertiary"),
1413
+ ...tagVariant(c, "warn"),
1414
+ ...tagVariant(c, "success"),
1415
+ ghost: {
1416
+ backgroundColor: "transparent",
1417
+ color: c["on-background"],
1418
+ "&.tone-solid": {
1419
+ backgroundColor: c["surface-variant"],
1420
+ color: c["on-surface-variant"]
1421
+ },
1422
+ "&.tone-outline": {
1423
+ backgroundColor: "transparent",
1424
+ borderColor: c.outline
1425
+ }
1426
+ },
1427
+ disabled: {
1428
+ backgroundColor: c["disabled-container"],
1429
+ color: c["on-disabled"],
1430
+ "&.tone-solid": {
1431
+ backgroundColor: c.disabled,
1432
+ color: c["on-disabled"]
1433
+ },
1434
+ "&.tone-outline": {
1435
+ backgroundColor: "transparent",
1436
+ borderColor: c.disabled
1437
+ }
1438
+ }
1439
+ },
1440
+ sizes: {
1441
+ sm: {
1442
+ height: 20,
1443
+ fontSize: 12,
1444
+ padding: "0 8px"
1445
+ },
1446
+ md: {
1447
+ height: 24,
1448
+ fontSize: 13,
1449
+ padding: "0 10px"
1450
+ },
1451
+ lg: {
1452
+ height: 32,
1453
+ fontSize: 15,
1454
+ padding: "0 12px"
1455
+ }
1456
+ }
1457
+ },
1458
+ tagInput: { options: {
1459
+ chipGap: "xs",
1460
+ chipSize: "md",
1461
+ minInputWidth: "12ch",
1462
+ listColor: "primary-surface",
1463
+ listShadow: "menu",
1464
+ listBorderRadius: "xs",
1465
+ maxSuggestions: 8
1466
+ } },
1467
+ calendar: {
1468
+ options: {
1469
+ dayBorderRadius: "max",
1470
+ typeface: "label",
1471
+ gap: "xxs",
1472
+ navPrevSymbol: "chevron_left",
1473
+ navNextSymbol: "chevron_right",
1474
+ weekdayFormat: "short",
1475
+ showOutsideDays: false,
1476
+ todayStyle: "outline"
1477
+ },
1478
+ fixed: {
1479
+ display: "inline-block",
1480
+ userSelect: "none"
1481
+ },
1482
+ sizes: {
1483
+ sm: {
1484
+ width: 28,
1485
+ height: 28,
1486
+ fontSize: 12
1487
+ },
1488
+ md: {
1489
+ width: 34,
1490
+ height: 34,
1491
+ fontSize: 13
1492
+ },
1493
+ lg: {
1494
+ width: 40,
1495
+ height: 40,
1496
+ fontSize: 15
1497
+ }
1498
+ }
1499
+ },
1500
+ dateInput: { options: {
1501
+ toggleSymbol: "calendar_month",
1502
+ popupShadow: "menu",
1503
+ popupBorderRadius: "xs",
1504
+ popupColor: "primary-surface"
1505
+ } },
1506
+ timeInput: { options: {
1507
+ toggleSymbol: "schedule",
1508
+ listColor: "primary-surface",
1509
+ listShadow: "menu",
1510
+ listBorderRadius: "xs",
1511
+ maxVisibleOptions: 7
1512
+ } },
1513
+ dateTimeInput: { options: {
1514
+ partGap: "sm",
1515
+ dividerColor: "outline"
1516
+ } },
1353
1517
  button: {
1354
1518
  options: {
1355
1519
  borderRadius: "max",
@@ -1653,14 +1817,14 @@ var buildComponents = (c) => ({
1653
1817
  typeface: "label"
1654
1818
  } }
1655
1819
  });
1656
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1820
+ var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1657
1821
  var deepMerge = (base, override) => {
1658
1822
  if (!override) return base;
1659
1823
  const out = { ...base };
1660
- for (const [key, value] of Object.entries(override)) out[key] = isRecord(value) && isRecord(out[key]) ? deepMerge(out[key], value) : value;
1824
+ for (const [key, value] of Object.entries(override)) out[key] = isRecord$1(value) && isRecord$1(out[key]) ? deepMerge(out[key], value) : value;
1661
1825
  return out;
1662
1826
  };
1663
- var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows = BaseShadows, borders, components }) => ({
1827
+ var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows = BaseShadows, borders, thicknesses, components }) => ({
1664
1828
  id,
1665
1829
  name,
1666
1830
  colors,
@@ -1669,7 +1833,10 @@ var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows =
1669
1833
  radii,
1670
1834
  shadows,
1671
1835
  spacing: BaseSpacing,
1672
- thicknesses: BaseThicknesses,
1836
+ thicknesses: {
1837
+ ...BaseThicknesses,
1838
+ ...thicknesses
1839
+ },
1673
1840
  icons: {
1674
1841
  ...BaseIcons,
1675
1842
  ...icons
@@ -1677,6 +1844,31 @@ var createTheme = ({ id, name, colors, icons = {}, radii = BaseRadii, shadows =
1677
1844
  components: deepMerge(buildComponents(colors), components)
1678
1845
  });
1679
1846
  /**
1847
+ * Re-attach the built-in icon set to a theme that traveled without it.
1848
+ *
1849
+ * A serialized theme is ~50 KB, ~71% of which is {@link BaseIcons} — bytes
1850
+ * every uni-core consumer already ships. Transports (MCP tool results, theme
1851
+ * JSON over HTTP) send the {@link dehydrateTheme} form and the receiver
1852
+ * hydrates, applying exactly the `{...BaseIcons, ...icons}` contract
1853
+ * {@link createTheme} applies at construction: the theme's own icons win,
1854
+ * built-ins fill the rest.
1855
+ */
1856
+ var hydrateTheme = (theme) => ({
1857
+ ...theme,
1858
+ icons: {
1859
+ ...BaseIcons,
1860
+ ...theme.icons
1861
+ }
1862
+ });
1863
+ /**
1864
+ * The wire form of a theme: icons identical to the built-in set are dropped,
1865
+ * so only genuine overrides travel. Reverse with {@link hydrateTheme}.
1866
+ */
1867
+ var dehydrateTheme = (theme) => ({
1868
+ ...theme,
1869
+ icons: Object.fromEntries(Object.entries(theme.icons).filter(([name, uri]) => BaseIcons[name] !== uri))
1870
+ });
1871
+ /**
1680
1872
  * Build a full {@link UniTheme} straight from a {@link PaletteConfig} — the
1681
1873
  * one-call path a theme builder uses to turn a brand color (or a seed +
1682
1874
  * scheme + category) into a complete, ready-to-apply theme.
@@ -1824,11 +2016,17 @@ var generateThemes = (input) => {
1824
2016
  };
1825
2017
  };
1826
2018
  /**
2019
+ * One-line human summary of a {@link ContrastReport}. Shared so the generated
2020
+ * theme file's header and the MCP's theme tools report identical wording.
2021
+ */
2022
+ 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`}`;
2023
+ /**
1827
2024
  * Convenience wrapper: {@link generateThemes} piped through `createTheme()`,
1828
- * returning a registration-ready light/dark {@link UniTheme} pair.
2025
+ * returning a registration-ready light/dark {@link UniTheme} pair alongside
2026
+ * the contrast report the colors were audited against.
1829
2027
  */
1830
2028
  var generateUniThemes = (input) => {
1831
- const { lightColors, darkColors, radii, lightShadows, darkShadows } = generateThemes(input);
2029
+ const { lightColors, darkColors, radii, lightShadows, darkShadows, report } = generateThemes(input);
1832
2030
  const name = input.name ?? "Brand";
1833
2031
  const id = name.replace(/\W+/g, "") || "Brand";
1834
2032
  return {
@@ -1845,7 +2043,8 @@ var generateUniThemes = (input) => {
1845
2043
  colors: darkColors,
1846
2044
  radii,
1847
2045
  shadows: darkShadows
1848
- })
2046
+ }),
2047
+ report
1849
2048
  };
1850
2049
  };
1851
2050
  //#endregion
@@ -1904,7 +2103,7 @@ var emitThemeFile = (input) => {
1904
2103
  input.shape && `--shape=${input.shape}`,
1905
2104
  !darkMode && "--dark-mode=false"
1906
2105
  ].filter(Boolean).join(" ");
1907
- const reportSummary = `${report.checks.length} contrast pairs checked · worst ${report.worstRatio}:1 · ${report.pass ? "all AA" : `${report.checks.filter((c) => !c.pass).length} failing`}`;
2106
+ const reportSummary = summarizeContrast(report);
1908
2107
  const modes = [{
1909
2108
  exportName: `${id}Light`,
1910
2109
  displayName: `${name} Light`,
@@ -2197,6 +2396,235 @@ var UniThemes = {
2197
2396
  };
2198
2397
  var DefaultThemeId = Object.keys(UniThemes)[0];
2199
2398
  //#endregion
2399
+ //#region src/concepts/theme/theme.validation.ts
2400
+ /**
2401
+ * Color tokens components cannot render without: the default variant pairs,
2402
+ * the base surfaces, and the disabled/warn states. Themes may define any
2403
+ * further tokens; these must exist.
2404
+ */
2405
+ var REQUIRED_COLOR_TOKENS = [
2406
+ "primary",
2407
+ "on-primary",
2408
+ "primary-container",
2409
+ "on-primary-container",
2410
+ "primary-surface",
2411
+ "on-primary-surface",
2412
+ "surface",
2413
+ "on-surface",
2414
+ "background",
2415
+ "on-background",
2416
+ "warn",
2417
+ "on-warn",
2418
+ "disabled",
2419
+ "on-disabled",
2420
+ "outline",
2421
+ "transparent"
2422
+ ];
2423
+ /** The canonical type scale; every theme must style each role. */
2424
+ var REQUIRED_TEXT_ROLES = [
2425
+ "display-large",
2426
+ "display-medium",
2427
+ "display-small",
2428
+ "headline-large",
2429
+ "headline-medium",
2430
+ "headline-small",
2431
+ "title-large",
2432
+ "title-medium",
2433
+ "title-small",
2434
+ "body-1-long",
2435
+ "body-1-short",
2436
+ "body-2-long",
2437
+ "body-2-short",
2438
+ "subtitle-1",
2439
+ "subtitle-2",
2440
+ "label",
2441
+ "button",
2442
+ "caption",
2443
+ "overline",
2444
+ "paragraph",
2445
+ "quote",
2446
+ "note"
2447
+ ];
2448
+ var REQUIRED_SPACING_SIZES = [
2449
+ "none",
2450
+ "xxs",
2451
+ "xs",
2452
+ "sm",
2453
+ "md",
2454
+ "lg",
2455
+ "xl"
2456
+ ];
2457
+ var REQUIRED_RADII_SIZES = [
2458
+ "none",
2459
+ "xxs",
2460
+ "xs",
2461
+ "sm",
2462
+ "md",
2463
+ "lg",
2464
+ "max"
2465
+ ];
2466
+ var REQUIRED_SHADOWS = [
2467
+ "raised",
2468
+ "menu",
2469
+ "dialog",
2470
+ "warn"
2471
+ ];
2472
+ var REQUIRED_THICKNESSES = [
2473
+ "thin",
2474
+ "standard",
2475
+ "thick"
2476
+ ];
2477
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2478
+ var isCssValue = (value) => typeof value === "string" || typeof value === "number";
2479
+ /** Nested style expression: string/number leaves, objects for selectors. */
2480
+ var checkStyleExpression = (value, path, issues) => {
2481
+ if (!isRecord(value)) {
2482
+ issues.push({
2483
+ path,
2484
+ message: "must be a style object"
2485
+ });
2486
+ return;
2487
+ }
2488
+ for (const [key, entry] of Object.entries(value)) {
2489
+ if (entry === void 0 || isCssValue(entry)) continue;
2490
+ if (isRecord(entry)) checkStyleExpression(entry, `${path}.${key}`, issues);
2491
+ else issues.push({
2492
+ path: `${path}.${key}`,
2493
+ message: "must be a string, number, or nested style object"
2494
+ });
2495
+ }
2496
+ };
2497
+ var checkStringRecord = (value, path, issues, required = [], allowNumbers = false) => {
2498
+ if (!isRecord(value)) {
2499
+ issues.push({
2500
+ path,
2501
+ message: "must be an object"
2502
+ });
2503
+ return;
2504
+ }
2505
+ for (const key of required) if (!(key in value)) issues.push({
2506
+ path: `${path}.${key}`,
2507
+ message: "is required"
2508
+ });
2509
+ for (const [key, entry] of Object.entries(value)) {
2510
+ if (entry === void 0) continue;
2511
+ if (!(allowNumbers ? isCssValue(entry) : typeof entry === "string")) issues.push({
2512
+ path: `${path}.${key}`,
2513
+ message: allowNumbers ? "must be a string or number" : "must be a string"
2514
+ });
2515
+ }
2516
+ };
2517
+ var checkTypography = (value, issues) => {
2518
+ if (!isRecord(value)) {
2519
+ issues.push({
2520
+ path: "typography",
2521
+ message: "must be an object"
2522
+ });
2523
+ return;
2524
+ }
2525
+ for (const role of REQUIRED_TEXT_ROLES) if (!(role in value)) issues.push({
2526
+ path: `typography.${role}`,
2527
+ message: "is required"
2528
+ });
2529
+ for (const [role, style] of Object.entries(value)) {
2530
+ if (!isRecord(style)) {
2531
+ issues.push({
2532
+ path: `typography.${role}`,
2533
+ message: "must be a TextStyle object"
2534
+ });
2535
+ continue;
2536
+ }
2537
+ if (typeof style["fontFamily"] !== "string") issues.push({
2538
+ path: `typography.${role}.fontFamily`,
2539
+ message: "must be a string"
2540
+ });
2541
+ for (const field of ["fontSize", "lineHeight"]) if (!isCssValue(style[field])) issues.push({
2542
+ path: `typography.${role}.${field}`,
2543
+ message: "must be a string or number"
2544
+ });
2545
+ }
2546
+ };
2547
+ var checkComponents = (value, issues) => {
2548
+ if (!isRecord(value)) {
2549
+ issues.push({
2550
+ path: "components",
2551
+ message: "must be an object"
2552
+ });
2553
+ return;
2554
+ }
2555
+ for (const [name, entry] of Object.entries(value)) {
2556
+ if (entry === void 0) continue;
2557
+ if (!isRecord(entry)) {
2558
+ issues.push({
2559
+ path: `components.${name}`,
2560
+ message: "must be a component theme object"
2561
+ });
2562
+ continue;
2563
+ }
2564
+ if (entry["fixed"] !== void 0) checkStyleExpression(entry["fixed"], `components.${name}.fixed`, issues);
2565
+ for (const section of ["variants", "sizes"]) {
2566
+ const styles = entry[section];
2567
+ if (styles === void 0) continue;
2568
+ if (!isRecord(styles)) {
2569
+ issues.push({
2570
+ path: `components.${name}.${section}`,
2571
+ message: "must be an object"
2572
+ });
2573
+ continue;
2574
+ }
2575
+ for (const [key, style] of Object.entries(styles)) if (style !== void 0) checkStyleExpression(style, `components.${name}.${section}.${key}`, issues);
2576
+ }
2577
+ if (entry["options"] !== void 0 && !isRecord(entry["options"])) issues.push({
2578
+ path: `components.${name}.options`,
2579
+ message: "must be an object"
2580
+ });
2581
+ }
2582
+ };
2583
+ /**
2584
+ * Validate a candidate theme, collecting every structural issue. Never
2585
+ * throws; a failed result carries the complete repair list.
2586
+ */
2587
+ var parseTheme = (input) => {
2588
+ const issues = [];
2589
+ if (!isRecord(input)) return {
2590
+ success: false,
2591
+ issues: [{
2592
+ path: "",
2593
+ message: "theme must be an object"
2594
+ }]
2595
+ };
2596
+ for (const field of ["id", "name"]) if (typeof input[field] !== "string" || input[field] === "") issues.push({
2597
+ path: field,
2598
+ message: "must be a non-empty string"
2599
+ });
2600
+ checkStringRecord(input["colors"], "colors", issues, REQUIRED_COLOR_TOKENS);
2601
+ checkTypography(input["typography"], issues);
2602
+ checkStringRecord(input["borders"], "borders", issues);
2603
+ checkStringRecord(input["radii"], "radii", issues, REQUIRED_RADII_SIZES);
2604
+ checkStringRecord(input["shadows"], "shadows", issues, REQUIRED_SHADOWS);
2605
+ checkStringRecord(input["spacing"], "spacing", issues, REQUIRED_SPACING_SIZES, true);
2606
+ checkStringRecord(input["thicknesses"], "thicknesses", issues, REQUIRED_THICKNESSES, true);
2607
+ checkStringRecord(input["icons"], "icons", issues);
2608
+ checkComponents(input["components"], issues);
2609
+ return issues.length === 0 ? {
2610
+ success: true,
2611
+ theme: input,
2612
+ issues: []
2613
+ } : {
2614
+ success: false,
2615
+ issues
2616
+ };
2617
+ };
2618
+ /** Format issues for logs and error messages. */
2619
+ var formatThemeIssues = (issues) => issues.map(({ path, message }) => path ? `${path} ${message}` : message).join("; ");
2620
+ /** Validate or throw, with every reason in the error message. */
2621
+ var assertTheme = (input) => {
2622
+ const result = parseTheme(input);
2623
+ if (!result.success) throw new Error(`Invalid UniTheme: ${formatThemeIssues(result.issues)}`);
2624
+ return result.theme;
2625
+ };
2626
+ var isUniTheme = (input) => parseTheme(input).success;
2627
+ //#endregion
2200
2628
  //#region src/concepts/typography/typography.records.ts
2201
2629
  var FontWeightMap = {
2202
2630
  thin: 100,
@@ -2247,6 +2675,6 @@ var debounce = (callback, timeout) => {
2247
2675
  };
2248
2676
  };
2249
2677
  //#endregion
2250
- export { BASE_PALETTE_CONFIG, BaseIcons, BaseTheme, CategoryChroma, CategoryLightness, CategorySaturation, DarkTheme, DefaultThemeId, EXPAND_DEFAULT_SPEED, EXPAND_MAX_DURATION, EXPAND_MIN_DURATION, EXPAND_REFERENCE_HEIGHT, FontWeightMap, HSLAToString, HSLToHex, HSLToRGB, LightTheme, RGBToHSL, RGBToString, RoleHues, ShadowCssMap, ShadowMap, ShapeRadii, UniThemes, Z_INDEX, classifyScheme, collapseFadeOut, contrastRatio, createTheme, createThemeFromPalette, cycle, darkColors, debounce, emitDtcgTokens, emitThemeFile, expandDuration, expandFadeIn, fadeIn, fadeOut, generateColors, generatePalette, generateShadows, generateThemes, generateUniThemes, getAnalogousHues, getComplimentaryHue, getDeviceOrientation, getDeviceSize, getSplitComplimentaryHues, getTriadicHues, getValue, gradient, hexToHSL, hexToOklch, hexToRgb, inferCategory, lightColors, oklchToHex, randomRangeValue, relativeLuminance, removeInputPlatformStyling, rgbToHex, schemeHues, svgToIconUri, toTypeface, toTypefaces, uniColor };
2678
+ export { BASE_PALETTE_CONFIG, BaseIcons, BaseTheme, CategoryChroma, CategoryLightness, CategorySaturation, DarkTheme, DefaultThemeId, EXPAND_DEFAULT_SPEED, EXPAND_MAX_DURATION, EXPAND_MIN_DURATION, EXPAND_REFERENCE_HEIGHT, FontWeightMap, HOVER_OR_KEYBOARD_FOCUS, HSLAToString, HSLToHex, HSLToRGB, LightTheme, REQUIRED_COLOR_TOKENS, REQUIRED_RADII_SIZES, REQUIRED_SHADOWS, REQUIRED_SPACING_SIZES, REQUIRED_TEXT_ROLES, REQUIRED_THICKNESSES, RGBToHSL, RGBToString, RoleHues, ShadowCssMap, ShadowMap, ShapeRadii, UniThemes, Z_INDEX, assertTheme, classifyScheme, collapseFadeOut, contrastRatio, createTheme, createThemeFromPalette, cycle, darkColors, debounce, dehydrateTheme, emitDtcgTokens, emitThemeFile, expandDuration, expandFadeIn, fadeIn, fadeOut, formatThemeIssues, generateColors, generatePalette, generateShadows, generateThemes, generateUniThemes, getAnalogousHues, getComplimentaryHue, getDeviceOrientation, getDeviceSize, getSplitComplimentaryHues, getTriadicHues, getValue, gradient, hexToHSL, hexToOklch, hexToRgb, hydrateTheme, inferCategory, isUniTheme, lightColors, oklchToHex, parseTheme, randomRangeValue, relativeLuminance, removeInputPlatformStyling, rgbToHex, schemeHues, summarizeContrast, svgToIconUri, toTypeface, toTypefaces, uniColor };
2251
2679
 
2252
2680
  //# sourceMappingURL=index.js.map