@chipmobilesdk/rn-theme 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +91 -23
  2. package/NOTICE +9 -0
  3. package/README.md +277 -187
  4. package/package.json +49 -50
  5. package/skills/sdk-rn-theme/SKILL.md +45 -48
  6. package/src/color/categorical.ts +33 -0
  7. package/src/color/contrast.ts +230 -0
  8. package/src/color/parse.ts +120 -0
  9. package/src/color/roles.ts +262 -0
  10. package/src/index.ts +23 -17
  11. package/src/integration/navigationTheme.ts +51 -0
  12. package/src/presets/byTone.ts +28 -0
  13. package/src/presets/layout.ts +38 -0
  14. package/src/presets/themePresets.ts +101 -0
  15. package/src/theme/buildThemes.ts +178 -0
  16. package/src/theme/errors.ts +34 -0
  17. package/src/theme/overrides.ts +142 -0
  18. package/src/theme/validateThemeConfig.ts +171 -0
  19. package/src/tokens/defaults.ts +103 -46
  20. package/src/tokens/typography.ts +144 -64
  21. package/src/types/brand.ts +16 -0
  22. package/src/types/colors.ts +80 -11
  23. package/src/types/index.ts +44 -5
  24. package/src/types/theme.ts +161 -0
  25. package/src/types/tokens.ts +47 -5
  26. package/src/types/typography.ts +46 -10
  27. package/src/unistyles/createUnistylesConfig.ts +165 -177
  28. package/src/unistyles/themeMode.ts +28 -0
  29. package/src/vendor/mcu/LICENSE +191 -0
  30. package/src/vendor/mcu/hct/cam16.ts +419 -0
  31. package/src/vendor/mcu/hct/hct.ts +216 -0
  32. package/src/vendor/mcu/hct/hct_solver.ts +557 -0
  33. package/src/vendor/mcu/hct/viewing_conditions.ts +121 -0
  34. package/src/vendor/mcu/index.ts +5 -0
  35. package/src/vendor/mcu/palettes/tonal_palette.ts +172 -0
  36. package/src/vendor/mcu/utils/color_utils.ts +302 -0
  37. package/src/vendor/mcu/utils/math_utils.ts +148 -0
  38. package/src/types/style.ts +0 -26
  39. package/src/unistyles/types.ts +0 -24
  40. package/src/unistyles/validateThemeConfig.ts +0 -154
  41. package/src/utils/colorGeneration.ts +0 -87
  42. package/src/utils/colorMath.ts +0 -55
