react-native-terra-ui 0.7.2 → 0.8.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 (252) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/lib/module/components/avatar/Avatar.js +20 -20
  4. package/lib/module/components/avatar/Avatar.js.map +1 -1
  5. package/lib/module/components/button/Button.js +11 -11
  6. package/lib/module/components/button/Button.js.map +1 -1
  7. package/lib/module/components/chip/Chip.js +32 -32
  8. package/lib/module/components/chip/Chip.js.map +1 -1
  9. package/lib/module/components/empty-state/EmptyState.js +3 -3
  10. package/lib/module/components/empty-state/EmptyState.js.map +1 -1
  11. package/lib/module/components/header/variants/LargeTitleHeader/index.js +30 -32
  12. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  13. package/lib/module/components/header/variants/TitleHeader/index.js +26 -18
  14. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  15. package/lib/module/components/icon/Icon.js +3 -3
  16. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js +2 -2
  17. package/lib/module/components/screen/Screen.js +63 -34
  18. package/lib/module/components/screen/Screen.js.map +1 -1
  19. package/lib/module/components/screen/ScreenContext.js +28 -9
  20. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  21. package/lib/module/components/screen/ScreenFlatList.js +4 -162
  22. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  23. package/lib/module/components/screen/ScreenList.js +81 -97
  24. package/lib/module/components/screen/ScreenList.js.map +1 -1
  25. package/lib/module/components/screen/ScreenScrollView.js +64 -115
  26. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  27. package/lib/module/components/screen/hooks/use-screen-ref.js +18 -0
  28. package/lib/module/components/screen/hooks/use-screen-ref.js.map +1 -0
  29. package/lib/module/components/screen/hooks/use-screen-scroll-handler.js +19 -0
  30. package/lib/module/components/screen/hooks/use-screen-scroll-handler.js.map +1 -0
  31. package/lib/module/components/screen/index.js.map +1 -1
  32. package/lib/module/components/screen/parts/ScreenFrame.js +47 -0
  33. package/lib/module/components/screen/parts/ScreenFrame.js.map +1 -0
  34. package/lib/module/components/screen/parts/ScreenHeaderAppearance.js +43 -0
  35. package/lib/module/components/screen/parts/ScreenHeaderAppearance.js.map +1 -0
  36. package/lib/module/components/screen/parts/ScreenPlaceholder.js +11 -13
  37. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -1
  38. package/lib/module/components/screen/parts/ScreenSafeArea.android.js +27 -0
  39. package/lib/module/components/screen/parts/ScreenSafeArea.android.js.map +1 -0
  40. package/lib/module/components/screen/parts/ScreenSafeArea.js +26 -0
  41. package/lib/module/components/screen/parts/ScreenSafeArea.js.map +1 -0
  42. package/lib/module/components/screen/utils.js +18 -9
  43. package/lib/module/components/screen/utils.js.map +1 -1
  44. package/lib/module/components/spinner/Spinner.js +2 -2
  45. package/lib/module/components/text/Text.js +1 -1
  46. package/lib/module/components/toast/Toast.js +13 -13
  47. package/lib/module/components/toast/Toast.js.map +1 -1
  48. package/lib/module/components/toolbar/Toolbar.js +2 -2
  49. package/lib/module/context/ThemeProvider.js +22 -39
  50. package/lib/module/context/ThemeProvider.js.map +1 -1
  51. package/lib/module/context/index.js +1 -1
  52. package/lib/module/context/index.js.map +1 -1
  53. package/lib/module/theme/breakpoints.js +1 -1
  54. package/lib/module/theme/define-config.js +21 -0
  55. package/lib/module/theme/define-config.js.map +1 -0
  56. package/lib/module/theme/diagnostics.js +13 -0
  57. package/lib/module/theme/diagnostics.js.map +1 -0
  58. package/lib/module/theme/errors.js +15 -0
  59. package/lib/module/theme/errors.js.map +1 -0
  60. package/lib/module/theme/index.js +7 -1
  61. package/lib/module/theme/index.js.map +1 -1
  62. package/lib/module/theme/legacy-tokens.js +119 -0
  63. package/lib/module/theme/legacy-tokens.js.map +1 -0
  64. package/lib/module/theme/registry.js +122 -130
  65. package/lib/module/theme/registry.js.map +1 -1
  66. package/lib/module/theme/resolve-config.js +346 -0
  67. package/lib/module/theme/resolve-config.js.map +1 -0
  68. package/lib/module/theme/runtime.js +57 -12
  69. package/lib/module/theme/runtime.js.map +1 -1
  70. package/lib/module/theme/screen-margin.js +6 -14
  71. package/lib/module/theme/screen-margin.js.map +1 -1
  72. package/lib/module/theme/screen-padding.js +17 -0
  73. package/lib/module/theme/screen-padding.js.map +1 -0
  74. package/lib/module/theme/selection-store.js +75 -0
  75. package/lib/module/theme/selection-store.js.map +1 -0
  76. package/lib/module/theme/theme.js +81 -36
  77. package/lib/module/theme/theme.js.map +1 -1
  78. package/lib/module/theme/tokens/canonical-color-keys.js +102 -0
  79. package/lib/module/theme/tokens/canonical-color-keys.js.map +1 -0
  80. package/lib/module/theme/tokens/dark.js +72 -88
  81. package/lib/module/theme/tokens/dark.js.map +1 -1
  82. package/lib/module/theme/tokens/light.js +69 -83
  83. package/lib/module/theme/tokens/light.js.map +1 -1
  84. package/lib/module/theme/tokens/primitives.js +14 -14
  85. package/lib/module/theme/tokens/primitives.js.map +1 -1
  86. package/lib/module/theme/tokens/validate.js +238 -0
  87. package/lib/module/theme/tokens/validate.js.map +1 -0
  88. package/lib/module/theme/unistyles-adapter.js +77 -0
  89. package/lib/module/theme/unistyles-adapter.js.map +1 -0
  90. package/lib/module/utils/accent-generator.js +476 -0
  91. package/lib/module/utils/accent-generator.js.map +1 -0
  92. package/lib/module/utils/accent-utils.js +52 -35
  93. package/lib/module/utils/accent-utils.js.map +1 -1
  94. package/lib/module/utils/color-utils.js +224 -7
  95. package/lib/module/utils/color-utils.js.map +1 -1
  96. package/lib/module/utils/deep-merge.js +1 -1
  97. package/lib/module/utils/deep-merge.js.map +1 -1
  98. package/lib/module/utils/resolve-theme-color.js +2 -2
  99. package/lib/typescript/src/components/chip/types.d.ts +1 -1
  100. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +1 -1
  101. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -1
  102. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +6 -5
  103. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +11 -5
  105. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  106. package/lib/typescript/src/components/icon/Icon.d.ts +2 -2
  107. package/lib/typescript/src/components/screen/Screen.d.ts +45 -16
  108. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  109. package/lib/typescript/src/components/screen/ScreenContext.d.ts +42 -26
  110. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  111. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +5 -46
  112. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  113. package/lib/typescript/src/components/screen/ScreenList.d.ts +62 -50
  114. package/lib/typescript/src/components/screen/ScreenList.d.ts.map +1 -1
  115. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +18 -58
  116. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  117. package/lib/typescript/src/components/screen/hooks/use-screen-ref.d.ts +5 -0
  118. package/lib/typescript/src/components/screen/hooks/use-screen-ref.d.ts.map +1 -0
  119. package/lib/typescript/src/components/screen/hooks/use-screen-scroll-handler.d.ts +53 -0
  120. package/lib/typescript/src/components/screen/hooks/use-screen-scroll-handler.d.ts.map +1 -0
  121. package/lib/typescript/src/components/screen/index.d.ts +2 -2
  122. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts +6 -0
  124. package/lib/typescript/src/components/screen/parts/ScreenFrame.d.ts.map +1 -0
  125. package/lib/typescript/src/components/screen/parts/ScreenHeaderAppearance.d.ts +21 -0
  126. package/lib/typescript/src/components/screen/parts/ScreenHeaderAppearance.d.ts.map +1 -0
  127. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +6 -8
  128. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -1
  129. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.android.d.ts +7 -0
  130. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.android.d.ts.map +1 -0
  131. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.d.ts +6 -0
  132. package/lib/typescript/src/components/screen/parts/ScreenSafeArea.d.ts.map +1 -0
  133. package/lib/typescript/src/components/screen/types.d.ts +27 -32
  134. package/lib/typescript/src/components/screen/types.d.ts.map +1 -1
  135. package/lib/typescript/src/components/screen/utils.d.ts +9 -8
  136. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  137. package/lib/typescript/src/components/text/Text.d.ts +1 -1
  138. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  139. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +2 -2
  140. package/lib/typescript/src/context/ThemeProvider.d.ts +29 -12
  141. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  142. package/lib/typescript/src/context/index.d.ts +1 -1
  143. package/lib/typescript/src/context/index.d.ts.map +1 -1
  144. package/lib/typescript/src/theme/breakpoints.d.ts +1 -1
  145. package/lib/typescript/src/theme/define-config.d.ts +23 -0
  146. package/lib/typescript/src/theme/define-config.d.ts.map +1 -0
  147. package/lib/typescript/src/theme/diagnostics.d.ts +4 -0
  148. package/lib/typescript/src/theme/diagnostics.d.ts.map +1 -0
  149. package/lib/typescript/src/theme/errors.d.ts +10 -0
  150. package/lib/typescript/src/theme/errors.d.ts.map +1 -0
  151. package/lib/typescript/src/theme/index.d.ts +10 -2
  152. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  153. package/lib/typescript/src/theme/legacy-tokens.d.ts +187 -0
  154. package/lib/typescript/src/theme/legacy-tokens.d.ts.map +1 -0
  155. package/lib/typescript/src/theme/registry.d.ts +12 -17
  156. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  157. package/lib/typescript/src/theme/resolve-config.d.ts +99 -0
  158. package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -0
  159. package/lib/typescript/src/theme/runtime.d.ts +25 -4
  160. package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
  161. package/lib/typescript/src/theme/screen-margin.d.ts +6 -14
  162. package/lib/typescript/src/theme/screen-margin.d.ts.map +1 -1
  163. package/lib/typescript/src/theme/screen-padding.d.ts +15 -0
  164. package/lib/typescript/src/theme/screen-padding.d.ts.map +1 -0
  165. package/lib/typescript/src/theme/selection-store.d.ts +41 -0
  166. package/lib/typescript/src/theme/selection-store.d.ts.map +1 -0
  167. package/lib/typescript/src/theme/theme.d.ts +54 -1
  168. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  169. package/lib/typescript/src/theme/tokens/canonical-color-keys.d.ts +95 -0
  170. package/lib/typescript/src/theme/tokens/canonical-color-keys.d.ts.map +1 -0
  171. package/lib/typescript/src/theme/tokens/dark.d.ts +56 -63
  172. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  173. package/lib/typescript/src/theme/tokens/light.d.ts +56 -71
  174. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  175. package/lib/typescript/src/theme/tokens/primitives.d.ts +11 -10
  176. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  177. package/lib/typescript/src/theme/tokens/validate.d.ts +34 -0
  178. package/lib/typescript/src/theme/tokens/validate.d.ts.map +1 -0
  179. package/lib/typescript/src/theme/types.d.ts +166 -86
  180. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  181. package/lib/typescript/src/theme/unistyles-adapter.d.ts +33 -0
  182. package/lib/typescript/src/theme/unistyles-adapter.d.ts.map +1 -0
  183. package/lib/typescript/src/utils/accent-generator.d.ts +103 -0
  184. package/lib/typescript/src/utils/accent-generator.d.ts.map +1 -0
  185. package/lib/typescript/src/utils/accent-utils.d.ts +15 -2
  186. package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
  187. package/lib/typescript/src/utils/color-utils.d.ts +59 -2
  188. package/lib/typescript/src/utils/color-utils.d.ts.map +1 -1
  189. package/lib/typescript/src/utils/deep-merge.d.ts +1 -0
  190. package/lib/typescript/src/utils/deep-merge.d.ts.map +1 -1
  191. package/lib/typescript/src/utils/resolve-theme-color.d.ts +2 -2
  192. package/package.json +1 -1
  193. package/src/components/avatar/Avatar.tsx +20 -20
  194. package/src/components/button/Button.tsx +11 -11
  195. package/src/components/chip/Chip.tsx +32 -32
  196. package/src/components/chip/types.ts +1 -1
  197. package/src/components/empty-state/EmptyState.tsx +4 -4
  198. package/src/components/header/variants/LargeTitleHeader/index.tsx +31 -30
  199. package/src/components/header/variants/TitleHeader/index.tsx +33 -22
  200. package/src/components/icon/Icon.tsx +4 -4
  201. package/src/components/page-indicator/hooks/use-page-indicator-colors.ts +2 -2
  202. package/src/components/screen/Screen.tsx +89 -43
  203. package/src/components/screen/ScreenContext.tsx +48 -28
  204. package/src/components/screen/ScreenFlatList.tsx +5 -191
  205. package/src/components/screen/ScreenList.tsx +197 -175
  206. package/src/components/screen/ScreenScrollView.tsx +92 -167
  207. package/src/components/screen/hooks/use-screen-ref.ts +24 -0
  208. package/src/components/screen/hooks/use-screen-scroll-handler.ts +31 -0
  209. package/src/components/screen/index.ts +8 -2
  210. package/src/components/screen/parts/ScreenFrame.tsx +49 -0
  211. package/src/components/screen/parts/ScreenHeaderAppearance.tsx +52 -0
  212. package/src/components/screen/parts/ScreenPlaceholder.tsx +16 -16
  213. package/src/components/screen/parts/ScreenSafeArea.android.tsx +22 -0
  214. package/src/components/screen/parts/ScreenSafeArea.tsx +20 -0
  215. package/src/components/screen/types.ts +29 -32
  216. package/src/components/screen/utils.ts +24 -11
  217. package/src/components/spinner/Spinner.tsx +2 -2
  218. package/src/components/text/Text.tsx +2 -2
  219. package/src/components/toast/Toast.tsx +13 -13
  220. package/src/components/toolbar/Toolbar.tsx +4 -4
  221. package/src/context/ThemeProvider.tsx +47 -51
  222. package/src/context/index.ts +1 -1
  223. package/src/theme/breakpoints.ts +1 -1
  224. package/src/theme/define-config.ts +27 -0
  225. package/src/theme/diagnostics.ts +11 -0
  226. package/src/theme/errors.ts +14 -0
  227. package/src/theme/index.ts +40 -1
  228. package/src/theme/legacy-tokens.ts +227 -0
  229. package/src/theme/registry.ts +119 -137
  230. package/src/theme/resolve-config.ts +406 -0
  231. package/src/theme/runtime.ts +52 -14
  232. package/src/theme/screen-margin.ts +6 -25
  233. package/src/theme/screen-padding.ts +26 -0
  234. package/src/theme/selection-store.ts +76 -0
  235. package/src/theme/theme.ts +104 -39
  236. package/src/theme/tokens/canonical-color-keys.ts +107 -0
  237. package/src/theme/tokens/dark.ts +91 -98
  238. package/src/theme/tokens/light.ts +88 -93
  239. package/src/theme/tokens/primitives.ts +14 -14
  240. package/src/theme/tokens/validate.ts +271 -0
  241. package/src/theme/types.ts +184 -95
  242. package/src/theme/unistyles-adapter.ts +75 -0
  243. package/src/utils/accent-generator.ts +530 -0
  244. package/src/utils/accent-utils.ts +66 -39
  245. package/src/utils/color-utils.ts +225 -8
  246. package/src/utils/deep-merge.ts +1 -1
  247. package/src/utils/resolve-theme-color.ts +2 -2
  248. package/lib/module/components/screen/parts/BottomSafeArea.js +0 -62
  249. package/lib/module/components/screen/parts/BottomSafeArea.js.map +0 -1
  250. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +0 -30
  251. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +0 -1
  252. package/src/components/screen/parts/BottomSafeArea.tsx +0 -59
