@multiplatform.one/theme 7.34.3 → 7.35.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.
- package/package.json +20 -5
- package/src/audit/constraintAudit.spec.ts +274 -87
- package/src/audit/constraintAudit.ts +135 -120
- package/src/audit/constraintScope.spec.ts +1 -1
- package/src/audit/designTokens.ts +42 -0
- package/src/audit/themeMatrix.spec.ts +83 -2
- package/src/audit/themeMatrix.ts +33 -1
- package/src/figma/figmaTokens.spec.ts +26 -34
- package/src/figma/figmaTokens.ts +26 -50
- package/src/fonts/createFontLoader.ts +4 -1
- package/src/menuRow.ts +4 -1
- package/src/theme/FontKnobStyles.tsx +2 -1
- package/src/theme/Preset.stories.tsx +0 -1
- package/src/theme/Preset.tsx +6 -1
- package/src/theme/PresetContext.ts +4 -0
- package/src/theme/Surface.spec.tsx +114 -67
- package/src/theme/Surface.stories.tsx +8 -25
- package/src/theme/Surface.tsx +87 -43
- package/src/theme/accentFloor.spec.ts +37 -11
- package/src/theme/activateThemeConfig.ts +22 -6
- package/src/theme/animations/animations.spec.ts +62 -0
- package/src/theme/animations/css.ts +9 -35
- package/src/theme/animations/reactNative.ts +98 -25
- package/src/theme/boundaryFloor.spec.ts +57 -0
- package/src/theme/chartPalette.spec.ts +56 -27
- package/src/theme/chartPalette.ts +33 -28
- package/src/theme/colorRules.spec.ts +32 -72
- package/src/theme/cookies.spec.ts +49 -18
- package/src/theme/cookies.ts +36 -7
- package/src/theme/cookiesPersistence.spec.ts +5 -5
- package/src/theme/createDefaultThemeConfig.spec.ts +182 -20
- package/src/theme/createDefaultThemeConfig.ts +34 -28
- package/src/theme/createThemes.ts +169 -218
- package/src/theme/defaults/accent.ts +3 -35
- package/src/theme/defaults/base.ts +14 -53
- package/src/theme/defaults/builderOptions.ts +68 -85
- package/src/theme/defaults/categoryFonts.ts +10 -0
- package/src/theme/defaults/fonts.spec.ts +84 -4
- package/src/theme/defaults/fonts.ts +166 -124
- package/src/theme/defaults/house.ts +48 -0
- package/src/theme/defaults/index.ts +1 -0
- package/src/theme/defaults/neutralRamp.spec.ts +52 -47
- package/src/theme/devtools/ColorLineVisualizer.tsx +6 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.setMarkers.spec.tsx +13 -6
- package/src/theme/devtools/ThemeDevtoolsPanel.tsx +14 -77
- package/src/theme/devtools/animationOptions.spec.ts +23 -37
- package/src/theme/devtools/animationOptions.ts +4 -6
- package/src/theme/devtools/buildSnapshot.ts +0 -3
- package/src/theme/devtools/knobIcons.tsx +4 -24
- package/src/theme/focusState.spec.ts +9 -30
- package/src/theme/index.ts +17 -0
- package/src/theme/intentFill.spec.ts +42 -0
- package/src/theme/knobs.ts +14 -42
- package/src/theme/layoutTokens.ts +15 -1
- package/src/theme/layoutTokensHooks.spec.tsx +8 -4
- package/src/theme/mergedTokens.ts +20 -0
- package/src/theme/motion.spec.ts +84 -0
- package/src/theme/motion.ts +53 -0
- package/src/theme/motionStops.ts +17 -0
- package/src/theme/motionTiming.ts +59 -0
- package/src/theme/mpo13Proof.spec.ts +43 -46
- package/src/theme/mpo19Proof.spec.ts +13 -14
- package/src/theme/namedInkFloor.spec.ts +60 -0
- package/src/theme/overlaySurfaces.tsx +330 -0
- package/src/theme/preset.types.ts +16 -0
- package/src/theme/presetPreview.ts +32 -0
- package/src/theme/presetRoot.spec.tsx +89 -0
- package/src/theme/presetStore.persistence.spec.ts +141 -0
- package/src/theme/presetStore.spec.ts +205 -0
- package/src/theme/presetStore.ts +286 -0
- package/src/theme/presetUserSync.spec.tsx +168 -0
- package/src/theme/presetUserSync.ts +47 -0
- package/src/theme/presets.spec.ts +22 -15
- package/src/theme/presets.ts +22 -10
- package/src/theme/radiusClass.spec.ts +60 -41
- package/src/theme/radiusClass.ts +17 -4
- package/src/theme/radiusClassTable.generated.ts +3 -3
- package/src/theme/readableColor.spec.ts +59 -152
- package/src/theme/readableColor.ts +34 -174
- package/src/theme/recipeInputs.ts +153 -295
- package/src/theme/recipes.ts +58 -18
- package/src/theme/resolveKnobs.spec.ts +528 -588
- package/src/theme/resolveKnobs.ts +250 -325
- package/src/theme/scale/ScaleStyles.native.tsx +19 -0
- package/src/theme/scale/ScaleStyles.tsx +48 -0
- package/src/theme/scale/activeScale.ts +30 -0
- package/src/theme/scale/deriveRecipeInputs.ts +110 -0
- package/src/theme/scale/generateScale.spec.ts +260 -0
- package/src/theme/scale/generateScale.ts +147 -0
- package/src/theme/scale/index.ts +54 -0
- package/src/theme/scale/installScaleConfig.native.spec.ts +77 -0
- package/src/theme/scale/installScaleConfig.native.ts +57 -0
- package/src/theme/scale/installScaleConfig.ts +7 -0
- package/src/theme/scale/scaleAttribute.ts +21 -0
- package/src/theme/scale/scaleCss.probe.spec.tsx +64 -0
- package/src/theme/scale/scaleCss.spec.ts +132 -0
- package/src/theme/scale/scaleCss.ts +95 -0
- package/src/theme/scale/scaleFonts.ts +106 -0
- package/src/theme/scale/scaleInputs.ts +322 -0
- package/src/theme/scale/themeScales.ts +181 -0
- package/src/theme/scale/useActiveScale.spec.tsx +129 -0
- package/src/theme/scale/useActiveScale.ts +95 -0
- package/src/theme/schemeStore.spec.ts +81 -0
- package/src/theme/schemeStore.ts +67 -0
- package/src/theme/shared.spec.ts +7 -6
- package/src/theme/shared.tsx +37 -3
- package/src/theme/sizeLadder.spec.ts +37 -36
- package/src/theme/sizeRecipes.spec.ts +179 -85
- package/src/theme/sizeRecipes.ts +48 -25
- package/src/theme/subsetThemes.spec.ts +9 -3
- package/src/theme/switchThumb.spec.ts +69 -0
- package/src/theme/syntaxFloor.spec.ts +11 -8
- package/src/theme/theme.hydration.native.spec.tsx +51 -0
- package/src/theme/theme.native.tsx +59 -21
- package/src/theme/theme.palette.spec.tsx +76 -0
- package/src/theme/theme.preset.native.spec.tsx +73 -0
- package/src/theme/theme.provider.spec.ts +143 -1
- package/src/theme/theme.tsx +61 -25
- package/src/theme/tileSizes.ts +6 -7
- package/src/theme/transitionProps.native.ts +25 -2
- package/src/theme/transitionProps.spec.ts +34 -0
- package/src/theme/transitionProps.ts +26 -2
- package/src/theme/usePresetSwitch.native.spec.tsx +68 -0
- package/src/theme/usePresetSwitch.native.ts +32 -0
- package/src/theme/usePresetSwitch.spec.tsx +103 -0
- package/src/theme/usePresetSwitch.ts +54 -0
- package/src/theme/useResolvedKnobs.hydration.spec.tsx +2 -2
- package/src/theme/useResolvedKnobs.ts +23 -90
- package/src/theme/useResolvedKnobsBehavior.spec.tsx +25 -30
- package/src/theme/useSchemePreference.native.ts +33 -0
- package/src/theme/useSchemePreference.spec.tsx +51 -0
- package/src/theme/useSchemePreference.ts +32 -0
- package/src/theme/useSchemePreference.web.spec.tsx +32 -0
- package/src/theme/useTheme.native.spec.tsx +68 -0
- package/src/theme/useTheme.native.ts +49 -24
- package/src/theme/useTheme.ts +7 -4
- package/types/audit/constraintAudit.d.ts +6 -5
- package/types/audit/constraintAudit.d.ts.map +1 -1
- package/types/audit/designTokens.d.ts +13 -0
- package/types/audit/designTokens.d.ts.map +1 -0
- package/types/audit/themeMatrix.d.ts +10 -0
- package/types/audit/themeMatrix.d.ts.map +1 -1
- package/types/figma/figmaTokens.d.ts +11 -28
- package/types/figma/figmaTokens.d.ts.map +1 -1
- package/types/fonts/createFontLoader.d.ts.map +1 -1
- package/types/menuRow.d.ts +4 -1
- package/types/menuRow.d.ts.map +1 -1
- package/types/theme/FontKnobStyles.d.ts.map +1 -1
- package/types/theme/Preset.d.ts.map +1 -1
- package/types/theme/PresetContext.d.ts +4 -0
- package/types/theme/PresetContext.d.ts.map +1 -1
- package/types/theme/Surface.d.ts +57 -26
- package/types/theme/Surface.d.ts.map +1 -1
- package/types/theme/activateThemeConfig.d.ts +5 -3
- package/types/theme/activateThemeConfig.d.ts.map +1 -1
- package/types/theme/animations/css.d.ts +27 -34
- package/types/theme/animations/css.d.ts.map +1 -1
- package/types/theme/animations/reactNative.d.ts +196 -45
- package/types/theme/animations/reactNative.d.ts.map +1 -1
- package/types/theme/chartPalette.d.ts +11 -3
- package/types/theme/chartPalette.d.ts.map +1 -1
- package/types/theme/colorRules.d.ts +1 -1
- package/types/theme/cookies.d.ts +12 -1
- package/types/theme/cookies.d.ts.map +1 -1
- package/types/theme/createDefaultThemeConfig.d.ts +13 -13
- package/types/theme/createDefaultThemeConfig.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts +58 -2
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/defaults/accent.d.ts.map +1 -1
- package/types/theme/defaults/base.d.ts.map +1 -1
- package/types/theme/defaults/builderOptions.d.ts.map +1 -1
- package/types/theme/defaults/categoryFonts.d.ts +10 -0
- package/types/theme/defaults/categoryFonts.d.ts.map +1 -1
- package/types/theme/defaults/fonts.d.ts +330 -30
- package/types/theme/defaults/fonts.d.ts.map +1 -1
- package/types/theme/defaults/house.d.ts +8 -0
- package/types/theme/defaults/house.d.ts.map +1 -0
- package/types/theme/defaults/index.d.ts +1 -0
- package/types/theme/defaults/index.d.ts.map +1 -1
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts +3 -3
- package/types/theme/devtools/ThemeDevtoolsPanel.d.ts.map +1 -1
- package/types/theme/devtools/animationOptions.d.ts.map +1 -1
- package/types/theme/devtools/buildSnapshot.d.ts.map +1 -1
- package/types/theme/devtools/knobIcons.d.ts +0 -3
- package/types/theme/devtools/knobIcons.d.ts.map +1 -1
- package/types/theme/index.d.ts +11 -0
- package/types/theme/index.d.ts.map +1 -1
- package/types/theme/knobs.d.ts +8 -36
- package/types/theme/knobs.d.ts.map +1 -1
- package/types/theme/layoutTokens.d.ts +13 -1
- package/types/theme/layoutTokens.d.ts.map +1 -1
- package/types/theme/mergedTokens.d.ts +11 -0
- package/types/theme/mergedTokens.d.ts.map +1 -0
- package/types/theme/motion.d.ts +17 -0
- package/types/theme/motion.d.ts.map +1 -0
- package/types/theme/motionStops.d.ts +3 -0
- package/types/theme/motionStops.d.ts.map +1 -0
- package/types/theme/motionTiming.d.ts +36 -0
- package/types/theme/motionTiming.d.ts.map +1 -0
- package/types/theme/overlaySurfaces.d.ts +70 -0
- package/types/theme/overlaySurfaces.d.ts.map +1 -0
- package/types/theme/preset.types.d.ts +16 -0
- package/types/theme/preset.types.d.ts.map +1 -1
- package/types/theme/presetPreview.d.ts +14 -0
- package/types/theme/presetPreview.d.ts.map +1 -0
- package/types/theme/presetStore.d.ts +40 -0
- package/types/theme/presetStore.d.ts.map +1 -0
- package/types/theme/presetUserSync.d.ts +7 -0
- package/types/theme/presetUserSync.d.ts.map +1 -0
- package/types/theme/presets.d.ts +12 -4
- package/types/theme/presets.d.ts.map +1 -1
- package/types/theme/radiusClass.d.ts +5 -2
- package/types/theme/radiusClass.d.ts.map +1 -1
- package/types/theme/radiusClassTable.generated.d.ts +3 -3
- package/types/theme/readableColor.d.ts +11 -49
- package/types/theme/readableColor.d.ts.map +1 -1
- package/types/theme/recipeInputs.d.ts +39 -70
- package/types/theme/recipeInputs.d.ts.map +1 -1
- package/types/theme/recipes.d.ts +79 -18
- package/types/theme/recipes.d.ts.map +1 -1
- package/types/theme/resolveKnobs.d.ts +64 -32
- package/types/theme/resolveKnobs.d.ts.map +1 -1
- package/types/theme/scale/ScaleStyles.d.ts +18 -0
- package/types/theme/scale/ScaleStyles.d.ts.map +1 -0
- package/types/theme/scale/ScaleStyles.native.d.ts +10 -0
- package/types/theme/scale/ScaleStyles.native.d.ts.map +1 -0
- package/types/theme/scale/activeScale.d.ts +13 -0
- package/types/theme/scale/activeScale.d.ts.map +1 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts +10 -0
- package/types/theme/scale/deriveRecipeInputs.d.ts.map +1 -0
- package/types/theme/scale/generateScale.d.ts +38 -0
- package/types/theme/scale/generateScale.d.ts.map +1 -0
- package/types/theme/scale/index.d.ts +17 -0
- package/types/theme/scale/index.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.d.ts +4 -0
- package/types/theme/scale/installScaleConfig.d.ts.map +1 -0
- package/types/theme/scale/installScaleConfig.native.d.ts +13 -0
- package/types/theme/scale/installScaleConfig.native.d.ts.map +1 -0
- package/types/theme/scale/scaleAttribute.d.ts +14 -0
- package/types/theme/scale/scaleAttribute.d.ts.map +1 -0
- package/types/theme/scale/scaleCss.d.ts +25 -0
- package/types/theme/scale/scaleCss.d.ts.map +1 -0
- package/types/theme/scale/scaleFonts.d.ts +16 -0
- package/types/theme/scale/scaleFonts.d.ts.map +1 -0
- package/types/theme/scale/scaleInputs.d.ts +66 -0
- package/types/theme/scale/scaleInputs.d.ts.map +1 -0
- package/types/theme/scale/themeScales.d.ts +52 -0
- package/types/theme/scale/themeScales.d.ts.map +1 -0
- package/types/theme/scale/useActiveScale.d.ts +31 -0
- package/types/theme/scale/useActiveScale.d.ts.map +1 -0
- package/types/theme/schemeStore.d.ts +6 -0
- package/types/theme/schemeStore.d.ts.map +1 -0
- package/types/theme/shared.d.ts +18 -0
- package/types/theme/shared.d.ts.map +1 -1
- package/types/theme/sizeRecipes.d.ts +20 -15
- package/types/theme/sizeRecipes.d.ts.map +1 -1
- package/types/theme/theme.d.ts +2 -1
- package/types/theme/theme.d.ts.map +1 -1
- package/types/theme/theme.native.d.ts +10 -1
- package/types/theme/theme.native.d.ts.map +1 -1
- package/types/theme/tileSizes.d.ts +6 -7
- package/types/theme/tileSizes.d.ts.map +1 -1
- package/types/theme/transitionProps.d.ts +3 -1
- package/types/theme/transitionProps.d.ts.map +1 -1
- package/types/theme/transitionProps.native.d.ts +3 -1
- package/types/theme/transitionProps.native.d.ts.map +1 -1
- package/types/theme/usePresetSwitch.d.ts +10 -0
- package/types/theme/usePresetSwitch.d.ts.map +1 -0
- package/types/theme/usePresetSwitch.native.d.ts +10 -0
- package/types/theme/usePresetSwitch.native.d.ts.map +1 -0
- package/types/theme/useResolvedKnobs.d.ts +13 -13
- package/types/theme/useResolvedKnobs.d.ts.map +1 -1
- package/types/theme/useSchemePreference.d.ts +8 -0
- package/types/theme/useSchemePreference.d.ts.map +1 -0
- package/types/theme/useSchemePreference.native.d.ts +8 -0
- package/types/theme/useSchemePreference.native.d.ts.map +1 -0
- package/types/theme/useTheme.d.ts +4 -2
- package/types/theme/useTheme.d.ts.map +1 -1
- package/types/theme/useTheme.native.d.ts +10 -1
- package/types/theme/useTheme.native.d.ts.map +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ScaleSource } from "./useActiveScale";
|
|
2
|
+
|
|
3
|
+
export function scaleStyleSheet(_scales: ScaleSource["scales"]): string {
|
|
4
|
+
return "";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function scaleAttributeValue(presetName?: string): string | undefined {
|
|
8
|
+
return presetName || undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ScaleStylesProps {
|
|
12
|
+
config?: ScaleSource;
|
|
13
|
+
presetName?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Native has no CSS variables; a scale switch remounts the provider subtree instead. */
|
|
17
|
+
export function ScaleStyles(_props: ScaleStylesProps) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useInsertionEffect, useMemo } from "react";
|
|
2
|
+
import { SCALE_ATTRIBUTE } from "./scaleCss";
|
|
3
|
+
import { uniqueScales } from "./themeScales";
|
|
4
|
+
import type { ScaleSource } from "./useActiveScale";
|
|
5
|
+
|
|
6
|
+
/** Every registered scale's repaint block, default first (it has none). */
|
|
7
|
+
export function scaleStyleSheet(scales: ScaleSource["scales"]): string {
|
|
8
|
+
return uniqueScales(scales)
|
|
9
|
+
.map((scale) => scale.css)
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The `data-mp-scale` value for a preset name; no name leaves the attribute off. */
|
|
15
|
+
export function scaleAttributeValue(presetName?: string): string | undefined {
|
|
16
|
+
return presetName || undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScaleStylesProps {
|
|
20
|
+
config?: ScaleSource;
|
|
21
|
+
presetName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Web scale repaint: one hoisted stylesheet with every registered scale's
|
|
26
|
+
* `:root[data-mp-scale=…]` block (server-rendered, so first paint is
|
|
27
|
+
* right), and the `<html>` attribute that selects the active one. Switching
|
|
28
|
+
* presets flips the attribute and every token-driven style repaints through
|
|
29
|
+
* its CSS variable with no re-render.
|
|
30
|
+
*/
|
|
31
|
+
export function ScaleStyles({ config, presetName }: ScaleStylesProps) {
|
|
32
|
+
const css = useMemo(() => scaleStyleSheet(config?.scales), [config?.scales]);
|
|
33
|
+
const attribute = scaleAttributeValue(presetName || config?.defaultPreset);
|
|
34
|
+
|
|
35
|
+
useInsertionEffect(() => {
|
|
36
|
+
if (typeof document === "undefined") return;
|
|
37
|
+
const root = document.documentElement;
|
|
38
|
+
if (attribute) root.setAttribute(SCALE_ATTRIBUTE, attribute);
|
|
39
|
+
else root.removeAttribute(SCALE_ATTRIBUTE);
|
|
40
|
+
}, [attribute]);
|
|
41
|
+
|
|
42
|
+
if (!css) return null;
|
|
43
|
+
return (
|
|
44
|
+
<style precedence="default" href="mp-scales">
|
|
45
|
+
{css}
|
|
46
|
+
</style>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ScaleTokens, generateScale } from "./generateScale";
|
|
2
|
+
import { RADIUS_KEYS, defaultScaleInputs } from "./scaleInputs";
|
|
3
|
+
|
|
4
|
+
const defaultTokens: ScaleTokens = generateScale(defaultScaleInputs).tokens;
|
|
5
|
+
|
|
6
|
+
let activeTokens: ScaleTokens = defaultTokens;
|
|
7
|
+
|
|
8
|
+
/** The token values of the installed scale (the default scale until a preset switches it). */
|
|
9
|
+
export function getActiveScaleTokens(): ScaleTokens {
|
|
10
|
+
return activeTokens;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Called by `applyThemeScale`; px readers below follow it on their next render. */
|
|
14
|
+
export function setActiveScaleTokens(tokens: ScaleTokens): void {
|
|
15
|
+
activeTokens = tokens;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The installed scale's radius for a px literal written against the default
|
|
20
|
+
* scale: the value
|
|
21
|
+
* at the same `$radius` key. A px that is not a default radius token passes
|
|
22
|
+
* through unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export function scaleRadiusPx(defaultPx: number): number {
|
|
25
|
+
if (activeTokens === defaultTokens || defaultPx === 0) return defaultPx;
|
|
26
|
+
for (const key of RADIUS_KEYS) {
|
|
27
|
+
if (key !== "true" && defaultTokens.radius[key] === defaultPx) return activeTokens.radius[key];
|
|
28
|
+
}
|
|
29
|
+
return defaultPx;
|
|
30
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { SizeRecipeInputs, SizeRecipeRatios, SizeRecipeTokenChannels } from "../recipeInputs";
|
|
2
|
+
import type { ScaleCore } from "./generateScale";
|
|
3
|
+
import {
|
|
4
|
+
CHIP_ICON_SHAPE,
|
|
5
|
+
CONTROL_ICON_SHAPE,
|
|
6
|
+
SIZE_KEYS,
|
|
7
|
+
SIZE_RECIPE_TOKENS,
|
|
8
|
+
SIZE_TOKEN_EXPONENTS,
|
|
9
|
+
type FontKey,
|
|
10
|
+
type RadiusKey,
|
|
11
|
+
type SizeKey,
|
|
12
|
+
type SizeRecipeToken,
|
|
13
|
+
} from "./scaleInputs";
|
|
14
|
+
|
|
15
|
+
/** iOS minimum hit target; touch heights never paint below it (LC-77). */
|
|
16
|
+
const TOUCH_FLOOR_PX = 44;
|
|
17
|
+
|
|
18
|
+
function stopOf(token: SizeRecipeToken): number {
|
|
19
|
+
return token === "$true" ? 4 : Number(token.slice(1));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function whole(token: SizeRecipeToken): Exclude<SizeRecipeToken, "$true"> {
|
|
23
|
+
return token === "$true" ? "$4" : token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The half-step ladder `$0, $0.5, $1, …`; `$0.25`/`$0.75` sit between its rungs. */
|
|
27
|
+
const HALF_STEP_KEYS = SIZE_KEYS.filter(
|
|
28
|
+
(key) => key !== "$true" && Number(key.slice(1)) % 0.5 === 0,
|
|
29
|
+
) as SizeKey[];
|
|
30
|
+
|
|
31
|
+
/** One half step below `token` (Tamagui `getSpace(…, { shift: -1 })` on the half-step ladder). */
|
|
32
|
+
function halfStepBelow(token: SizeRecipeToken): SizeKey {
|
|
33
|
+
const target = stopOf(token) - 0.5;
|
|
34
|
+
let found: SizeKey = "$0";
|
|
35
|
+
for (const key of HALF_STEP_KEYS) {
|
|
36
|
+
if (Number(key.slice(1)) <= target) found = key;
|
|
37
|
+
}
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `$radius` has whole keys only; a half step sits halfway between its neighbours. */
|
|
42
|
+
function radiusAt(radius: Record<RadiusKey, number>, token: SizeRecipeToken): number {
|
|
43
|
+
const stop = stopOf(token);
|
|
44
|
+
const below = radius[String(Math.floor(stop)) as RadiusKey];
|
|
45
|
+
const above = radius[String(Math.ceil(stop)) as RadiusKey];
|
|
46
|
+
return Math.round((below + above) / 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function label(scale: ScaleCore, token: SizeRecipeToken, shift: number): number {
|
|
50
|
+
const key = String(Math.max(1, Math.min(16, Math.ceil(stopOf(token)) + shift))) as FontKey;
|
|
51
|
+
return scale.fonts.bodySizes[key];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ratiosAt(row: SizeRecipeTokenChannels, height: number): SizeRecipeRatios {
|
|
55
|
+
return {
|
|
56
|
+
padX: row.paddingHorizontal / height,
|
|
57
|
+
font: row.fontSize / height,
|
|
58
|
+
icon: row.iconSize / height,
|
|
59
|
+
gap: row.radius / height,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Size-recipe inputs read off a generated scale: height = `$size`, button
|
|
65
|
+
* padX = `$space` at the same key, input/chip padX one half step below,
|
|
66
|
+
* radius (and so gap, LC-75) = `$radius`, label from body type and icon from
|
|
67
|
+
* the type-relative control ladder. Nothing here is a second px source.
|
|
68
|
+
*/
|
|
69
|
+
export function deriveRecipeInputs(scale: ScaleCore): SizeRecipeInputs {
|
|
70
|
+
const { control, type } = scale.inputs;
|
|
71
|
+
const { size, space, radius } = scale.tokens;
|
|
72
|
+
const tokenHeights: Record<string, number> = {};
|
|
73
|
+
const tokenTable = {} as Record<SizeRecipeToken, SizeRecipeTokenChannels>;
|
|
74
|
+
const chipTokenTable = {} as Record<SizeRecipeToken, SizeRecipeTokenChannels>;
|
|
75
|
+
const inputPadX: Partial<Record<SizeRecipeToken, number>> = {};
|
|
76
|
+
|
|
77
|
+
for (const token of SIZE_RECIPE_TOKENS) {
|
|
78
|
+
const r = radiusAt(radius, token);
|
|
79
|
+
const padBelow = space[halfStepBelow(token)];
|
|
80
|
+
tokenHeights[token] = size[token];
|
|
81
|
+
inputPadX[token] = padBelow;
|
|
82
|
+
tokenTable[token] = {
|
|
83
|
+
paddingHorizontal: space[token],
|
|
84
|
+
fontSize: label(scale, token, 0),
|
|
85
|
+
iconSize: Math.round(CONTROL_ICON_SHAPE[whole(token)] * type),
|
|
86
|
+
radius: r,
|
|
87
|
+
};
|
|
88
|
+
chipTokenTable[token] = {
|
|
89
|
+
paddingHorizontal: padBelow,
|
|
90
|
+
fontSize: label(scale, token, -1),
|
|
91
|
+
iconSize: Math.round(CHIP_ICON_SHAPE[whole(token)] * type),
|
|
92
|
+
radius: r,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const height = tokenHeights.$4;
|
|
97
|
+
return {
|
|
98
|
+
control,
|
|
99
|
+
type,
|
|
100
|
+
tokenSteps: { ...SIZE_TOKEN_EXPONENTS },
|
|
101
|
+
tokenHeights,
|
|
102
|
+
tokenTable,
|
|
103
|
+
chipTokenTable,
|
|
104
|
+
inputPadX,
|
|
105
|
+
ratios: ratiosAt(tokenTable.$4, height),
|
|
106
|
+
chipRatios: ratiosAt(chipTokenTable.$4, height),
|
|
107
|
+
radiusPx: radius.true,
|
|
108
|
+
touchFloor: TOUCH_FLOOR_PX,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generator reproduces Tamagui v5 at v5ScaleInputs, key by key, and
|
|
3
|
+
* keeps v5's proportions at any other inputs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defaultConfig } from "@tamagui/config/v5";
|
|
7
|
+
import { tokens as v5Tokens } from "@tamagui/themes/v5";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { DEFAULT_SIZE_RECIPE_INPUTS } from "../recipeInputs";
|
|
10
|
+
import { generateScale, sizeToSpace } from "./generateScale";
|
|
11
|
+
import {
|
|
12
|
+
FONT_KEYS,
|
|
13
|
+
RADIUS_KEYS,
|
|
14
|
+
SIZE_KEYS,
|
|
15
|
+
SIZE_RECIPE_TOKENS,
|
|
16
|
+
defaultScaleInputs,
|
|
17
|
+
v5ScaleInputs,
|
|
18
|
+
resolveScaleInputs,
|
|
19
|
+
} from "./scaleInputs";
|
|
20
|
+
|
|
21
|
+
type Table = Record<string, number>;
|
|
22
|
+
|
|
23
|
+
function numbers(raw: unknown): Table {
|
|
24
|
+
const out: Table = {};
|
|
25
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
26
|
+
const val =
|
|
27
|
+
value && typeof value === "object" && "val" in value
|
|
28
|
+
? (value as { val: unknown }).val
|
|
29
|
+
: value;
|
|
30
|
+
out[key.replace(/^\$/, "")] = val as number;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function expectKeyByKey(actual: Table, expected: Table, label: string) {
|
|
36
|
+
expect(Object.keys(actual).sort(), `${label} keys`).toEqual(Object.keys(expected).sort());
|
|
37
|
+
for (const [key, px] of Object.entries(expected)) {
|
|
38
|
+
expect(actual[key], `${label}.${key}`).toBe(px);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const scale = generateScale(v5ScaleInputs, "web");
|
|
43
|
+
|
|
44
|
+
describe("generateScale(v5ScaleInputs) is Tamagui v5", () => {
|
|
45
|
+
for (const group of ["size", "space", "radius", "zIndex"] as const) {
|
|
46
|
+
it(`tokens.${group} equals @tamagui/themes/v5 on every key`, () => {
|
|
47
|
+
expectKeyByKey(scale.tokens[group], v5Tokens[group] as unknown as Table, group);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it("keeps v5's key order, so the emitted :root variables do too", () => {
|
|
52
|
+
for (const group of ["size", "space", "radius", "zIndex"] as const) {
|
|
53
|
+
expect(Object.keys(scale.tokens[group]), group).toEqual(Object.keys(v5Tokens[group]));
|
|
54
|
+
}
|
|
55
|
+
expect(Object.keys(scale.tokens)).toEqual(Object.keys(v5Tokens));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (const face of ["body", "heading"] as const) {
|
|
59
|
+
it(`${face} size and lineHeight equal @tamagui/config/v5 fonts.${face}`, () => {
|
|
60
|
+
const font = defaultConfig.fonts[face];
|
|
61
|
+
expectKeyByKey(scale.fonts[`${face}Sizes`], numbers(font.size), `${face}.size`);
|
|
62
|
+
expectKeyByKey(
|
|
63
|
+
scale.fonts[`${face}LineHeights`],
|
|
64
|
+
numbers(font.lineHeight),
|
|
65
|
+
`${face}.lineHeight`,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it("native follows v5's iOS ladder: 17pt body, size + 5 leading, 120% headings", () => {
|
|
71
|
+
const native = generateScale(v5ScaleInputs, "native").fonts;
|
|
72
|
+
const ios: Table = {
|
|
73
|
+
1: 11,
|
|
74
|
+
2: 12,
|
|
75
|
+
3: 15,
|
|
76
|
+
4: 17,
|
|
77
|
+
true: 17,
|
|
78
|
+
5: 20,
|
|
79
|
+
6: 22,
|
|
80
|
+
7: 24,
|
|
81
|
+
8: 28,
|
|
82
|
+
9: 32,
|
|
83
|
+
10: 40,
|
|
84
|
+
11: 46,
|
|
85
|
+
12: 52,
|
|
86
|
+
13: 60,
|
|
87
|
+
14: 70,
|
|
88
|
+
15: 85,
|
|
89
|
+
16: 100,
|
|
90
|
+
};
|
|
91
|
+
expectKeyByKey(native.bodySizes, ios, "native body.size");
|
|
92
|
+
for (const key of FONT_KEYS) {
|
|
93
|
+
expect(native.bodyLineHeights[key], `native body.lineHeight.${key}`).toBe(ios[key] + 5);
|
|
94
|
+
expect(native.headingLineHeights[key], `native heading.lineHeight.${key}`).toBe(
|
|
95
|
+
Math.round(ios[key] * 1.2),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("sizeToSpace is Tamagui's, on every v5 size", () => {
|
|
101
|
+
for (const key of SIZE_KEYS) {
|
|
102
|
+
expect(sizeToSpace(v5Tokens.size[key]), key).toBe(v5Tokens.space[key]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("ladder shape", () => {
|
|
108
|
+
it("the house default has a 40px control and the proportional size ladder", () => {
|
|
109
|
+
const { tokens, fonts } = generateScale(defaultScaleInputs, "web");
|
|
110
|
+
expect(tokens.size).toMatchObject({
|
|
111
|
+
$1: 18,
|
|
112
|
+
"$1.5": 22,
|
|
113
|
+
$2: 26,
|
|
114
|
+
"$2.5": 30,
|
|
115
|
+
$3: 32,
|
|
116
|
+
"$3.5": 36,
|
|
117
|
+
$4: 40,
|
|
118
|
+
$true: 40,
|
|
119
|
+
"$4.5": 44,
|
|
120
|
+
$5: 48,
|
|
121
|
+
$6: 58,
|
|
122
|
+
});
|
|
123
|
+
expect(tokens.space).toMatchObject({ $2: 6, $3: 12, $4: 16, $true: 16, $5: 21 });
|
|
124
|
+
expect(tokens.radius).toMatchObject({ 1: 3, 2: 4, 3: 6, 4: 8, true: 8, 5: 9, 6: 14 });
|
|
125
|
+
expect(fonts.bodySizes.true).toBe(15);
|
|
126
|
+
expect(fonts.bodyLineHeights.true).toBe(23);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("control text reads the platform body table, with chips one key below", () => {
|
|
130
|
+
for (const platform of ["web", "native"] as const) {
|
|
131
|
+
const { fonts, recipeInputs } = generateScale(defaultScaleInputs, platform);
|
|
132
|
+
for (const token of SIZE_RECIPE_TOKENS) {
|
|
133
|
+
const stop = token === "$true" ? 4 : Math.ceil(Number(token.slice(1)));
|
|
134
|
+
expect(recipeInputs.tokenTable[token]?.fontSize).toBe(
|
|
135
|
+
fonts.bodySizes[String(stop) as keyof typeof fonts.bodySizes],
|
|
136
|
+
);
|
|
137
|
+
expect(recipeInputs.chipTokenTable[token]?.fontSize).toBe(
|
|
138
|
+
fonts.bodySizes[String(Math.max(1, stop - 1)) as keyof typeof fonts.bodySizes],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
it("$true is $4 in every group", () => {
|
|
144
|
+
for (const control of [36, 40, 44, 52]) {
|
|
145
|
+
const { tokens, fonts } = generateScale({ control, type: 15 });
|
|
146
|
+
expect(tokens.size.$true).toBe(tokens.size.$4);
|
|
147
|
+
expect(tokens.space.$true).toBe(tokens.space.$4);
|
|
148
|
+
expect(tokens.radius.true).toBe(tokens.radius[4]);
|
|
149
|
+
expect(fonts.bodySizes.true).toBe(fonts.bodySizes[4]);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("size, space, radius and type never step down", () => {
|
|
154
|
+
for (const control of [36, 40, 44, 52]) {
|
|
155
|
+
const { tokens, fonts } = generateScale({ control, type: 16 });
|
|
156
|
+
const ladder = (keys: readonly string[], table: Table) => keys.map((key) => table[key]);
|
|
157
|
+
for (const [label, values] of [
|
|
158
|
+
["size", ladder(SIZE_KEYS, tokens.size)],
|
|
159
|
+
["space", ladder(SIZE_KEYS, tokens.space)],
|
|
160
|
+
["radius", ladder(RADIUS_KEYS, tokens.radius)],
|
|
161
|
+
["body", ladder(FONT_KEYS, fonts.bodySizes)],
|
|
162
|
+
] as const) {
|
|
163
|
+
for (let i = 1; i < values.length; i++) {
|
|
164
|
+
expect(values[i], `${label}[${i}] at control ${control}`).toBeGreaterThanOrEqual(
|
|
165
|
+
values[i - 1],
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("negative space mirrors positive space", () => {
|
|
173
|
+
const { space } = generateScale({ control: 40, type: 15 }).tokens;
|
|
174
|
+
for (const key of SIZE_KEYS.slice(1)) {
|
|
175
|
+
const negative = `-${key.slice(1)}` as keyof typeof space;
|
|
176
|
+
expect(space[negative], negative).toBe(-space[key]);
|
|
177
|
+
}
|
|
178
|
+
expect(space).not.toHaveProperty("-0");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("control 40 puts $4 at 40, $space.4 at 16 and $radius.true at 8; type stays 15", () => {
|
|
182
|
+
const { tokens, fonts } = generateScale({ control: 40, type: 15 });
|
|
183
|
+
expect(tokens.size.$4).toBe(40);
|
|
184
|
+
expect(tokens.space.$4).toBe(16);
|
|
185
|
+
expect(tokens.radius.true).toBe(8);
|
|
186
|
+
expect(fonts.bodySizes.true).toBe(15);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("type scales the body and heading ladders and nothing else", () => {
|
|
190
|
+
const larger = generateScale({ control: 44, type: 17 });
|
|
191
|
+
expect(larger.fonts.bodySizes.true).toBe(17);
|
|
192
|
+
expect(larger.fonts.headingSizes).toEqual(larger.fonts.bodySizes);
|
|
193
|
+
expect(larger.tokens).toEqual(scale.tokens);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("unit sets the fine-grain keys, snap rounds the rest, radius sets $radius.true", () => {
|
|
197
|
+
expect(resolveScaleInputs(defaultScaleInputs)).toEqual({
|
|
198
|
+
control: 40,
|
|
199
|
+
type: 15,
|
|
200
|
+
unit: 4,
|
|
201
|
+
snap: 2,
|
|
202
|
+
radius: 9 / 44,
|
|
203
|
+
});
|
|
204
|
+
const { size, radius } = generateScale({
|
|
205
|
+
control: 44,
|
|
206
|
+
type: 15,
|
|
207
|
+
unit: 3,
|
|
208
|
+
snap: 4,
|
|
209
|
+
radius: 0.5,
|
|
210
|
+
}).tokens;
|
|
211
|
+
expect([size["$0.25"], size["$0.5"], size["$0.75"]]).toEqual([1.5, 3, 6]);
|
|
212
|
+
for (const key of SIZE_KEYS.filter((k) => !["$0.25", "$0.5", "$0.75"].includes(k))) {
|
|
213
|
+
expect(size[key] % 4, key).toBe(0);
|
|
214
|
+
}
|
|
215
|
+
expect(radius.true).toBe(22);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("recipe inputs come off the same scale", () => {
|
|
220
|
+
it("the default scale's recipe inputs ARE the default size-recipe inputs", () => {
|
|
221
|
+
expect(generateScale(defaultScaleInputs).recipeInputs).toEqual(DEFAULT_SIZE_RECIPE_INPUTS);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("height, padX and radius at every recipe key are the scale's own tokens", () => {
|
|
225
|
+
for (const control of [40, 44, 48]) {
|
|
226
|
+
const { tokens, recipeInputs } = generateScale({ control, type: 15 });
|
|
227
|
+
for (const token of SIZE_RECIPE_TOKENS) {
|
|
228
|
+
expect(recipeInputs.tokenHeights[token], `${control} ${token} height`).toBe(
|
|
229
|
+
tokens.size[token],
|
|
230
|
+
);
|
|
231
|
+
expect(recipeInputs.tokenTable[token]?.paddingHorizontal, `${control} ${token} padX`).toBe(
|
|
232
|
+
tokens.space[token],
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
expect(recipeInputs.tokenTable.$4?.radius).toBe(tokens.radius[4]);
|
|
236
|
+
expect(recipeInputs.radiusPx).toBe(tokens.radius.true);
|
|
237
|
+
expect(recipeInputs.control).toBe(control);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("input padX is one half step below: $4 reads $space.3.5, $6 reads $space.5", () => {
|
|
242
|
+
const { tokens, recipeInputs } = generateScale({ control: 40, type: 15 });
|
|
243
|
+
expect(recipeInputs.inputPadX.$4).toBe(tokens.space["$3.5"]);
|
|
244
|
+
expect(recipeInputs.inputPadX.$true).toBe(tokens.space["$3.5"]);
|
|
245
|
+
expect(recipeInputs.inputPadX.$6).toBe(tokens.space.$5);
|
|
246
|
+
expect(recipeInputs.inputPadX.$1).toBe(tokens.space["$0.5"]);
|
|
247
|
+
expect(recipeInputs.chipTokenTable.$4?.paddingHorizontal).toBe(recipeInputs.inputPadX.$4);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("$4 at v5 inputs keeps v5 geometry and uses the body font", () => {
|
|
251
|
+
expect(scale.recipeInputs.tokenHeights.$4).toBe(44);
|
|
252
|
+
expect(scale.recipeInputs.tokenTable.$4).toEqual({
|
|
253
|
+
paddingHorizontal: 18,
|
|
254
|
+
fontSize: 15,
|
|
255
|
+
iconSize: 16,
|
|
256
|
+
radius: 9,
|
|
257
|
+
});
|
|
258
|
+
expect(scale.recipeInputs.tokenTable.$true).toEqual(scale.recipeInputs.tokenTable.$4);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { SizeRecipeInputs } from "../recipeInputs";
|
|
2
|
+
import { deriveRecipeInputs } from "./deriveRecipeInputs";
|
|
3
|
+
import {
|
|
4
|
+
type FontKey,
|
|
5
|
+
FONT_KEYS,
|
|
6
|
+
NATIVE_TYPE_SHAPE,
|
|
7
|
+
type RadiusKey,
|
|
8
|
+
RADIUS_KEYS,
|
|
9
|
+
RADIUS_SHAPE,
|
|
10
|
+
type ResolvedScaleInputs,
|
|
11
|
+
type ScaleInputs,
|
|
12
|
+
SIZE_KEYS,
|
|
13
|
+
SIZE_SHAPE,
|
|
14
|
+
type SizeKey,
|
|
15
|
+
SPACE_SHAPE,
|
|
16
|
+
type SpaceKey,
|
|
17
|
+
SUBUNIT_SHAPE,
|
|
18
|
+
type SubunitKey,
|
|
19
|
+
TYPE_SHAPE,
|
|
20
|
+
ZINDEX,
|
|
21
|
+
type ZIndexKey,
|
|
22
|
+
defaultScaleInputs,
|
|
23
|
+
resolveScaleInputs,
|
|
24
|
+
} from "./scaleInputs";
|
|
25
|
+
|
|
26
|
+
/** Raw Tamagui token groups, in `@tamagui/themes/v5` key order (the order `getCSS()` emits). */
|
|
27
|
+
export type ScaleTokens = {
|
|
28
|
+
radius: Record<RadiusKey, number>;
|
|
29
|
+
zIndex: Record<ZIndexKey, number>;
|
|
30
|
+
space: Record<SpaceKey, number>;
|
|
31
|
+
size: Record<SizeKey, number>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ScaleFonts = {
|
|
35
|
+
bodySizes: Record<FontKey, number>;
|
|
36
|
+
bodyLineHeights: Record<FontKey, number>;
|
|
37
|
+
headingSizes: Record<FontKey, number>;
|
|
38
|
+
headingLineHeights: Record<FontKey, number>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ScaleCore = {
|
|
42
|
+
inputs: ResolvedScaleInputs;
|
|
43
|
+
tokens: ScaleTokens;
|
|
44
|
+
fonts: ScaleFonts;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type GeneratedScale = ScaleCore & { recipeInputs: SizeRecipeInputs };
|
|
48
|
+
|
|
49
|
+
export type ScalePlatform = "web" | "native";
|
|
50
|
+
|
|
51
|
+
/** `@tamagui/themes/v5` `sizeToSpace`, the function v5 derives every `$space` from. */
|
|
52
|
+
export function sizeToSpace(v: number): number {
|
|
53
|
+
if (v === 0) return 0;
|
|
54
|
+
if (v === 2) return 0.5;
|
|
55
|
+
if (v === 4) return 1;
|
|
56
|
+
if (v === 8) return 1.5;
|
|
57
|
+
if (v <= 16) return Math.round(v * 0.333);
|
|
58
|
+
return Math.floor(v * 0.7 - 12);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Same test `@tamagui/config/v5` uses to pick its web or native type ladder. */
|
|
62
|
+
export function scalePlatform(): ScalePlatform {
|
|
63
|
+
return process.env.TAMAGUI_TARGET === "native" ? "native" : "web";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function snapTo(px: number, grid: number): number {
|
|
67
|
+
return Math.round(px / grid) * grid;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Half-px precision below 2px keeps v5's 0.5/1/1.5 hairline spaces.
|
|
71
|
+
function snapSpace(px: number): number {
|
|
72
|
+
return px < 2 ? Math.round(px * 2) / 2 : Math.round(px);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isSubunit(key: SizeKey): key is SubunitKey {
|
|
76
|
+
return Object.hasOwn(SUBUNIT_SHAPE, key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function generateScaleTokens(inputs: ResolvedScaleInputs): ScaleTokens {
|
|
80
|
+
const size = {} as Record<SizeKey, number>;
|
|
81
|
+
for (const key of SIZE_KEYS) {
|
|
82
|
+
size[key] = isSubunit(key)
|
|
83
|
+
? SUBUNIT_SHAPE[key] * inputs.unit
|
|
84
|
+
: snapTo(SIZE_SHAPE[key] * inputs.control, inputs.snap);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const spaceTrue = sizeToSpace(size.$true);
|
|
88
|
+
const space = {} as Record<SpaceKey, number>;
|
|
89
|
+
for (const key of SIZE_KEYS) {
|
|
90
|
+
space[key] = snapSpace(SPACE_SHAPE[key] * spaceTrue);
|
|
91
|
+
}
|
|
92
|
+
for (const key of SIZE_KEYS.slice(1)) {
|
|
93
|
+
const px = space[key];
|
|
94
|
+
space[`-${key.slice(1)}` as SpaceKey] = px === 0 ? 0 : -px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const radiusTrue = Math.round(inputs.control * inputs.radius);
|
|
98
|
+
const radius = {} as Record<RadiusKey, number>;
|
|
99
|
+
for (const key of RADIUS_KEYS) {
|
|
100
|
+
radius[key] = Math.round(RADIUS_SHAPE[key] * radiusTrue);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { radius, zIndex: { ...ZINDEX }, space, size };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// v5 web leading: 150% tapering toward ~142% at display sizes; native is size + 5 (iOS HIG).
|
|
107
|
+
function bodyLineHeight(size: number, platform: ScalePlatform): number {
|
|
108
|
+
if (platform === "native") return Math.round(size + 5);
|
|
109
|
+
return Math.round(size * (1.5 - Math.max(0, (size - 20) * 0.004)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function headingLineHeight(size: number, platform: ScalePlatform): number {
|
|
113
|
+
return Math.round(platform === "native" ? size * 1.2 : size * 1.12 + 5);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function generateScaleFonts(type: number, platform: ScalePlatform): ScaleFonts {
|
|
117
|
+
const shape = platform === "native" ? NATIVE_TYPE_SHAPE : TYPE_SHAPE;
|
|
118
|
+
const bodySizes = {} as Record<FontKey, number>;
|
|
119
|
+
const bodyLineHeights = {} as Record<FontKey, number>;
|
|
120
|
+
const headingLineHeights = {} as Record<FontKey, number>;
|
|
121
|
+
for (const key of FONT_KEYS) {
|
|
122
|
+
const size = Math.round(shape[key] * type);
|
|
123
|
+
bodySizes[key] = size;
|
|
124
|
+
bodyLineHeights[key] = bodyLineHeight(size, platform);
|
|
125
|
+
headingLineHeights[key] = headingLineHeight(size, platform);
|
|
126
|
+
}
|
|
127
|
+
return { bodySizes, bodyLineHeights, headingSizes: { ...bodySizes }, headingLineHeights };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One scale from a few inputs: the Tamagui `$size`/`$space`/`$radius`/`$zIndex`
|
|
132
|
+
* token groups, the body and heading type tables, and the size-recipe inputs
|
|
133
|
+
* Button/Input read, all from the same numbers so `$size` props and
|
|
134
|
+
* `knobProps.control` cannot disagree.
|
|
135
|
+
*/
|
|
136
|
+
export function generateScale(
|
|
137
|
+
inputs: Partial<ScaleInputs> = defaultScaleInputs,
|
|
138
|
+
platform: ScalePlatform = scalePlatform(),
|
|
139
|
+
): GeneratedScale {
|
|
140
|
+
const resolved = resolveScaleInputs(inputs);
|
|
141
|
+
const core: ScaleCore = {
|
|
142
|
+
inputs: resolved,
|
|
143
|
+
tokens: generateScaleTokens(resolved),
|
|
144
|
+
fonts: generateScaleFonts(resolved.type, platform),
|
|
145
|
+
};
|
|
146
|
+
return { ...core, recipeInputs: deriveRecipeInputs(core) };
|
|
147
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { deriveRecipeInputs } from "./deriveRecipeInputs";
|
|
2
|
+
export {
|
|
3
|
+
generateScale,
|
|
4
|
+
generateScaleFonts,
|
|
5
|
+
generateScaleTokens,
|
|
6
|
+
scalePlatform,
|
|
7
|
+
sizeToSpace,
|
|
8
|
+
} from "./generateScale";
|
|
9
|
+
export type {
|
|
10
|
+
GeneratedScale,
|
|
11
|
+
ScaleCore,
|
|
12
|
+
ScaleFonts,
|
|
13
|
+
ScalePlatform,
|
|
14
|
+
ScaleTokens,
|
|
15
|
+
} from "./generateScale";
|
|
16
|
+
export {
|
|
17
|
+
CHIP_ICON_SHAPE,
|
|
18
|
+
CONTROL_ICON_SHAPE,
|
|
19
|
+
FONT_KEYS,
|
|
20
|
+
NATIVE_TYPE_SHAPE,
|
|
21
|
+
RADIUS_KEYS,
|
|
22
|
+
RADIUS_SHAPE,
|
|
23
|
+
SIZE_KEYS,
|
|
24
|
+
SIZE_SHAPE,
|
|
25
|
+
SPACE_SHAPE,
|
|
26
|
+
SUBUNIT_SHAPE,
|
|
27
|
+
TYPE_SHAPE,
|
|
28
|
+
ZINDEX_KEYS,
|
|
29
|
+
defaultScaleInputs,
|
|
30
|
+
v5ScaleInputs,
|
|
31
|
+
resolveScaleInputs,
|
|
32
|
+
scaleInputDefaults,
|
|
33
|
+
} from "./scaleInputs";
|
|
34
|
+
export type {
|
|
35
|
+
FontKey,
|
|
36
|
+
RadiusKey,
|
|
37
|
+
ResolvedScaleInputs,
|
|
38
|
+
ScaleInputs,
|
|
39
|
+
SizeKey,
|
|
40
|
+
SpaceKey,
|
|
41
|
+
SubunitKey,
|
|
42
|
+
ZIndexKey,
|
|
43
|
+
} from "./scaleInputs";
|
|
44
|
+
export { SCALE_ATTRIBUTE, scaleCss, scaleRootSelectors } from "./scaleCss";
|
|
45
|
+
export type { ScaleCssSource } from "./scaleCss";
|
|
46
|
+
export { rescaleFont, scaleFontSet } from "./scaleFonts";
|
|
47
|
+
export { DEFAULT_SCALE_ID, buildThemeScales, uniqueScales } from "./themeScales";
|
|
48
|
+
export type { ThemeScale, ThemeScaleBuildOptions } from "./themeScales";
|
|
49
|
+
export { applyThemeScale, resolveThemeScale, useActiveScale } from "./useActiveScale";
|
|
50
|
+
export type { PublishedScale, ScaleSource } from "./useActiveScale";
|
|
51
|
+
export { ScaleStyles, scaleAttributeValue, scaleStyleSheet } from "./ScaleStyles";
|
|
52
|
+
export type { ScaleStylesProps } from "./ScaleStyles";
|
|
53
|
+
export { scaleAttributeFromCookie, scaleAttributeScript } from "./scaleAttribute";
|
|
54
|
+
export { getActiveScaleTokens, scaleRadiusPx, setActiveScaleTokens } from "./activeScale";
|