@boostdev/design-system-react-native 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,38 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to `@boostdev/design-system-react-native` are documented here.
4
+ ## [0.6.0] - 2026-09-29
5
+ ### Added
6
+ - Feat(theme): radius overrides for buttons and fields
7
+
8
+ ThemeProvider takes `tokens.radius` ({ button, iconButton, field }, in
9
+ points), the native equivalent of the web `--button_radius`. The
10
+ defaults are 0.5.1's values (m, full, m), so nothing changes for an app
11
+ that does not set it. `useTheme()` returns the resolved `radius` next
12
+ to `colors` and `isDark`, and `ThemeRadius` is exported.
13
+
14
+ A nested ThemeProvider merges its radius over its parent's, so a
15
+ provider that only recolors a subtree keeps the app's corners. Colors
16
+ still merge over the foundation, not over the parent.
17
+
18
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ([`d8200d3`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/d8200d39ed24b30658a6c45692883dd1128898c1))
19
+
20
+ ### Changed
21
+ - Refactor(button): corner radius comes from the theme
22
+
23
+ A text button uses `radius.button` and an icon-only button
24
+ `radius.iconButton` instead of the hardcoded border.radius.m and
25
+ border.radius.full. A borderRadius in `style` still wins.
26
+
27
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ([`d672541`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/d6725412c7a6406203b6887e34512a09994c5265))
28
+ - Refactor(form): fields take their corner radius from the theme
29
+
30
+ FormInput, Select, Combobox, Textarea and the FileInput drop zone use
31
+ `radius.field` instead of the hardcoded border.radius.m. NumberInput
32
+ uses it for the outer corners on its steppers.
33
+
34
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ([`17b93bf`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/17b93bfa554c060b1ae83ca488b738d9a307b4b9))
35
+
4
36
  ## [0.5.1] - 2026-09-28
5
37
  ### Fixed
6
38
  - Fix(button): text buttons get medium corners
package/README.md CHANGED
@@ -34,7 +34,7 @@ export default function App() {
34
34
  }