@@ -1,12 +1,34 @@
1
- /** Small color helpers for deriving accent shades from a single hue. */
1
+ /**
2
+ * Colour helpers shared by the accent generator, the contrast diagnostics and
3
+ * the public `shade` / `withAlpha` / `readableOn` utilities.
4
+ *
5
+ * Everything here is pure sRGB maths with no runtime dependency:
6
+ * - parsing of `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`,
7
+ * - WCAG 2.x relative luminance / contrast with sRGB alpha compositing,
8
+ * - OKLab / OKLCH conversions (Björn Ottosson's published matrices) with
9
+ * sRGB gamut mapping by chroma reduction at constant lightness and hue.
10
+ */
2
11
 
3
- interface RGB {
12
+ export interface RGB {
4
13
  r: number;
5
14
  g: number;
6
15
  b: number;
7
16
  }
8
17
 
9
- const clamp = (n: number): number => Math.max(0, Math.min(255, Math.round(n)));
18
+ /** A parsed sRGB colour with 0-255 channels and a 0-1 alpha. */
19
+ export interface RGBA extends RGB {
20
+ a: number;
21
+ }
22
+
23
+ /** OKLCH coordinates: L in [0, 1], C ≥ 0, h in degrees [0, 360). */
24
+ export interface OKLCH {
25
+ l: number;
26
+ c: number;
27
+ h: number;
28
+ }
29
+
30
+ const clamp255 = (n: number): number => Math.max(0, Math.min(255, Math.round(n)));
31
+ const clamp01 = (n: number): number => Math.max(0, Math.min(1, n));
10
32
 
11
33
  function parseHex(hex: string): RGB | null {
12
34
  const hex6 = /^#?([0-9a-f]{6})$/i.exec(hex.trim())?.[1];
@@ -16,7 +38,201 @@ function parseHex(hex: string): RGB | null {
16
38
  }
17
39
 
18
40
  const toHex = ({ r, g, b }: RGB): string =>
19
- `#${[r, g, b].map((v) => clamp(v).toString(16).padStart(2, "0")).join("")}`;
41
+ `#${[r, g, b].map((v) => clamp255(v).toString(16).padStart(2, "0")).join("")}`;
42
+
43
+ /** Formats 0-255 channels as a lowercase `#rrggbb` string (channels are rounded and clamped). */
44
+ export function rgbToHex(rgb: RGB): string {
45
+ return toHex(rgb);
46
+ }
47
+
48
+ /**
49
+ * Parses an sRGB colour string. Accepts `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`
50
+ * (case-insensitive) and `rgb(r, g, b)` / `rgba(r, g, b, a)` with integer or
51
+ * percentage channels. Returns `null` for anything else (named colours,
52
+ * `transparent`, hsl(), gradients, platform dynamic colours, ...).
53
+ */
54
+ export function parseColor(value: unknown): RGBA | null {
55
+ if (typeof value !== "string") return null;
56
+ const input = value.trim();
57
+
58
+ const hex = /^#([0-9a-f]{3,8})$/i.exec(input)?.[1];
59
+ if (hex !== undefined) {
60
+ if (hex.length === 3 || hex.length === 4) {
61
+ const [r, g, b, a] = [...hex].map((ch) => Number.parseInt(ch + ch, 16));
62
+ return { r: r ?? 0, g: g ?? 0, b: b ?? 0, a: hex.length === 4 ? (a ?? 255) / 255 : 1 };
63
+ }
64
+ if (hex.length === 6 || hex.length === 8) {
65
+ const channels = [0, 2, 4, 6]
66
+ .filter((start) => start < hex.length)
67
+ .map((start) => Number.parseInt(hex.slice(start, start + 2), 16));
68
+ const [r, g, b, a] = channels;
69
+ return { r: r ?? 0, g: g ?? 0, b: b ?? 0, a: hex.length === 8 ? (a ?? 255) / 255 : 1 };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ const fn = /^rgba?\(\s*([^)]*)\)$/i.exec(input)?.[1];
75
+ if (fn !== undefined) {
76
+ const parts = fn.split(/\s*[,/]\s*|\s+/).filter((part) => part.length > 0);
77
+ if (parts.length !== 3 && parts.length !== 4) return null;
78
+ const channel = (part: string): number | null => {
79
+ const percent = part.endsWith("%");
80
+ const n = Number(percent ? part.slice(0, -1) : part);
81
+ if (!Number.isFinite(n)) return null;
82
+ return clamp255(percent ? (n / 100) * 255 : n);
83
+ };
84
+ const alpha = (part: string | undefined): number | null => {
85
+ if (part === undefined) return 1;
86
+ const percent = part.endsWith("%");
87
+ const n = Number(percent ? part.slice(0, -1) : part);
88
+ if (!Number.isFinite(n)) return null;
89
+ return clamp01(percent ? n / 100 : n);
90
+ };
91
+ const r = channel(parts[0] as string);
92
+ const g = channel(parts[1] as string);
93
+ const b = channel(parts[2] as string);
94
+ const a = alpha(parts[3]);
95
+ if (r === null || g === null || b === null || a === null) return null;
96
+ return { r, g, b, a };
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ /** Source-over compositing of `front` at `alpha` onto an opaque `back`, in gamma-encoded sRGB. */
103
+ export function compositeOver(front: RGB, back: RGB, alpha: number): RGB {
104
+ const a = clamp01(alpha);
105
+ return {
106
+ r: front.r * a + back.r * (1 - a),
107
+ g: front.g * a + back.g * (1 - a),
108
+ b: front.b * a + back.b * (1 - a),
109
+ };
110
+ }
111
+
112
+ const linearizeChannel = (channel: number): number => {
113
+ const srgb = channel / 255;
114
+ return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
115
+ };
116
+
117
+ const delinearizeChannel = (linear: number): number => {
118
+ const v = Math.max(0, Math.min(1, linear));
119
+ return (v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055) * 255;
120
+ };
121
+
122
+ /** WCAG 2.x relative luminance of an opaque sRGB colour (0-255 channels). */
123
+ export function relativeLuminance(rgb: RGB): number {
124
+ return (
125
+ 0.2126 * linearizeChannel(rgb.r) + 0.7152 * linearizeChannel(rgb.g) + 0.0722 * linearizeChannel(rgb.b)
126
+ );
127
+ }
128
+
129
+ /** WCAG 2.x contrast ratio between two opaque sRGB colours (≥ 1). */
130
+ export function contrastRatio(a: RGB, b: RGB): number {
131
+ const la = relativeLuminance(a);
132
+ const lb = relativeLuminance(b);
133
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
134
+ }
135
+
136
+ /**
137
+ * Contrast of a foreground on a background, both composited over an opaque
138
+ * `surface`, with `opacity` applied to the whole control afterwards (the way a
139
+ * pressed Button or Toast action fades as one unit). Matches the reference
140
+ * calculation in `theme/__tests__/color-contrast.test.ts`.
141
+ */
142
+ export function compositedContrast(fg: RGBA, bg: RGBA, surface: RGB, opacity = 1): number {
143
+ const background = compositeOver(bg, surface, bg.a);
144
+ const foreground = compositeOver(fg, background, fg.a);
145
+ return contrastRatio(
146
+ compositeOver(foreground, surface, opacity),
147
+ compositeOver(background, surface, opacity)
148
+ );
149
+ }
150
+
151
+ // ─── OKLab / OKLCH ────────────────────────────────────────────────────────────
152
+ // Matrices from https://bottosson.github.io/posts/oklab/ (sRGB D65).
153
+
154
+ const cbrt = (x: number): number => (x < 0 ? -((-x) ** (1 / 3)) : x ** (1 / 3));
155
+
156
+ function linearSrgbToOklab(r: number, g: number, b: number): [number, number, number] {
157
+ const l = cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
158
+ const m = cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
159
+ const s = cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
160
+ return [
161
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
162
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
163
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
164
+ ];
165
+ }
166
+
167
+ function oklabToLinearSrgb(L: number, a: number, b: number): [number, number, number] {
168
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
169
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
170
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
171
+ return [
172
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
173
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
174
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
175
+ ];
176
+ }
177
+
178
+ /** Converts 0-255 sRGB channels to OKLCH. Achromatic input yields `c ≈ 0` and `h = 0`. */
179
+ export function rgbToOklch(rgb: RGB): OKLCH {
180
+ const [L, a, b] = linearSrgbToOklab(
181
+ linearizeChannel(rgb.r),
182
+ linearizeChannel(rgb.g),
183
+ linearizeChannel(rgb.b)
184
+ );
185
+ const c = Math.hypot(a, b);
186
+ if (c < 1e-4) return { l: clamp01(L), c: 0, h: 0 };
187
+ const h = (Math.atan2(b, a) * 180) / Math.PI;
188
+ return { l: clamp01(L), c, h: h < 0 ? h + 360 : h };
189
+ }
190
+
191
+ function oklchToLinear({ l, c, h }: OKLCH): [number, number, number] {
192
+ const rad = (h * Math.PI) / 180;
193
+ return oklabToLinearSrgb(l, c * Math.cos(rad), c * Math.sin(rad));
194
+ }
195
+
196
+ const GAMUT_EPSILON = 1e-4;
197
+
198
+ const inGamut = (linear: readonly number[]): boolean =>
199
+ linear.every((v) => v >= -GAMUT_EPSILON && v <= 1 + GAMUT_EPSILON);
200
+
201
+ /**
202
+ * Converts OKLCH to 0-255 sRGB channels. Colours outside the sRGB gamut are
203
+ * mapped by reducing chroma at constant lightness and hue (binary search)
204
+ * until they fit, so the requested lightness — what the contrast targets
205
+ * depend on — is preserved as closely as the gamut allows.
206
+ */
207
+ export function oklchToRgb(color: OKLCH): RGB {
208
+ const l = clamp01(color.l);
209
+ const h = ((color.h % 360) + 360) % 360;
210
+ let c = Math.max(0, color.c);
211
+ let linear = oklchToLinear({ l, c, h });
212
+ if (!inGamut(linear)) {
213
+ let lo = 0;
214
+ let hi = c;
215
+ for (let i = 0; i < 24; i += 1) {
216
+ const mid = (lo + hi) / 2;
217
+ if (inGamut(oklchToLinear({ l, c: mid, h }))) lo = mid;
218
+ else hi = mid;
219
+ }
220
+ c = lo;
221
+ linear = oklchToLinear({ l, c, h });
222
+ }
223
+ return {
224
+ r: delinearizeChannel(linear[0]),
225
+ g: delinearizeChannel(linear[1]),
226
+ b: delinearizeChannel(linear[2]),
227
+ };
228
+ }
229
+
230
+ /** `oklchToRgb` followed by `rgbToHex`. */
231
+ export function oklchToHex(color: OKLCH): string {
232
+ return toHex(oklchToRgb(color));
233
+ }
234
+
235
+ // ─── Public shade / alpha / readable helpers (unchanged behaviour) ────────────
20
236
 
21
237
  /**
22
238
  * Shifts a hex color toward black (negative `amount`) or white (positive),
@@ -41,11 +257,12 @@ export function withAlpha(hex: string, alpha: number): string {
41
257
  return `rgba(${c.r}, ${c.g}, ${c.b}, ${alpha})`;
42
258
  }
43
259
 
44
- /** Picks black or white for legible text/icons on top of `hex`. */
260
+ /** Picks the higher-contrast black/white foreground for an opaque 6-digit hex. */
45
261
  export function readableOn(hex: string): string {
46
262
  const c = parseHex(hex);
47
263
  if (!c) return "#ffffff";
48
- // Perceived luminance (0–1).
49
- const luminance = (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) / 255;
50
- return luminance > 0.6 ? "#000000" : "#ffffff";
264
+ const luminance = relativeLuminance(c);
265
+ const blackContrast = (luminance + 0.05) / 0.05;
266
+ const whiteContrast = 1.05 / (luminance + 0.05);
267
+ return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
51
268
  }
@@ -5,7 +5,7 @@ export type DeepPartial<T> = T extends (infer U)[]
5
5
  ? { [K in keyof T]?: DeepPartial<T[K]> }
6
6
  : T;
7
7
 
8
- const isPlainObject = (value: unknown): value is Record<string, unknown> =>
8
+ export const isPlainObject = (value: unknown): value is Record<string, unknown> =>
9
9
  typeof value === "object" &&
10
10
  value !== null &&
11
11
  !Array.isArray(value) &&
@@ -12,8 +12,8 @@ const isColorLiteral = (value: string): boolean =>
12
12
  * Raw literals pass through unchanged.
13
13
  *
14
14
  * @example
15
- * resolveThemeColor('text.default', theme) // → '#020618'
16
- * resolveThemeColor('action.bg.primary', theme) // → '#009966'
15
+ * resolveThemeColor('text.primary', theme) // → '#020618'
16
+ * resolveThemeColor('accent.solid.bg', theme) // → '#009966'
17
17
  * resolveThemeColor('#ff0000', theme) // → '#ff0000'
18
18
  */
19
19
  export function resolveThemeColor(token: ColorToken | undefined, theme: TerraTheme): string | undefined {
@@ -1,62 +0,0 @@
1
- "use strict";
2
-
3
- import { jsx as _jsx } from "react/jsx-runtime";
4
- /**
5
- * `SafeAreaView` is not re-exported from the `react-native-screens` root, so it
6
- * has to be reached by path — and it has to be the TypeScript source entry, the
7
- * one the package points React Native at. The built `lib/module` copy of the
8
- * native component never went through React Native's codegen, so loading it
9
- * fails at bundle time with "Could not find component config for native
10
- * component".
11
- *
12
- * `require` rather than `import` so the dependency's own `.tsx` is not pulled
13
- * into this package's build, where it trips compiler options it was never
14
- * written against.
15
- */
16
- // eslint-disable-next-line @typescript-eslint/no-require-imports
17
- const {
18
- SafeAreaView
19
- } = require("react-native-screens/src/components/safe-area");
20
- const BOTTOM_EDGE = {
21
- bottom: true
22
- };
23
- const SPACER = {
24
- flex: 0
25
- };
26
-
27
- /**
28
- * Bottom clearance that measures itself.
29
- *
30
- * The insets Unistyles reports are the window's, which cover the system bars and
31
- * nothing else — 24dp for the gesture bar on a Pixel — while a native tab bar
32
- * sits on top of them for another 80dp and overlaps the content beneath it.
33
- * `react-native-screens` tracks that second one separately, as an *interface*
34
- * inset, and reports it to its own `SafeAreaView`; asking a view is the only way
35
- * to learn it. Neither `runtime.insets.bottom` nor `useSafeAreaInsets()` knows
36
- * about it — both read 24 on a tab screen — and a constant would be wrong the
37
- * moment the bar is absent, which is every pushed screen.
38
- *
39
- * `insetType="all"` covers system and interface both, so this view owns the
40
- * whole bottom edge — which is why the scroll containers pad only their margin;
41
- * adding the window inset there too would double-count the gesture bar.
42
- * `"interface"` alone would be wrong: it measures the same on a tab screen (the
43
- * bar's inset already spans the gesture bar beneath it) but reports nothing on a
44
- * pushed screen, which would then lose its gesture-bar clearance.
45
- *
46
- * `insetType` is Android-only; on iOS the bottom safe area of a view inside a
47
- * tab controller already includes the bar. Measured on a tab screen: 132dp on a
48
- * Pixel, 95pt on an iPhone 17 — against the 24dp / 34pt the window insets
49
- * report, which is the gap this closes.
50
- *
51
- * `flex: 0` cancels the `flex: 1` that `SafeAreaView` sets on itself, which
52
- * would otherwise stretch it into the leftover space of a list shorter than the
53
- * viewport. With no children, its height is exactly the padding it applies.
54
- */
55
- export function BottomSafeArea() {
56
- return /*#__PURE__*/_jsx(SafeAreaView, {
57
- edges: BOTTOM_EDGE,
58
- insetType: "all",
59
- style: SPACER
60
- });
61
- }
62
- //# sourceMappingURL=BottomSafeArea.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["SafeAreaView","require","BOTTOM_EDGE","bottom","SPACER","flex","BottomSafeArea","_jsx","edges","insetType","style"],"sourceRoot":"../../../../../src","sources":["components/screen/parts/BottomSafeArea.tsx"],"mappings":";;;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;EAAEA;AAAa,CAAC,GAAGC,OAAO,CAAC,+CAA+C,CAE/E;AAED,MAAMC,WAAW,GAAG;EAAEC,MAAM,EAAE;AAAK,CAAU;AAC7C,MAAMC,MAAM,GAAG;EAAEC,IAAI,EAAE;AAAE,CAAU;;AAEnC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAAA,EAAG;EAC/B,oBAAOC,IAAA,CAACP,YAAY;IAACQ,KAAK,EAAEN,WAAY;IAACO,SAAS,EAAC,KAAK;IAACC,KAAK,EAAEN;EAAO,CAAE,CAAC;AAC5E","ignoreList":[]}
@@ -1,30 +0,0 @@
1
- /**
2
- * Bottom clearance that measures itself.
3
- *
4
- * The insets Unistyles reports are the window's, which cover the system bars and
5
- * nothing else — 24dp for the gesture bar on a Pixel — while a native tab bar
6
- * sits on top of them for another 80dp and overlaps the content beneath it.
7
- * `react-native-screens` tracks that second one separately, as an *interface*
8
- * inset, and reports it to its own `SafeAreaView`; asking a view is the only way
9
- * to learn it. Neither `runtime.insets.bottom` nor `useSafeAreaInsets()` knows
10
- * about it — both read 24 on a tab screen — and a constant would be wrong the
11
- * moment the bar is absent, which is every pushed screen.
12
- *
13
- * `insetType="all"` covers system and interface both, so this view owns the
14
- * whole bottom edge — which is why the scroll containers pad only their margin;
15
- * adding the window inset there too would double-count the gesture bar.
16
- * `"interface"` alone would be wrong: it measures the same on a tab screen (the
17
- * bar's inset already spans the gesture bar beneath it) but reports nothing on a
18
- * pushed screen, which would then lose its gesture-bar clearance.
19
- *
20
- * `insetType` is Android-only; on iOS the bottom safe area of a view inside a
21
- * tab controller already includes the bar. Measured on a tab screen: 132dp on a
22
- * Pixel, 95pt on an iPhone 17 — against the 24dp / 34pt the window insets
23
- * report, which is the gap this closes.
24
- *
25
- * `flex: 0` cancels the `flex: 1` that `SafeAreaView` sets on itself, which
26
- * would otherwise stretch it into the leftover space of a list shorter than the
27
- * viewport. With no children, its height is exactly the padding it applies.
28
- */
29
- export declare function BottomSafeArea(): import("react").JSX.Element;
30
- //# sourceMappingURL=BottomSafeArea.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"BottomSafeArea.d.ts","sourceRoot":"","sources":["../../../../../../src/components/screen/parts/BottomSafeArea.tsx"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,cAAc,gCAE7B"}
@@ -1,59 +0,0 @@
1
- import type { ComponentType } from "react";
2
-
3
- type SafeAreaViewProps = {
4
- edges: { readonly bottom: true };
5
- insetType: "all";
6
- style: { readonly flex: 0 };
7
- };
8
-
9
- /**
10
- * `SafeAreaView` is not re-exported from the `react-native-screens` root, so it
11
- * has to be reached by path — and it has to be the TypeScript source entry, the
12
- * one the package points React Native at. The built `lib/module` copy of the
13
- * native component never went through React Native's codegen, so loading it
14
- * fails at bundle time with "Could not find component config for native
15
- * component".
16
- *
17
- * `require` rather than `import` so the dependency's own `.tsx` is not pulled
18
- * into this package's build, where it trips compiler options it was never
19
- * written against.
20
- */
21
- // eslint-disable-next-line @typescript-eslint/no-require-imports
22
- const { SafeAreaView } = require("react-native-screens/src/components/safe-area") as {
23
- SafeAreaView: ComponentType<SafeAreaViewProps>;
24
- };
25
-
26
- const BOTTOM_EDGE = { bottom: true } as const;
27
- const SPACER = { flex: 0 } as const;
28
-
29
- /**
30
- * Bottom clearance that measures itself.
31
- *
32
- * The insets Unistyles reports are the window's, which cover the system bars and
33
- * nothing else — 24dp for the gesture bar on a Pixel — while a native tab bar
34
- * sits on top of them for another 80dp and overlaps the content beneath it.
35
- * `react-native-screens` tracks that second one separately, as an *interface*
36
- * inset, and reports it to its own `SafeAreaView`; asking a view is the only way
37
- * to learn it. Neither `runtime.insets.bottom` nor `useSafeAreaInsets()` knows
38
- * about it — both read 24 on a tab screen — and a constant would be wrong the
39
- * moment the bar is absent, which is every pushed screen.
40
- *
41
- * `insetType="all"` covers system and interface both, so this view owns the
42
- * whole bottom edge — which is why the scroll containers pad only their margin;
43
- * adding the window inset there too would double-count the gesture bar.
44
- * `"interface"` alone would be wrong: it measures the same on a tab screen (the
45
- * bar's inset already spans the gesture bar beneath it) but reports nothing on a
46
- * pushed screen, which would then lose its gesture-bar clearance.
47
- *
48
- * `insetType` is Android-only; on iOS the bottom safe area of a view inside a
49
- * tab controller already includes the bar. Measured on a tab screen: 132dp on a
50
- * Pixel, 95pt on an iPhone 17 — against the 24dp / 34pt the window insets
51
- * report, which is the gap this closes.
52
- *
53
- * `flex: 0` cancels the `flex: 1` that `SafeAreaView` sets on itself, which
54
- * would otherwise stretch it into the leftover space of a list shorter than the
55
- * viewport. With no children, its height is exactly the padding it applies.
56
- */
57
- export function BottomSafeArea() {
58
- return <SafeAreaView edges={BOTTOM_EDGE} insetType="all" style={SPACER} />;
59
- }