@cyclone-ui/theme-builder 0.0.4 → 0.0.5
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/CHANGELOG.md +11 -0
- package/dist/index.cjs +1 -1
- package/dist/preprocessor.cjs +1 -1
- package/dist/preprocessor.d.cts +23 -1
- package/dist/preprocessor.d.cts.map +1 -1
- package/dist/preprocessor.d.mts +22 -1
- package/dist/preprocessor.mjs +1 -1
- package/dist/preprocessor.mjs.map +1 -1
- package/package.json +1 -1
- package/src/preprocessor.ts +224 -241
- package/test/preprocessor.spec.ts +170 -191
- package/test/shiki-theme-mapping.spec.ts +24 -27
- package/test/storybook-theme-mapping.spec.ts +29 -29
package/src/preprocessor.ts
CHANGED
|
@@ -416,6 +416,12 @@ const CONTRAST_PRIMITIVES: Record<ParentTheme, string> = {
|
|
|
416
416
|
light: "#0c0c0d"
|
|
417
417
|
};
|
|
418
418
|
|
|
419
|
+
/** Active colors already near the contrast primitive jump to these instead. */
|
|
420
|
+
const PURE_CONTRAST_COLORS: Record<ParentTheme, string> = {
|
|
421
|
+
dark: "#ffffff",
|
|
422
|
+
light: "#000000"
|
|
423
|
+
};
|
|
424
|
+
|
|
419
425
|
function parentThemeFromDescription(
|
|
420
426
|
description: unknown
|
|
421
427
|
): ParentTheme | undefined {
|
|
@@ -453,17 +459,12 @@ const COLOR_STATE_ACTIVE: ColorStateVariant = {
|
|
|
453
459
|
brightness: 1.25
|
|
454
460
|
};
|
|
455
461
|
|
|
456
|
-
const SURFACE_LOW_SATURATION_COLOR_STATE_ACTIVE: ColorStateVariant = {
|
|
457
|
-
name: "active",
|
|
458
|
-
brightness: 1.075
|
|
459
|
-
};
|
|
460
|
-
|
|
461
462
|
const SUBTLE_LOW_SATURATION_COLOR_STATE_ACTIVE: ColorStateVariant = {
|
|
462
463
|
name: "active",
|
|
463
464
|
brightness: 1.125
|
|
464
465
|
};
|
|
465
466
|
|
|
466
|
-
const
|
|
467
|
+
const HAIRLINE_COLOR_STATE_ACTIVE: ColorStateVariant = {
|
|
467
468
|
name: "active",
|
|
468
469
|
brightness: 1.4
|
|
469
470
|
};
|
|
@@ -517,23 +518,42 @@ const SUBTLE_COLOR_STATE_VARIANTS: ColorStateVariants = {
|
|
|
517
518
|
};
|
|
518
519
|
|
|
519
520
|
/**
|
|
520
|
-
*
|
|
521
|
-
* primitive,
|
|
521
|
+
* Backgrounds shift their own lightness instead of jumping to the contrast
|
|
522
|
+
* primitive, at about half the change of other colors' states.
|
|
522
523
|
*/
|
|
523
|
-
const
|
|
524
|
+
const BACKGROUND_COLOR_STATE_INACTIVE: ColorStateVariant = {
|
|
525
|
+
name: "inactive",
|
|
526
|
+
brightness: 0.9
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const BACKGROUND_COLOR_STATE_DISABLED: ColorStateVariant = {
|
|
530
|
+
...COLOR_STATE_DISABLED,
|
|
531
|
+
opacity: 0.675,
|
|
532
|
+
saturation: 0.925
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const BACKGROUND_COLOR_STATE_VARIANTS: ColorStateVariants = {
|
|
524
536
|
lowSaturation: [
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
537
|
+
{ name: "active", brightness: 1.0375 },
|
|
538
|
+
BACKGROUND_COLOR_STATE_INACTIVE,
|
|
539
|
+
BACKGROUND_COLOR_STATE_DISABLED
|
|
528
540
|
],
|
|
529
|
-
saturated:
|
|
541
|
+
saturated: [
|
|
542
|
+
{ name: "active", brightness: 1.125 },
|
|
543
|
+
BACKGROUND_COLOR_STATE_INACTIVE,
|
|
544
|
+
BACKGROUND_COLOR_STATE_DISABLED
|
|
545
|
+
]
|
|
530
546
|
};
|
|
531
547
|
|
|
532
548
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
549
|
+
* Color groups (`color.base`, `color.danger`, …) that get their own Tamagui
|
|
550
|
+
* child theme. Each defines a `primary` color and optionally a `secondary`
|
|
551
|
+
* (low-emphasis background); missing `secondary`, `on-primary`, and
|
|
552
|
+
* `on-secondary` companions are generated, along with state variants and
|
|
553
|
+
* focus rings.
|
|
535
554
|
*/
|
|
536
|
-
const
|
|
555
|
+
const THEME_COLOR_KEYS = [
|
|
556
|
+
"base",
|
|
537
557
|
"danger",
|
|
538
558
|
"warning",
|
|
539
559
|
"info",
|
|
@@ -541,63 +561,50 @@ const STATUS_COLOR_KEYS = [
|
|
|
541
561
|
"success"
|
|
542
562
|
] as const;
|
|
543
563
|
|
|
544
|
-
/**
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
* copies, not for primary, secondary, or tertiary.
|
|
549
|
-
*/
|
|
550
|
-
const BASE_THEME_COLOR_NAMES: Record<string, string> = {
|
|
551
|
-
primary: "base",
|
|
552
|
-
tertiary: "base-subtle"
|
|
564
|
+
/** State variants for the tokens inside each theme color group. */
|
|
565
|
+
const THEME_COLOR_STATE_VARIANTS: Record<string, ColorStateVariants> = {
|
|
566
|
+
primary: DEFAULT_COLOR_STATE_VARIANTS,
|
|
567
|
+
secondary: SUBTLE_COLOR_STATE_VARIANTS
|
|
553
568
|
};
|
|
554
569
|
|
|
555
|
-
/**
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
/** Every token in this group receives state variants. */
|
|
562
|
-
const COLOR_STATE_GROUP_KEY = "surface";
|
|
570
|
+
/** Color tokens with these `group`s receive state variants. */
|
|
571
|
+
const COLOR_STATE_TOKEN_GROUPS: Record<string, ColorStateVariants> = {
|
|
572
|
+
foreground: DEFAULT_COLOR_STATE_VARIANTS,
|
|
573
|
+
background: BACKGROUND_COLOR_STATE_VARIANTS
|
|
574
|
+
};
|
|
563
575
|
|
|
564
576
|
/** Standalone color tokens that receive state variants. */
|
|
565
577
|
const COLOR_STATE_VARIANTS: Record<string, ColorStateVariants> = {
|
|
566
|
-
|
|
578
|
+
hairline: {
|
|
567
579
|
lowSaturation: [
|
|
568
580
|
INCREASED_LOW_SATURATION_COLOR_STATE_ACTIVE,
|
|
569
581
|
LOW_SATURATION_COLOR_STATE_INACTIVE,
|
|
570
582
|
COLOR_STATE_DISABLED
|
|
571
583
|
],
|
|
572
584
|
saturated: [
|
|
573
|
-
|
|
585
|
+
HAIRLINE_COLOR_STATE_ACTIVE,
|
|
574
586
|
COLOR_STATE_INACTIVE,
|
|
575
587
|
COLOR_STATE_DISABLED
|
|
576
588
|
]
|
|
577
|
-
}
|
|
578
|
-
...Object.fromEntries(
|
|
579
|
-
THEME_COLOR_KEYS.flatMap(key => [
|
|
580
|
-
[key, DEFAULT_COLOR_STATE_VARIANTS],
|
|
581
|
-
[`${key}-subtle`, SUBTLE_COLOR_STATE_VARIANTS]
|
|
582
|
-
])
|
|
583
|
-
)
|
|
589
|
+
}
|
|
584
590
|
};
|
|
585
591
|
|
|
586
592
|
/** OKLCH chroma below which a color uses the low-saturation state variants. */
|
|
587
593
|
const LOW_SATURATION_CHROMA = 0.02;
|
|
588
594
|
|
|
589
|
-
const LOW_SATURATION_RING_OPACITY = 0.
|
|
595
|
+
const LOW_SATURATION_RING_OPACITY = 0.1;
|
|
590
596
|
const SATURATED_RING_OPACITY = 0.15;
|
|
591
597
|
const MINIMUM_SUBTLE_CONTRAST_RATIO = 7.25;
|
|
592
598
|
const MINIMUM_INVERSE_CONTRAST_RATIO = 2;
|
|
593
599
|
const SUBTLE_COLOR_LIGHTNESS_STEP = 0.01;
|
|
594
600
|
const MAX_SUBTLE_COLOR_ADJUSTMENTS = 100;
|
|
595
601
|
const SUBTLE_COLOR_GAMUT_SEARCH_ITERATIONS = 32;
|
|
596
|
-
/** OKLCH lightness shift for
|
|
597
|
-
const CONTRAST_SIDE_ACTIVE_LIGHTNESS_SHIFT = 0.
|
|
602
|
+
/** OKLCH lightness shift for hovered colors already near the contrast primitive. */
|
|
603
|
+
const CONTRAST_SIDE_ACTIVE_LIGHTNESS_SHIFT = 0.25;
|
|
598
604
|
|
|
599
605
|
function isStateVariantKey(key: string): boolean {
|
|
600
606
|
return (
|
|
607
|
+
key.toLowerCase().endsWith("-hovered") ||
|
|
601
608
|
key.toLowerCase().endsWith("-active") ||
|
|
602
609
|
key.toLowerCase().endsWith("-inactive") ||
|
|
603
610
|
key.toLowerCase().endsWith("-disabled")
|
|
@@ -773,12 +780,13 @@ function addMissingRingTokens(dictionary: unknown): unknown {
|
|
|
773
780
|
let changed = false;
|
|
774
781
|
|
|
775
782
|
for (const name of THEME_COLOR_KEYS) {
|
|
776
|
-
|
|
783
|
+
const group = color[name];
|
|
784
|
+
if (!isPlainObject(group) || !isTokenNode(group.primary)) {
|
|
777
785
|
continue;
|
|
778
786
|
}
|
|
779
787
|
|
|
780
|
-
const ringColor = `{color.${name}}`;
|
|
781
|
-
const gap = ringLayer("{color.
|
|
788
|
+
const ringColor = `{color.${name}.primary}`;
|
|
789
|
+
const gap = ringLayer("{color.elevated}", 3);
|
|
782
790
|
const variants: Record<string, [string, unknown[]]> = {
|
|
783
791
|
[name]: [`${name} 3px focus ring with no gap`, [ringLayer(ringColor, 3)]],
|
|
784
792
|
[`${name}-subtle`]: [
|
|
@@ -804,7 +812,7 @@ function addMissingRingTokens(dictionary: unknown): unknown {
|
|
|
804
812
|
$description: `Generated ${description}`,
|
|
805
813
|
$type: "shadow",
|
|
806
814
|
$value: value,
|
|
807
|
-
theme:
|
|
815
|
+
theme: name
|
|
808
816
|
};
|
|
809
817
|
changed = true;
|
|
810
818
|
}
|
|
@@ -970,41 +978,41 @@ function createOnColor(hex: string): "#FAFAFA" | "#151518" {
|
|
|
970
978
|
function createMutedColor(
|
|
971
979
|
hex: string,
|
|
972
980
|
parentTheme: ParentTheme
|
|
973
|
-
): {
|
|
981
|
+
): { secondary: string; onMuted?: string } {
|
|
974
982
|
const { lightness, chroma, hue, alpha } = hexToOklch(hex);
|
|
975
|
-
const
|
|
976
|
-
const onMutedDirection = -
|
|
977
|
-
let
|
|
983
|
+
const secondaryDirection = parentTheme === "light" ? 1 : -1;
|
|
984
|
+
const onMutedDirection = -secondaryDirection;
|
|
985
|
+
let secondaryLightness = lightness;
|
|
978
986
|
let onMutedLightness = lightness;
|
|
979
|
-
let
|
|
987
|
+
let secondary = hex;
|
|
980
988
|
let onMuted = hex;
|
|
981
989
|
|
|
982
990
|
for (let index = 0; index < MAX_SUBTLE_COLOR_ADJUSTMENTS; index += 1) {
|
|
983
|
-
|
|
984
|
-
|
|
991
|
+
secondaryLightness = clamp01(
|
|
992
|
+
secondaryLightness + secondaryDirection * SUBTLE_COLOR_LIGHTNESS_STEP
|
|
985
993
|
);
|
|
986
|
-
|
|
987
|
-
if (contrastRatio(
|
|
988
|
-
return onMuted === hex ? {
|
|
994
|
+
secondary = oklchToGamutMappedHex(secondaryLightness, chroma, hue, alpha);
|
|
995
|
+
if (contrastRatio(secondary, onMuted) >= MINIMUM_SUBTLE_CONTRAST_RATIO) {
|
|
996
|
+
return onMuted === hex ? { secondary } : { secondary, onMuted };
|
|
989
997
|
}
|
|
990
998
|
|
|
991
999
|
onMutedLightness = clamp01(
|
|
992
1000
|
onMutedLightness + onMutedDirection * SUBTLE_COLOR_LIGHTNESS_STEP
|
|
993
1001
|
);
|
|
994
1002
|
onMuted = oklchToGamutMappedHex(onMutedLightness, chroma, hue, alpha);
|
|
995
|
-
if (contrastRatio(
|
|
996
|
-
return {
|
|
1003
|
+
if (contrastRatio(secondary, onMuted) >= MINIMUM_SUBTLE_CONTRAST_RATIO) {
|
|
1004
|
+
return { secondary, onMuted };
|
|
997
1005
|
}
|
|
998
1006
|
|
|
999
1007
|
if (
|
|
1000
|
-
(
|
|
1008
|
+
(secondaryLightness === 0 || secondaryLightness === 1) &&
|
|
1001
1009
|
(onMutedLightness === 0 || onMutedLightness === 1)
|
|
1002
1010
|
) {
|
|
1003
1011
|
break;
|
|
1004
1012
|
}
|
|
1005
1013
|
}
|
|
1006
1014
|
|
|
1007
|
-
throw new Error(`Unable to generate an accessible
|
|
1015
|
+
throw new Error(`Unable to generate an accessible secondary pair for ${hex}`);
|
|
1008
1016
|
}
|
|
1009
1017
|
|
|
1010
1018
|
function isLightColor(hex: string, parentTheme?: ParentTheme): boolean {
|
|
@@ -1031,6 +1039,20 @@ function isBlackColor(hex: string): boolean {
|
|
|
1031
1039
|
return hexToOklch(hex).lightness <= 0.25;
|
|
1032
1040
|
}
|
|
1033
1041
|
|
|
1042
|
+
/** Hovered and active states share the contrast primitive behavior. */
|
|
1043
|
+
function isInteractionState(variant: ColorStateVariant): boolean {
|
|
1044
|
+
return variant.name === "hovered" || variant.name === "active";
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function isNearContrastPrimitive(
|
|
1048
|
+
hex: string,
|
|
1049
|
+
parentTheme?: ParentTheme
|
|
1050
|
+
): boolean {
|
|
1051
|
+
return parentTheme === "dark"
|
|
1052
|
+
? isWhiteColor(hex)
|
|
1053
|
+
: parentTheme === "light" && isBlackColor(hex);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1034
1056
|
/** Resolve a state multiplier from the source color and parent theme. */
|
|
1035
1057
|
function directedBrightnessFactor(
|
|
1036
1058
|
hex: string,
|
|
@@ -1050,7 +1072,7 @@ function contrastPrimitive(
|
|
|
1050
1072
|
}
|
|
1051
1073
|
|
|
1052
1074
|
/**
|
|
1053
|
-
*
|
|
1075
|
+
* Hovered colors that already sit near the parent theme's contrast primitive
|
|
1054
1076
|
* (near-white in dark themes, near-black in light themes) would barely change
|
|
1055
1077
|
* by jumping to it, so their lightness shifts toward the middle instead.
|
|
1056
1078
|
*/
|
|
@@ -1059,19 +1081,37 @@ function contrastSideLightnessShift(
|
|
|
1059
1081
|
variant: ColorStateVariant,
|
|
1060
1082
|
parentTheme?: ParentTheme
|
|
1061
1083
|
): number | undefined {
|
|
1062
|
-
if (
|
|
1084
|
+
if (
|
|
1085
|
+
variant.name !== "hovered" ||
|
|
1086
|
+
!contrastPrimitive(variant, parentTheme) ||
|
|
1087
|
+
!isNearContrastPrimitive(hex, parentTheme)
|
|
1088
|
+
) {
|
|
1063
1089
|
return undefined;
|
|
1064
1090
|
}
|
|
1065
1091
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1092
|
+
return parentTheme === "dark"
|
|
1093
|
+
? -CONTRAST_SIDE_ACTIVE_LIGHTNESS_SHIFT
|
|
1094
|
+
: CONTRAST_SIDE_ACTIVE_LIGHTNESS_SHIFT;
|
|
1095
|
+
}
|
|
1069
1096
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1097
|
+
/**
|
|
1098
|
+
* The color an interaction state jumps to. Active colors already near the
|
|
1099
|
+
* contrast primitive go to pure white (dark) or pure black (light) so they
|
|
1100
|
+
* never lose contrast.
|
|
1101
|
+
*/
|
|
1102
|
+
function stateContrastColor(
|
|
1103
|
+
hex: string,
|
|
1104
|
+
variant: ColorStateVariant,
|
|
1105
|
+
parentTheme?: ParentTheme
|
|
1106
|
+
): string | undefined {
|
|
1107
|
+
const primitive = contrastPrimitive(variant, parentTheme);
|
|
1073
1108
|
|
|
1074
|
-
return
|
|
1109
|
+
return primitive &&
|
|
1110
|
+
parentTheme &&
|
|
1111
|
+
variant.name === "active" &&
|
|
1112
|
+
isNearContrastPrimitive(hex, parentTheme)
|
|
1113
|
+
? PURE_CONTRAST_COLORS[parentTheme]
|
|
1114
|
+
: primitive;
|
|
1075
1115
|
}
|
|
1076
1116
|
|
|
1077
1117
|
function stateBrightnessFactor(
|
|
@@ -1084,7 +1124,7 @@ function stateBrightnessFactor(
|
|
|
1084
1124
|
}
|
|
1085
1125
|
|
|
1086
1126
|
if (contrastPrimitive(variant, parentTheme)) {
|
|
1087
|
-
return variant
|
|
1127
|
+
return isInteractionState(variant) ? 1 : variant.brightness;
|
|
1088
1128
|
}
|
|
1089
1129
|
|
|
1090
1130
|
return directedBrightnessFactor(hex, variant.brightness, parentTheme);
|
|
@@ -1097,14 +1137,16 @@ function applyStateTransform(
|
|
|
1097
1137
|
): string {
|
|
1098
1138
|
const shift = contrastSideLightnessShift(hex, variant, parentTheme);
|
|
1099
1139
|
const primitive =
|
|
1100
|
-
shift === undefined
|
|
1140
|
+
shift === undefined
|
|
1141
|
+
? stateContrastColor(hex, variant, parentTheme)
|
|
1142
|
+
: undefined;
|
|
1101
1143
|
let transformed = primitive ?? hex;
|
|
1102
1144
|
|
|
1103
1145
|
if (shift !== undefined) {
|
|
1104
1146
|
transformed = applyLightnessShift(hex, shift);
|
|
1105
1147
|
} else if (
|
|
1106
1148
|
variant.brightness !== undefined &&
|
|
1107
|
-
!(primitive && variant
|
|
1149
|
+
!(primitive && isInteractionState(variant))
|
|
1108
1150
|
) {
|
|
1109
1151
|
transformed = applyBrightness(
|
|
1110
1152
|
transformed,
|
|
@@ -1137,14 +1179,18 @@ function variantDetail(
|
|
|
1137
1179
|
): string {
|
|
1138
1180
|
const details: string[] = [];
|
|
1139
1181
|
const shift = contrastSideLightnessShift(hex, variant, parentTheme);
|
|
1140
|
-
const primitive =
|
|
1182
|
+
const primitive = stateContrastColor(hex, variant, parentTheme);
|
|
1141
1183
|
|
|
1142
1184
|
if (shift !== undefined) {
|
|
1143
1185
|
details.push(
|
|
1144
1186
|
`${Math.round(Math.abs(shift) * 100)}% ${shift < 0 ? "darker" : "lighter"}`
|
|
1145
1187
|
);
|
|
1146
|
-
} else if (primitive && variant
|
|
1147
|
-
details.push(
|
|
1188
|
+
} else if (primitive && isInteractionState(variant)) {
|
|
1189
|
+
details.push(
|
|
1190
|
+
parentTheme && primitive !== CONTRAST_PRIMITIVES[parentTheme]
|
|
1191
|
+
? `pure ${parentTheme === "dark" ? "white" : "black"}`
|
|
1192
|
+
: `${parentTheme} contrast primitive`
|
|
1193
|
+
);
|
|
1148
1194
|
} else if (variant.brightness !== undefined) {
|
|
1149
1195
|
const factor = stateBrightnessFactor(hex, variant, parentTheme);
|
|
1150
1196
|
const percent = Math.round((factor - 1) * 100);
|
|
@@ -1222,9 +1268,14 @@ function addColorStateTokens(
|
|
|
1222
1268
|
continue;
|
|
1223
1269
|
}
|
|
1224
1270
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
: variants.saturated
|
|
1271
|
+
// Hovered states are generated from the matching active variant.
|
|
1272
|
+
const tokenVariants = (
|
|
1273
|
+
isLowSaturation(hex) ? variants.lowSaturation : variants.saturated
|
|
1274
|
+
).flatMap(variant =>
|
|
1275
|
+
variant.name === "active"
|
|
1276
|
+
? [{ ...variant, name: "hovered" }, variant]
|
|
1277
|
+
: [variant]
|
|
1278
|
+
);
|
|
1228
1279
|
const tokenParentTheme =
|
|
1229
1280
|
parentThemeFromDescription(token.$description) ?? parentTheme;
|
|
1230
1281
|
|
|
@@ -1247,144 +1298,65 @@ function addColorStateTokens(
|
|
|
1247
1298
|
}
|
|
1248
1299
|
|
|
1249
1300
|
/**
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1301
|
+
* Fill in a theme color group (`color.danger`, …): generate a missing
|
|
1302
|
+
* `secondary` low-emphasis background from `primary` and the parent theme,
|
|
1303
|
+
* `on-primary` / `on-secondary` text colors, and state variants for
|
|
1304
|
+
* `primary` and `secondary`. Every token in the group gets the group name as
|
|
1305
|
+
* its `theme` and `group`, so Tamagui places it in that child theme (e.g.
|
|
1306
|
+
* `danger.on-primary` → `onPrimary` in the `danger` theme).
|
|
1255
1307
|
*/
|
|
1256
|
-
function
|
|
1257
|
-
|
|
1308
|
+
function addThemeColorGroupTokens(
|
|
1309
|
+
name: string,
|
|
1310
|
+
group: Record<string, unknown>,
|
|
1258
1311
|
tree: unknown,
|
|
1259
1312
|
parentTheme?: ParentTheme
|
|
1260
1313
|
): Record<string, unknown> {
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1314
|
+
const token = group.primary;
|
|
1315
|
+
const hex = isTokenNode(token) && resolveColorHex(token.$value, tree);
|
|
1316
|
+
const type = isTokenNode(token) && resolveType(token);
|
|
1317
|
+
if (!isTokenNode(token) || !hex || (type && type !== "color")) {
|
|
1318
|
+
return group;
|
|
1319
|
+
}
|
|
1267
1320
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1321
|
+
let result: Record<string, unknown> = { ...group };
|
|
1322
|
+
result["on-primary"] ??= {
|
|
1323
|
+
...token,
|
|
1324
|
+
$type: "color",
|
|
1325
|
+
$value: createOnColor(hex),
|
|
1326
|
+
$description: `Generated text and icon color on ${name} primary backgrounds`
|
|
1327
|
+
};
|
|
1273
1328
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
delete result[`on-${name}`];
|
|
1280
|
-
result[`on-${name}-accent`] ??= isTokenNode(onToken)
|
|
1281
|
-
? { ...onToken, theme: name, group: name }
|
|
1282
|
-
: {
|
|
1283
|
-
...token,
|
|
1284
|
-
$type: "color",
|
|
1285
|
-
$value: createOnColor(hex),
|
|
1286
|
-
$description: `Generated text and icon color on ${name} backgrounds`,
|
|
1287
|
-
theme: name,
|
|
1288
|
-
group: name
|
|
1289
|
-
};
|
|
1290
|
-
|
|
1291
|
-
result[`${name}-accent`] ??= {
|
|
1329
|
+
let onSecondary: string | undefined;
|
|
1330
|
+
if (parentTheme && !("secondary" in group)) {
|
|
1331
|
+
const pair = createMutedColor(hex, parentTheme);
|
|
1332
|
+
result.secondary = {
|
|
1333
|
+
...token,
|
|
1292
1334
|
$type: "color",
|
|
1293
|
-
$value:
|
|
1294
|
-
$description: `
|
|
1295
|
-
theme: name,
|
|
1296
|
-
group: name
|
|
1335
|
+
$value: pair.secondary,
|
|
1336
|
+
$description: `Generated low-emphasis ${name} background for the ${parentTheme} theme`
|
|
1297
1337
|
};
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
result[`${name}-subtle`] = {
|
|
1303
|
-
...token,
|
|
1304
|
-
$type: "color",
|
|
1305
|
-
$value: pair.subtle,
|
|
1306
|
-
$description: `Generated low-emphasis ${name} background for the ${parentTheme} theme`
|
|
1307
|
-
};
|
|
1308
|
-
onMuted = pair.onMuted ?? hex;
|
|
1309
|
-
} else if (isTokenNode(color[`${name}-subtle`])) {
|
|
1310
|
-
const subtleHex = resolveColorHex(
|
|
1311
|
-
(color[`${name}-subtle`] as Record<string, unknown>).$value,
|
|
1312
|
-
tree
|
|
1313
|
-
);
|
|
1314
|
-
onMuted = subtleHex && createOnColor(subtleHex);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
if (onMuted && !(`on-${name}-subtle` in color)) {
|
|
1318
|
-
result[`on-${name}-subtle`] = {
|
|
1319
|
-
...token,
|
|
1320
|
-
$type: "color",
|
|
1321
|
-
$value: onMuted,
|
|
1322
|
-
$description: `Generated text and icon color on ${name}-subtle backgrounds`
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1338
|
+
onSecondary = pair.onMuted ?? hex;
|
|
1339
|
+
} else if (isTokenNode(group.secondary)) {
|
|
1340
|
+
const secondaryHex = resolveColorHex(group.secondary.$value, tree);
|
|
1341
|
+
onSecondary = secondaryHex && createOnColor(secondaryHex);
|
|
1325
1342
|
}
|
|
1326
1343
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
/** Strip the `on-` prefix and `-subtle` / state suffixes from a token name. */
|
|
1331
|
-
function colorTokenRoot(name: string): string {
|
|
1332
|
-
return name
|
|
1333
|
-
.replace(/^on-/, "")
|
|
1334
|
-
.replace(/-(?:active|inactive|disabled)$/i, "")
|
|
1335
|
-
.replace(/-subtle$/, "");
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
/**
|
|
1339
|
-
* Resolve the Tamagui theme for a color token name: the theme color name for
|
|
1340
|
-
* `base` and status colors (including their `on-*`, `*-subtle`, and state
|
|
1341
|
-
* variants). Primary, secondary, and tertiary colors stay in the root light
|
|
1342
|
-
* and dark themes so every child theme inherits them.
|
|
1343
|
-
*/
|
|
1344
|
-
function colorTokenTheme(name: string): string | undefined {
|
|
1345
|
-
const root = colorTokenRoot(name);
|
|
1346
|
-
|
|
1347
|
-
return (THEME_COLOR_KEYS as readonly string[]).includes(root)
|
|
1348
|
-
? root
|
|
1349
|
-
: undefined;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Copy each primary and secondary color (and its `on-*` color) into the
|
|
1354
|
-
* `base` theme as `base` / `base-subtle`.
|
|
1355
|
-
*/
|
|
1356
|
-
function addBaseThemeAliases(
|
|
1357
|
-
color: Record<string, unknown>
|
|
1358
|
-
): Record<string, unknown> {
|
|
1359
|
-
const result = { ...color };
|
|
1360
|
-
for (const [name, token] of Object.entries(color)) {
|
|
1361
|
-
const root = colorTokenRoot(name);
|
|
1362
|
-
const baseName = BASE_THEME_COLOR_NAMES[root];
|
|
1363
|
-
if (!baseName || !isTokenNode(token)) {
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// Copied rather than referenced so states resolve inside nested
|
|
1368
|
-
// `light` / `dark` color groups.
|
|
1369
|
-
result[name.replace(root, baseName)] ??= {
|
|
1344
|
+
if (onSecondary && !("on-secondary" in group)) {
|
|
1345
|
+
result["on-secondary"] = {
|
|
1370
1346
|
...token,
|
|
1371
|
-
|
|
1372
|
-
|
|
1347
|
+
$type: "color",
|
|
1348
|
+
$value: onSecondary,
|
|
1349
|
+
$description: `Generated text and icon color on ${name} secondary backgrounds`
|
|
1373
1350
|
};
|
|
1374
1351
|
}
|
|
1375
1352
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1353
|
+
for (const [key, variants] of Object.entries(THEME_COLOR_STATE_VARIANTS)) {
|
|
1354
|
+
result = addColorStateTokens(result, tree, variants, key, parentTheme);
|
|
1355
|
+
}
|
|
1378
1356
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
): Record<string, unknown> {
|
|
1383
|
-
const result = { ...color };
|
|
1384
|
-
for (const [name, token] of Object.entries(color)) {
|
|
1385
|
-
const theme = colorTokenTheme(name);
|
|
1386
|
-
if (theme && isTokenNode(token) && !("theme" in token)) {
|
|
1387
|
-
result[name] = { ...token, theme };
|
|
1357
|
+
for (const [key, value] of Object.entries(result)) {
|
|
1358
|
+
if (isTokenNode(value)) {
|
|
1359
|
+
result[key] = { theme: name, group: name, ...value };
|
|
1388
1360
|
}
|
|
1389
1361
|
}
|
|
1390
1362
|
|
|
@@ -1392,15 +1364,17 @@ function addColorTokenThemes(
|
|
|
1392
1364
|
}
|
|
1393
1365
|
|
|
1394
1366
|
/**
|
|
1395
|
-
* Add a `group` (the main token name) to each state
|
|
1396
|
-
*
|
|
1367
|
+
* Add a `group` (the main token name) to each state and `on-*` color token
|
|
1368
|
+
* that does not define one, e.g. `hairline-active` → `hairline`.
|
|
1397
1369
|
*/
|
|
1398
1370
|
function addColorTokenGroups(
|
|
1399
1371
|
group: Record<string, unknown>
|
|
1400
1372
|
): Record<string, unknown> {
|
|
1401
1373
|
const result = { ...group };
|
|
1402
1374
|
for (const [name, token] of Object.entries(group)) {
|
|
1403
|
-
const root =
|
|
1375
|
+
const root = name
|
|
1376
|
+
.replace(/^on-/, "")
|
|
1377
|
+
.replace(/-(?:hovered|active|inactive|disabled)$/i, "");
|
|
1404
1378
|
if (root !== name && isTokenNode(token) && !("group" in token)) {
|
|
1405
1379
|
result[name] = { ...token, group: root };
|
|
1406
1380
|
}
|
|
@@ -1442,26 +1416,37 @@ function injectColorStateVariants(
|
|
|
1442
1416
|
);
|
|
1443
1417
|
}
|
|
1444
1418
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1419
|
+
let withStates = result;
|
|
1420
|
+
if (key === "color") {
|
|
1421
|
+
for (const name of THEME_COLOR_KEYS) {
|
|
1422
|
+
const group = withStates[name];
|
|
1423
|
+
if (isPlainObject(group) && !isTokenNode(group)) {
|
|
1424
|
+
withStates[name] = addThemeColorGroupTokens(
|
|
1425
|
+
name,
|
|
1426
|
+
group,
|
|
1427
|
+
tree,
|
|
1428
|
+
parentTheme
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1456
1432
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1433
|
+
for (const [tokenKey, token] of Object.entries(withStates)) {
|
|
1434
|
+
const variants =
|
|
1435
|
+
isTokenNode(token) && typeof token.group === "string"
|
|
1436
|
+
? COLOR_STATE_TOKEN_GROUPS[token.group]
|
|
1437
|
+
: undefined;
|
|
1438
|
+
if (variants) {
|
|
1439
|
+
withStates = addColorStateTokens(
|
|
1440
|
+
withStates,
|
|
1461
1441
|
tree,
|
|
1442
|
+
variants,
|
|
1443
|
+
tokenKey,
|
|
1462
1444
|
parentTheme
|
|
1463
|
-
)
|
|
1464
|
-
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1465
1450
|
for (const [tokenKey, variants] of Object.entries(COLOR_STATE_VARIANTS)) {
|
|
1466
1451
|
if (isTokenNode(withStates[tokenKey])) {
|
|
1467
1452
|
withStates = addColorStateTokens(
|
|
@@ -1474,31 +1459,29 @@ function injectColorStateVariants(
|
|
|
1474
1459
|
}
|
|
1475
1460
|
}
|
|
1476
1461
|
|
|
1477
|
-
return key === "color"
|
|
1478
|
-
? addColorTokenGroups(addColorTokenThemes(withStates))
|
|
1479
|
-
: withStates;
|
|
1462
|
+
return key === "color" ? addColorTokenGroups(withStates) : withStates;
|
|
1480
1463
|
}
|
|
1481
1464
|
|
|
1482
1465
|
/**
|
|
1483
1466
|
* Style Dictionary preprocessor: rewrite color `$value`s (and nested shadow
|
|
1484
1467
|
* colors) to hex so generators emit `#rrggbb` / `#rrggbbaa` instead of
|
|
1485
|
-
* `oklch()`.
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* `accent`), then active, inactive, and disabled variants are added to the
|
|
1492
|
-
* surface colors, border, and each theme color and its `*-subtle` color.
|
|
1468
|
+
* `oklch()`. Root colors (emphasis, ink, subtle, subtlest, hairline, backgrounds)
|
|
1469
|
+
* stay in the root light and dark themes without rings. For the `base` group and each
|
|
1470
|
+
* status group, a missing `secondary` background and `on-primary` /
|
|
1471
|
+
* `on-secondary` text colors are generated, then hovered, active, inactive,
|
|
1472
|
+
* and disabled variants are added to the hairline, `foreground` and
|
|
1473
|
+
* `background` group colors, and each theme color group's `primary` and `secondary`.
|
|
1493
1474
|
* Low-saturation colors derive their states from the parent theme's contrast
|
|
1494
|
-
* primitive (
|
|
1495
|
-
* colors already near that primitive shift toward the middle
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1475
|
+
* primitive (backgrounds use a half-strength shift of their own lightness; hovered
|
|
1476
|
+
* colors already near that primitive shift toward the middle, while active
|
|
1477
|
+
* ones become pure white or black); saturated
|
|
1478
|
+
* colors scale their own lightness. Tokens in a theme color group get the
|
|
1479
|
+
* group name as `theme` and `group`, unless already set; root state and `on-*`
|
|
1480
|
+
* tokens get their main token name as `group` (e.g. `hairline-active` →
|
|
1481
|
+
* `hairline`).
|
|
1499
1482
|
* Finally, focus rings are generated for `base` and each status color.
|
|
1500
1483
|
*/
|
|
1501
|
-
function tamaguiPreprocessor(
|
|
1484
|
+
export function tamaguiPreprocessor(
|
|
1502
1485
|
dictionary: PreprocessedTokens
|
|
1503
1486
|
): PreprocessedTokens {
|
|
1504
1487
|
const converted = walk(dictionary) as PreprocessedTokens;
|