35
35
  ```
36
36
 
37
- `ThemeProvider` auto-detects the system color scheme (light/dark) via `useColorScheme`, or you can pass `colorScheme="light" | "dark"` to force one. Inside the tree, `useTheme()` exposes `{ colors, isDark }` where `colors` is the full foundation token palette (`colorBg`, `colorOnBg`, `colorInteractive`, …).
37
+ `ThemeProvider` auto-detects the system color scheme (light/dark) via `useColorScheme`, or you can pass `colorScheme="light" | "dark"` to force one. Inside the tree, `useTheme()` exposes `{ colors, isDark, radius }` where `colors` is the full foundation token palette (`colorBg`, `colorOnBg`, `colorInteractive`, …) and `radius` holds the corner radii of the controls (see [Control radius](#control-radius)).
38
38
 
39
39
  ## Brand theming (token overrides)
40
40
 
@@ -62,6 +62,40 @@ Unspecified tokens keep their foundation values. Available token names are the k
62
62
  Pass a stable `tokens` reference (a module-level constant, not an inline object
63
63
  literal) — the provider memoizes the merged colors on that reference.
64
64
 
65
+ ### Control radius
66
+
67
+ The corner radius of buttons and fields comes from the theme, not from the
68
+ components — the React Native equivalent of the web `--button_radius` custom
69
+ property. Override it with `tokens.radius`, in points; it is the same in light
70
+ and dark:
71
+
72
+ ```tsx
73
+ import { ThemeProvider, border } from '@boostdev/design-system-react-native';
74
+ import type { ThemeTokenOverrides } from '@boostdev/design-system-react-native';
75
+
76
+ const tokens: ThemeTokenOverrides = {
77
+ light: { colorInteractive: '#00c853' },
78
+ radius: { button: border.radius.s, iconButton: border.radius.s, field: border.radius.s },
79
+ };
80
+
81
+ <ThemeProvider tokens={tokens}>
82
+ <App />
83
+ </ThemeProvider>;
84
+ ```
85
+
86
+ | Key | Applies to | Default |
87
+ | ------------ | ------------------------------------------------------------------------- | --------------------------- |
88
+ | `button` | `Button` with a text label | `border.radius.m` (16) |
89
+ | `iconButton` | `Button` with `isIconOnly` | `border.radius.full` (9999) |
90
+ | `field` | `FormInput`, `Select`, `Combobox`, `Textarea`, `FileInput`, `NumberInput` | `border.radius.m` (16) |
91
+
92
+ Unspecified keys keep their default. A nested `ThemeProvider` inherits its
93
+ parent's radii and merges its own `radius` over them, so a provider that only
94
+ recolors a subtree keeps the app's corners. (Colors differ: a nested provider
95
+ merges its colors over the foundation, not over its parent.) A `borderRadius`
96
+ in a `Button`'s `style` still wins over the theme. Your own controls can read
97
+ the same values through `useTheme().radius` (type `ThemeRadius`).
98
+
65
99
  ## React Navigation compatibility
66
100
 
67
101
  The library also ships a thin, **opt-in** bridge that produces a [React Navigation](https://reactnavigation.org/) `Theme` from the same foundation tokens. The bridge is purely additive — nothing about the standalone API changes when you use it, and `@react-navigation/native` is only required if you actually consume the bridge.
package/dist/index.cjs CHANGED
@@ -114,15 +114,25 @@ var lineHeight = {
114
114
 
115
115
  // src/native/ThemeContext.tsx
116
116
  var import_jsx_runtime = require("react/jsx-runtime");
117
+ var defaultRadius = {
118
+ button: import_native.border.radius.m,
119
+ iconButton: import_native.border.radius.full,
120
+ field: import_native.border.radius.m
121
+ };
117
122
  var ThemeContext = (0, import_react.createContext)(null);
118
123
  function ThemeProvider({ children, colorScheme, tokens }) {
119
124
  const systemScheme = (0, import_react_native.useColorScheme)();
120
125
  const isDark = (colorScheme ?? systemScheme) === "dark";
126
+ const parentRadius = (0, import_react.useContext)(ThemeContext)?.radius ?? defaultRadius;
121
127
  const value = (0, import_react.useMemo)(() => {
122
128
  const base = isDark ? import_native.colors.dark : import_native.colors.light;
123
129
  const overrides = isDark ? tokens?.dark : tokens?.light;
124
- return { colors: { ...base, ...overrides }, isDark };
125
- }, [isDark, tokens]);
130
+ return {
131
+ colors: { ...base, ...overrides },
132
+ isDark,
133
+ radius: { ...parentRadius, ...tokens?.radius }
134
+ };
135
+ }, [isDark, tokens, parentRadius]);
126
136
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ThemeContext.Provider, { value, children });
127
137
  }
128
138
  function useTheme() {
@@ -2269,7 +2279,7 @@ function Button({
2269
2279
  accessibilityLabel,
2270
2280
  style
2271
2281
  }) {
2272
- const { colors: colors2 } = useTheme();
2282
+ const { colors: colors2, radius } = useTheme();
2273
2283
  const { height, px, fontSize } = sizeTokens[size];
2274
2284
  const primaryColor = colors2.colorInteractive;
2275
2285
  const onPrimaryColor = colors2.colorOnInteractive;
@@ -2278,8 +2288,8 @@ function Button({
2278
2288
  const containerStyle = {
2279
2289
  minHeight: height,
2280
2290
  paddingHorizontal: isIconOnly ? import_native.spacing.xs : px,
2281
- // A text button has medium corners on every size; an icon-only button stays round.
2282
- borderRadius: isIconOnly ? import_native.border.radius.full : import_native.border.radius.m,
2291
+ // Corners come from the theme (`tokens.radius`); a borderRadius in `style` still wins.
2292
+ borderRadius: isIconOnly ? radius.iconButton : radius.button,
2283
2293
  opacity: disabled ? 0.4 : 1,
2284
2294
  // Icon-only: square with equal xs padding on both axes.
2285
2295
  ...isIconOnly && { aspectRatio: 1, paddingVertical: import_native.spacing.xs },
@@ -2683,7 +2693,6 @@ var styles33 = import_react_native37.StyleSheet.create({
2683
2693
  paddingHorizontal: import_native.spacing.m,
2684
2694
  borderWidth: 2,
2685
2695
  borderStyle: "dashed",
2686
- borderRadius: import_native.border.radius.m,
2687
2696
  minHeight: 120
2688
2697
  },
2689
2698
  icon: {
@@ -2724,7 +2733,7 @@ function FileInput({
2724
2733
  fileNames = [],
2725
2734
  style
2726
2735
  }) {
2727
- const { colors: colors2 } = useTheme();
2736
+ const { colors: colors2, radius } = useTheme();
2728
2737
  const borderColor = error ? colors2.colorError : colors2.colorBgSubtle;
2729
2738
  return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_react_native37.View, { style: [styles33.container, style], testID: name, children: [
2730
2739
  /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
@@ -2745,6 +2754,7 @@ function FileInput({
2745
2754
  styles33.dropZone,
2746
2755
  {
2747
2756
  borderColor,
2757
+ borderRadius: radius.field,
2748
2758
  backgroundColor: "transparent"
2749
2759
  },
2750
2760
  pressed && { opacity: 0.7 },
@@ -2779,7 +2789,8 @@ var styles34 = import_react_native38.StyleSheet.create({
2779
2789
  gap: import_native.spacing.xxs
2780
2790
  },
2781
2791
  /**
2782
- * Draws the rounded border and provides the fixed 44pt touch-target height.
2792
+ * Draws the rounded border (`radius.field` from the theme) and provides the fixed
2793
+ * 44pt touch-target height.
2783
2794
  * justifyContent: 'center' vertically centers the inner TextInput.
2784
2795
  * position: 'relative' is required for the dropdown's absolute positioning.
2785
2796
  */
@@ -2787,8 +2798,7 @@ var styles34 = import_react_native38.StyleSheet.create({
2787
2798
  position: "relative",
2788
2799
  height: INPUT_HEIGHT,
2789
2800
  justifyContent: "center",
2790
- borderWidth: 1,
2791
- borderRadius: import_native.border.radius.m
2801
+ borderWidth: 1
2792
2802
  },
2793
2803
  /**
2794
2804
  * No vertical padding: iOS centers text automatically inside the wrapper height.
@@ -2841,7 +2851,7 @@ function Combobox({
2841
2851
  hint,
2842
2852
  style
2843
2853
  }) {
2844
- const { colors: colors2 } = useTheme();
2854
+ const { colors: colors2, radius } = useTheme();
2845
2855
  const selectedOption = (0, import_react10.useMemo)(
2846
2856
  () => options.find((o) => o.value === value),
2847
2857
  [options, value]
@@ -2879,6 +2889,7 @@ function Combobox({
2879
2889
  styles34.inputWrapper,
2880
2890
  {
2881
2891
  borderColor,
2892
+ borderRadius: radius.field,
2882
2893
  backgroundColor: colors2.colorBg
2883
2894
  },
2884
2895
  disabled && { opacity: 0.4 }
@@ -3079,14 +3090,14 @@ var styles36 = import_react_native40.StyleSheet.create({
3079
3090
  gap: import_native.spacing.xxs
3080
3091
  },
3081
3092
  /**
3082
- * The wrapper draws the rounded border and provides at least 44pt touch-target height.
3093
+ * The wrapper draws the rounded border (`radius.field` from the theme) and provides
3094
+ * at least 44pt touch-target height.
3083
3095
  * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
3084
3096
  */
3085
3097
  inputWrapper: {
3086
3098
  minHeight: INPUT_HEIGHT2,
3087
3099
  justifyContent: "center",
3088
- borderWidth: 1,
3089
- borderRadius: import_native.border.radius.m
3100
+ borderWidth: 1
3090
3101
  },
3091
3102
  /**
3092
3103
  * No vertical padding: iOS centers text automatically inside the wrapper height.
@@ -3119,7 +3130,7 @@ function FormInput({
3119
3130
  onFocus,
3120
3131
  style
3121
3132
  }) {
3122
- const { colors: colors2 } = useTheme();
3133
+ const { colors: colors2, radius } = useTheme();
3123
3134
  const [focused, setFocused] = (0, import_react12.useState)(false);
3124
3135
  const borderColor = error ? colors2.colorError : focused ? colors2.colorInteractive : colors2.colorOnBg;
3125
3136
  return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_react_native40.View, { style: [styles36.container, style], children: [
@@ -3131,6 +3142,7 @@ function FormInput({
3131
3142
  styles36.inputWrapper,
3132
3143
  {
3133
3144
  borderColor,
3145
+ borderRadius: radius.field,
3134
3146
  backgroundColor: colors2.colorBg
3135
3147
  },
3136
3148
  disabled && { opacity: 0.4 }
@@ -3187,7 +3199,6 @@ var styles37 = import_react_native41.StyleSheet.create({
3187
3199
  paddingHorizontal: import_native.spacing.s,
3188
3200
  paddingVertical: import_native.spacing.xs,
3189
3201
  borderWidth: 1,
3190
- borderRadius: import_native.border.radius.m,
3191
3202
  minHeight: 100,
3192
3203
  textAlignVertical: "top"
3193
3204
  }
@@ -3207,7 +3218,7 @@ function Textarea({
3207
3218
  onFocus,
3208
3219
  style
3209
3220
  }) {
3210
- const { colors: colors2 } = useTheme();
3221
+ const { colors: colors2, radius } = useTheme();
3211
3222
  const [focused, setFocused] = (0, import_react13.useState)(false);
3212
3223
  const borderColor = error ? colors2.colorError : focused ? colors2.colorInteractive : colors2.colorOnBg;
3213
3224
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_react_native41.View, { style: [styles37.container, style], children: [
@@ -3238,6 +3249,7 @@ function Textarea({
3238
3249
  styles37.textarea,
3239
3250
  {
3240
3251
  borderColor,
3252
+ borderRadius: radius.field,
3241
3253
  color: colors2.colorOnBg,
3242
3254
  backgroundColor: colors2.colorBg
3243
3255
  },
@@ -3265,7 +3277,6 @@ var styles38 = import_react_native42.StyleSheet.create({
3265
3277
  paddingHorizontal: import_native.spacing.s,
3266
3278
  paddingVertical: import_native.spacing.xs,
3267
3279
  borderWidth: 1,
3268
- borderRadius: import_native.border.radius.m,
3269
3280
  minHeight: 44
3270
3281
  },
3271
3282
  triggerText: {
@@ -3330,7 +3341,7 @@ function Select({
3330
3341
  onChange,
3331
3342
  style
3332
3343
  }) {
3333
- const { colors: colors2 } = useTheme();
3344
+ const { colors: colors2, radius } = useTheme();
3334
3345
  const [open, setOpen] = (0, import_react14.useState)(false);
3335
3346
  const selectedOption = options.find((o) => o.value === value);
3336
3347
  const displayText = selectedOption?.label ?? placeholder;
@@ -3355,6 +3366,7 @@ function Select({
3355
3366
  styles38.trigger,
3356
3367
  {
3357
3368
  borderColor,
3369
+ borderRadius: radius.field,
3358
3370
  backgroundColor: colors2.colorBg
3359
3371
  },
3360
3372
  disabled && { opacity: 0.4 }
@@ -3650,14 +3662,11 @@ var styles40 = import_react_native44.StyleSheet.create({
3650
3662
  justifyContent: "center",
3651
3663
  borderWidth: 1
3652
3664
  },
3665
+ // The steppers round only the field's outer corners, with `radius.field` from the theme.
3653
3666
  stepperStart: {
3654
- borderTopStartRadius: import_native.border.radius.m,
3655
- borderBottomStartRadius: import_native.border.radius.m,
3656
3667
  borderEndWidth: 0
3657
3668
  },
3658
3669
  stepperEnd: {
3659
- borderTopEndRadius: import_native.border.radius.m,
3660
- borderBottomEndRadius: import_native.border.radius.m,
3661
3670
  borderStartWidth: 0
3662
3671
  },
3663
3672
  stepperText: {
@@ -3708,7 +3717,7 @@ function NumberInput({
3708
3717
  onChange,
3709
3718
  style
3710
3719
  }) {
3711
- const { colors: colors2 } = useTheme();
3720
+ const { colors: colors2, radius } = useTheme();
3712
3721
  const isControlled = value !== void 0;
3713
3722
  const [internalValue, setInternalValue] = (0, import_react16.useState)(defaultValue ?? 0);
3714
3723
  const currentValue = isControlled ? value : internalValue;
@@ -3740,6 +3749,8 @@ function NumberInput({
3740
3749
  styles40.stepperStart,
3741
3750
  {
3742
3751
  borderColor,
3752
+ borderTopStartRadius: radius.field,
3753
+ borderBottomStartRadius: radius.field,
3743
3754
  backgroundColor: colors2.colorBgSubtle
3744
3755
  },
3745
3756
  pressed && { opacity: 0.7 },
@@ -3805,6 +3816,8 @@ function NumberInput({
3805
3816
  styles40.stepperEnd,
3806
3817
  {
3807
3818
  borderColor,
3819
+ borderTopEndRadius: radius.field,
3820
+ borderBottomEndRadius: radius.field,
3808
3821
  backgroundColor: colors2.colorBgSubtle
3809
3822
  },
3810
3823
  pressed && { opacity: 0.7 },
package/dist/index.d.cts CHANGED
@@ -18,6 +18,15 @@ declare const lineHeight: {
18
18
  type ThemeColorName = keyof typeof colors.light;
19
19
  /** Semantic theme colors, widened to `string` so brand overrides typecheck. */
20
20
  type ThemeColors = Readonly<Record<ThemeColorName, string>>;
21
+ /** Corner radii of the controls, in points. */
22
+ interface ThemeRadius {
23
+ /** Buttons with a text label. */
24
+ button: number;
25
+ /** Icon-only buttons (`isIconOnly`). */
26
+ iconButton: number;
27
+ /** Input fields: FormInput, Select, Combobox, Textarea, FileInput, NumberInput. */
28
+ field: number;
29
+ }
21
30
  /**
22
31
  * Per-scheme token overrides, merged over the foundation colors.
23
32
  * The React Native equivalent of overriding tokens in the web
@@ -26,10 +35,17 @@ type ThemeColors = Readonly<Record<ThemeColorName, string>>;
26
35
  interface ThemeTokenOverrides {
27
36
  light?: Partial<Record<ThemeColorName, string>>;
28
37
  dark?: Partial<Record<ThemeColorName, string>>;
38
+ /**
39
+ * Radius overrides, the same in light and dark. The native equivalent of the
40
+ * web `--button_radius` custom property. Merged over the parent provider's
41
+ * radii, so a nested provider that only overrides colors keeps the app's radii.
42
+ */
43
+ radius?: Partial<ThemeRadius>;
29
44
  }
30
45
  interface ThemeContextValue {
31
46
  colors: ThemeColors;
32
47
  isDark: boolean;
48
+ radius: ThemeRadius;
33
49
  }
34
50
  interface ThemeProviderProps {
35
51
  children: ReactNode;
@@ -667,4 +683,4 @@ interface MessageProps {
667
683
  }
668
684
  declare function Message({ message, type, style }: MessageProps): react_jsx_runtime.JSX.Element | null;
669
685
 
670
- export { Accordion, type AccordionItem, Alert, Avatar, Badge, Breadcrumb, type BreadcrumbItem, Button, ButtonContainer, type ButtonContainerProps, ButtonGroup, type ButtonGroupProps, type ButtonProps, Calendar, Card, Carousel, Checkbox, CheckboxGroup, Collapsible, type CollapsibleProps, Combobox, type ComboboxOption, type CreateReactNavigationThemeOptions, type DescriptionItem, DescriptionList, Dialog, DropdownMenu, type DropdownMenuItem, FileInput, FormInput, IconWrapper, type IconWrapperProps, Label, Link, Loading, Message, NotificationBanner, NumberInput, Pagination, Popover, Progress, ProgressCircle, Radio, RadioGroup, Rating, type ReactNavigationFontStyle, type ReactNavigationTheme, type ReactNavigationThemeColors, type ReactNavigationThemeFonts, ScaledText, ScaledTextInput, SectionHeader, SegmentedControl, Select, type SelectOption, Separator, SideSheet, Skeleton, Slider, Switch, type TabItem, Table, type TableColumn, Tabs, Textarea, type ThemeColorName, type ThemeColors, ThemeProvider, type ThemeTokenOverrides, ToastProvider, Tooltip, Typography, createReactNavigationTheme, lineHeight, maxFontScaleFor, maxFontScaleForStyle, useReactNavigationTheme, useTheme, useToast };
686
+ export { Accordion, type AccordionItem, Alert, Avatar, Badge, Breadcrumb, type BreadcrumbItem, Button, ButtonContainer, type ButtonContainerProps, ButtonGroup, type ButtonGroupProps, type ButtonProps, Calendar, Card, Carousel, Checkbox, CheckboxGroup, Collapsible, type CollapsibleProps, Combobox, type ComboboxOption, type CreateReactNavigationThemeOptions, type DescriptionItem, DescriptionList, Dialog, DropdownMenu, type DropdownMenuItem, FileInput, FormInput, IconWrapper, type IconWrapperProps, Label, Link, Loading, Message, NotificationBanner, NumberInput, Pagination, Popover, Progress, ProgressCircle, Radio, RadioGroup, Rating, type ReactNavigationFontStyle, type ReactNavigationTheme, type ReactNavigationThemeColors, type ReactNavigationThemeFonts, ScaledText, ScaledTextInput, SectionHeader, SegmentedControl, Select, type SelectOption, Separator, SideSheet, Skeleton, Slider, Switch, type TabItem, Table, type TableColumn, Tabs, Textarea, type ThemeColorName, type ThemeColors, ThemeProvider, type ThemeRadius, type ThemeTokenOverrides, ToastProvider, Tooltip, Typography, createReactNavigationTheme, lineHeight, maxFontScaleFor, maxFontScaleForStyle, useReactNavigationTheme, useTheme, useToast };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,15 @@ declare const lineHeight: {
18
18
  type ThemeColorName = keyof typeof colors.light;
19
19
  /** Semantic theme colors, widened to `string` so brand overrides typecheck. */
20
20
  type ThemeColors = Readonly<Record<ThemeColorName, string>>;
21
+ /** Corner radii of the controls, in points. */
22
+ interface ThemeRadius {
23
+ /** Buttons with a text label. */
24
+ button: number;
25
+ /** Icon-only buttons (`isIconOnly`). */
26
+ iconButton: number;
27
+ /** Input fields: FormInput, Select, Combobox, Textarea, FileInput, NumberInput. */
28
+ field: number;
29
+ }
21
30
  /**
22
31
  * Per-scheme token overrides, merged over the foundation colors.
23
32
  * The React Native equivalent of overriding tokens in the web
@@ -26,10 +35,17 @@ type ThemeColors = Readonly<Record<ThemeColorName, string>>;
26
35
  interface ThemeTokenOverrides {
27
36
  light?: Partial<Record<ThemeColorName, string>>;
28
37
  dark?: Partial<Record<ThemeColorName, string>>;
38
+ /**
39
+ * Radius overrides, the same in light and dark. The native equivalent of the
40
+ * web `--button_radius` custom property. Merged over the parent provider's
41
+ * radii, so a nested provider that only overrides colors keeps the app's radii.
42
+ */
43
+ radius?: Partial<ThemeRadius>;
29
44
  }
30
45
  interface ThemeContextValue {
31
46
  colors: ThemeColors;
32
47
  isDark: boolean;
48
+ radius: ThemeRadius;
33
49
  }
34
50
  interface ThemeProviderProps {
35
51
  children: ReactNode;
@@ -667,4 +683,4 @@ interface MessageProps {
667
683
  }
668
684
  declare function Message({ message, type, style }: MessageProps): react_jsx_runtime.JSX.Element | null;
669
685
 
670
- export { Accordion, type AccordionItem, Alert, Avatar, Badge, Breadcrumb, type BreadcrumbItem, Button, ButtonContainer, type ButtonContainerProps, ButtonGroup, type ButtonGroupProps, type ButtonProps, Calendar, Card, Carousel, Checkbox, CheckboxGroup, Collapsible, type CollapsibleProps, Combobox, type ComboboxOption, type CreateReactNavigationThemeOptions, type DescriptionItem, DescriptionList, Dialog, DropdownMenu, type DropdownMenuItem, FileInput, FormInput, IconWrapper, type IconWrapperProps, Label, Link, Loading, Message, NotificationBanner, NumberInput, Pagination, Popover, Progress, ProgressCircle, Radio, RadioGroup, Rating, type ReactNavigationFontStyle, type ReactNavigationTheme, type ReactNavigationThemeColors, type ReactNavigationThemeFonts, ScaledText, ScaledTextInput, SectionHeader, SegmentedControl, Select, type SelectOption, Separator, SideSheet, Skeleton, Slider, Switch, type TabItem, Table, type TableColumn, Tabs, Textarea, type ThemeColorName, type ThemeColors, ThemeProvider, type ThemeTokenOverrides, ToastProvider, Tooltip, Typography, createReactNavigationTheme, lineHeight, maxFontScaleFor, maxFontScaleForStyle, useReactNavigationTheme, useTheme, useToast };
686
+ export { Accordion, type AccordionItem, Alert, Avatar, Badge, Breadcrumb, type BreadcrumbItem, Button, ButtonContainer, type ButtonContainerProps, ButtonGroup, type ButtonGroupProps, type ButtonProps, Calendar, Card, Carousel, Checkbox, CheckboxGroup, Collapsible, type CollapsibleProps, Combobox, type ComboboxOption, type CreateReactNavigationThemeOptions, type DescriptionItem, DescriptionList, Dialog, DropdownMenu, type DropdownMenuItem, FileInput, FormInput, IconWrapper, type IconWrapperProps, Label, Link, Loading, Message, NotificationBanner, NumberInput, Pagination, Popover, Progress, ProgressCircle, Radio, RadioGroup, Rating, type ReactNavigationFontStyle, type ReactNavigationTheme, type ReactNavigationThemeColors, type ReactNavigationThemeFonts, ScaledText, ScaledTextInput, SectionHeader, SegmentedControl, Select, type SelectOption, Separator, SideSheet, Skeleton, Slider, Switch, type TabItem, Table, type TableColumn, Tabs, Textarea, type ThemeColorName, type ThemeColors, ThemeProvider, type ThemeRadius, type ThemeTokenOverrides, ToastProvider, Tooltip, Typography, createReactNavigationTheme, lineHeight, maxFontScaleFor, maxFontScaleForStyle, useReactNavigationTheme, useTheme, useToast };
package/dist/index.js CHANGED
@@ -15,15 +15,25 @@ var lineHeight = {
15
15
 
16
16
  // src/native/ThemeContext.tsx
17
17
  import { jsx } from "react/jsx-runtime";
18
+ var defaultRadius = {
19
+ button: border.radius.m,
20
+ iconButton: border.radius.full,
21
+ field: border.radius.m
22
+ };
18
23
  var ThemeContext = createContext(null);
19
24
  function ThemeProvider({ children, colorScheme, tokens }) {
20
25
  const systemScheme = useColorScheme();
21
26
  const isDark = (colorScheme ?? systemScheme) === "dark";
27
+ const parentRadius = useContext(ThemeContext)?.radius ?? defaultRadius;
22
28
  const value = useMemo(() => {
23
29
  const base = isDark ? colors.dark : colors.light;
24
30
  const overrides = isDark ? tokens?.dark : tokens?.light;
25
- return { colors: { ...base, ...overrides }, isDark };
26
- }, [isDark, tokens]);
31
+ return {
32
+ colors: { ...base, ...overrides },
33
+ isDark,
34
+ radius: { ...parentRadius, ...tokens?.radius }
35
+ };
36
+ }, [isDark, tokens, parentRadius]);
27
37
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value, children });
28
38
  }
29
39
  function useTheme() {
@@ -2170,7 +2180,7 @@ function Button({
2170
2180
  accessibilityLabel,
2171
2181
  style
2172
2182
  }) {
2173
- const { colors: colors2 } = useTheme();
2183
+ const { colors: colors2, radius } = useTheme();
2174
2184
  const { height, px, fontSize } = sizeTokens[size];
2175
2185
  const primaryColor = colors2.colorInteractive;
2176
2186
  const onPrimaryColor = colors2.colorOnInteractive;
@@ -2179,8 +2189,8 @@ function Button({
2179
2189
  const containerStyle = {
2180
2190
  minHeight: height,
2181
2191
  paddingHorizontal: isIconOnly ? spacing.xs : px,
2182
- // A text button has medium corners on every size; an icon-only button stays round.
2183
- borderRadius: isIconOnly ? border.radius.full : border.radius.m,
2192
+ // Corners come from the theme (`tokens.radius`); a borderRadius in `style` still wins.
2193
+ borderRadius: isIconOnly ? radius.iconButton : radius.button,
2184
2194
  opacity: disabled ? 0.4 : 1,
2185
2195
  // Icon-only: square with equal xs padding on both axes.
2186
2196
  ...isIconOnly && { aspectRatio: 1, paddingVertical: spacing.xs },
@@ -2584,7 +2594,6 @@ var styles33 = StyleSheet34.create({
2584
2594
  paddingHorizontal: spacing.m,
2585
2595
  borderWidth: 2,
2586
2596
  borderStyle: "dashed",
2587
- borderRadius: border.radius.m,
2588
2597
  minHeight: 120
2589
2598
  },
2590
2599
  icon: {
@@ -2625,7 +2634,7 @@ function FileInput({
2625
2634
  fileNames = [],
2626
2635
  style
2627
2636
  }) {
2628
- const { colors: colors2 } = useTheme();
2637
+ const { colors: colors2, radius } = useTheme();
2629
2638
  const borderColor = error ? colors2.colorError : colors2.colorBgSubtle;
2630
2639
  return /* @__PURE__ */ jsxs22(View29, { style: [styles33.container, style], testID: name, children: [
2631
2640
  /* @__PURE__ */ jsx38(
@@ -2646,6 +2655,7 @@ function FileInput({
2646
2655
  styles33.dropZone,
2647
2656
  {
2648
2657
  borderColor,
2658
+ borderRadius: radius.field,
2649
2659
  backgroundColor: "transparent"
2650
2660
  },
2651
2661
  pressed && { opacity: 0.7 },
@@ -2680,7 +2690,8 @@ var styles34 = StyleSheet35.create({
2680
2690
  gap: spacing.xxs
2681
2691
  },
2682
2692
  /**
2683
- * Draws the rounded border and provides the fixed 44pt touch-target height.
2693
+ * Draws the rounded border (`radius.field` from the theme) and provides the fixed
2694
+ * 44pt touch-target height.
2684
2695
  * justifyContent: 'center' vertically centers the inner TextInput.
2685
2696
  * position: 'relative' is required for the dropdown's absolute positioning.
2686
2697
  */
@@ -2688,8 +2699,7 @@ var styles34 = StyleSheet35.create({
2688
2699
  position: "relative",
2689
2700
  height: INPUT_HEIGHT,
2690
2701
  justifyContent: "center",
2691
- borderWidth: 1,
2692
- borderRadius: border.radius.m
2702
+ borderWidth: 1
2693
2703
  },
2694
2704
  /**
2695
2705
  * No vertical padding: iOS centers text automatically inside the wrapper height.
@@ -2742,7 +2752,7 @@ function Combobox({
2742
2752
  hint,
2743
2753
  style
2744
2754
  }) {
2745
- const { colors: colors2 } = useTheme();
2755
+ const { colors: colors2, radius } = useTheme();
2746
2756
  const selectedOption = useMemo2(
2747
2757
  () => options.find((o) => o.value === value),
2748
2758
  [options, value]
@@ -2780,6 +2790,7 @@ function Combobox({
2780
2790
  styles34.inputWrapper,
2781
2791
  {
2782
2792
  borderColor,
2793
+ borderRadius: radius.field,
2783
2794
  backgroundColor: colors2.colorBg
2784
2795
  },
2785
2796
  disabled && { opacity: 0.4 }
@@ -2980,14 +2991,14 @@ var styles36 = StyleSheet37.create({
2980
2991
  gap: spacing.xxs
2981
2992
  },
2982
2993
  /**
2983
- * The wrapper draws the rounded border and provides at least 44pt touch-target height.
2994
+ * The wrapper draws the rounded border (`radius.field` from the theme) and provides
2995
+ * at least 44pt touch-target height.
2984
2996
  * justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
2985
2997
  */
2986
2998
  inputWrapper: {
2987
2999
  minHeight: INPUT_HEIGHT2,
2988
3000
  justifyContent: "center",
2989
- borderWidth: 1,
2990
- borderRadius: border.radius.m
3001
+ borderWidth: 1
2991
3002
  },
2992
3003
  /**
2993
3004
  * No vertical padding: iOS centers text automatically inside the wrapper height.
@@ -3020,7 +3031,7 @@ function FormInput({
3020
3031
  onFocus,
3021
3032
  style
3022
3033
  }) {
3023
- const { colors: colors2 } = useTheme();
3034
+ const { colors: colors2, radius } = useTheme();
3024
3035
  const [focused, setFocused] = useState8(false);
3025
3036
  const borderColor = error ? colors2.colorError : focused ? colors2.colorInteractive : colors2.colorOnBg;
3026
3037
  return /* @__PURE__ */ jsxs24(View32, { style: [styles36.container, style], children: [
@@ -3032,6 +3043,7 @@ function FormInput({
3032
3043
  styles36.inputWrapper,
3033
3044
  {
3034
3045
  borderColor,
3046
+ borderRadius: radius.field,
3035
3047
  backgroundColor: colors2.colorBg
3036
3048
  },
3037
3049
  disabled && { opacity: 0.4 }
@@ -3088,7 +3100,6 @@ var styles37 = StyleSheet38.create({
3088
3100
  paddingHorizontal: spacing.s,
3089
3101
  paddingVertical: spacing.xs,
3090
3102
  borderWidth: 1,
3091
- borderRadius: border.radius.m,
3092
3103
  minHeight: 100,
3093
3104
  textAlignVertical: "top"
3094
3105
  }
@@ -3108,7 +3119,7 @@ function Textarea({
3108
3119
  onFocus,
3109
3120
  style
3110
3121
  }) {
3111
- const { colors: colors2 } = useTheme();
3122
+ const { colors: colors2, radius } = useTheme();
3112
3123
  const [focused, setFocused] = useState9(false);
3113
3124
  const borderColor = error ? colors2.colorError : focused ? colors2.colorInteractive : colors2.colorOnBg;
3114
3125
  return /* @__PURE__ */ jsxs25(View33, { style: [styles37.container, style], children: [
@@ -3139,6 +3150,7 @@ function Textarea({
3139
3150
  styles37.textarea,
3140
3151
  {
3141
3152
  borderColor,
3153
+ borderRadius: radius.field,
3142
3154
  color: colors2.colorOnBg,
3143
3155
  backgroundColor: colors2.colorBg
3144
3156
  },
@@ -3166,7 +3178,6 @@ var styles38 = StyleSheet39.create({
3166
3178
  paddingHorizontal: spacing.s,
3167
3179
  paddingVertical: spacing.xs,
3168
3180
  borderWidth: 1,
3169
- borderRadius: border.radius.m,
3170
3181
  minHeight: 44
3171
3182
  },
3172
3183
  triggerText: {
@@ -3231,7 +3242,7 @@ function Select({
3231
3242
  onChange,
3232
3243
  style
3233
3244
  }) {
3234
- const { colors: colors2 } = useTheme();
3245
+ const { colors: colors2, radius } = useTheme();
3235
3246
  const [open, setOpen] = useState10(false);
3236
3247
  const selectedOption = options.find((o) => o.value === value);
3237
3248
  const displayText = selectedOption?.label ?? placeholder;
@@ -3256,6 +3267,7 @@ function Select({
3256
3267
  styles38.trigger,
3257
3268
  {
3258
3269
  borderColor,
3270
+ borderRadius: radius.field,
3259
3271
  backgroundColor: colors2.colorBg
3260
3272
  },
3261
3273
  disabled && { opacity: 0.4 }
@@ -3551,14 +3563,11 @@ var styles40 = StyleSheet41.create({
3551
3563
  justifyContent: "center",
3552
3564
  borderWidth: 1
3553
3565
  },
3566
+ // The steppers round only the field's outer corners, with `radius.field` from the theme.
3554
3567
  stepperStart: {
3555
- borderTopStartRadius: border.radius.m,
3556
- borderBottomStartRadius: border.radius.m,
3557
3568
  borderEndWidth: 0
3558
3569
  },
3559
3570
  stepperEnd: {
3560
- borderTopEndRadius: border.radius.m,
3561
- borderBottomEndRadius: border.radius.m,
3562
3571
  borderStartWidth: 0
3563
3572
  },
3564
3573
  stepperText: {
@@ -3609,7 +3618,7 @@ function NumberInput({
3609
3618
  onChange,
3610
3619
  style
3611
3620
  }) {
3612
- const { colors: colors2 } = useTheme();
3621
+ const { colors: colors2, radius } = useTheme();
3613
3622
  const isControlled = value !== void 0;
3614
3623
  const [internalValue, setInternalValue] = useState12(defaultValue ?? 0);
3615
3624
  const currentValue = isControlled ? value : internalValue;
@@ -3641,6 +3650,8 @@ function NumberInput({
3641
3650
  styles40.stepperStart,
3642
3651
  {
3643
3652
  borderColor,
3653
+ borderTopStartRadius: radius.field,
3654
+ borderBottomStartRadius: radius.field,
3644
3655
  backgroundColor: colors2.colorBgSubtle
3645
3656
  },
3646
3657
  pressed && { opacity: 0.7 },
@@ -3706,6 +3717,8 @@ function NumberInput({
3706
3717
  styles40.stepperEnd,
3707
3718
  {
3708
3719
  borderColor,
3720
+ borderTopEndRadius: radius.field,
3721
+ borderBottomEndRadius: radius.field,
3709
3722
  backgroundColor: colors2.colorBgSubtle
3710
3723
  },
3711
3724
  pressed && { opacity: 0.7 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-react-native",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
5
5
  "keywords": [
6
6
  "react-native",
@@ -47,7 +47,15 @@ export default function App() {
47
47
 
48
48
  ## Shape
49
49
 
50
- A button with a text label has `border.radius.m` corners on every size, the same as the form fields. An icon-only button (`isIconOnly`) stays round (`border.radius.full`). A `borderRadius` in `style` overrides both.
50
+ The corner radius comes from the theme, not from the button: a button with a text label uses `radius.button` on every size, an icon-only button (`isIconOnly`) uses `radius.iconButton`. By default these are `border.radius.m` (the same as the form fields) and `border.radius.full` (round). Set them per app through `ThemeProvider`, the native equivalent of the web `--button_radius`:
51
+
52
+ ```tsx
53
+ <ThemeProvider tokens={{ radius: { button: border.radius.s, iconButton: border.radius.s } }}>
54
+ <App />
55
+ </ThemeProvider>
56
+ ```
57
+
58
+ A nested `ThemeProvider` keeps its parent's radii unless it sets its own. A `borderRadius` in `style` overrides the theme.
51
59
 
52
60
  ## Icons
53
61
 
@@ -56,7 +64,7 @@ A button with a text label has `border.radius.m` corners on every size, the same
56
64
 
57
65
  ### Icon-only (1:1 aspect ratio)
58
66
 
59
- Set `isIconOnly` to render a round 1:1 button with equal `spacing.xs` padding on both axes. Composable with any `variant`. Always pair with `accessibilityLabel` for assistive tech.
67
+ Set `isIconOnly` to render a 1:1 button (round by default, see [Shape](#shape)) with equal `spacing.xs` padding on both axes. Composable with any `variant`. Always pair with `accessibilityLabel` for assistive tech.
60
68
 
61
69
  <Canvas of={Stories.IconOnly} />
62
70
  <Canvas of={Stories.IconOnlyOutline} />