@@ -1,64 +1,144 @@
1
- import type {
2
- TypographyConfig,
3
- TypographyToken,
4
- Typography,
5
- RawTypography,
6
- } from '../types/typography';
7
-
8
- interface ScaleEntry {
9
- size: number;
10
- weight: TypographyToken['fontWeight'];
11
- lineHeightRatio: number;
12
- letterSpacing?: number;
13
- }
14
-
15
- const DEFAULT_BODY_SIZE = 16;
16
-
17
- const SCALE: Record<keyof RawTypography, ScaleEntry> = {
18
- display: {
19
- size: 57,
20
- weight: '700',
21
- lineHeightRatio: 1.12,
22
- letterSpacing: -0.25,
23
- },
24
- headline: { size: 32, weight: '600', lineHeightRatio: 1.25 },
25
- title: { size: 22, weight: '600', lineHeightRatio: 1.27 },
26
- body: { size: 16, weight: '400', lineHeightRatio: 1.5 },
27
- label: { size: 14, weight: '500', lineHeightRatio: 1.43, letterSpacing: 0.1 },
28
- caption: {
29
- size: 12,
30
- weight: '400',
31
- lineHeightRatio: 1.33,
32
- letterSpacing: 0.4,
33
- },
34
- };
35
-
36
- export function buildTypography(config?: TypographyConfig): Typography {
37
- const fontFamily = config?.fontFamily ?? 'System';
38
- const baseScale =
39
- (config?.baseFontSize ?? DEFAULT_BODY_SIZE) / DEFAULT_BODY_SIZE;
40
- const scale = baseScale * (config?.fontScale ?? 1);
41
-
42
- function makeToken(entry: ScaleEntry): TypographyToken {
43
- const fontSize = Math.round(entry.size * scale);
44
- return {
45
- fontSize,
46
- fontWeight: entry.weight,
47
- lineHeight: Math.round(fontSize * entry.lineHeightRatio),
48
- fontFamily,
49
- ...(entry.letterSpacing != null && {
50
- letterSpacing: entry.letterSpacing,
51
- }),
52
- };
53
- }
54
-
55
- return {
56
- fontFamily,
57
- display: makeToken(SCALE.display),
58
- headline: makeToken(SCALE.headline),
59
- title: makeToken(SCALE.title),
60
- body: makeToken(SCALE.body),
61
- label: makeToken(SCALE.label),
62
- caption: makeToken(SCALE.caption),
63
- };
64
- }
1
+ import type {
2
+ FontConfig,
3
+ FontToken,
4
+ FontWeightName,
5
+ FontWeights,
6
+ FontWeightValue,
7
+ Typography,
8
+ TypographyConfig,
9
+ TypographyRole,
10
+ TypographyStyleName,
11
+ TypographyToken,
12
+ } from '../types/typography';
13
+
14
+ export type Platform = 'ios' | 'android';
15
+
16
+ const DEFAULT_FONT_FAMILY = 'System';
17
+ const DEFAULT_BODY_SIZE = 16;
18
+
19
+ export const FONT_WEIGHTS: FontWeights = {
20
+ regular: '400',
21
+ medium: '500',
22
+ semibold: '600',
23
+ bold: '700',
24
+ heavy: '800',
25
+ };
26
+
27
+ const WEIGHT_ORDER: FontWeightName[] = ['regular', 'medium', 'semibold', 'bold', 'heavy'];
28
+
29
+ /** Android: the per-weight family already carries the weight. */
30
+ const ANDROID_MAPPED_WEIGHT: FontWeightValue = 'normal';
31
+
32
+ interface RoleScale {
33
+ /** Small, medium, large. Medium keeps the 0.2.x size (plan D-6). */
34
+ sizes: [number, number, number];
35
+ weight: FontWeightName;
36
+ lineHeightRatio: number;
37
+ letterSpacing?: number;
38
+ }
39
+
40
+ const SCALE: Record<TypographyRole, RoleScale> = {
41
+ display: { sizes: [45, 57, 64], weight: 'bold', lineHeightRatio: 1.12, letterSpacing: -0.25 },
42
+ headline: { sizes: [28, 32, 36], weight: 'semibold', lineHeightRatio: 1.25 },
43
+ title: { sizes: [18, 22, 24], weight: 'semibold', lineHeightRatio: 1.27 },
44
+ body: { sizes: [14, 16, 18], weight: 'regular', lineHeightRatio: 1.5 },
45
+ label: { sizes: [12, 14, 16], weight: 'medium', lineHeightRatio: 1.43, letterSpacing: 0.1 },
46
+ caption: { sizes: [11, 12, 13], weight: 'regular', lineHeightRatio: 1.33, letterSpacing: 0.4 },
47
+ };
48
+
49
+ const SIZE_SUFFIXES = ['Small', '', 'Large'] as const;
50
+
51
+ /** The configured weight closest to the requested one; ties go to the heavier. */
52
+ function nearestWeight(
53
+ requested: FontWeightName,
54
+ available: Partial<Record<FontWeightName, string>>,
55
+ ): FontWeightName | undefined {
56
+ const target = WEIGHT_ORDER.indexOf(requested);
57
+
58
+ return WEIGHT_ORDER.filter(weight => available[weight] != null).sort(
59
+ (a, b) =>
60
+ Math.abs(WEIGHT_ORDER.indexOf(a) - target) - Math.abs(WEIGHT_ORDER.indexOf(b) - target) ||
61
+ WEIGHT_ORDER.indexOf(b) - WEIGHT_ORDER.indexOf(a),
62
+ )[0];
63
+ }
64
+
65
+ function warnMissingWeight(platform: Platform, weight: FontWeightName, used: FontWeightName) {
66
+ if (__DEV__) {
67
+ console.warn(
68
+ `[rn-theme] typography.fonts.${platform} has no "${weight}" font; using "${used}" instead.`,
69
+ );
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Resolves the family and weight a style needs for one named weight on one
75
+ * platform (spec US-2).
76
+ */
77
+ export function buildFonts(config: TypographyConfig | undefined, platform: Platform) {
78
+ const fonts: FontConfig = config?.fonts ?? {};
79
+ const family = fonts.family ?? config?.fontFamily ?? DEFAULT_FONT_FAMILY;
80
+ const perWeight = fonts[platform];
81
+ const warned = new Set<FontWeightName>();
82
+
83
+ const resolve = (weight: FontWeightName): FontToken => {
84
+ // iOS picks weights inside one family, so a partial iOS map only renames
85
+ // the weights it lists; Android needs a file per weight, hence nearest.
86
+ const nearest =
87
+ platform === 'ios' ? (perWeight?.[weight] && weight) : perWeight && nearestWeight(weight, perWeight);
88
+
89
+ if (!perWeight || !nearest) {
90
+ return { fontFamily: family, fontWeight: FONT_WEIGHTS[weight] };
91
+ }
92
+
93
+ if (nearest !== weight && !warned.has(weight)) {
94
+ warned.add(weight);
95
+ warnMissingWeight(platform, weight, nearest);
96
+ }
97
+
98
+ const fontFamily = perWeight[nearest] as string;
99
+
100
+ return {
101
+ fontFamily,
102
+ fontWeight: platform === 'android' ? ANDROID_MAPPED_WEIGHT : FONT_WEIGHTS[weight],
103
+ };
104
+ };
105
+
106
+ const font = {} as Record<FontWeightName, FontToken>;
107
+
108
+ for (const weight of WEIGHT_ORDER) {
109
+ font[weight] = resolve(weight);
110
+ }
111
+
112
+ return { family, font };
113
+ }
114
+
115
+ export function buildTypography(
116
+ config: TypographyConfig | undefined,
117
+ font: Record<FontWeightName, FontToken>,
118
+ family: string,
119
+ ): Typography {
120
+ const scale = ((config?.baseFontSize ?? DEFAULT_BODY_SIZE) / DEFAULT_BODY_SIZE) * (config?.fontScale ?? 1);
121
+ const typography = { fontFamily: family } as Typography;
122
+
123
+ for (const role of Object.keys(SCALE) as TypographyRole[]) {
124
+ const entry = SCALE[role];
125
+
126
+ entry.sizes.forEach((size, index) => {
127
+ const fontSize = Math.round(size * scale);
128
+ const token: TypographyToken = {
129
+ fontSize,
130
+ lineHeight: Math.round(fontSize * entry.lineHeightRatio),
131
+ ...font[entry.weight],
132
+ ...(entry.letterSpacing != null && { letterSpacing: entry.letterSpacing }),
133
+ };
134
+
135
+ typography[`${role}${SIZE_SUFFIXES[index]}` as TypographyStyleName] = token;
136
+ });
137
+ }
138
+
139
+ return typography;
140
+ }
141
+
142
+ export function buildFontWeight(): FontWeights {
143
+ return { ...FONT_WEIGHTS };
144
+ }
@@ -1,5 +1,21 @@
1
+ /**
2
+ * Opaque brand colours. Each keeps its hue and chroma; roles take fixed tones.
3
+ *
4
+ * Accepted colour strings everywhere in the package: `#RGB`, `#RGBA`,
5
+ * `#RRGGBB`, `#RRGGBBAA`, `rgb(r, g, b)`, `rgba(r, g, b, a)`.
6
+ */
1
7
  export interface BrandColors {
2
8
  primary: string;
3
9
  secondary: string;
4
10
  tertiary: string;
5
11
  }
12
+
13
+ /** Optional opaque seeds replacing the default neutral and semantic hues. */
14
+ export interface SeedColors {
15
+ /** Without it, neutrals take the primary hue at a very low chroma. */
16
+ neutral?: string;
17
+ error?: string;
18
+ success?: string;
19
+ warning?: string;
20
+ info?: string;
21
+ }
@@ -1,23 +1,92 @@
1
- export interface ThemeColors {
1
+ export type ColorScheme = 'light' | 'dark';
2
+
3
+ /** Colours whose roles come in sets of four: X, onX, XContainer, onXContainer. */
4
+ export type AccentRoleName =
5
+ | 'primary'
6
+ | 'secondary'
7
+ | 'tertiary'
8
+ | 'error'
9
+ | 'success'
10
+ | 'warning'
11
+ | 'info';
12
+
13
+ export interface ColorRoles {
2
14
  primary: string;
3
- primaryContainer: string;
4
15
  onPrimary: string;
16
+ primaryContainer: string;
17
+ onPrimaryContainer: string;
5
18
  secondary: string;
6
- secondaryContainer: string;
7
19
  onSecondary: string;
20
+ secondaryContainer: string;
21
+ onSecondaryContainer: string;
8
22
  tertiary: string;
9
- tertiaryContainer: string;
10
23
  onTertiary: string;
11
- background: string;
12
- surface: string;
13
- surfaceVariant: string;
14
- onBackground: string;
15
- onSurface: string;
16
- border: string;
24
+ tertiaryContainer: string;
25
+ onTertiaryContainer: string;
26
+ /** Error text/fill; ≥ 4.5:1 on `background` and `surface`. */
17
27
  error: string;
28
+ onError: string;
29
+ errorContainer: string;
30
+ onErrorContainer: string;
18
31
  success: string;
32
+ onSuccess: string;
33
+ successContainer: string;
34
+ onSuccessContainer: string;
19
35
  warning: string;
36
+ onWarning: string;
37
+ warningContainer: string;
38
+ onWarningContainer: string;
20
39
  info: string;
21
- disabled: string;
40
+ onInfo: string;
41
+ infoContainer: string;
42
+ onInfoContainer: string;
43
+ background: string;
44
+ onBackground: string;
45
+ surface: string;
46
+ onSurface: string;
47
+ surfaceVariant: string;
48
+ /** Muted text; ≥ 4.5:1 on `background`, `surface` and `surfaceVariant`. */
49
+ onSurfaceVariant: string;
50
+ surfaceContainerLowest: string;
51
+ surfaceContainerLow: string;
52
+ surfaceContainer: string;
53
+ surfaceContainerHigh: string;
54
+ surfaceContainerHighest: string;
55
+ inverseSurface: string;
56
+ inverseOnSurface: string;
57
+ inversePrimary: string;
58
+ /** Boundary of interactive components; ≥ 3:1 on `background` and `surface`. */
59
+ outline: string;
60
+ /** Decorative dividers; no contrast guarantee. */
61
+ outlineVariant: string;
62
+ scrim: string;
22
63
  shadow: string;
64
+ /**
65
+ * Inactive content. A state colour, exempt from contrast targets
66
+ * (WCAG 1.4.3) — never use it for readable muted text; that is
67
+ * `onSurfaceVariant`.
68
+ */
69
+ disabled: string;
70
+ }
71
+
72
+ export type StandardToneName =
73
+ | 'neutral'
74
+ | 'primary'
75
+ | 'info'
76
+ | 'success'
77
+ | 'warning'
78
+ | 'error';
79
+
80
+ /** A matched colour set for one tone, for use with `byTone`. */
81
+ export interface ToneColors {
82
+ /** Text or icon in this tone on `background`/`surface` (≥ 4.5:1). */
83
+ fg: string;
84
+ container: string;
85
+ /** Text on `container` (≥ 4.5:1). */
86
+ onContainer: string;
87
+ /** Boundary in this tone (≥ 3:1 on `background`). */
88
+ border: string;
89
+ fill: string;
90
+ /** Text on `fill` (≥ 4.5:1). */
91
+ onFill: string;
23
92
  }
@@ -1,11 +1,50 @@
1
- export type { BrandColors } from './brand';
2
- export type { ThemeColors } from './colors';
3
- export type { TypographyConfig, TypographyToken, Typography } from './typography';
1
+ export type { BrandColors, SeedColors } from './brand';
4
2
  export type {
5
- SpacingTokens,
3
+ AccentRoleName,
4
+ ColorRoles,
5
+ ColorScheme,
6
+ StandardToneName,
7
+ ToneColors,
8
+ } from './colors';
9
+ export type {
10
+ FontConfig,
11
+ FontToken,
12
+ FontWeightName,
13
+ FontWeights,
14
+ FontWeightValue,
15
+ Typography,
16
+ TypographyConfig,
17
+ TypographyRole,
18
+ TypographyStyleName,
19
+ TypographyToken,
20
+ } from './typography';
21
+ export type {
22
+ BorderWidthTokens,
23
+ EasingCurve,
24
+ MotionTokens,
25
+ OpacityTokens,
6
26
  RadiusTokens,
7
27
  ShadowToken,
8
28
  ShadowTokens,
29
+ SizeTokens,
30
+ SpacingKey,
31
+ SpacingTokens,
9
32
  ZIndexTokens,
10
33
  } from './tokens';
11
- export type { DeepPartial, StyleConfig, StyleTheme } from './style';
34
+ export type {
35
+ BaseTheme,
36
+ BorderPresets,
37
+ BoxPresets,
38
+ DeepPartial,
39
+ EmptyExtension,
40
+ ExtendContext,
41
+ InlinePreset,
42
+ NamedThemeConfig,
43
+ StackPreset,
44
+ TextPreset,
45
+ TextPresets,
46
+ Theme,
47
+ ThemeConfig,
48
+ ThemeName,
49
+ Themes,
50
+ } from './theme';
@@ -0,0 +1,161 @@
1
+ import type { BrandColors, SeedColors } from './brand';
2
+ import type { ColorRoles, ColorScheme, StandardToneName, ToneColors } from './colors';
3
+ import type {
4
+ BorderWidthTokens,
5
+ MotionTokens,
6
+ OpacityTokens,
7
+ RadiusTokens,
8
+ ShadowTokens,
9
+ SizeTokens,
10
+ SpacingKey,
11
+ SpacingTokens,
12
+ ZIndexTokens,
13
+ } from './tokens';
14
+ import type {
15
+ FontToken,
16
+ FontWeightName,
17
+ FontWeights,
18
+ Typography,
19
+ TypographyConfig,
20
+ TypographyToken,
21
+ } from './typography';
22
+
23
+ export type DeepPartial<T> = {
24
+ [K in keyof T]?: T[K] extends readonly unknown[]
25
+ ? T[K]
26
+ : T[K] extends object
27
+ ? DeepPartial<T[K]>
28
+ : T[K];
29
+ };
30
+
31
+ /** A typography token with its colour, ready to spread into a text style. */
32
+ export interface TextPreset extends TypographyToken {
33
+ color: string;
34
+ }
35
+
36
+ export interface TextPresets {
37
+ body: TextPreset;
38
+ bodyMuted: TextPreset;
39
+ label: TextPreset;
40
+ labelMuted: TextPreset;
41
+ caption: TextPreset;
42
+ title: TextPreset;
43
+ headline: TextPreset;
44
+ /** Uses the contrast-exempt `disabled` colour. */
45
+ disabled: TextPreset;
46
+ }
47
+
48
+ export interface StackPreset {
49
+ flexDirection: 'column';
50
+ gap: number;
51
+ }
52
+
53
+ export interface InlinePreset {
54
+ flexDirection: 'row';
55
+ alignItems: 'center';
56
+ gap: number;
57
+ }
58
+
59
+ export interface BorderPresets {
60
+ /** Hairline divider on every side (`outlineVariant`). */
61
+ hairline: { borderWidth: number; borderColor: string };
62
+ /** Divider above (`outlineVariant`). */
63
+ top: { borderTopWidth: number; borderTopColor: string };
64
+ /** Divider below (`outlineVariant`). */
65
+ bottom: { borderBottomWidth: number; borderBottomColor: string };
66
+ /** Boundary of an interactive component (`outline`, ≥ 3:1). */
67
+ all: { borderWidth: number; borderColor: string };
68
+ }
69
+
70
+ export interface BoxPresets {
71
+ outlined: {
72
+ borderWidth: number;
73
+ borderColor: string;
74
+ borderRadius: number;
75
+ padding: number;
76
+ gap: number;
77
+ };
78
+ card: { backgroundColor: string; borderRadius: number; padding: number; gap: number };
79
+ sheet: {
80
+ backgroundColor: string;
81
+ borderTopLeftRadius: number;
82
+ borderTopRightRadius: number;
83
+ padding: number;
84
+ gap: number;
85
+ };
86
+ touchTarget: { minWidth: number; minHeight: number; justifyContent: 'center' };
87
+ }
88
+
89
+ export interface BaseTheme<TTone extends string = never> {
90
+ color: ColorRoles;
91
+ tone: Record<StandardToneName | TTone, ToneColors>;
92
+ typography: Typography;
93
+ /** Family + weight per named weight, resolved for the current platform. */
94
+ font: Record<FontWeightName, FontToken>;
95
+ fontWeight: FontWeights;
96
+ spacing: SpacingTokens;
97
+ radius: RadiusTokens;
98
+ borderWidth: BorderWidthTokens;
99
+ size: SizeTokens;
100
+ opacity: OpacityTokens;
101
+ motion: MotionTokens;
102
+ shadow: ShadowTokens;
103
+ zIndex: ZIndexTokens;
104
+ /** Ceiling for `maxFontSizeMultiplier` on the app's `Text`. */
105
+ maxFontScale: number;
106
+ /** Chart series colours (≥ 3:1 on `surface`), starting at the primary hue. */
107
+ categorical: string[];
108
+ text: TextPresets;
109
+ stack: Record<SpacingKey, StackPreset>;
110
+ inline: Record<SpacingKey, InlinePreset>;
111
+ border: BorderPresets;
112
+ box: BoxPresets;
113
+ }
114
+
115
+ export type EmptyExtension = Record<never, never>;
116
+
117
+ export type Theme<TExt extends object = EmptyExtension, TTone extends string = never> = BaseTheme<TTone> &
118
+ TExt;
119
+
120
+ export type ThemeName<TName extends string = never> = 'light' | 'dark' | TName;
121
+
122
+ export type Themes<
123
+ TExt extends object = EmptyExtension,
124
+ TTone extends string = never,
125
+ TName extends string = never,
126
+ > = Record<ThemeName<TName>, Theme<TExt, TTone>>;
127
+
128
+ export interface ExtendContext<TTone extends string = never, TName extends string = never> {
129
+ name: ThemeName<TName>;
130
+ scheme: ColorScheme;
131
+ theme: BaseTheme<TTone>;
132
+ }
133
+
134
+ /** An extra theme beyond light and dark. Omitted colours come from the base config. */
135
+ export interface NamedThemeConfig<TTone extends string = never> {
136
+ scheme: ColorScheme;
137
+ brandColors?: BrandColors;
138
+ seeds?: SeedColors;
139
+ tones?: Partial<Record<TTone, string>>;
140
+ }
141
+
142
+ /** Extension keys may not replace base groups. */
143
+ type NoBaseKeys = { [K in keyof BaseTheme]?: never };
144
+
145
+ export interface ThemeConfig<
146
+ TExt extends object = EmptyExtension,
147
+ TTone extends string = never,
148
+ TName extends string = never,
149
+ > {
150
+ brandColors: BrandColors;
151
+ seeds?: SeedColors;
152
+ /** Custom tones from one opaque seed each, e.g. `{ accent: '#BD260F' }`. */
153
+ tones?: Record<TTone, string>;
154
+ typography?: TypographyConfig;
155
+ /** App-specific token groups, derived per theme from the generated tokens. */
156
+ extend?: (context: ExtendContext<TTone, TName>) => TExt & NoBaseKeys;
157
+ themes?: Record<TName, NamedThemeConfig<TTone>>;
158
+ overrides?: NoInfer<Partial<Record<ThemeName<TName>, DeepPartial<Theme<TExt, TTone>>>>>;
159
+ /** Throw instead of warning when an override breaks a contrast guarantee. */
160
+ strict?: boolean;
161
+ }
@@ -1,13 +1,23 @@
1
1
  export interface SpacingTokens {
2
+ none: number;
3
+ xxs: number;
2
4
  xs: number;
3
5
  sm: number;
6
+ smPlus: number;
4
7
  md: number;
8
+ mdPlus: number;
5
9
  lg: number;
6
10
  xl: number;
11
+ xlPlus: number;
7
12
  xxl: number;
13
+ xxxl: number;
8
14
  }
9
15
 
16
+ export type SpacingKey = keyof SpacingTokens;
17
+
10
18
  export interface RadiusTokens {
19
+ none: number;
20
+ xs: number;
11
21
  sm: number;
12
22
  md: number;
13
23
  lg: number;
@@ -15,12 +25,44 @@ export interface RadiusTokens {
15
25
  full: number;
16
26
  }
17
27
 
28
+ export interface BorderWidthTokens {
29
+ hairline: number;
30
+ thin: number;
31
+ thick: number;
32
+ }
33
+
34
+ export interface SizeTokens {
35
+ /** Minimum touch target side, in points (WCAG 2.5.8 / standards V). */
36
+ touchTarget: number;
37
+ icon: { sm: number; md: number; lg: number };
38
+ }
39
+
40
+ /** Material 3 state-layer and disabled opacities. */
41
+ export interface OpacityTokens {
42
+ hover: number;
43
+ focus: number;
44
+ pressed: number;
45
+ dragged: number;
46
+ disabledContent: number;
47
+ disabledContainer: number;
48
+ }
49
+
50
+ /** Cubic-bezier control points `[x1, y1, x2, y2]`, e.g. for `Easing.bezier`. */
51
+ export type EasingCurve = [number, number, number, number];
52
+
53
+ export interface MotionTokens {
54
+ duration: { short: number; medium: number; long: number };
55
+ easing: {
56
+ standard: EasingCurve;
57
+ emphasized: EasingCurve;
58
+ decelerate: EasingCurve;
59
+ accelerate: EasingCurve;
60
+ };
61
+ }
62
+
18
63
  export interface ShadowToken {
19
- shadowColor: string;
20
- shadowOffset: { width: number; height: number };
21
- shadowOpacity: number;
22
- shadowRadius: number;
23
- elevation: number;
64
+ /** CSS-style shadow list, rendered by React Native on iOS and Android. */
65
+ boxShadow: string;
24
66
  }
25
67
 
26
68
  export interface ShadowTokens {
@@ -1,26 +1,62 @@
1
+ export type FontWeightName = 'regular' | 'medium' | 'semibold' | 'bold' | 'heavy';
2
+
3
+ /** The weight strings the package emits; each is a valid React Native `fontWeight`. */
4
+ export type FontWeightValue = 'normal' | '400' | '500' | '600' | '700' | '800' | '900';
5
+
6
+ /**
7
+ * Custom font files per weight. Android cannot pick a weight inside a family,
8
+ * so every weight needs its own family name there; iOS resolves weights within
9
+ * one family unless a per-weight name is given.
10
+ */
11
+ export interface FontConfig {
12
+ /** Family used where no per-weight name applies. */
13
+ family?: string;
14
+ ios?: Partial<Record<FontWeightName, string>>;
15
+ android?: Partial<Record<FontWeightName, string>>;
16
+ }
17
+
1
18
  export interface TypographyConfig {
19
+ /** Single family for every weight (0.2.x behaviour). Defaults to `System`. */
2
20
  fontFamily?: string;
21
+ fonts?: FontConfig;
3
22
  baseFontSize?: number;
23
+ /** Design-time multiplier. It does not track the OS text-size setting. */
4
24
  fontScale?: number;
25
+ /**
26
+ * Ceiling for the OS text-size multiplier, applied by the app's `Text`
27
+ * through `maxFontSizeMultiplier`. Defaults to 2 (WCAG 1.4.4).
28
+ */
29
+ maxFontScale?: number;
5
30
  }
6
31
 
32
+ export type TypographyRole = 'display' | 'headline' | 'title' | 'body' | 'label' | 'caption';
33
+
34
+ export type TypographyStyleName =
35
+ | TypographyRole
36
+ | `${TypographyRole}Small`
37
+ | `${TypographyRole}Large`;
38
+
7
39
  export interface TypographyToken {
8
40
  fontSize: number;
9
- fontWeight: '400' | '500' | '600' | '700' | '800' | '900';
10
41
  lineHeight: number;
11
- fontFamily?: string;
42
+ fontWeight: FontWeightValue;
43
+ fontFamily: string;
12
44
  letterSpacing?: number;
13
45
  }
14
46
 
15
- export interface RawTypography {
16
- display: TypographyToken;
17
- headline: TypographyToken;
18
- title: TypographyToken;
19
- body: TypographyToken;
20
- label: TypographyToken;
21
- caption: TypographyToken;
47
+ /** The six 0.2.x role names resolve to the medium size. */
48
+ export type Typography = { fontFamily: string } & Record<TypographyStyleName, TypographyToken>;
49
+
50
+ /** The fixed weight scale behind the weight names. */
51
+ export interface FontWeights {
52
+ regular: '400';
53
+ medium: '500';
54
+ semibold: '600';
55
+ bold: '700';
56
+ heavy: '800';
22
57
  }
23
58
 
24
- export interface Typography extends RawTypography {
59
+ export interface FontToken {
25
60
  fontFamily: string;
61
+ fontWeight: FontWeightValue;
26
62
  }