@mrmeg/expo-ui 0.27.1 → 0.29.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 (155) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LLM_USAGE.md +241 -37
  3. package/README.md +591 -136
  4. package/bin/expo-ui-icons.mjs +509 -0
  5. package/dist/components/Accordion.js +12 -6
  6. package/dist/components/AnimatedView.d.ts +1 -1
  7. package/dist/components/AnimatedView.js +5 -2
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +2 -2
  10. package/dist/components/Badge.js +5 -1
  11. package/dist/components/BottomSheet.d.ts +29 -1
  12. package/dist/components/BottomSheet.js +107 -30
  13. package/dist/components/Button.d.ts +20 -4
  14. package/dist/components/Button.js +60 -43
  15. package/dist/components/Card.d.ts +1 -1
  16. package/dist/components/Card.js +24 -4
  17. package/dist/components/Carousel.js +2 -2
  18. package/dist/components/Checkbox.js +51 -44
  19. package/dist/components/Collapsible.js +7 -3
  20. package/dist/components/Dialog.js +87 -16
  21. package/dist/components/Drawer.d.ts +1 -1
  22. package/dist/components/Drawer.js +65 -55
  23. package/dist/components/DropdownMenu.js +11 -7
  24. package/dist/components/EmptyState.d.ts +2 -2
  25. package/dist/components/EmptyState.js +5 -2
  26. package/dist/components/Hydrated.d.ts +31 -0
  27. package/dist/components/Hydrated.js +30 -0
  28. package/dist/components/Icon.d.ts +71 -28
  29. package/dist/components/Icon.js +70 -28
  30. package/dist/components/IconGlyph.d.ts +15 -0
  31. package/dist/components/IconGlyph.js +32 -0
  32. package/dist/components/IconGlyph.native.d.ts +10 -0
  33. package/dist/components/IconGlyph.native.js +30 -0
  34. package/dist/components/InputOTP.js +5 -2
  35. package/dist/components/Item.d.ts +103 -4
  36. package/dist/components/Item.js +168 -15
  37. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  38. package/dist/components/KeyboardAvoidingView.js +4 -1
  39. package/dist/components/Label.js +3 -2
  40. package/dist/components/MaxWidthContainer.js +1 -1
  41. package/dist/components/Notification.js +45 -27
  42. package/dist/components/Popover.d.ts +18 -5
  43. package/dist/components/Popover.js +72 -8
  44. package/dist/components/Progress.js +8 -6
  45. package/dist/components/RadioGroup.js +52 -49
  46. package/dist/components/Screen.d.ts +61 -0
  47. package/dist/components/Screen.js +62 -0
  48. package/dist/components/SectionHeader.d.ts +1 -1
  49. package/dist/components/SegmentedControl.d.ts +12 -4
  50. package/dist/components/SegmentedControl.js +94 -4
  51. package/dist/components/Select.js +21 -26
  52. package/dist/components/Skeleton.js +24 -10
  53. package/dist/components/Slider.d.ts +11 -3
  54. package/dist/components/Slider.js +73 -6
  55. package/dist/components/StatCard.d.ts +4 -2
  56. package/dist/components/StatCard.js +3 -1
  57. package/dist/components/StyledText.d.ts +7 -1
  58. package/dist/components/StyledText.js +5 -3
  59. package/dist/components/Switch.d.ts +1 -1
  60. package/dist/components/Switch.js +20 -30
  61. package/dist/components/Tabs.d.ts +2 -2
  62. package/dist/components/Tabs.js +13 -24
  63. package/dist/components/TextInput.d.ts +1 -1
  64. package/dist/components/TextInput.js +71 -26
  65. package/dist/components/ThemeSelector.d.ts +52 -0
  66. package/dist/components/ThemeSelector.js +90 -0
  67. package/dist/components/Toggle.d.ts +5 -49
  68. package/dist/components/Toggle.js +53 -29
  69. package/dist/components/ToggleGroup.d.ts +4 -8
  70. package/dist/components/ToggleGroup.js +52 -34
  71. package/dist/components/Tooltip.d.ts +1 -1
  72. package/dist/components/Tooltip.js +9 -3
  73. package/dist/components/UIProvider.d.ts +22 -2
  74. package/dist/components/UIProvider.js +15 -3
  75. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  76. package/dist/components/bottomSheetDismiss.js +44 -0
  77. package/dist/components/iconBuiltins.generated.d.ts +143 -0
  78. package/dist/components/iconBuiltins.generated.js +25 -0
  79. package/dist/components/iconTypes.d.ts +37 -0
  80. package/dist/components/iconTypes.js +1 -0
  81. package/dist/components/index.d.ts +47 -44
  82. package/dist/components/index.js +3 -0
  83. package/dist/components/keyboardDismiss.js +12 -4
  84. package/dist/components/nativeTextField.android.js +13 -9
  85. package/dist/constants/colors.d.ts +40 -5
  86. package/dist/constants/colors.js +26 -8
  87. package/dist/constants/fonts.d.ts +41 -2
  88. package/dist/constants/fonts.js +46 -7
  89. package/dist/constants/index.d.ts +5 -4
  90. package/dist/constants/index.js +1 -0
  91. package/dist/constants/interaction.d.ts +18 -0
  92. package/dist/constants/interaction.js +18 -0
  93. package/dist/constants/spacing.d.ts +2 -1
  94. package/dist/constants/spacing.js +2 -1
  95. package/dist/design-system.json +429 -132
  96. package/dist/hooks/index.d.ts +12 -7
  97. package/dist/hooks/index.js +5 -0
  98. package/dist/hooks/useDimensions.d.ts +13 -8
  99. package/dist/hooks/useDimensions.js +123 -48
  100. package/dist/hooks/useFocusVisible.d.ts +30 -0
  101. package/dist/hooks/useFocusVisible.js +45 -0
  102. package/dist/hooks/useFontStyle.d.ts +2 -2
  103. package/dist/hooks/useFontStyle.js +3 -2
  104. package/dist/hooks/useHydrated.d.ts +13 -0
  105. package/dist/hooks/useHydrated.js +19 -0
  106. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  107. package/dist/hooks/useNotificationOffset.js +34 -0
  108. package/dist/hooks/useResources.d.ts +21 -20
  109. package/dist/hooks/useResources.js +72 -68
  110. package/dist/hooks/useScalePress.d.ts +8 -5
  111. package/dist/hooks/useScalePress.js +16 -9
  112. package/dist/hooks/useShape.d.ts +24 -0
  113. package/dist/hooks/useShape.js +24 -0
  114. package/dist/hooks/useStaggeredEntrance.js +9 -7
  115. package/dist/hooks/useTheme.d.ts +3 -3
  116. package/dist/hooks/useTheme.js +177 -66
  117. package/dist/hooks/useWindowInsets.d.ts +13 -0
  118. package/dist/hooks/useWindowInsets.js +23 -0
  119. package/dist/icons/LICENSE.lucide +43 -0
  120. package/dist/icons/lucide.json +2144 -0
  121. package/dist/index.d.ts +5 -5
  122. package/dist/lib/composeRefs.d.ts +8 -0
  123. package/dist/lib/composeRefs.js +20 -0
  124. package/dist/lib/haptics.d.ts +12 -0
  125. package/dist/lib/haptics.js +21 -0
  126. package/dist/lib/index.d.ts +6 -5
  127. package/dist/lib/index.js +1 -0
  128. package/dist/lib/interFonts.d.ts +15 -0
  129. package/dist/lib/interFonts.js +1 -0
  130. package/dist/lib/interFonts.native.d.ts +3 -0
  131. package/dist/lib/interFonts.native.js +20 -0
  132. package/dist/lib/popoverPlacement.d.ts +36 -0
  133. package/dist/lib/popoverPlacement.js +29 -0
  134. package/dist/lib/themedStyles.d.ts +1 -1
  135. package/dist/lib/useAnimatedValue.d.ts +15 -0
  136. package/dist/lib/useAnimatedValue.js +19 -0
  137. package/dist/lib/webHitTarget.d.ts +40 -0
  138. package/dist/lib/webHitTarget.js +37 -0
  139. package/dist/state/feedbackStore.d.ts +24 -0
  140. package/dist/state/feedbackStore.js +16 -0
  141. package/dist/state/globalUIStore.d.ts +27 -0
  142. package/dist/state/globalUIStore.js +23 -0
  143. package/dist/state/index.d.ts +7 -5
  144. package/dist/state/index.js +2 -0
  145. package/dist/state/initialScheme.d.ts +27 -0
  146. package/dist/state/initialScheme.js +29 -0
  147. package/dist/state/notify.d.ts +1 -1
  148. package/dist/state/themeColorScope.d.ts +1 -1
  149. package/dist/state/themeStore.d.ts +69 -16
  150. package/dist/state/themeStore.js +65 -38
  151. package/llms-full.md +59 -10
  152. package/llms.txt +10 -1
  153. package/package.json +79 -4
  154. package/dist/components/iconRegistry.generated.d.ts +0 -148
  155. package/dist/components/iconRegistry.generated.js +0 -296
