@chipmobilesdk/rn-theme 0.2.1 → 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 +297 -154
  4. package/package.json +49 -50
  5. package/skills/sdk-rn-theme/SKILL.md +45 -30
  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,154 +0,0 @@
1
- import tinycolor from 'tinycolor2';
2
- import type { BrandColors } from '../types/brand';
3
- import type { StyleConfig } from '../types/style';
4
- import type { TypographyConfig } from '../types/typography';
5
-
6
- const BRAND_COLOR_KEYS = ['primary', 'secondary', 'tertiary'] as const;
7
-
8
- type ThemeName = 'light' | 'dark';
9
-
10
- interface ThemeOptionsShape {
11
- CSSVars?: unknown;
12
- nativeBreakpointsMode?: unknown;
13
- initialTheme?: unknown;
14
- adaptiveThemes?: unknown;
15
- }
16
-
17
- export function validateStyleConfig(config: StyleConfig): void {
18
- if (!isRecord(config)) {
19
- throw new Error('Theme config must be an object.');
20
- }
21
-
22
- validateBrandColors(config.brandColors);
23
- validateTypographyConfig(config.typographyConfig);
24
- }
25
-
26
- export function validateBrandColors(brandColors: BrandColors): void {
27
- if (!isRecord(brandColors)) {
28
- throw new Error('Theme config must include brandColors.');
29
- }
30
-
31
- for (const key of BRAND_COLOR_KEYS) {
32
- const color = brandColors[key];
33
-
34
- if (typeof color !== 'string' || color.trim().length === 0) {
35
- throw new Error(`brandColors.${key} must be a non-empty color string.`);
36
- }
37
-
38
- if (!tinycolor(color).isValid()) {
39
- throw new Error(`brandColors.${key} must be a valid color string.`);
40
- }
41
- }
42
- }
43
-
44
- export function validateTypographyConfig(config?: TypographyConfig): void {
45
- if (config == null) {
46
- return;
47
- }
48
-
49
- if (!isRecord(config)) {
50
- throw new Error('typographyConfig must be an object when provided.');
51
- }
52
- const typographyConfig = config as {
53
- fontFamily?: unknown;
54
- baseFontSize?: unknown;
55
- fontScale?: unknown;
56
- };
57
-
58
- if (typographyConfig.fontFamily != null) {
59
- if (
60
- typeof typographyConfig.fontFamily !== 'string' ||
61
- typographyConfig.fontFamily.trim().length === 0
62
- ) {
63
- throw new Error(
64
- 'typographyConfig.fontFamily must be a non-empty string.',
65
- );
66
- }
67
- }
68
-
69
- if (typographyConfig.baseFontSize != null) {
70
- assertPositiveFiniteNumber(
71
- typographyConfig.baseFontSize,
72
- 'typographyConfig.baseFontSize',
73
- );
74
- }
75
-
76
- if (typographyConfig.fontScale != null) {
77
- assertPositiveFiniteNumber(
78
- typographyConfig.fontScale,
79
- 'typographyConfig.fontScale',
80
- );
81
- }
82
- }
83
-
84
- export function validateThemeOptions(options?: ThemeOptionsShape): void {
85
- if (options == null) {
86
- return;
87
- }
88
-
89
- if (!isRecord(options)) {
90
- throw new Error('Theme options must be an object when provided.');
91
- }
92
-
93
- const hasInitialTheme = options.initialTheme != null;
94
- const hasAdaptiveThemes = options.adaptiveThemes === true;
95
-
96
- if (hasInitialTheme && hasAdaptiveThemes) {
97
- throw new Error('Use either initialTheme or adaptiveThemes, not both.');
98
- }
99
-
100
- if (options.initialTheme != null) {
101
- validateInitialTheme(options.initialTheme);
102
- }
103
-
104
- if (
105
- options.adaptiveThemes != null &&
106
- typeof options.adaptiveThemes !== 'boolean'
107
- ) {
108
- throw new Error('adaptiveThemes must be a boolean when provided.');
109
- }
110
-
111
- if (options.CSSVars != null && typeof options.CSSVars !== 'boolean') {
112
- throw new Error('CSSVars must be a boolean when provided.');
113
- }
114
-
115
- if (
116
- options.nativeBreakpointsMode != null &&
117
- options.nativeBreakpointsMode !== 'pixels' &&
118
- options.nativeBreakpointsMode !== 'points'
119
- ) {
120
- throw new Error("nativeBreakpointsMode must be 'pixels' or 'points'.");
121
- }
122
- }
123
-
124
- function validateInitialTheme(initialTheme: unknown): void {
125
- if (typeof initialTheme === 'function') {
126
- const resolvedTheme = initialTheme();
127
-
128
- if (!isThemeName(resolvedTheme)) {
129
- throw new Error("initialTheme resolver must return 'light' or 'dark'.");
130
- }
131
-
132
- return;
133
- }
134
-
135
- if (!isThemeName(initialTheme)) {
136
- throw new Error(
137
- "initialTheme must be 'light', 'dark', or a resolver function.",
138
- );
139
- }
140
- }
141
-
142
- function assertPositiveFiniteNumber(value: unknown, fieldName: string): void {
143
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
144
- throw new Error(`${fieldName} must be a positive finite number.`);
145
- }
146
- }
147
-
148
- function isThemeName(value: unknown): value is ThemeName {
149
- return value === 'light' || value === 'dark';
150
- }
151
-
152
- function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
153
- return typeof value === 'object' && value !== null && !Array.isArray(value);
154
- }
@@ -1,87 +0,0 @@
1
- import type { BrandColors } from '../types/brand';
2
- import type { ThemeColors } from '../types/colors';
3
- import {
4
- getContrastColor,
5
- adjustLightness,
6
- adjustSaturation,
7
- toContainerColor,
8
- } from './colorMath';
9
-
10
- export function generateLightPalette(brand: BrandColors): ThemeColors {
11
- const primaryContainer = toContainerColor(brand.primary, false);
12
- const secondaryContainer = toContainerColor(brand.secondary, false);
13
- const tertiaryContainer = toContainerColor(brand.tertiary, false);
14
-
15
- return {
16
- primary: brand.primary,
17
- primaryContainer,
18
- onPrimary: getContrastColor(brand.primary),
19
-
20
- secondary: brand.secondary,
21
- secondaryContainer,
22
- onSecondary: getContrastColor(brand.secondary),
23
-
24
- tertiary: brand.tertiary,
25
- tertiaryContainer,
26
- onTertiary: getContrastColor(brand.tertiary),
27
-
28
- background: '#F8F9FA',
29
- surface: '#FFFFFF',
30
- surfaceVariant: adjustSaturation(adjustLightness(brand.primary, 48), -20),
31
-
32
- onBackground: '#1A1A1A',
33
- onSurface: '#1A1A1A',
34
-
35
- border: '#E0E0E0',
36
-
37
- // Semantic — WCAG-compliant accessible values for light backgrounds
38
- error: '#B3261E',
39
- success: '#2E7D32',
40
- warning: '#E65100',
41
- info: '#0277BD',
42
- disabled: '#9E9E9E',
43
- shadow: '#000000',
44
- };
45
- }
46
-
47
- export function generateDarkPalette(brand: BrandColors): ThemeColors {
48
- // Brighten brand colors slightly so they read clearly on dark backgrounds
49
- const darkPrimary = adjustLightness(brand.primary, 10);
50
- const darkSecondary = adjustLightness(brand.secondary, 10);
51
- const darkTertiary = adjustLightness(brand.tertiary, 10);
52
-
53
- const primaryContainer = toContainerColor(brand.primary, true);
54
- const secondaryContainer = toContainerColor(brand.secondary, true);
55
- const tertiaryContainer = toContainerColor(brand.tertiary, true);
56
-
57
- return {
58
- primary: darkPrimary,
59
- primaryContainer,
60
- onPrimary: getContrastColor(darkPrimary),
61
-
62
- secondary: darkSecondary,
63
- secondaryContainer,
64
- onSecondary: getContrastColor(darkSecondary),
65
-
66
- tertiary: darkTertiary,
67
- tertiaryContainer,
68
- onTertiary: getContrastColor(darkTertiary),
69
-
70
- background: '#121212',
71
- surface: '#1E1E1E',
72
- surfaceVariant: '#2D2D2D',
73
-
74
- onBackground: '#E6E6E6',
75
- onSurface: '#E6E6E6',
76
-
77
- border: '#3A3A3A',
78
-
79
- // Semantic — dark surfaces that keep light text readable
80
- error: '#8C1D18',
81
- success: '#1B5E20',
82
- warning: '#8A4B00',
83
- info: '#01579B',
84
- disabled: '#4A4A4A',
85
- shadow: '#000000',
86
- };
87
- }
@@ -1,55 +0,0 @@
1
- import tinycolor from 'tinycolor2';
2
-
3
- /** Returns the relative luminance of a color (0–1). */
4
- export function getLuminance(color: string): number {
5
- return tinycolor(color).getLuminance();
6
- }
7
-
8
- /**
9
- * Returns '#FFFFFF' or '#000000' — whichever has higher contrast
10
- * against the given background color (WCAG AA).
11
- */
12
- export function getContrastColor(background: string): '#FFFFFF' | '#000000' {
13
- return tinycolor.isReadable('#FFFFFF', background, { level: 'AA', size: 'small' })
14
- ? '#FFFFFF'
15
- : '#000000';
16
- }
17
-
18
- /**
19
- * Adjusts lightness.
20
- * Positive delta = lighten, negative delta = darken.
21
- */
22
- export function adjustLightness(color: string, delta: number): string {
23
- const tc = tinycolor(color);
24
- return delta >= 0
25
- ? tc.lighten(delta).toHexString()
26
- : tc.darken(Math.abs(delta)).toHexString();
27
- }
28
-
29
- /**
30
- * Adjusts saturation.
31
- * Positive delta = saturate, negative delta = desaturate.
32
- */
33
- export function adjustSaturation(color: string, delta: number): string {
34
- const tc = tinycolor(color);
35
- return delta >= 0
36
- ? tc.saturate(delta).toHexString()
37
- : tc.desaturate(Math.abs(delta)).toHexString();
38
- }
39
-
40
- /** Returns whether two colors meet WCAG AA contrast requirements. */
41
- export function isReadable(foreground: string, background: string): boolean {
42
- return tinycolor.isReadable(foreground, background, { level: 'AA', size: 'small' });
43
- }
44
-
45
- /**
46
- * Produces a container shade from a brand color.
47
- * For dark themes: darkens + desaturates.
48
- * For light themes: lightens + desaturates.
49
- */
50
- export function toContainerColor(color: string, isDark: boolean): string {
51
- const tc = tinycolor(color);
52
- return isDark
53
- ? tc.darken(25).desaturate(10).toHexString()
54
- : tc.lighten(35).desaturate(15).toHexString();
55
- }