@boostdev/design-system-react-native 0.6.0 → 0.7.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,18 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to `@boostdev/design-system-react-native` are documented here.
4
+ ## [0.7.0] - 2026-09-29
5
+ ### Added
6
+ - Feat(button): ghost is outline without the border
7
+
8
+ `variant="ghost"` was a deprecated alias of `outline`. It is now its own
9
+ variant: a transparent background with the outline's text and icon
10
+ colour, and no border. For a quiet button, such as one beside a heading.
11
+ `outline` keeps its 1 px border. ButtonGroup stories that showed a
12
+ bordered second button now use `outline`.
13
+
14
+ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ([`3be03dd`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/3be03dd7d56117bc37cb7d0323d1f533be7ae16f))
15
+
4
16
  ## [0.6.0] - 2026-09-29
5
17
  ### Added
6
18
  - Feat(theme): radius overrides for buttons and fields
package/dist/index.cjs CHANGED
@@ -2284,7 +2284,6 @@ function Button({
2284
2284
  const primaryColor = colors2.colorInteractive;
2285
2285
  const onPrimaryColor = colors2.colorOnInteractive;
2286
2286
  const outlineTextColor = colors2.colorInteractiveOnBg;
2287
- const isOutline = variant === "outline" || variant === "ghost";
2288
2287
  const containerStyle = {
2289
2288
  minHeight: height,
2290
2289
  paddingHorizontal: isIconOnly ? import_native.spacing.xs : px,
@@ -2297,11 +2296,12 @@ function Button({
2297
2296
  // instead, so a wrapped label grows the button from minHeight rather than clipping.
2298
2297
  ...isIconOnly ? { height } : { paddingVertical: import_native.spacing.xxs },
2299
2298
  ...variant === "default" && { backgroundColor: primaryColor },
2300
- ...isOutline && {
2299
+ ...variant === "outline" && {
2301
2300
  backgroundColor: "transparent",
2302
2301
  borderWidth: 1,
2303
2302
  borderColor: outlineTextColor
2304
- }
2303
+ },
2304
+ ...variant === "ghost" && { backgroundColor: "transparent" }
2305
2305
  };
2306
2306
  const textColor = variant === "default" ? onPrimaryColor : outlineTextColor;
2307
2307
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
package/dist/index.js CHANGED
@@ -2185,7 +2185,6 @@ function Button({
2185
2185
  const primaryColor = colors2.colorInteractive;
2186
2186
  const onPrimaryColor = colors2.colorOnInteractive;
2187
2187
  const outlineTextColor = colors2.colorInteractiveOnBg;
2188
- const isOutline = variant === "outline" || variant === "ghost";
2189
2188
  const containerStyle = {
2190
2189
  minHeight: height,
2191
2190
  paddingHorizontal: isIconOnly ? spacing.xs : px,
@@ -2198,11 +2197,12 @@ function Button({
2198
2197
  // instead, so a wrapped label grows the button from minHeight rather than clipping.
2199
2198
  ...isIconOnly ? { height } : { paddingVertical: spacing.xxs },
2200
2199
  ...variant === "default" && { backgroundColor: primaryColor },
2201
- ...isOutline && {
2200
+ ...variant === "outline" && {
2202
2201
  backgroundColor: "transparent",
2203
2202
  borderWidth: 1,
2204
2203
  borderColor: outlineTextColor
2205
- }
2204
+ },
2205
+ ...variant === "ghost" && { backgroundColor: "transparent" }
2206
2206
  };
2207
2207
  const textColor = variant === "default" ? onPrimaryColor : outlineTextColor;
2208
2208
  return /* @__PURE__ */ jsxs16(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostdev/design-system-react-native",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",
@@ -38,6 +38,7 @@ export default function App() {
38
38
 
39
39
  <Canvas of={Stories.Default} />
40
40
  <Canvas of={Stories.Outline} />
41
+ <Canvas of={Stories.Ghost} />
41
42
 
42
43
  ## Sizes
43
44
 
@@ -68,6 +69,7 @@ Set `isIconOnly` to render a 1:1 button (round by default, see [Shape](#shape))
68
69
 
69
70
  <Canvas of={Stories.IconOnly} />
70
71
  <Canvas of={Stories.IconOnlyOutline} />
72
+ <Canvas of={Stories.IconOnlyGhost} />
71
73
  <Canvas of={Stories.AllIconOnly} />
72
74
 
73
75
  ## All variants × sizes
@@ -86,7 +88,7 @@ Set `isIconOnly` to render a 1:1 button (round by default, see [Shape](#shape))
86
88
 
87
89
  | Prop | Type | Default | Description |
88
90
  |------|------|---------|-------------|
89
- | `variant` | `"default" \| "outline"` | `"default"` | Visual style (`"ghost"` is a deprecated alias for `"outline"`) |
91
+ | `variant` | `"default" \| "outline" \| "ghost"` | `"default"` | Visual style: `ghost` is `outline` without the border |
90
92
  | `size` | `"small" \| "medium" \| "large"` | `"medium"` | Height and horizontal padding |
91
93
  | `disabled` | boolean | `false` | Prevents interaction, reduces opacity |
92
94
  | `onPress` | `() => void` | — | Called when the button is pressed |
@@ -72,6 +72,32 @@ describe('Button (native)', () => {
72
72
  expect(style).toMatch(/(^|[;\s])height:\s*56px/);
73
73
  });
74
74
 
75
+ it('draws a ghost button without a border', () => {
76
+ renderWithTheme(<Button variant="ghost">Meer</Button>);
77
+ const style = screen.getByRole('button').getAttribute('style') ?? '';
78
+ expect(style).not.toMatch(/border-(top-|right-|bottom-|left-)?width:\s*[1-9]/);
79
+ expect(style).not.toMatch(/border-(top-|right-|bottom-|left-)?color:/);
80
+ expect(style).toMatch(/background-color:\s*rgba\(0, 0, 0, 0\)/);
81
+ });
82
+
83
+ it('draws an icon-only ghost button without a border', () => {
84
+ renderWithTheme(
85
+ <Button isIconOnly variant="ghost" accessibilityLabel="Meer">
86
+ <span>✓</span>
87
+ </Button>,
88
+ );
89
+ const style = screen.getByRole('button', { name: 'Meer' }).getAttribute('style') ?? '';
90
+ expect(style).not.toMatch(/border-(top-|right-|bottom-|left-)?width:\s*[1-9]/);
91
+ expect(style).not.toMatch(/border-(top-|right-|bottom-|left-)?color:/);
92
+ expect(style).toMatch(/background-color:\s*rgba\(0, 0, 0, 0\)/);
93
+ });
94
+
95
+ it('keeps the border on an outline button', () => {
96
+ renderWithTheme(<Button variant="outline">Meer</Button>);
97
+ const style = screen.getByRole('button').getAttribute('style') ?? '';
98
+ expect(style).toMatch(/border-(top-|right-|bottom-|left-)?width:\s*1px/);
99
+ });
100
+
75
101
  it.each(['small', 'medium', 'large'] as const)(
76
102
  'gives a %s text button the medium border radius by default',
77
103
  (size) => {
@@ -37,8 +37,8 @@ const meta: Meta<ButtonStoryArgs> = {
37
37
  argTypes: {
38
38
  variant: {
39
39
  control: 'select',
40
- options: ['default', 'outline'],
41
- description: 'Visual style. `default` is filled; `outline` is transparent with a border.',
40
+ options: ['default', 'outline', 'ghost'],
41
+ description: 'Visual style. `default` is filled; `outline` is transparent with a border; `ghost` is transparent without one.',
42
42
  table: { defaultValue: { summary: 'default' } },
43
43
  },
44
44
  size: {
@@ -79,6 +79,7 @@ export const Playground: Story = {
79
79
 
80
80
  export const Default: Story = { args: { children: 'Default' } };
81
81
  export const Outline: Story = { args: { children: 'Outline', variant: 'outline' } };
82
+ export const Ghost: Story = { args: { children: 'Ghost', variant: 'ghost' } };
82
83
  export const Small: Story = { args: { children: 'Small', size: 'small' } };
83
84
  export const Medium: Story = { args: { children: 'Medium', size: 'medium' } };
84
85
  export const Large: Story = { args: { children: 'Large', size: 'large' } };
@@ -104,10 +105,19 @@ export const IconOnlyOutline: Story = {
104
105
  },
105
106
  };
106
107
 
108
+ export const IconOnlyGhost: Story = {
109
+ args: {
110
+ variant: 'ghost',
111
+ isIconOnly: true,
112
+ accessibilityLabel: 'Confirm',
113
+ children: CheckIcon,
114
+ },
115
+ };
116
+
107
117
  export const AllVariants: Story = {
108
118
  render: () => (
109
119
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
110
- {(['default', 'outline'] as const).map(v => (
120
+ {(['default', 'outline', 'ghost'] as const).map(v => (
111
121
  <div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
112
122
  {(['small', 'medium', 'large'] as const).map(s => (
113
123
  <Button key={s} variant={v} size={s}>{v} {s}</Button>
@@ -121,7 +131,7 @@ export const AllVariants: Story = {
121
131
  export const AllIconOnly: Story = {
122
132
  render: () => (
123
133
  <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
124
- {(['default', 'outline'] as const).map(v => (
134
+ {(['default', 'outline', 'ghost'] as const).map(v => (
125
135
  <div key={v} style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
126
136
  {(['small', 'medium', 'large'] as const).map(s => (
127
137
  <Button
@@ -4,7 +4,7 @@ import { ScaledText as Text } from '../../../native/ScaledText';
4
4
  import { spacing, font } from '../../../native/tokens';
5
5
  import { useTheme } from '../../../native/ThemeContext';
6
6
 
7
- type ButtonVariant = 'default' | 'outline' | 'ghost'; // 'ghost' is deprecated, use 'outline'
7
+ type ButtonVariant = 'default' | 'outline' | 'ghost'; // 'ghost' is 'outline' without the border
8
8
  type ButtonSize = 'small' | 'medium' | 'large';
9
9
 
10
10
  export interface ButtonProps {
@@ -59,13 +59,13 @@ export function Button({
59
59
  const { colors, radius } = useTheme();
60
60
  const { height, px, fontSize } = sizeTokens[size];
61
61
 
62
- // Color model: filled = interactive bg, outline = transparent bg + interactive_on-bg border/text
62
+ // Color model: filled = interactive bg, outline = transparent bg + interactive_on-bg border/text,
63
+ // ghost = outline without the border, for a quiet button such as one beside a heading
63
64
  const primaryColor = colors.colorInteractive;
64
65
  const onPrimaryColor = colors.colorOnInteractive;
65
66
  // Outline uses colorInteractiveOnBg for text/border — colorInteractive may not meet 3:1 on white
66
67
  const outlineTextColor = colors.colorInteractiveOnBg;
67
68
 
68
- const isOutline = variant === 'outline' || variant === 'ghost';
69
69
  const containerStyle: ViewStyle = {
70
70
  minHeight: height,
71
71
  paddingHorizontal: isIconOnly ? spacing.xs : px,
@@ -78,11 +78,12 @@ export function Button({
78
78
  // instead, so a wrapped label grows the button from minHeight rather than clipping.
79
79
  ...(isIconOnly ? { height } : { paddingVertical: spacing.xxs }),
80
80
  ...(variant === 'default' && { backgroundColor: primaryColor }),
81
- ...(isOutline && {
81
+ ...(variant === 'outline' && {
82
82
  backgroundColor: 'transparent',
83
83
  borderWidth: 1,
84
84
  borderColor: outlineTextColor,
85
85
  }),
86
+ ...(variant === 'ghost' && { backgroundColor: 'transparent' }),
86
87
  };
87
88
 
88
89
  const textColor = variant === 'default' ? onPrimaryColor : outlineTextColor;
@@ -22,7 +22,7 @@ export const Flow: Story = {
22
22
  render: (args) => (
23
23
  <ButtonGroup {...args} variant="flow">
24
24
  <Button>Primary</Button>
25
- <Button variant="ghost">Secondary</Button>
25
+ <Button variant="outline">Secondary</Button>
26
26
  </ButtonGroup>
27
27
  ),
28
28
  };
@@ -30,7 +30,7 @@ export const Card: Story = {
30
30
  render: (args) => (
31
31
  <ButtonGroup {...args} variant="card">
32
32
  <Button>Confirm</Button>
33
- <Button variant="ghost">Cancel</Button>
33
+ <Button variant="outline">Cancel</Button>
34
34
  </ButtonGroup>
35
35
  ),
36
36
  };
@@ -38,7 +38,7 @@ export const Modal: Story = {
38
38
  render: (args) => (
39
39
  <ButtonGroup {...args} variant="modal">
40
40
  <Button>Save</Button>
41
- <Button variant="ghost">Discard</Button>
41
+ <Button variant="outline">Discard</Button>
42
42
  </ButtonGroup>
43
43
  ),
44
44
  };