@@ -16,6 +16,7 @@ export * from "./Drawer.js";
16
16
  export * from "./DropdownMenu.js";
17
17
  export * from "./EmptyState.js";
18
18
  export * from "./ErrorBoundary.js";
19
+ export * from "./Hydrated.js";
19
20
  export * from "./Icon.js";
20
21
  export * from "./InputOTP.js";
21
22
  export * from "./Item.js";
@@ -23,6 +24,7 @@ export * from "./KeyboardAvoidingView.js";
23
24
  export * from "./Label.js";
24
25
  export * from "./MaxWidthContainer.js";
25
26
  export * from "./Notification.js";
27
+ export * from "./Screen.js";
26
28
  export * from "./Popover.js";
27
29
  export * from "./Progress.js";
28
30
  export * from "./RadioGroup.js";
@@ -38,6 +40,7 @@ export * from "./StyledText.js";
38
40
  export * from "./Switch.js";
39
41
  export * from "./Tabs.js";
40
42
  export * from "./TextInput.js";
43
+ export * from "./ThemeSelector.js";
41
44
  export * from "./Toggle.js";
42
45
  export * from "./ToggleGroup.js";
43
46
  export * from "./Tooltip.js";
@@ -28,7 +28,7 @@
28
28
  * slop; release coordinates also enforce it when move delivery was missed.
29
29
  * Cancellation and multitouch drop the tap without rearming another finger.
30
30
  */
31
- import { useMemo, useRef } from "react";
31
+ import { useLayoutEffect, useMemo, useRef } from "react";
32
32
  import { Platform } from "react-native";
33
33
  import { KeyboardController, useKeyboardState } from "./keyboardController";
34
34
  import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
@@ -164,8 +164,12 @@ function createTapTracker() {
164
164
  * for that.
165
165
  */
166
166
  export function useTextInputSurfaceResponder(focus) {
167
+ // Latest `focus` for the memoized handlers, synced after each commit (before
168
+ // any touch can be dispatched) instead of written during render.
167
169
  const focusRef = useRef(focus);
168
- focusRef.current = focus;
170
+ useLayoutEffect(() => {
171
+ focusRef.current = focus;
172
+ });
169
173
  return useMemo(() => {
170
174
  const tap = createTapTracker();
171
175
  return {
@@ -219,7 +223,9 @@ export function resetAncestorClaimWarningForTests() {
219
223
  */
220
224
  export function useAncestorClaimWarning(inner, message) {
221
225
  const messageRef = useRef(message);
222
- messageRef.current = message;
226
+ useLayoutEffect(() => {
227
+ messageRef.current = message;
228
+ });
223
229
  return useMemo(() => {
224
230
  if (!__DEV__ || Platform.OS !== "android")
225
231
  return inner;
@@ -249,7 +255,9 @@ export function useAncestorClaimWarning(inner, message) {
249
255
  export function useKeyboardDismissResponder() {
250
256
  const isVisible = useKeyboardState((state) => state.isVisible);
251
257
  const isVisibleRef = useRef(isVisible);
252
- isVisibleRef.current = isVisible;
258
+ useLayoutEffect(() => {
259
+ isVisibleRef.current = isVisible;
260
+ });
253
261
  return useMemo(() => {
254
262
  if (Platform.OS === "web")
255
263
  return {};
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
34
  * Only `@expo/ui/jetpack-compose` and `@expo/ui/jetpack-compose/modifiers` are
35
35
  * imported, both in the peer's export map since SDK 56.
36
36
  */
37
- import { useImperativeHandle, useRef } from "react";
37
+ import { useImperativeHandle, useRef, useState } from "react";
38
38
  import { BasicTextField, Box, Text, useNativeState, } from "@expo/ui/jetpack-compose";
39
39
  import { fillMaxWidth, onSizeChanged, padding, paddingAll, semantics, testID as testIDModifier, } from "@expo/ui/jetpack-compose/modifiers";
40
40
  // region Prop mapping (ported verbatim from @expo/ui's TextInput/utils.ts)
@@ -145,8 +145,10 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
145
145
  const numberOfLines = numberOfLinesProp ?? rows;
146
146
  const keyboardType = keyboardTypeProp ?? inputModeToKeyboardType(inputMode);
147
147
  const returnKeyType = returnKeyTypeProp ?? enterKeyHintToReturnKeyType(enterKeyHint);
148
- const initialFallbackRef = useRef(defaultValue ?? "");
149
- const fallback = useNativeState(initialFallbackRef.current);
148
+ // Pinned to the first render's `defaultValue`, as an uncontrolled default
149
+ // is, whatever `useNativeState` does with a later argument.
150
+ const [initialFallback] = useState(defaultValue ?? "");
151
+ const fallback = useNativeState(initialFallback);
150
152
  const state = (value ?? fallback);
151
153
  const innerRef = useRef(null);
152
154
  const isFocusedRef = useRef(false);
@@ -176,12 +178,14 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
176
178
  // Sticky for the life of the mounted field: revealing a password
177
179
  // (`secureTextEntry` -> false on the same element) must keep the password
178
180
  // keyboard options unchanged so the IME neither restarts nor starts
179
- // suggesting/learning the now-visible text. Written during render like the
180
- // wrapper's own `activeSecureRef`; a fresh mount starts from the prop.
181
- const everSecureRef = useRef(!!secureTextEntry);
182
- if (secureTextEntry)
183
- everSecureRef.current = true;
184
- const passwordField = everSecureRef.current;
181
+ // suggesting/learning the now-visible text. The latch is state adjusted
182
+ // during render (React re-runs this render before committing it), and the
183
+ // `||` makes the very render that turns secure on already read as a password
184
+ // field; a fresh mount starts from the prop.
185
+ const [everSecure, setEverSecure] = useState(!!secureTextEntry);
186
+ if (secureTextEntry && !everSecure)
187
+ setEverSecure(true);
188
+ const passwordField = everSecure || !!secureTextEntry;
185
189
  const mappedKeyboardType = keyboardType ? mapKeyboardType(keyboardType) : undefined;
186
190
  const resolvedKeyboardType = passwordField
187
191
  ? passwordKeyboardType(mappedKeyboardType)
@@ -1,4 +1,4 @@
1
- import { navigationFonts } from "./fonts";
1
+ import { navigationFonts } from "./fonts.js";
2
2
  /**
3
3
  * Raw color palette - use semantic colors in components instead
4
4
  *
@@ -38,10 +38,36 @@ declare const palette: {
38
38
  readonly red500: "#EF4444";
39
39
  readonly red400: "#F87171";
40
40
  };
41
+ /**
42
+ * Extra color tokens an app declares for itself, so it stops keeping a side
43
+ * palette. Empty here; an app augments it once and the keys become part of
44
+ * `ThemeColors` everywhere — `setColors`, `ThemeColorScope`,
45
+ * `useTheme().theme.colors`, `getThemeCssVariables`:
46
+ *
47
+ * ```ts
48
+ * declare module "@mrmeg/expo-ui/constants" {
49
+ * interface ThemeColorExtensions {
50
+ * brandGold: string;
51
+ * }
52
+ * }
53
+ * useThemeStore.getState().setColors({
54
+ * light: { brandGold: "#c9a227" },
55
+ * dark: { brandGold: "#ffe066" },
56
+ * });
57
+ * ```
58
+ *
59
+ * The package ships no value for an extension key: provide it in both schemes
60
+ * or the token is `undefined` at runtime in the scheme that lacks it. On web
61
+ * the built-in tokens are `var(--c-*)` references and extension values are
62
+ * literals per scheme, so with any override `theme.colors` is a new object
63
+ * when the scheme changes (the identity caveat every override already has).
64
+ */
65
+ export interface ThemeColorExtensions {
66
+ }
41
67
  /**
42
68
  * Semantic color interface - what colors mean in the UI
43
69
  */
44
- export interface ThemeColors {
70
+ export interface ThemeColors extends ThemeColorExtensions {
45
71
  surfaceSunken: string;
46
72
  background: string;
47
73
  foreground: string;
@@ -86,14 +112,21 @@ export interface Colors {
86
112
  light: Theme;
87
113
  dark: Theme;
88
114
  }
115
+ /**
116
+ * The package's own tokens: `ThemeColors` minus whatever an app added through
117
+ * `ThemeColorExtensions`. The default palettes, the CSS-variable table and
118
+ * `resolveRawColor` are typed on this, because the package has no value for an
119
+ * extension key.
120
+ */
121
+ export type ThemeColorTokens = Omit<ThemeColors, keyof ThemeColorExtensions>;
89
122
  /**
90
123
  * Raw hex/rgba theme colors, always literal strings on every platform. Use
91
124
  * these for sinks that cannot take CSS `var()` — e.g. `<meta name="theme-color">`,
92
125
  * color parsing/math — and for generating the CSS variable definitions below.
93
126
  */
94
127
  export declare const rawThemeColors: {
95
- light: ThemeColors;
96
- dark: ThemeColors;
128
+ light: ThemeColorTokens;
129
+ dark: ThemeColorTokens;
97
130
  };
98
131
  /**
99
132
  * Resolves a theme color value to a literal color string. On web, theme
@@ -114,7 +147,9 @@ export declare function resolveRawColor(color: string, scheme: "light" | "dark")
114
147
  * paint before any theme script stamps `data-theme`.
115
148
  *
116
149
  * Apps that override brand colors can pass those overrides so the emitted
117
- * CSS matches their theme.
150
+ * CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
151
+ * in the overrides get a variable too, per scheme that names them; the
152
+ * package has no default to fall back to for those.
118
153
  */
119
154
  export declare function getThemeCssVariables(overrides?: {
120
155
  light?: Partial<ThemeColors>;
@@ -139,6 +139,7 @@ const themeColorTokens = Object.keys(lightTheme.colors);
139
139
  function cssVarName(token) {
140
140
  return `--c-${token.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
141
141
  }
142
+ const themeColorTokenSet = new Set(themeColorTokens);
142
143
  // Reverse lookup: "var(--c-surface-sunken)" → "surfaceSunken"
143
144
  const varToToken = new Map(themeColorTokens.map((token) => [`var(${cssVarName(token)})`, token]));
144
145
  /**
@@ -176,17 +177,27 @@ function toRgbTriplet(value) {
176
177
  * paint before any theme script stamps `data-theme`.
177
178
  *
178
179
  * Apps that override brand colors can pass those overrides so the emitted
179
- * CSS matches their theme.
180
+ * CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
181
+ * in the overrides get a variable too, per scheme that names them; the
182
+ * package has no default to fall back to for those.
180
183
  */
181
184
  export function getThemeCssVariables(overrides) {
182
- const declarations = (scheme) => themeColorTokens
183
- .map((token) => {
184
- const value = overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token];
185
+ const extensionTokens = [
186
+ ...Object.keys(overrides?.light ?? {}),
187
+ ...Object.keys(overrides?.dark ?? {}),
188
+ ].filter((token, index, all) => !themeColorTokenSet.has(token) && all.indexOf(token) === index);
189
+ const declaration = (token, value) => {
185
190
  const name = cssVarName(token);
186
191
  const rgb = toRgbTriplet(value);
187
192
  return `${name}: ${value};${rgb ? ` ${name}-rgb: ${rgb};` : ""}`;
188
- })
189
- .join("\n ");
193
+ };
194
+ const declarations = (scheme) => [
195
+ ...themeColorTokens.map((token) => declaration(token, overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token])),
196
+ ...extensionTokens.flatMap((token) => {
197
+ const value = overrides?.[scheme]?.[token];
198
+ return typeof value === "string" ? [declaration(token, value)] : [];
199
+ }),
200
+ ].join("\n ");
190
201
  return `
191
202
  :root {
192
203
  ${declarations("light")}
@@ -209,10 +220,17 @@ export function getThemeCssVariables(overrides) {
209
220
  // values (no CSS engine). The `navigation` maps stay literal on both
210
221
  // platforms: @react-navigation internals color-parse their theme values.
211
222
  const webVarColors = Object.fromEntries(themeColorTokens.map((token) => [token, `var(${cssVarName(token)})`]));
223
+ // Web: both schemes hold the same `var(--c-*)` references, so they share ONE
224
+ // `colors` object. A scheme switch then keeps `theme.colors` identical, so
225
+ // anything memoized on it keeps its result; the CSS variables do the
226
+ // re-theming. `colors.light` and `colors.dark` stay distinct objects:
227
+ // `dark`, `navigation`, and `fonts` differ per scheme.
228
+ // The base themes carry the package tokens only; extension keys (typed on
229
+ // `ThemeColors` once an app augments it) arrive through `setColors`.
212
230
  export const colors = Platform.OS === "web"
213
231
  ? {
214
- light: { ...lightTheme, colors: { ...webVarColors } },
215
- dark: { ...darkTheme, colors: { ...webVarColors } },
232
+ light: { ...lightTheme, colors: webVarColors },
233
+ dark: { ...darkTheme, colors: webVarColors },
216
234
  }
217
235
  : {
218
236
  light: lightTheme,
@@ -34,8 +34,39 @@ export type FontFamilyMap = Record<FontVariant, Record<FontFamilyWeight, string>
34
34
  * A per-variant override group. Partial per weight: missing weights fall back
35
35
  * to the group's own `regular` (not the package default), so an app that
36
36
  * registers only Regular + Medium never leaks a package face into its brand.
37
+ *
38
+ * `italic` maps weights to real italic faces. An italic weight that is missing
39
+ * falls back to the group's italic `regular`; a group with no `italic` map
40
+ * renders italic as `fontStyle: "italic"` on the upright face (the OS or
41
+ * browser synthesizes the slant).
42
+ */
43
+ export type FontFamilyOverride = Partial<Record<FontFamilyWeight, string>> & {
44
+ italic?: Partial<Record<FontFamilyWeight, string>>;
45
+ };
46
+ /**
47
+ * The package's serif. `"georgia"` (default) is the single system face on
48
+ * every platform; `"newsreader"` is a real serif family — four weights and a
49
+ * 400 italic — that `useResources({ serif: "newsreader" })` loads and turns on.
50
+ * Nothing about Newsreader ships to an app that keeps the default.
37
51
  */
38
- export type FontFamilyOverride = Partial<Record<FontFamilyWeight, string>>;
52
+ export type SerifPreset = "georgia" | "newsreader";
53
+ /**
54
+ * The Newsreader faces the `"newsreader"` preset resolves to. Native names
55
+ * match `@expo-google-fonts/newsreader`'s per-weight exports, which the app
56
+ * registers through `useResources({ serif: "newsreader", serifFonts })`; web
57
+ * is one stack the injected Google Fonts stylesheet serves, weight numeric.
58
+ */
59
+ export declare const newsreaderFamilies: {
60
+ readonly web: "\"Newsreader\", Georgia, 'Times New Roman', serif";
61
+ readonly native: {
62
+ light: string;
63
+ regular: string;
64
+ medium: string;
65
+ semibold: string;
66
+ bold: string;
67
+ };
68
+ readonly nativeItalic: "Newsreader_400Regular_Italic";
69
+ };
39
70
  /**
40
71
  * Font overrides a host app can inject to brand the package.
41
72
  *
@@ -65,6 +96,14 @@ export type ResolvedFontStyle = {
65
96
  fontFamily: string;
66
97
  /** Present only under the `"numeric"` weight strategy (web). */
67
98
  fontWeight?: "400" | "500" | "600" | "700";
99
+ /** Present only when italic was asked for and no italic face exists. */
100
+ fontStyle?: "italic";
101
+ };
102
+ export type FontStyleOptions = {
103
+ /** Render italic: a real italic face when one exists, else a synthesized slant. */
104
+ italic?: boolean;
105
+ /** The active serif preset (`useThemeStore().serifPreset`); defaults to Georgia. */
106
+ serifPreset?: SerifPreset;
68
107
  };
69
108
  /**
70
109
  * Resolve the `fontFamily` (+ optional numeric `fontWeight`) for a variant and
@@ -82,7 +121,7 @@ export type ResolvedFontStyle = {
82
121
  * own family per weight on web too, and must declare
83
122
  * `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
84
123
  */
85
- export declare function resolveFontStyle(overrides: FontOverrides, variant: FontVariant, weight: FontFamilyWeight): ResolvedFontStyle;
124
+ export declare function resolveFontStyle(overrides: FontOverrides, variant: FontVariant, weight: FontFamilyWeight, options?: FontStyleOptions): ResolvedFontStyle;
86
125
  /**
87
126
  * Typography scale — Tailwind-matching sizes for consistent text hierarchy.
88
127
  * Components can progressively adopt these tokens.
@@ -79,6 +79,23 @@ const sansSerifFamilies = isWebRuntime
79
79
  semibold: "sans-serif",
80
80
  bold: "sans-serif",
81
81
  };
82
+ /**
83
+ * The Newsreader faces the `"newsreader"` preset resolves to. Native names
84
+ * match `@expo-google-fonts/newsreader`'s per-weight exports, which the app
85
+ * registers through `useResources({ serif: "newsreader", serifFonts })`; web
86
+ * is one stack the injected Google Fonts stylesheet serves, weight numeric.
87
+ */
88
+ export const newsreaderFamilies = {
89
+ web: "\"Newsreader\", Georgia, 'Times New Roman', serif",
90
+ native: {
91
+ light: "Newsreader_400Regular",
92
+ regular: "Newsreader_400Regular",
93
+ medium: "Newsreader_500Medium",
94
+ semibold: "Newsreader_600SemiBold",
95
+ bold: "Newsreader_700Bold",
96
+ },
97
+ nativeItalic: "Newsreader_400Regular_Italic",
98
+ };
82
99
  /** The package's built-in families. Override per-app via `setFonts`. */
83
100
  export const fontFamilies = {
84
101
  serif: serifFamilies,
@@ -120,18 +137,40 @@ const WEB_FONT_WEIGHTS = {
120
137
  * own family per weight on web too, and must declare
121
138
  * `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
122
139
  */
123
- export function resolveFontStyle(overrides, variant, weight) {
124
- const defaults = fontFamilies[variant];
140
+ export function resolveFontStyle(overrides, variant, weight, options = {}) {
141
+ const { italic = false, serifPreset = "georgia" } = options;
142
+ const isWeb = Platform.OS === "web";
125
143
  const overrideGroup = overrides.families?.[variant];
126
- const fontFamily = overrideGroup
144
+ // Precedence for the upright face: app override → serif preset → package default.
145
+ const preset = variant === "serif" && serifPreset === "newsreader" && !overrideGroup;
146
+ const defaults = preset
147
+ ? isWeb
148
+ ? { light: newsreaderFamilies.web, regular: newsreaderFamilies.web, medium: newsreaderFamilies.web, semibold: newsreaderFamilies.web, bold: newsreaderFamilies.web }
149
+ : newsreaderFamilies.native
150
+ : fontFamilies[variant];
151
+ const upright = overrideGroup
127
152
  ? overrideGroup[weight] ?? overrideGroup.regular ?? defaults[weight]
128
153
  : defaults[weight];
129
- const strategy = Platform.OS === "web"
154
+ // A real italic face: the app's for this weight, else its italic regular,
155
+ // else the preset's native italic file. Web preset italics ride on
156
+ // `fontStyle` (the stylesheet serves the 400 italic under one family).
157
+ const italicFace = !italic
158
+ ? undefined
159
+ : overrideGroup?.italic
160
+ ? overrideGroup.italic[weight] ?? overrideGroup.italic.regular
161
+ : preset && !isWeb
162
+ ? newsreaderFamilies.nativeItalic
163
+ : undefined;
164
+ const fontFamily = italicFace ?? upright;
165
+ const synthesized = italic && italicFace === undefined;
166
+ const strategy = isWeb
130
167
  ? overrides.webWeightStrategy ?? defaultWebWeightStrategy
131
168
  : "family";
132
- return strategy === "numeric"
133
- ? { fontFamily, fontWeight: WEB_FONT_WEIGHTS[weight] }
134
- : { fontFamily };
169
+ return {
170
+ fontFamily,
171
+ ...(strategy === "numeric" && { fontWeight: WEB_FONT_WEIGHTS[weight] }),
172
+ ...(synthesized && { fontStyle: "italic" }),
173
+ };
135
174
  }
136
175
  export const typography = {
137
176
  xs: { fontSize: 12, lineHeight: 16 },
@@ -1,4 +1,5 @@
1
- export * from "./colors";
2
- export * from "./fonts";
3
- export * from "./motion";
4
- export * from "./spacing";
1
+ export * from "./colors.js";
2
+ export * from "./fonts.js";
3
+ export * from "./motion.js";
4
+ export * from "./spacing.js";
5
+ export * from "./interaction.js";
@@ -2,3 +2,4 @@ export * from "./colors.js";
2
2
  export * from "./fonts.js";
3
3
  export * from "./motion.js";
4
4
  export * from "./spacing.js";
5
+ export * from "./interaction.js";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Interaction state tokens.
3
+ *
4
+ * One opacity for "pressed", one for "disabled", and the two press scales the
5
+ * kit's pressables share, so a Button, a pressable Card and a row read as the
6
+ * same system under the finger. Components layer these after their base
7
+ * styles; a caller `style` still wins.
8
+ */
9
+ export declare const interaction: {
10
+ /** Opacity of a filled surface while pressed. */
11
+ readonly pressedOpacity: 0.85;
12
+ /** Opacity of a disabled control and its label. */
13
+ readonly disabledOpacity: 0.5;
14
+ /** Press scale for large surfaces (Button, Card, Item). */
15
+ readonly pressedScale: 0.97;
16
+ /** Press scale for small controls (Switch, Checkbox, Toggle). */
17
+ readonly controlPressedScale: 0.92;
18
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Interaction state tokens.
3
+ *
4
+ * One opacity for "pressed", one for "disabled", and the two press scales the
5
+ * kit's pressables share, so a Button, a pressable Card and a row read as the
6
+ * same system under the finger. Components layer these after their base
7
+ * styles; a caller `style` still wins.
8
+ */
9
+ export const interaction = {
10
+ /** Opacity of a filled surface while pressed. */
11
+ pressedOpacity: 0.85,
12
+ /** Opacity of a disabled control and its label. */
13
+ disabledOpacity: 0.5,
14
+ /** Press scale for large surfaces (Button, Card, Item). */
15
+ pressedScale: 0.97,
16
+ /** Press scale for small controls (Switch, Checkbox, Toggle). */
17
+ controlPressedScale: 0.92,
18
+ };
@@ -30,6 +30,7 @@ export declare const spacing: {
30
30
  readonly cardPadding: 16;
31
31
  readonly dialogPadding: 20;
32
32
  readonly touchTarget: 44;
33
+ readonly minTarget: 24;
33
34
  readonly buttonPadding: 10;
34
35
  readonly inputPadding: 10;
35
36
  readonly listItemSpacing: 8;
@@ -47,5 +48,5 @@ export declare const spacing: {
47
48
  readonly iconLg: 32;
48
49
  readonly iconXl: 48;
49
50
  };
50
- export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 10, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
51
+ export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, minTarget: 24, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 10, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
51
52
  export declare const space: (multiplier: number) => number;
@@ -34,6 +34,7 @@ export const spacing = {
34
34
  cardPadding: 16, // Default card / stat card padding
35
35
  dialogPadding: 20, // Default dialog / alert dialog padding
36
36
  touchTarget: 44, // Minimum comfortable native touch target
37
+ minTarget: 24, // Smallest pointer target the kit draws on web (WCAG 2.5.8 minimum)
37
38
  // Component-specific
38
39
  buttonPadding: 10, // Default button padding
39
40
  inputPadding: 10, // Default input padding
@@ -55,6 +56,6 @@ export const spacing = {
55
56
  iconXl: 48,
56
57
  };
57
58
  // Export individual constants for convenience
58
- export const { base, xxs, xs, sm, smd, md, mdl, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, sectionSpacing, rowPaddingY, rowPaddingX, rowGap, rowMinHeight, formRowMinHeight, cardPadding, dialogPadding, touchTarget, buttonPadding, inputPadding, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
59
+ export const { base, xxs, xs, sm, smd, md, mdl, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, sectionSpacing, rowPaddingY, rowPaddingX, rowGap, rowMinHeight, formRowMinHeight, cardPadding, dialogPadding, touchTarget, minTarget, buttonPadding, inputPadding, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
59
60
  // Helper function to multiply base spacing
60
61
  export const space = (multiplier) => spacing.base * multiplier;